diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index 4ec92d5c69..c264942013 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -356,6 +356,7 @@ 1403FF0C9A5259CF2D63D98C /* NewProfileUserActivityProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4E46A7B951582DFB9DA8130 /* NewProfileUserActivityProvider.swift */; }; 19BCC6AE85FA7F15895BFDD0 /* NewProfileCertificatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43830C1F0B97682B9CC38DB /* NewProfileCertificatesView.swift */; }; 209E27FAF79E14028BA3E27E /* NewProfileStreakNotificationsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0135D703CAD3B1C16BC95B63 /* NewProfileStreakNotificationsDataFlow.swift */; }; + 224F80151254989529458041 /* NewProfileCreatedCoursesAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A5A3AC14BE54DE59AD0C0DE /* NewProfileCreatedCoursesAssembly.swift */; }; 2431A273489AF7D40AB21348 /* NewProfileStreakNotificationsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9DB0A6C0B38CE13676ED50 /* NewProfileStreakNotificationsInteractor.swift */; }; 245107514E8F81CF9A8C4C44 /* DownloadARQuickLookView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FDC926BE23A9131D45DFF0 /* DownloadARQuickLookView.swift */; }; 247608C654D7C9A7C5994A12 /* NewProfileCertificatesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC88362446F322F9B299287F /* NewProfileCertificatesViewController.swift */; }; @@ -752,6 +753,8 @@ 39AB2324EB824124A83D7534 /* UserCoursesInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09AF7B8FAACF8A4D31AF7583 /* UserCoursesInteractor.swift */; }; 46115BB4590732AAB387046D /* NewProfileUserActivityAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C03F8E163838C4E026CAE5 /* NewProfileUserActivityAssembly.swift */; }; 53973CE3C780CC9E39EA65A5 /* NewProfileDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1A67C6A3908851371ABC4C8 /* NewProfileDataFlow.swift */; }; + 5469EC1AD797F38AC790B9E7 /* NewProfileCreatedCoursesInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D375711B0841DF75C1D6C6D /* NewProfileCreatedCoursesInteractor.swift */; }; + 5D239816094AD2C3575E81CC /* NewProfileCreatedCoursesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FDCEA2CEBE8396085B5648 /* NewProfileCreatedCoursesView.swift */; }; 5E5D029C495513662075EC03 /* NewProfileAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6FF71A3DD5EA47EA1BD4084 /* NewProfileAssembly.swift */; }; 62E980270B31473DE5E16CDD /* SubmissionsAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98C9C16C33663FD3D7021 /* SubmissionsAssembly.swift */; }; 62E98034865DC79E2B5C5D67 /* BaseExploreDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E9838546792D675BC7EA54 /* BaseExploreDataFlow.swift */; }; @@ -1227,6 +1230,7 @@ 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 */; }; + 64AF865C5174A2A525C5FB8A /* NewProfileCreatedCoursesPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5E1D17883366854D97EDB /* NewProfileCreatedCoursesPresenter.swift */; }; 64F1FC49DFF47555128FA13C /* NewProfileCertificatesPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BCAF185A5BB14E3BFB36948 /* NewProfileCertificatesPresenter.swift */; }; 6670CEAE1D677A5EADD37531 /* UserCoursesAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC26997F83B299C9E3FCC5D /* UserCoursesAssembly.swift */; }; 6ADEFFDE33B06A7049CF8E2F /* NewProfileCertificatesAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC925F9685393FA44977549 /* NewProfileCertificatesAssembly.swift */; }; @@ -1250,7 +1254,9 @@ 942F44578E193384B1E4DAD9 /* UserCoursesPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582946156484A5655EF72E43 /* UserCoursesPresenter.swift */; }; 96A3827B7E57E4B055221311 /* NewProfileUserActivityPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9217C4CCCAFEF3BDE7DD27A5 /* NewProfileUserActivityPresenter.swift */; }; 9FAD76150240B4F8C13ADFA2 /* NewProfileUserActivityInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EE6AAA8E539D30FAF22B0B /* NewProfileUserActivityInteractor.swift */; }; + AA4038BFF0FB9031371EBB38 /* NewProfileCreatedCoursesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBE5DD451A3D87D0E23671D3 /* NewProfileCreatedCoursesViewController.swift */; }; AAB4F45B011C05D59E6EEEEB /* DownloadARQuickLookPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DCDAA39E6747A12CC239E3 /* DownloadARQuickLookPresenter.swift */; }; + B2FE12ABF98ED99EAF727405 /* NewProfileCreatedCoursesDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099C818F5A180372348FC30 /* NewProfileCreatedCoursesDataFlow.swift */; }; C0DD3AD5A7FEEA4555312455 /* NewProfileAchievementsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF3FBAFFE1578394C476293 /* NewProfileAchievementsDataFlow.swift */; }; C2537B043956035AEFCABAB5 /* DownloadARQuickLookOutputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD2CFBDE7B71C461F988B1B5 /* DownloadARQuickLookOutputProtocol.swift */; }; C95BEF69AB5100E263ED66F6 /* DownloadARQuickLookAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = B259C3826C752171E0305872 /* DownloadARQuickLookAssembly.swift */; }; @@ -1260,6 +1266,7 @@ DA1ACE98741C42B9DCBBFB02 /* NewProfileCertificatesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E4F5E8235E0245E5D5CB6BC /* NewProfileCertificatesProvider.swift */; }; E36410A645B2D9008B9FD40B /* Pods_Stepic.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F98579F526E5A4D162C3356 /* Pods_Stepic.framework */; }; E68D5D39475BA651C54FD4C9 /* NewProfileStreakNotificationsAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05F0E49F1F2245F037EE6305 /* NewProfileStreakNotificationsAssembly.swift */; }; + ECD21878641B360C12E8C585 /* NewProfileCreatedCoursesOutputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4A385A37C2127557150147 /* NewProfileCreatedCoursesOutputProtocol.swift */; }; F26EBB85670ECB562C65D84C /* DownloadARQuickLookInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC129A0368739A641E6ED26A /* DownloadARQuickLookInteractor.swift */; }; F7B2524CCF4BE272FB48CECF /* NewProfilePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F8FAC809538C9222DD6ABC9 /* NewProfilePresenter.swift */; }; FBB6AC66F23F6493B8F5E225 /* NewProfileAchievementsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1BB507BCF6F48523222D6D3 /* NewProfileAchievementsPresenter.swift */; }; @@ -1695,6 +1702,7 @@ 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 = ""; }; 27CF7A377B4C8BC247ECBD29 /* NewProfileUserActivityViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileUserActivityViewController.swift; sourceTree = ""; }; + 2C00186524C71404006C5094 /* Model_new_profile_created_courses_v56.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_new_profile_created_courses_v56.xcdatamodel; sourceTree = ""; }; 2C01BB67233CD92C00C8DCF0 /* Require.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Require.swift; sourceTree = ""; }; 2C01D3A922DDB7EA00C84CEE /* DefaultsContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultsContainer.swift; sourceTree = ""; }; 2C01D3AB22DDB98900C84CEE /* LaunchDefaultsContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LaunchDefaultsContainer.swift; sourceTree = ""; }; @@ -2108,10 +2116,12 @@ 2CFC5ABB228ADFC400B5248A /* StepsPersistenceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepsPersistenceService.swift; sourceTree = ""; }; 2CFDA8181FBB3CFD0098A441 /* Model_sections_with_course_id_v21.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_sections_with_course_id_v21.xcdatamodel; sourceTree = ""; }; 2CFDB1231F559F9A00B8035C /* AvatarImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarImageView.swift; sourceTree = ""; }; + 2FA5E1D17883366854D97EDB /* NewProfileCreatedCoursesPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileCreatedCoursesPresenter.swift; sourceTree = ""; }; 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 = ""; }; 4026ACBBA0C3F084DA6D5A6E /* NewProfileStreakNotificationsView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileStreakNotificationsView.swift; 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 = ""; }; + 5099C818F5A180372348FC30 /* NewProfileCreatedCoursesDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileCreatedCoursesDataFlow.swift; sourceTree = ""; }; 53EE6AAA8E539D30FAF22B0B /* NewProfileUserActivityInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileUserActivityInteractor.swift; sourceTree = ""; }; 582946156484A5655EF72E43 /* UserCoursesPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UserCoursesPresenter.swift; sourceTree = ""; }; 58C6ED5894DADC5B17310B92 /* NewProfileStreakNotificationsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileStreakNotificationsPresenter.swift; sourceTree = ""; }; @@ -2593,6 +2603,7 @@ 62E98FD93BED7AFCD27CC1CD /* SettingsInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsInteractor.swift; sourceTree = ""; }; 62E98FEB6C1841DD9CFE9D1F /* CourseInfoTabReviewsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseInfoTabReviewsViewModel.swift; sourceTree = ""; }; 69DE4213EB9F9BB5C2C25E78 /* NewProfileInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileInteractor.swift; sourceTree = ""; }; + 6A5A3AC14BE54DE59AD0C0DE /* NewProfileCreatedCoursesAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileCreatedCoursesAssembly.swift; sourceTree = ""; }; 72D1EE8518225C9F49BE44EA /* NewProfileUserActivityView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileUserActivityView.swift; sourceTree = ""; }; 75A30D817521E42507A608B3 /* NewProfileCertificatesInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileCertificatesInteractor.swift; sourceTree = ""; }; 7B9DB0A6C0B38CE13676ED50 /* NewProfileStreakNotificationsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileStreakNotificationsInteractor.swift; sourceTree = ""; }; @@ -2609,6 +2620,7 @@ 8F8FAC809538C9222DD6ABC9 /* NewProfilePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfilePresenter.swift; sourceTree = ""; }; 9217C4CCCAFEF3BDE7DD27A5 /* NewProfileUserActivityPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileUserActivityPresenter.swift; sourceTree = ""; }; 938533EAAD61D57EF139C60C /* Pods_StepicTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_StepicTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9D375711B0841DF75C1D6C6D /* NewProfileCreatedCoursesInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileCreatedCoursesInteractor.swift; sourceTree = ""; }; 9E4F5E8235E0245E5D5CB6BC /* NewProfileCertificatesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileCertificatesProvider.swift; sourceTree = ""; }; 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; }; @@ -2629,11 +2641,14 @@ D1BB507BCF6F48523222D6D3 /* NewProfileAchievementsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileAchievementsPresenter.swift; sourceTree = ""; }; D43830C1F0B97682B9CC38DB /* NewProfileCertificatesView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileCertificatesView.swift; sourceTree = ""; }; D6FF71A3DD5EA47EA1BD4084 /* NewProfileAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileAssembly.swift; sourceTree = ""; }; + D7FDCEA2CEBE8396085B5648 /* NewProfileCreatedCoursesView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileCreatedCoursesView.swift; sourceTree = ""; }; + DD4A385A37C2127557150147 /* NewProfileCreatedCoursesOutputProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileCreatedCoursesOutputProtocol.swift; sourceTree = ""; }; E4A0BB5098635CCE7F5825B0 /* NewProfileAchievementsView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileAchievementsView.swift; sourceTree = ""; }; ECD9168AF529D0D527B1DCDB /* Pods-StepicTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-StepicTests.debug.xcconfig"; path = "Target Support Files/Pods-StepicTests/Pods-StepicTests.debug.xcconfig"; sourceTree = ""; }; EF17244B78AAAC345805C0DA /* NewProfileAchievementsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileAchievementsViewController.swift; sourceTree = ""; }; F4B98B071C1C8E3FB13F9717 /* NewProfileAchievementsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileAchievementsInteractor.swift; sourceTree = ""; }; F4E46A7B951582DFB9DA8130 /* NewProfileUserActivityProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileUserActivityProvider.swift; sourceTree = ""; }; + FBE5DD451A3D87D0E23671D3 /* NewProfileCreatedCoursesViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileCreatedCoursesViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -2783,6 +2798,14 @@ name = Frameworks; sourceTree = ""; }; + 2A75C1500270B3ADADDC4ADC /* InputOutput */ = { + isa = PBXGroup; + children = ( + DD4A385A37C2127557150147 /* NewProfileCreatedCoursesOutputProtocol.swift */, + ); + path = InputOutput; + sourceTree = ""; + }; 2C0176C32188A49100DDB9D0 /* Analytics */ = { isa = PBXGroup; children = ( @@ -3175,6 +3198,7 @@ children = ( 5F48944D8F8DBE3A61FECCAA /* Achievements */, 0E839B9EC258A7A3342CE5FA /* Certificates */, + BCB55E0AE9D5E53C3E063901 /* CreatedCourses */, 2C8BCC2424869A1100DFB009 /* Details */, B0EC82D3DB3A11DA382B3D42 /* StreakNotifications */, E7DABB29D2991EED361BCE28 /* UserActivity */, @@ -6771,6 +6795,20 @@ path = StreakNotifications; sourceTree = ""; }; + BCB55E0AE9D5E53C3E063901 /* CreatedCourses */ = { + isa = PBXGroup; + children = ( + 6A5A3AC14BE54DE59AD0C0DE /* NewProfileCreatedCoursesAssembly.swift */, + 5099C818F5A180372348FC30 /* NewProfileCreatedCoursesDataFlow.swift */, + 9D375711B0841DF75C1D6C6D /* NewProfileCreatedCoursesInteractor.swift */, + 2FA5E1D17883366854D97EDB /* NewProfileCreatedCoursesPresenter.swift */, + D7FDCEA2CEBE8396085B5648 /* NewProfileCreatedCoursesView.swift */, + FBE5DD451A3D87D0E23671D3 /* NewProfileCreatedCoursesViewController.swift */, + 2A75C1500270B3ADADDC4ADC /* InputOutput */, + ); + path = CreatedCourses; + sourceTree = ""; + }; C5254161C4484A82273DE3C7 /* UserCourses */ = { isa = PBXGroup; children = ( @@ -8567,6 +8605,13 @@ DA1ACE98741C42B9DCBBFB02 /* NewProfileCertificatesProvider.swift in Sources */, 19BCC6AE85FA7F15895BFDD0 /* NewProfileCertificatesView.swift in Sources */, 247608C654D7C9A7C5994A12 /* NewProfileCertificatesViewController.swift in Sources */, + 224F80151254989529458041 /* NewProfileCreatedCoursesAssembly.swift in Sources */, + B2FE12ABF98ED99EAF727405 /* NewProfileCreatedCoursesDataFlow.swift in Sources */, + 5469EC1AD797F38AC790B9E7 /* NewProfileCreatedCoursesInteractor.swift in Sources */, + 64AF865C5174A2A525C5FB8A /* NewProfileCreatedCoursesPresenter.swift in Sources */, + 5D239816094AD2C3575E81CC /* NewProfileCreatedCoursesView.swift in Sources */, + AA4038BFF0FB9031371EBB38 /* NewProfileCreatedCoursesViewController.swift in Sources */, + ECD21878641B360C12E8C585 /* NewProfileCreatedCoursesOutputProtocol.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -9011,6 +9056,7 @@ 08D1EF6E1BB5618700BE84E6 /* Model.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + 2C00186524C71404006C5094 /* Model_new_profile_created_courses_v56.xcdatamodel */, 2C0A7D5024C53D7F000A66B5 /* Model_new_profile_certificates_v55.xcdatamodel */, 2C32664224B00BEF0013C473 /* Model_course_purchases_v54.xcdatamodel */, 2C84D42B24AFFCD300A77EF1 /* Model_new_profile_v53.xcdatamodel */, @@ -9068,7 +9114,7 @@ 0802AC531C7222B200C4F3E6 /* Model_v2.xcdatamodel */, 08D1EF6F1BB5618700BE84E6 /* Model.xcdatamodel */, ); - currentVersion = 2C0A7D5024C53D7F000A66B5 /* Model_new_profile_certificates_v55.xcdatamodel */; + currentVersion = 2C00186524C71404006C5094 /* Model_new_profile_created_courses_v56.xcdatamodel */; path = Model.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/Stepic/Legacy/Analytics/Events/AmplitudeAnalyticsEvents.swift b/Stepic/Legacy/Analytics/Events/AmplitudeAnalyticsEvents.swift index 8151eb7213..144b6e75d5 100644 --- a/Stepic/Legacy/Analytics/Events/AmplitudeAnalyticsEvents.swift +++ b/Stepic/Legacy/Analytics/Events/AmplitudeAnalyticsEvents.swift @@ -477,6 +477,7 @@ extension AnalyticsEvent { case story(id: Int) case deepLink(url: String) case notification + case profile(id: Int) case unknown var name: String { @@ -499,6 +500,8 @@ extension AnalyticsEvent { return "deeplink" case .notification: return "notification" + case .profile: + return "profile" case .unknown: return "unknown" } @@ -512,6 +515,8 @@ extension AnalyticsEvent { return ["query": query] case .collection(let id): return ["collection": id] + case .profile(let id): + return ["profile": id] case .query(let courseListType): var params: [String: Any] = [ "type": courseListType.analyticName diff --git a/Stepic/Legacy/Model/Entities/User/User+CoreDataProperties.swift b/Stepic/Legacy/Model/Entities/User/User+CoreDataProperties.swift index f226a6f87b..20019aaf0d 100644 --- a/Stepic/Legacy/Model/Entities/User/User+CoreDataProperties.swift +++ b/Stepic/Legacy/Model/Entities/User/User+CoreDataProperties.swift @@ -29,6 +29,7 @@ extension User { @NSManaged var managedReputation: NSNumber? @NSManaged var managedReputationRank: NSNumber? @NSManaged var managedJoinDate: Date? + @NSManaged var managedCreatedCoursesArray: NSObject? @NSManaged var managedCreatedCoursesCount: NSNumber? @NSManaged var managedSolvedStepsCount: NSNumber? @NSManaged var managedCreatedLessonsCount: NSNumber? @@ -207,6 +208,15 @@ extension User { } } + var createdCoursesArray: [Course.IdType] { + get { + (self.managedCreatedCoursesArray as? [Int]) ?? [] + } + set { + self.managedCreatedCoursesArray = newValue as NSObject? + } + } + var createdCoursesCount: Int { get { self.managedCreatedCoursesCount?.intValue ?? 0 diff --git a/Stepic/Legacy/Model/Model.xcdatamodeld/.xccurrentversion b/Stepic/Legacy/Model/Model.xcdatamodeld/.xccurrentversion index 50767228fc..95ec7e4de8 100644 --- a/Stepic/Legacy/Model/Model.xcdatamodeld/.xccurrentversion +++ b/Stepic/Legacy/Model/Model.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - Model_new_profile_certificates_v55.xcdatamodel + Model_new_profile_created_courses_v56.xcdatamodel diff --git a/Stepic/Legacy/Model/Model.xcdatamodeld/Model_new_profile_created_courses_v56.xcdatamodel/contents b/Stepic/Legacy/Model/Model.xcdatamodeld/Model_new_profile_created_courses_v56.xcdatamodel/contents new file mode 100644 index 0000000000..d57bf79df8 --- /dev/null +++ b/Stepic/Legacy/Model/Model.xcdatamodeld/Model_new_profile_created_courses_v56.xcdatamodel/contents @@ -0,0 +1,391 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Stepic/Legacy/Model/Network/Endpoints/CoursesAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/CoursesAPI.swift index d14115f647..1d32779d2e 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/CoursesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/CoursesAPI.swift @@ -45,6 +45,7 @@ final class CoursesAPI: APIEndpoint { func retrieve( tag: Int? = nil, + teacher: Int? = nil, featured: Bool? = nil, enrolled: Bool? = nil, excludeEnded: Bool? = nil, @@ -93,6 +94,10 @@ final class CoursesAPI: APIEndpoint { params["tag"] = tag } + if let teacher = teacher { + params["teacher"] = teacher + } + params["page"] = page return self.retrieve.requestWithFetching( @@ -134,7 +139,8 @@ final class CoursesAPI: APIEndpoint { } enum Order: String { - case activityDescending = "-activity" + case activityDesc = "-activity" + case popularityDesc = "-popularity" } } diff --git a/Stepic/Sources/Modules/CourseList/Provider/CourseListNetworkService.swift b/Stepic/Sources/Modules/CourseList/Provider/CourseListNetworkService.swift index d88b8d2613..6697efb650 100644 --- a/Stepic/Sources/Modules/CourseList/Provider/CourseListNetworkService.swift +++ b/Stepic/Sources/Modules/CourseList/Provider/CourseListNetworkService.swift @@ -90,7 +90,7 @@ final class PopularCourseListNetworkService: BaseCourseListNetworkService, Cours Promise { seal in self.coursesAPI.retrieve( isCataloged: true, - order: .activityDescending, + order: .activityDesc, language: self.type.language.popularCoursesParameter, page: page ).done { result in @@ -114,7 +114,7 @@ final class TagCourseListNetworkService: BaseCourseListNetworkService, CourseLis Promise { seal in self.coursesAPI.retrieve( tag: self.type.id, - order: .activityDescending, + order: .activityDesc, language: self.type.language.languageString, page: page ).done { result in @@ -183,3 +183,26 @@ final class SearchResultCourseListNetworkService: BaseCourseListNetworkService, } } } + +final class TeacherCourseListNetworkService: BaseCourseListNetworkService, CourseListNetworkServiceProtocol { + let type: TeacherCourseListType + + init(type: TeacherCourseListType, coursesAPI: CoursesAPI) { + self.type = type + super.init(coursesAPI: coursesAPI) + } + + func fetch(page: Int) -> Promise<([Course], Meta)> { + Promise { seal in + self.coursesAPI.retrieve( + teacher: self.type.teacherID, + order: .popularityDesc, + page: page + ).done { result in + seal.fulfill(result) + }.catch { _ in + seal.reject(Error.fetchFailed) + } + } + } +} diff --git a/Stepic/Sources/Modules/CourseList/Provider/CourseListPersistenceStorage.swift b/Stepic/Sources/Modules/CourseList/Provider/CourseListPersistenceStorage.swift index d2785b3c1d..57a21d51c6 100644 --- a/Stepic/Sources/Modules/CourseList/Provider/CourseListPersistenceStorage.swift +++ b/Stepic/Sources/Modules/CourseList/Provider/CourseListPersistenceStorage.swift @@ -32,3 +32,23 @@ final class PassiveCourseListPersistenceStorage: CourseListPersistenceStorage { func getCoursesList() -> [Course.IdType] { self.list } } + +final class CreatedCoursesCourseListPersistenceStorage: CourseListPersistenceStorage { + private let teacherID: User.IdType + + private var teacherEntity: User? { + User.fetchById(self.teacherID)?.first + } + + init(teacherID: User.IdType) { + self.teacherID = teacherID + } + + func update(newCachedList: [Course.IdType]) { + self.teacherEntity?.createdCoursesArray = newCachedList + } + + func getCoursesList() -> [Course.IdType] { + self.teacherEntity?.createdCoursesArray ?? [] + } +} diff --git a/Stepic/Sources/Modules/CourseList/Provider/CourseListTypes.swift b/Stepic/Sources/Modules/CourseList/Provider/CourseListTypes.swift index 0a12bbce47..b070b1532d 100644 --- a/Stepic/Sources/Modules/CourseList/Provider/CourseListTypes.swift +++ b/Stepic/Sources/Modules/CourseList/Provider/CourseListTypes.swift @@ -45,6 +45,12 @@ struct SearchResultCourseListType: CourseListType { var analyticName: String { "search_result_course_list" } } +struct TeacherCourseListType: CourseListType { + let teacherID: User.IdType + + var analyticName: String { "teacher_course_list" } +} + // MARK: - Services factory final class CourseListServicesFactory { @@ -100,6 +106,10 @@ final class CourseListServicesFactory { ) } else if self.type is SearchResultCourseListType { return nil + } else if let type = self.type as? TeacherCourseListType { + return CourseListPersistenceService( + storage: CreatedCoursesCourseListPersistenceStorage(teacherID: type.teacherID) + ) } else { fatalError("Unsupported course list type") } @@ -126,6 +136,8 @@ final class CourseListServicesFactory { coursesAPI: self.coursesAPI, searchResultsAPI: self.searchResultsAPI ) + } else if let type = self.type as? TeacherCourseListType { + return TeacherCourseListNetworkService(type: type, coursesAPI: self.coursesAPI) } else { fatalError("Unsupported course list type") } diff --git a/Stepic/Sources/Modules/CourseList/Views/CourseListColorMode.swift b/Stepic/Sources/Modules/CourseList/Views/CourseListColorMode.swift index eef5a1c427..0af3400907 100644 --- a/Stepic/Sources/Modules/CourseList/Views/CourseListColorMode.swift +++ b/Stepic/Sources/Modules/CourseList/Views/CourseListColorMode.swift @@ -3,6 +3,7 @@ import Foundation enum CourseListColorMode { case light case dark + case clear static var `default`: CourseListColorMode { .light } } @@ -10,7 +11,7 @@ enum CourseListColorMode { extension CourseListColorMode { var exploreBlockHeaderViewAppearance: ExploreBlockHeaderView.Appearance { switch self { - case .light: + case .light, .clear: return .init( titleLabelColor: .stepikPrimaryText, showAllButtonColor: .stepikTertiaryText @@ -24,23 +25,16 @@ extension CourseListColorMode { } var exploreBlockContainerViewAppearance: ExploreBlockContainerView.Appearance { - switch self { - case .light: - var appearance = ExploreBlockContainerView.Appearance() - appearance.backgroundColor = self.exploreBlockContainerViewBackgroundColor - return appearance - case .dark: - var appearance = ExploreBlockContainerView.Appearance() - appearance.backgroundColor = self.exploreBlockContainerViewBackgroundColor - return appearance - } + var appearance = ExploreBlockContainerView.Appearance() + appearance.backgroundColor = self.exploreBlockContainerViewBackgroundColor + return appearance } private var exploreBlockContainerViewBackgroundColor: UIColor { if #available(iOS 13.0, *) { return UIColor { (traitCollection: UITraitCollection) -> UIColor in switch self { - case .light: + case .light, .clear: return .stepikBackground case .dark: if traitCollection.userInterfaceStyle == .dark { @@ -51,7 +45,7 @@ extension CourseListColorMode { } } else { switch self { - case .light: + case .light, .clear: return .white case .dark: return .stepikAccentFixed @@ -65,7 +59,7 @@ extension CourseListColorMode { var courseWidgetStatsViewAppearance: CourseWidgetStatsView.Appearance { switch self { - case .light: + case .light, .clear: return .init( imagesRenderingBackgroundColor: .stepikAccent, imagesRenderingTintColor: .stepikGreenFixed, @@ -89,7 +83,7 @@ extension CourseListColorMode { ) switch self { - case .light: + case .light, .clear: appearance.textColor = .stepikPrimaryText case .dark: appearance.textColor = .white @@ -105,7 +99,7 @@ extension CourseListColorMode { ) switch self { - case .light: + case .light, .clear: appearance.textColor = .stepikSecondaryText case .dark: appearance.textColor = UIColor.dynamic( @@ -119,7 +113,7 @@ extension CourseListColorMode { var courseWidgetBorderColor: UIColor { switch self { - case .light: + case .light, .clear: return .dynamic(light: .stepikGrey8Fixed, dark: .stepikSeparator) case .dark: if #available(iOS 13.0, *) { diff --git a/Stepic/Sources/Modules/CourseList/Views/CourseListView.swift b/Stepic/Sources/Modules/CourseList/Views/CourseListView.swift index 98a21d7fcc..e2bb10d8a8 100644 --- a/Stepic/Sources/Modules/CourseList/Views/CourseListView.swift +++ b/Stepic/Sources/Modules/CourseList/Views/CourseListView.swift @@ -139,6 +139,8 @@ class CourseListView: UIView { return self.appearance.lightModeBackgroundColor case .dark: return self.appearance.darkModeBackgroundColor + case .clear: + return .clear } } @@ -186,6 +188,11 @@ extension CourseListView: ProgrammaticallyInitializableViewProtocol { DarkCourseListCollectionViewCell.self, forCellWithReuseIdentifier: DarkCourseListCollectionViewCell.defaultReuseIdentifier ) + case .clear: + self.collectionView.register( + ClearCourseListCollectionViewCell.self, + forCellWithReuseIdentifier: ClearCourseListCollectionViewCell.defaultReuseIdentifier + ) } self.collectionView.register( @@ -445,6 +452,18 @@ final class HorizontalCourseListView: CourseListView { fatalError("init(coder:) has not been implemented") } + override func updateCollectionViewData(delegate: UICollectionViewDelegate, dataSource: UICollectionViewDataSource) { + if dataSource.collectionView(self.collectionView, numberOfItemsInSection: 0) == 1 { + self.horizontalCourseFlowLayout.rowsCount = 1 + } else { + self.horizontalCourseFlowLayout.rowsCount = self.rowsCount + } + + super.updateCollectionViewData(delegate: delegate, dataSource: dataSource) + + self.invalidateIntrinsicContentSize() + } + override func calculateItemSize() -> CGSize { if self.isAdaptiveColumnsCount { let (columnsCount, columnWidth) = self.calculateAdaptiveLayoutColumnAttributes( @@ -519,3 +538,18 @@ private class DarkCourseListCollectionViewCell: CourseListCollectionViewCell { String(describing: CourseListCollectionViewCell.self) } } + +private class ClearCourseListCollectionViewCell: CourseListCollectionViewCell { + override init(frame: CGRect) { + super.init(frame: frame, colorMode: .clear) + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + static var defaultReuseIdentifier: String { + String(describing: CourseListCollectionViewCell.self) + } +} diff --git a/Stepic/Sources/Modules/CourseList/Views/Layouts/HorizontalCourseListFlowLayout.swift b/Stepic/Sources/Modules/CourseList/Views/Layouts/HorizontalCourseListFlowLayout.swift index 11ecae57b3..6ca317a6ac 100644 --- a/Stepic/Sources/Modules/CourseList/Views/Layouts/HorizontalCourseListFlowLayout.swift +++ b/Stepic/Sources/Modules/CourseList/Views/Layouts/HorizontalCourseListFlowLayout.swift @@ -12,7 +12,7 @@ extension HorizontalCourseListFlowLayout { final class HorizontalCourseListFlowLayout: BaseListFlowLayout { let appearance: Appearance - let rowsCount: Int + var rowsCount: Int var columnsCount: Int private var _contentWidth: CGFloat = 0 diff --git a/Stepic/Sources/Modules/CourseList/Views/Widget/CourseWidgetView.swift b/Stepic/Sources/Modules/CourseList/Views/Widget/CourseWidgetView.swift index dd1a7b4060..4a3e5d5834 100644 --- a/Stepic/Sources/Modules/CourseList/Views/Widget/CourseWidgetView.swift +++ b/Stepic/Sources/Modules/CourseList/Views/Widget/CourseWidgetView.swift @@ -75,6 +75,7 @@ final class CourseWidgetView: UIView { self.colorMode = colorMode super.init(frame: frame) + self.setupView() self.addSubviews() self.makeConstraints() } @@ -123,6 +124,12 @@ final class CourseWidgetView: UIView { } extension CourseWidgetView: ProgrammaticallyInitializableViewProtocol { + func setupView() { + if self.colorMode == .clear { + self.backgroundColor = .stepikBackground + } + } + func addSubviews() { self.addSubview(self.coverView) self.addSubview(self.titleLabel) diff --git a/Stepic/Sources/Modules/NewProfile/NewProfileDataFlow.swift b/Stepic/Sources/Modules/NewProfile/NewProfileDataFlow.swift index 7af13f7f1b..845eb99f71 100644 --- a/Stepic/Sources/Modules/NewProfile/NewProfileDataFlow.swift +++ b/Stepic/Sources/Modules/NewProfile/NewProfileDataFlow.swift @@ -5,6 +5,7 @@ enum NewProfile { enum Submodule: String, UniqueIdentifiable { case streakNotifications + case createdCourses case userActivity case achievements case certificates diff --git a/Stepic/Sources/Modules/NewProfile/NewProfileInteractor.swift b/Stepic/Sources/Modules/NewProfile/NewProfileInteractor.swift index 591e4b2757..673a2a3865 100644 --- a/Stepic/Sources/Modules/NewProfile/NewProfileInteractor.swift +++ b/Stepic/Sources/Modules/NewProfile/NewProfileInteractor.swift @@ -334,6 +334,14 @@ extension NewProfileInteractor: NewProfileCertificatesOutputProtocol { } } +// MARK: - NewProfileInteractor: NewProfileCreatedCoursesOutputProtocol - + +extension NewProfileInteractor: NewProfileCreatedCoursesOutputProtocol { + func handleCreatedCoursesEmptyState() { + self.presenter.presentSubmoduleEmptyState(response: .init(module: .createdCourses)) + } +} + // MARK: - NewProfileInteractor: SettingsOutputProtocol - extension NewProfileInteractor: SettingsOutputProtocol { diff --git a/Stepic/Sources/Modules/NewProfile/NewProfileViewController.swift b/Stepic/Sources/Modules/NewProfile/NewProfileViewController.swift index 11ee640c35..e7802abab0 100644 --- a/Stepic/Sources/Modules/NewProfile/NewProfileViewController.swift +++ b/Stepic/Sources/Modules/NewProfile/NewProfileViewController.swift @@ -17,7 +17,7 @@ final class NewProfileViewController: UIViewController, ControllerWithStepikPlac } fileprivate static let submodulesOrder: [NewProfile.Submodule] = [ - .streakNotifications, .userActivity, .achievements, .certificates, .details + .streakNotifications, .createdCourses, .userActivity, .achievements, .certificates, .details ] var placeholderContainer = StepikPlaceholderControllerContainer() @@ -161,6 +161,16 @@ final class NewProfileViewController: UIViewController, ControllerWithStepikPlac self.isPlaceholderShown = false self.newProfileView?.configure(viewModel: viewModel) + let shouldShowCreatedCourses: Bool = { + switch self.currentCreatedCoursesState { + case .visible: + return true + case .hidden: + return false + } + }() + self.refreshCreatedCoursesState(shouldShowCreatedCourses ? .visible(teacherID: viewModel.userID) : .hidden) + let shouldShowStreakNotifications = viewModel.isCurrentUserProfile self.refreshStreakNotificationsState(shouldShowStreakNotifications ? .visible : .hidden) @@ -246,6 +256,64 @@ final class NewProfileViewController: UIViewController, ControllerWithStepikPlac } } + // MARK: Created Courses + + private enum CreatedCoursesState { + case visible(teacherID: User.IdType) + case hidden + } + + private var currentCreatedCoursesState = CreatedCoursesState.visible(teacherID: 0) + + private func refreshCreatedCoursesState(_ state: CreatedCoursesState) { + switch state { + case .visible(let teacherID): + guard self.getSubmodule(type: NewProfile.Submodule.createdCourses) == nil else { + return + } + + let assembly = NewProfileCreatedCoursesAssembly( + output: self.interactor as? NewProfileCreatedCoursesOutputProtocol + ) + let viewController = assembly.makeModule() + + let headerView = NewProfileBlockHeaderView() + headerView.titleText = NSLocalizedString("NewProfileBlockTitleCreatedCourses", comment: "") + headerView.onShowAllButtonClick = { [weak self] in + let assembly = FullscreenCourseListAssembly(courseListType: TeacherCourseListType(teacherID: teacherID)) + self?.push(module: assembly.makeModule()) + } + + let appearance = NewProfileBlockContainerView.Appearance( + backgroundColor: .clear, + contentViewInsets: .zero + ) + let containerView = NewProfileBlockContainerView( + headerView: headerView, + contentView: viewController.view, + appearance: appearance + ) + + self.registerSubmodule( + .init( + viewController: viewController, + view: containerView, + type: NewProfile.Submodule.createdCourses + ) + ) + + if let moduleInput = assembly.moduleInput { + self.interactor.doSubmodulesRegistration( + request: .init(submodules: [NewProfile.Submodule.createdCourses.uniqueIdentifier: moduleInput]) + ) + } + case .hidden: + if let submodule = self.getSubmodule(type: NewProfile.Submodule.createdCourses) { + self.removeSubmodule(submodule) + } + } + } + // MARK: User Activity private func refreshUserActivityState() { @@ -470,6 +538,9 @@ extension NewProfileViewController: NewProfileViewControllerProtocol { func displaySubmoduleEmptyState(viewModel: NewProfile.SubmoduleEmptyStatePresentation.ViewModel) { switch viewModel.module { + case .createdCourses: + self.currentCreatedCoursesState = .hidden + self.refreshCreatedCoursesState(self.currentCreatedCoursesState) case .certificates: self.currentCertificatesState = .hidden self.refreshCertificatesState(self.currentCertificatesState) diff --git a/Stepic/Sources/Modules/NewProfile/Views/Block/NewProfileBlockContainerView.swift b/Stepic/Sources/Modules/NewProfile/Views/Block/NewProfileBlockContainerView.swift index 49606d8751..3e80a4be9a 100644 --- a/Stepic/Sources/Modules/NewProfile/Views/Block/NewProfileBlockContainerView.swift +++ b/Stepic/Sources/Modules/NewProfile/Views/Block/NewProfileBlockContainerView.swift @@ -4,7 +4,7 @@ import UIKit extension NewProfileBlockContainerView { struct Appearance { let separatorColor = UIColor.stepikSeparator - let backgroundColor = UIColor.stepikSecondaryGroupedBackground + var backgroundColor = UIColor.stepikSecondaryGroupedBackground let headerViewInsets = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) var contentViewInsets = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) diff --git a/Stepic/Sources/Modules/NewProfileSubmodules/CreatedCourses/InputOutput/NewProfileCreatedCoursesOutputProtocol.swift b/Stepic/Sources/Modules/NewProfileSubmodules/CreatedCourses/InputOutput/NewProfileCreatedCoursesOutputProtocol.swift new file mode 100644 index 0000000000..0feae7f096 --- /dev/null +++ b/Stepic/Sources/Modules/NewProfileSubmodules/CreatedCourses/InputOutput/NewProfileCreatedCoursesOutputProtocol.swift @@ -0,0 +1,5 @@ +import Foundation + +protocol NewProfileCreatedCoursesOutputProtocol: AnyObject { + func handleCreatedCoursesEmptyState() +} diff --git a/Stepic/Sources/Modules/NewProfileSubmodules/CreatedCourses/NewProfileCreatedCoursesAssembly.swift b/Stepic/Sources/Modules/NewProfileSubmodules/CreatedCourses/NewProfileCreatedCoursesAssembly.swift new file mode 100644 index 0000000000..45427e4da2 --- /dev/null +++ b/Stepic/Sources/Modules/NewProfileSubmodules/CreatedCourses/NewProfileCreatedCoursesAssembly.swift @@ -0,0 +1,26 @@ +import UIKit + +final class NewProfileCreatedCoursesAssembly: Assembly { + var moduleInput: NewProfileSubmoduleProtocol? + + private weak var moduleOutput: NewProfileCreatedCoursesOutputProtocol? + + init(output: NewProfileCreatedCoursesOutputProtocol? = nil) { + self.moduleOutput = output + } + + func makeModule() -> UIViewController { + let presenter = NewProfileCreatedCoursesPresenter() + let interactor = NewProfileCreatedCoursesInteractor( + presenter: presenter, + networkReachabilityService: NetworkReachabilityService() + ) + let viewController = NewProfileCreatedCoursesViewController(interactor: interactor) + + presenter.viewController = viewController + self.moduleInput = interactor + interactor.moduleOutput = self.moduleOutput + + return viewController + } +} diff --git a/Stepic/Sources/Modules/NewProfileSubmodules/CreatedCourses/NewProfileCreatedCoursesDataFlow.swift b/Stepic/Sources/Modules/NewProfileSubmodules/CreatedCourses/NewProfileCreatedCoursesDataFlow.swift new file mode 100644 index 0000000000..0cfa9b31ad --- /dev/null +++ b/Stepic/Sources/Modules/NewProfileSubmodules/CreatedCourses/NewProfileCreatedCoursesDataFlow.swift @@ -0,0 +1,80 @@ +import Foundation + +enum NewProfileCreatedCourses { + /// Show courses + enum CoursesLoad { + struct Request {} + + struct Response { + let teacherID: User.IdType + } + + struct ViewModel { + let teacherID: User.IdType + } + } + + /// Try to set online status + enum OnlineModeReset { + struct Request { + let module: CourseListInputProtocol + } + } + + /// Present course syllabus + enum CourseSyllabusPresentation { + struct Response { + let course: Course + let courseViewSource: AnalyticsEvent.CourseViewSource + } + + struct ViewModel { + let courseID: Course.IdType + let courseViewSource: AnalyticsEvent.CourseViewSource + } + } + + /// Present course info + enum CourseInfoPresentation { + struct Response { + let course: Course + let courseViewSource: AnalyticsEvent.CourseViewSource + } + + struct ViewModel { + let courseID: Course.IdType + let courseViewSource: AnalyticsEvent.CourseViewSource + } + } + + /// Present last step in course + enum LastStepPresentation { + struct Response { + let course: Course + let isAdaptive: Bool + let courseViewSource: AnalyticsEvent.CourseViewSource + } + + struct ViewModel { + @available(*, deprecated, message: "Target modules can't be initialized w/o model") + let course: Course + @available(*, deprecated, message: "Target modules can't be initialized w/o model") + let isAdaptive: Bool + let courseViewSource: AnalyticsEvent.CourseViewSource + } + } + + /// Present authorization + enum PresentAuthorization { + struct Response {} + + struct ViewModel {} + } + + /// Present error + enum PresentError { + struct Response {} + + struct ViewModel {} + } +} diff --git a/Stepic/Sources/Modules/NewProfileSubmodules/CreatedCourses/NewProfileCreatedCoursesInteractor.swift b/Stepic/Sources/Modules/NewProfileSubmodules/CreatedCourses/NewProfileCreatedCoursesInteractor.swift new file mode 100644 index 0000000000..3e4d0ff638 --- /dev/null +++ b/Stepic/Sources/Modules/NewProfileSubmodules/CreatedCourses/NewProfileCreatedCoursesInteractor.swift @@ -0,0 +1,77 @@ +import Foundation +import PromiseKit + +protocol NewProfileCreatedCoursesInteractorProtocol { + func doCoursesLoad(request: NewProfileCreatedCourses.CoursesLoad.Request) + func doOnlineModeReset(request: NewProfileCreatedCourses.OnlineModeReset.Request) +} + +final class NewProfileCreatedCoursesInteractor: NewProfileCreatedCoursesInteractorProtocol { + weak var moduleOutput: NewProfileCreatedCoursesOutputProtocol? + + private let presenter: NewProfileCreatedCoursesPresenterProtocol + private let networkReachabilityService: NetworkReachabilityServiceProtocol + + private var currentUserID: User.IdType? + + init( + presenter: NewProfileCreatedCoursesPresenterProtocol, + networkReachabilityService: NetworkReachabilityServiceProtocol + ) { + self.presenter = presenter + self.networkReachabilityService = networkReachabilityService + } + + func doCoursesLoad(request: NewProfileCreatedCourses.CoursesLoad.Request) { + if let currentUserID = self.currentUserID { + self.presenter.presentCourses(response: .init(teacherID: currentUserID)) + } + } + + func doOnlineModeReset(request: NewProfileCreatedCourses.OnlineModeReset.Request) { + if self.networkReachabilityService.isReachable { + request.module.setOnlineStatus() + } + } +} + +extension NewProfileCreatedCoursesInteractor: NewProfileSubmoduleProtocol { + func update(with user: User, isCurrentUserProfile: Bool, isOnline: Bool) { + if self.currentUserID != user.id { + self.currentUserID = user.id + self.doCoursesLoad(request: .init()) + } + } +} + +extension NewProfileCreatedCoursesInteractor: CourseListOutputProtocol { + func presentCourseInfo(course: Course, viewSource: AnalyticsEvent.CourseViewSource) { + self.presenter.presentCourseInfo(response: .init(course: course, courseViewSource: viewSource)) + } + + func presentCourseSyllabus(course: Course, viewSource: AnalyticsEvent.CourseViewSource) { + self.presenter.presentCourseSyllabus(response: .init(course: course, courseViewSource: viewSource)) + } + + func presentLastStep(course: Course, isAdaptive: Bool, viewSource: AnalyticsEvent.CourseViewSource) { + self.presenter.presentLastStep( + response: .init(course: course, isAdaptive: isAdaptive, courseViewSource: viewSource) + ) + } + + func presentAuthorization() { + self.presenter.presentAuthorization(response: .init()) + } + + func presentPaidCourseInfo(course: Course) { + self.presentCourseInfo(course: course, viewSource: .profile(id: self.currentUserID.require())) + } + + func presentEmptyState(sourceModule: CourseListInputProtocol) { + self.moduleOutput?.handleCreatedCoursesEmptyState() + } + + func presentError(sourceModule: CourseListInputProtocol) { + self.presenter.presentError(response: .init()) + } +} diff --git a/Stepic/Sources/Modules/NewProfileSubmodules/CreatedCourses/NewProfileCreatedCoursesPresenter.swift b/Stepic/Sources/Modules/NewProfileSubmodules/CreatedCourses/NewProfileCreatedCoursesPresenter.swift new file mode 100644 index 0000000000..b8bdd5af0d --- /dev/null +++ b/Stepic/Sources/Modules/NewProfileSubmodules/CreatedCourses/NewProfileCreatedCoursesPresenter.swift @@ -0,0 +1,48 @@ +import UIKit + +protocol NewProfileCreatedCoursesPresenterProtocol { + func presentCourses(response: NewProfileCreatedCourses.CoursesLoad.Response) + func presentCourseInfo(response: NewProfileCreatedCourses.CourseInfoPresentation.Response) + func presentCourseSyllabus(response: NewProfileCreatedCourses.CourseSyllabusPresentation.Response) + func presentLastStep(response: NewProfileCreatedCourses.LastStepPresentation.Response) + func presentAuthorization(response: NewProfileCreatedCourses.PresentAuthorization.Response) + func presentError(response: NewProfileCreatedCourses.PresentError.Response) +} + +final class NewProfileCreatedCoursesPresenter: NewProfileCreatedCoursesPresenterProtocol { + weak var viewController: NewProfileCreatedCoursesViewControllerProtocol? + + func presentCourses(response: NewProfileCreatedCourses.CoursesLoad.Response) { + self.viewController?.displayCourses(viewModel: .init(teacherID: response.teacherID)) + } + + func presentCourseInfo(response: NewProfileCreatedCourses.CourseInfoPresentation.Response) { + self.viewController?.displayCourseInfo( + viewModel: .init(courseID: response.course.id, courseViewSource: response.courseViewSource) + ) + } + + func presentCourseSyllabus(response: NewProfileCreatedCourses.CourseSyllabusPresentation.Response) { + self.viewController?.displayCourseSyllabus( + viewModel: .init(courseID: response.course.id, courseViewSource: response.courseViewSource) + ) + } + + func presentLastStep(response: NewProfileCreatedCourses.LastStepPresentation.Response) { + self.viewController?.displayLastStep( + viewModel: .init( + course: response.course, + isAdaptive: response.isAdaptive, + courseViewSource: response.courseViewSource + ) + ) + } + + func presentAuthorization(response: NewProfileCreatedCourses.PresentAuthorization.Response) { + self.viewController?.displayAuthorization(viewModel: .init()) + } + + func presentError(response: NewProfileCreatedCourses.PresentError.Response) { + self.viewController?.displayError(viewModel: .init()) + } +} diff --git a/Stepic/Sources/Modules/NewProfileSubmodules/CreatedCourses/NewProfileCreatedCoursesView.swift b/Stepic/Sources/Modules/NewProfileSubmodules/CreatedCourses/NewProfileCreatedCoursesView.swift new file mode 100644 index 0000000000..10dc7c7ba8 --- /dev/null +++ b/Stepic/Sources/Modules/NewProfileSubmodules/CreatedCourses/NewProfileCreatedCoursesView.swift @@ -0,0 +1,38 @@ +import SnapKit +import UIKit + +extension NewProfileCreatedCoursesView { + struct Appearance {} +} + +final class NewProfileCreatedCoursesView: UIView { + let appearance: Appearance + + private var contentView: UIView? + + init( + frame: CGRect = .zero, + appearance: Appearance = Appearance() + ) { + self.appearance = appearance + super.init(frame: frame) + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func attachContentView(_ view: UIView) { + self.contentView?.removeFromSuperview() + + self.addSubview(view) + view.translatesAutoresizingMaskIntoConstraints = false + view.snp.makeConstraints { make in + make.edges.equalToSuperview() + make.width.equalToSuperview() + } + + self.contentView = view + } +} diff --git a/Stepic/Sources/Modules/NewProfileSubmodules/CreatedCourses/NewProfileCreatedCoursesViewController.swift b/Stepic/Sources/Modules/NewProfileSubmodules/CreatedCourses/NewProfileCreatedCoursesViewController.swift new file mode 100644 index 0000000000..5660b67cd6 --- /dev/null +++ b/Stepic/Sources/Modules/NewProfileSubmodules/CreatedCourses/NewProfileCreatedCoursesViewController.swift @@ -0,0 +1,128 @@ +import UIKit + +protocol NewProfileCreatedCoursesViewControllerProtocol: AnyObject { + func displayCourses(viewModel: NewProfileCreatedCourses.CoursesLoad.ViewModel) + func displayCourseInfo(viewModel: NewProfileCreatedCourses.CourseInfoPresentation.ViewModel) + func displayCourseSyllabus(viewModel: NewProfileCreatedCourses.CourseSyllabusPresentation.ViewModel) + func displayLastStep(viewModel: NewProfileCreatedCourses.LastStepPresentation.ViewModel) + func displayAuthorization(viewModel: NewProfileCreatedCourses.PresentAuthorization.ViewModel) + func displayError(viewModel: NewProfileCreatedCourses.PresentError.ViewModel) +} + +final class NewProfileCreatedCoursesViewController: UIViewController, ControllerWithStepikPlaceholder { + private let interactor: NewProfileCreatedCoursesInteractorProtocol + + private var teacherID: User.IdType? + private var submoduleViewController: UIViewController? + + var placeholderContainer = StepikPlaceholderControllerContainer() + + init(interactor: NewProfileCreatedCoursesInteractorProtocol) { + self.interactor = interactor + 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 = NewProfileCreatedCoursesView(frame: UIScreen.main.bounds) + self.view = view + } + + override func viewDidLoad() { + super.viewDidLoad() + + self.registerPlaceholder( + placeholder: StepikPlaceholder( + .noConnection, + action: { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.isPlaceholderShown = false + strongSelf.refreshSubmodule() + } + ), + for: .connectionError + ) + } + + private func refreshSubmodule() { + self.submoduleViewController?.removeFromParent() + + guard let teacherID = self.teacherID else { + return + } + + let courseListAssembly = HorizontalCourseListAssembly( + type: TeacherCourseListType(teacherID: teacherID), + colorMode: .clear, + courseViewSource: .profile(id: teacherID), + output: self.interactor as? CourseListOutputProtocol + ) + let courseListViewController = courseListAssembly.makeModule() + self.addChild(courseListViewController) + + self.submoduleViewController = courseListViewController + + if let profileCreatedCoursesView = self.view as? NewProfileCreatedCoursesView { + profileCreatedCoursesView.attachContentView(courseListViewController.view) + } + + if let moduleInput = courseListAssembly.moduleInput { + self.interactor.doOnlineModeReset(request: .init(module: moduleInput)) + } + } +} + +extension NewProfileCreatedCoursesViewController: NewProfileCreatedCoursesViewControllerProtocol { + func displayCourses(viewModel: NewProfileCreatedCourses.CoursesLoad.ViewModel) { + self.teacherID = viewModel.teacherID + self.refreshSubmodule() + } + + func displayCourseInfo(viewModel: NewProfileCreatedCourses.CourseInfoPresentation.ViewModel) { + let assembly = CourseInfoAssembly( + courseID: viewModel.courseID, + initialTab: .info, + courseViewSource: viewModel.courseViewSource + ) + let viewController = assembly.makeModule() + self.navigationController?.pushViewController(viewController, animated: true) + } + + func displayCourseSyllabus(viewModel: NewProfileCreatedCourses.CourseSyllabusPresentation.ViewModel) { + let assembly = CourseInfoAssembly( + courseID: viewModel.courseID, + initialTab: .syllabus, + courseViewSource: viewModel.courseViewSource + ) + let viewController = assembly.makeModule() + self.navigationController?.pushViewController(viewController, animated: true) + } + + func displayLastStep(viewModel: NewProfileCreatedCourses.LastStepPresentation.ViewModel) { + guard let navigationController = self.navigationController else { + return + } + + LastStepRouter.continueLearning( + for: viewModel.course, + isAdaptive: viewModel.isAdaptive, + using: navigationController, + courseViewSource: viewModel.courseViewSource + ) + } + + func displayAuthorization(viewModel: NewProfileCreatedCourses.PresentAuthorization.ViewModel) { + RoutingManager.auth.routeFrom(controller: self, success: nil, cancel: nil) + } + + func displayError(viewModel: NewProfileCreatedCourses.PresentError.ViewModel) { + self.showPlaceholder(for: .connectionError) + } +} diff --git a/Stepic/en.lproj/Localizable.strings b/Stepic/en.lproj/Localizable.strings index 73fab9aa98..fd50f53d58 100644 --- a/Stepic/en.lproj/Localizable.strings +++ b/Stepic/en.lproj/Localizable.strings @@ -981,6 +981,7 @@ NewProfileRatingCourses1 = "%@ Course"; NewProfileRatingCourses234 = "%@ Courses"; NewProfileRatingCourses567890 = "%@ Courses"; +NewProfileBlockTitleCreatedCourses = "Created Courses"; NewProfileBlockTitleActivity = "Activity"; NewProfileBlockTitleAchievements = "Achievements"; NewProfileBlockTitleCertificates = "Certificates"; diff --git a/Stepic/ru.lproj/Localizable.strings b/Stepic/ru.lproj/Localizable.strings index 44d5d84cc5..4633f5978d 100644 --- a/Stepic/ru.lproj/Localizable.strings +++ b/Stepic/ru.lproj/Localizable.strings @@ -982,6 +982,7 @@ NewProfileRatingCourses1 = "%@ курс"; NewProfileRatingCourses234 = "%@ курса"; NewProfileRatingCourses567890 = "%@ курсов"; +NewProfileBlockTitleCreatedCourses = "Созданные курсы"; NewProfileBlockTitleActivity = "Активность"; NewProfileBlockTitleAchievements = "Достижения"; NewProfileBlockTitleCertificates = "Сертификаты";