diff --git a/Gemfile b/Gemfile index ca8b674679..eeea97b170 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source "https://rubygems.org" ruby "2.6.5" -gem "fastlane", "2.194.0" +gem "fastlane", "2.195.0" gem "cocoapods", "1.11.2" gem "generamba", "1.5.0" diff --git a/Gemfile.lock b/Gemfile.lock index b0cd7f244d..bc67a209c5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,8 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.3) + CFPropertyList (3.0.4) + rexml activesupport (6.1.4.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) @@ -16,8 +17,8 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.502.0) - aws-sdk-core (3.121.0) + aws-partitions (1.507.0) + aws-sdk-core (3.121.1) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.239.0) aws-sigv4 (~> 1.1) @@ -86,7 +87,7 @@ GEM ethon (0.14.0) ffi (>= 1.15.0) excon (0.85.0) - faraday (1.7.2) + faraday (1.8.0) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -111,7 +112,7 @@ GEM faraday_middleware (1.1.0) faraday (~> 1.0) fastimage (2.2.5) - fastlane (2.194.0) + fastlane (2.195.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -178,14 +179,14 @@ GEM google-apis-core (>= 0.4, < 2.a) google-apis-playcustomapp_v1 (0.5.0) google-apis-core (>= 0.4, < 2.a) - google-apis-storage_v1 (0.6.0) + google-apis-storage_v1 (0.8.0) google-apis-core (>= 0.4, < 2.a) google-cloud-core (1.6.0) google-cloud-env (~> 1.0) google-cloud-errors (~> 1.0) google-cloud-env (1.5.0) faraday (>= 0.17.3, < 2.0) - google-cloud-errors (1.1.0) + google-cloud-errors (1.2.0) google-cloud-storage (1.34.1) addressable (~> 2.5) digest-crc (~> 0.4) @@ -194,13 +195,13 @@ GEM google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (0.17.1) + googleauth (1.0.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.15) + signet (>= 0.16, < 2.a) highline (2.0.3) http-cookie (1.0.4) domain_name (~> 0.5) @@ -282,7 +283,7 @@ PLATFORMS DEPENDENCIES cocoapods (= 1.11.2) - fastlane (= 2.194.0) + fastlane (= 2.195.0) fastlane-plugin-firebase_app_distribution generamba (= 1.5.0) diff --git a/Podfile b/Podfile index 5ced95c9cf..6e29464634 100644 --- a/Podfile +++ b/Podfile @@ -13,7 +13,7 @@ project 'Stepic', 'Develop Release' => :release def shared_pods - pod 'Alamofire', '5.4.3' + pod 'Alamofire', '5.4.4' pod 'Atributika', '4.10.1' pod 'SwiftyJSON', '5.0.0' pod 'SDWebImage', '5.11.0' diff --git a/Podfile.lock b/Podfile.lock index a06a969728..47259911ec 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -2,7 +2,7 @@ PODS: - ActionSheetPicker-3.0 (2.7.1) - Agrume (5.6.13): - SwiftyGif - - Alamofire (5.4.3) + - Alamofire (5.4.4) - Amplitude (8.3.1) - AppAuth (1.4.0): - AppAuth/Core (= 1.4.0) @@ -211,7 +211,7 @@ PODS: DEPENDENCIES: - ActionSheetPicker-3.0 (= 2.7.1) - Agrume (= 5.6.13) - - Alamofire (= 5.4.3) + - Alamofire (= 5.4.4) - Amplitude (= 8.3.1) - Atributika (= 4.10.1) - BEMCheckBox (= 1.4.1) @@ -352,7 +352,7 @@ CHECKOUT OPTIONS: SPEC CHECKSUMS: ActionSheetPicker-3.0: 36da254b97a09ff89679ecb8b8510bd3e5bdc773 Agrume: 21b96a1138abc0f890211bfcb12f8b1e3464b4c1 - Alamofire: e447a2774a40c996748296fa2c55112fdbbc42f9 + Alamofire: f3b09a368f1582ab751b3fff5460276e0d2cf5c9 Amplitude: c25a44ad72b84e684fcf67c478c1fb5c95e286a3 AppAuth: 31bcec809a638d7bd2f86ea8a52bd45f6e81e7c7 Atributika: 47e778507cfb3cd2c996278b0285221a62e97d71 @@ -413,6 +413,6 @@ SPEC CHECKSUMS: VK-ios-sdk: 5bcf00a2014a7323f98db9328b603d4f96635caa YandexMobileMetrica: 9e713c16bb6aca0ba63b84c8d7b8b86d32f4ecc4 -PODFILE CHECKSUM: 55ed750956baf820114531709f1e0c7623cd1fe4 +PODFILE CHECKSUM: 05babbcbe89c1b506c8f34035937ce3cf66b692c COCOAPODS: 1.11.2 diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index cbd23b5736..e2f2ea476e 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -755,6 +755,7 @@ 2C7E293726B05680008581F4 /* StepQuizReviewStatusesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7E293626B05680008581F4 /* StepQuizReviewStatusesView.swift */; }; 2C7EFCED24D08CFF003A4E93 /* NewProfileSocialProfilesSkeletonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7EFCEC24D08CFF003A4E93 /* NewProfileSocialProfilesSkeletonView.swift */; }; 2C7EFCEF24D08D80003A4E93 /* NewProfileSocialProfilesSkeletonProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7EFCEE24D08D80003A4E93 /* NewProfileSocialProfilesSkeletonProfileView.swift */; }; + 2C7F13CC26FE1A1400866E4C /* CourseInfoTabNewsBadgesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7F13CB26FE1A1400866E4C /* CourseInfoTabNewsBadgesView.swift */; }; 2C7F4160269484DA00BD5C48 /* CourseInfoTabReviewsSummaryRatingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7F415F269484DA00BD5C48 /* CourseInfoTabReviewsSummaryRatingView.swift */; }; 2C7F416226948CCE00BD5C48 /* CourseInfoTabReviewsSummaryDistributionProgressesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7F416126948CCE00BD5C48 /* CourseInfoTabReviewsSummaryDistributionProgressesView.swift */; }; 2C7F4164269492DF00BD5C48 /* CourseInfoTabReviewsSummaryDistributionCountsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7F4163269492DF00BD5C48 /* CourseInfoTabReviewsSummaryDistributionCountsView.swift */; }; @@ -784,6 +785,7 @@ 2C85EC0925599AD10059EF97 /* CatalogBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C85EC0825599AD10059EF97 /* CatalogBlock.swift */; }; 2C85EC0E25599E9C0059EF97 /* CatalogBlockKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C85EC0D25599E9C0059EF97 /* CatalogBlockKind.swift */; }; 2C85EC162559A57D0059EF97 /* CatalogBlockAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C85EC152559A57D0059EF97 /* CatalogBlockAppearance.swift */; }; + 2C85FCEE26FDE60700BD6BB9 /* CourseInfoTabNewsBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C85FCED26FDE60700BD6BB9 /* CourseInfoTabNewsBadgeView.swift */; }; 2C87A7A12446502900933CA4 /* UsersPersistenceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C87A7A02446502900933CA4 /* UsersPersistenceService.swift */; }; 2C87A7A424465B5900933CA4 /* ProfilesPersistenceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C87A7A324465B5800933CA4 /* ProfilesPersistenceService.swift */; }; 2C87A7A82446646600933CA4 /* UserActivityEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C87A7A72446646600933CA4 /* UserActivityEntity.swift */; }; @@ -1484,6 +1486,7 @@ 62E9888DF0D85074AFE0092C /* StepViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98395E3AF46CA25556A98 /* StepViewController.swift */; }; 62E9889E935597A0ED849B0C /* ContentLanguageSwitchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E986932B6744B0958ABE36 /* ContentLanguageSwitchViewController.swift */; }; 62E988A30D23C70799B8E4C3 /* CourseInfoTabInfoInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98CF68C4ED03FB36FA4F3 /* CourseInfoTabInfoInteractor.swift */; }; + 62E988A3F6BFDF9F0A9777BF /* CourseInfoTabNewsStatisticsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98A19D9B85303250354DA /* CourseInfoTabNewsStatisticsView.swift */; }; 62E988B311FB626588A8615D /* CodeDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98F4FA1EEC6CD7841F0D5 /* CodeDetailsView.swift */; }; 62E988B9C5AE71A1CC86A209 /* LessonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E987AE3DC45621DD9315B4 /* LessonViewController.swift */; }; 62E988C4572F2A9C97348C83 /* CodeQuizView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E9806901565B551EF1942F /* CodeQuizView.swift */; }; @@ -2564,6 +2567,7 @@ 2C4AD01423E301C50049B7B0 /* DiscussionThreadsAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscussionThreadsAPI.swift; sourceTree = ""; }; 2C4AD01823E304140049B7B0 /* DiscussionThreadsNetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscussionThreadsNetworkService.swift; sourceTree = ""; }; 2C4AD01A23E305FA0049B7B0 /* DiscussionThreadsPersistenceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscussionThreadsPersistenceService.swift; sourceTree = ""; }; + 2C4B971E270362D600B3AA8F /* Model_user_course_can_be_reviewed_v90.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_user_course_can_be_reviewed_v90.xcdatamodel; sourceTree = ""; }; 2C4BBF10203DC668000A4250 /* plyr.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = plyr.js; sourceTree = ""; }; 2C4BBF12203DC668000A4250 /* plyr.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = plyr.css; sourceTree = ""; }; 2C4BE7DB221325C000AEAC34 /* Model_course_review_v32.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_course_review_v32.xcdatamodel; sourceTree = ""; }; @@ -2718,6 +2722,7 @@ 2C7E293626B05680008581F4 /* StepQuizReviewStatusesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizReviewStatusesView.swift; sourceTree = ""; }; 2C7EFCEC24D08CFF003A4E93 /* NewProfileSocialProfilesSkeletonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewProfileSocialProfilesSkeletonView.swift; sourceTree = ""; }; 2C7EFCEE24D08D80003A4E93 /* NewProfileSocialProfilesSkeletonProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewProfileSocialProfilesSkeletonProfileView.swift; sourceTree = ""; }; + 2C7F13CB26FE1A1400866E4C /* CourseInfoTabNewsBadgesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseInfoTabNewsBadgesView.swift; sourceTree = ""; }; 2C7F415F269484DA00BD5C48 /* CourseInfoTabReviewsSummaryRatingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseInfoTabReviewsSummaryRatingView.swift; sourceTree = ""; }; 2C7F416126948CCE00BD5C48 /* CourseInfoTabReviewsSummaryDistributionProgressesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseInfoTabReviewsSummaryDistributionProgressesView.swift; sourceTree = ""; }; 2C7F4163269492DF00BD5C48 /* CourseInfoTabReviewsSummaryDistributionCountsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseInfoTabReviewsSummaryDistributionCountsView.swift; sourceTree = ""; }; @@ -2748,6 +2753,7 @@ 2C85EC0825599AD10059EF97 /* CatalogBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogBlock.swift; sourceTree = ""; }; 2C85EC0D25599E9C0059EF97 /* CatalogBlockKind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogBlockKind.swift; sourceTree = ""; }; 2C85EC152559A57D0059EF97 /* CatalogBlockAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogBlockAppearance.swift; sourceTree = ""; }; + 2C85FCED26FDE60700BD6BB9 /* CourseInfoTabNewsBadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseInfoTabNewsBadgeView.swift; sourceTree = ""; }; 2C87A7A02446502900933CA4 /* UsersPersistenceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsersPersistenceService.swift; sourceTree = ""; }; 2C87A7A324465B5800933CA4 /* ProfilesPersistenceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilesPersistenceService.swift; sourceTree = ""; }; 2C87A7A52446635E00933CA4 /* Model_user_activity_v50.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_user_activity_v50.xcdatamodel; sourceTree = ""; }; @@ -3520,6 +3526,7 @@ 62E989FAD86F79364CC2EF89 /* ProgressesNetworkService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressesNetworkService.swift; sourceTree = ""; }; 62E989FEFDC26A706157D1AB /* Analytics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Analytics.swift; sourceTree = ""; }; 62E98A1234FE9DA8BC81202A /* SettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; + 62E98A19D9B85303250354DA /* CourseInfoTabNewsStatisticsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseInfoTabNewsStatisticsView.swift; sourceTree = ""; }; 62E98A1A76183E4780F9343C /* FullscreenCourseListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullscreenCourseListViewController.swift; sourceTree = ""; }; 62E98A1F1D23C325EB6288F9 /* WriteCommentOutputProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WriteCommentOutputProtocol.swift; sourceTree = ""; }; 62E98A2F0861B457FAABF490 /* CourseListDataFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseListDataFlow.swift; sourceTree = ""; }; @@ -4983,6 +4990,15 @@ path = SocialProfiles; sourceTree = ""; }; + 2C7F13CA26FE19F400866E4C /* Badge */ = { + isa = PBXGroup; + children = ( + 2C7F13CB26FE1A1400866E4C /* CourseInfoTabNewsBadgesView.swift */, + 2C85FCED26FDE60700BD6BB9 /* CourseInfoTabNewsBadgeView.swift */, + ); + path = Badge; + sourceTree = ""; + }; 2C7F415E26947F9E00BD5C48 /* Summary */ = { isa = PBXGroup; children = ( @@ -6887,7 +6903,9 @@ isa = PBXGroup; children = ( 2CF68CFF26FA022400EBB023 /* CourseInfoTabNewsCellView.swift */, + 62E98A19D9B85303250354DA /* CourseInfoTabNewsStatisticsView.swift */, 2CF68CFC26FA01F900EBB023 /* CourseInfoTabNewsTableViewCell.swift */, + 2C7F13CA26FE19F400866E4C /* Badge */, ); path = Cell; sourceTree = ""; @@ -10827,6 +10845,7 @@ 2CBC5AF52682437C0000F2D1 /* CourseRevenueTabPurchasesViewModel.swift in Sources */, 2C79F61821873CD9004CC082 /* NotificationsRequestOnlySettingsAlertPresenter.swift in Sources */, 2CCBDBE524AF93B8001A5A83 /* NewProfileDetailsViewModel.swift in Sources */, + 2C7F13CC26FE1A1400866E4C /* CourseInfoTabNewsBadgesView.swift in Sources */, 081B7E2A1BAC208200554153 /* StandardsExtensions.swift in Sources */, 2CA867D225892B050006576E /* GridSimpleCourseListWidgetView.swift in Sources */, 2C3A1F5E24F3B1D300B4070F /* FillBlanksFeedback.swift in Sources */, @@ -11038,6 +11057,7 @@ 2CE9BF4A248D09FC004F6659 /* BlocksPersistenceService.swift in Sources */, 2CCDD80724F8A7D7006644A8 /* ApplicationShortcutService.swift in Sources */, 08F485A51C57AF2E000165AA /* FreeAnswerReply.swift in Sources */, + 2C85FCEE26FDE60700BD6BB9 /* CourseInfoTabNewsBadgeView.swift in Sources */, 2C0A10A7268B279A001D4023 /* CourseBenefitByMonthsAPI.swift in Sources */, 2CCB4B1A26E77CED0056C44E /* AnnouncementsAPI.swift in Sources */, 08C1FC331F41E74500E14B46 /* QuizPresenter.swift in Sources */, @@ -12360,6 +12380,7 @@ 1009EF884D5CAC070717C737 /* CourseInfoTabNewsView.swift in Sources */, D064B6AE4E478249DCE7B7F8 /* CourseInfoTabNewsViewController.swift in Sources */, 2BF2D5AC65ACA8B668CF93A6 /* CourseInfoTabNewsInputProtocol.swift in Sources */, + 62E988A3F6BFDF9F0A9777BF /* CourseInfoTabNewsStatisticsView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -12620,7 +12641,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 375; + CURRENT_PROJECT_VERSION = 378; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Production.plist"; @@ -12645,7 +12666,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 375; + CURRENT_PROJECT_VERSION = 378; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Production.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -12787,7 +12808,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 375; + CURRENT_PROJECT_VERSION = 378; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Production.plist"; @@ -12817,7 +12838,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 375; + CURRENT_PROJECT_VERSION = 378; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; @@ -12908,7 +12929,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 375; + CURRENT_PROJECT_VERSION = 378; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Develop.plist"; @@ -12960,7 +12981,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 375; + CURRENT_PROJECT_VERSION = 378; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Develop.plist"; @@ -13041,7 +13062,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 375; + CURRENT_PROJECT_VERSION = 378; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Develop.plist"; @@ -13089,7 +13110,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 375; + CURRENT_PROJECT_VERSION = 378; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Develop.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -13610,7 +13631,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 375; + CURRENT_PROJECT_VERSION = 378; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Release.plist"; @@ -13664,7 +13685,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 375; + CURRENT_PROJECT_VERSION = 378; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Release.plist"; @@ -13746,7 +13767,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 375; + CURRENT_PROJECT_VERSION = 378; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Release.plist"; @@ -13794,7 +13815,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 375; + CURRENT_PROJECT_VERSION = 378; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Release.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -14268,6 +14289,7 @@ 08D1EF6E1BB5618700BE84E6 /* Model.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + 2C4B971E270362D600B3AA8F /* Model_user_course_can_be_reviewed_v90.xcdatamodel */, 2C55B7D326FA66ED0022B822 /* Model_announcements_v89.xcdatamodel */, 2C1966A626E797B3000D5B06 /* Model_announcements_v88.xcdatamodel */, 2C98423726DE4B9C0098E36B /* Model_search_results_v87.xcdatamodel */, @@ -14359,7 +14381,7 @@ 0802AC531C7222B200C4F3E6 /* Model_v2.xcdatamodel */, 08D1EF6F1BB5618700BE84E6 /* Model.xcdatamodel */, ); - currentVersion = 2C55B7D326FA66ED0022B822 /* Model_announcements_v89.xcdatamodel */; + currentVersion = 2C4B971E270362D600B3AA8F /* Model_user_course_can_be_reviewed_v90.xcdatamodel */; path = Model.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/Stepic.xcodeproj/xcshareddata/xcschemes/Stepic Production.xcscheme b/Stepic.xcodeproj/xcshareddata/xcschemes/Stepic Production.xcscheme index e1234bc6e8..368492f397 100644 --- a/Stepic.xcodeproj/xcshareddata/xcschemes/Stepic Production.xcscheme +++ b/Stepic.xcodeproj/xcshareddata/xcschemes/Stepic Production.xcscheme @@ -129,6 +129,11 @@ + + CFBundlePackageType APPL CFBundleShortVersionString - 1.191-develop + 1.192-develop CFBundleSignature ???? CFBundleURLTypes @@ -62,7 +62,7 @@ CFBundleVersion - 375 + 378 FacebookAppID 171127739724012 FacebookDisplayName diff --git a/Stepic/Info-Production.plist b/Stepic/Info-Production.plist index f2db6d06c9..97a3a98a06 100644 --- a/Stepic/Info-Production.plist +++ b/Stepic/Info-Production.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.191 + 1.192 CFBundleSignature ???? CFBundleURLTypes @@ -62,7 +62,7 @@ CFBundleVersion - 375 + 378 FacebookAppID 171127739724012 FacebookDisplayName diff --git a/Stepic/Info-Release.plist b/Stepic/Info-Release.plist index 29e5a40d7a..71f707961f 100644 --- a/Stepic/Info-Release.plist +++ b/Stepic/Info-Release.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.191-release + 1.192-release CFBundleSignature ???? CFBundleURLTypes @@ -62,7 +62,7 @@ CFBundleVersion - 375 + 378 FacebookAppID 171127739724012 FacebookDisplayName diff --git a/Stepic/Legacy/Analytics/Events/AmplitudeAnalyticsEvents.swift b/Stepic/Legacy/Analytics/Events/AmplitudeAnalyticsEvents.swift index c534579652..4b8a92568b 100644 --- a/Stepic/Legacy/Analytics/Events/AmplitudeAnalyticsEvents.swift +++ b/Stepic/Legacy/Analytics/Events/AmplitudeAnalyticsEvents.swift @@ -170,7 +170,7 @@ extension AnalyticsEvent { ) } - static func сourseContentSearched( + static func courseContentSearched( id: Int, title: String?, query: String, diff --git a/Stepic/Legacy/Analytics/Events/AnalyticsEvents.swift b/Stepic/Legacy/Analytics/Events/AnalyticsEvents.swift index 6d4e284c11..d6291cffbb 100644 --- a/Stepic/Legacy/Analytics/Events/AnalyticsEvents.swift +++ b/Stepic/Legacy/Analytics/Events/AnalyticsEvents.swift @@ -218,16 +218,6 @@ extension AnalyticsEvent { static let videoPlayerOpened = AnalyticsEvent(name: "video_player_opened") - static func videoPlayerDidChangeQuality(quality: String, deviceModel: String) -> AnalyticsEvent { - AnalyticsEvent( - name: "video_quality_changed", - parameters: [ - "quality": quality, - "device": deviceModel - ] - ) - } - // MARK: - Certificates - static func certificateOpened(grade: Int, courseName: String) -> AnalyticsEvent { diff --git a/Stepic/Legacy/Analytics/Events/NotificationAlertsAnalytics.swift b/Stepic/Legacy/Analytics/Events/NotificationAlertsAnalytics.swift index 4891c4b39c..e1c35771c3 100644 --- a/Stepic/Legacy/Analytics/Events/NotificationAlertsAnalytics.swift +++ b/Stepic/Legacy/Analytics/Events/NotificationAlertsAnalytics.swift @@ -36,7 +36,10 @@ struct NotificationAlertsAnalytics { func reportCustomAlertInteractionResult(_ result: InteractionResult) { self.analytics.send( - .requestNotificationsAuthorizationCustomAlertInteracted(source: self.source.description, result: result.rawValue) + .requestNotificationsAuthorizationCustomAlertInteracted( + source: self.source.description, + result: result.rawValue + ) ) } diff --git a/Stepic/Legacy/Controllers/Alerts/RateAppViewController/RateAppViewController.swift b/Stepic/Legacy/Controllers/Alerts/RateAppViewController/RateAppViewController.swift index eeaae82bf1..fca0f36f42 100644 --- a/Stepic/Legacy/Controllers/Alerts/RateAppViewController/RateAppViewController.swift +++ b/Stepic/Legacy/Controllers/Alerts/RateAppViewController/RateAppViewController.swift @@ -33,6 +33,8 @@ final class RateAppViewController: UIViewController { var lessonProgress: String? + private lazy var analytics: Analytics = StepikAnalytics.shared + private var defaultAnalyticsParams: [String: Any] { if let progress = self.lessonProgress { return ["lesson_progress": progress] @@ -142,7 +144,7 @@ final class RateAppViewController: UIViewController { var params = defaultAnalyticsParams params["rating"] = rating - StepikAnalytics.shared.send(.rateAppTapped(parameters: params)) + self.analytics.send(.rateAppTapped(parameters: params)) if rating < 4 { self.buttonState = .email @@ -169,7 +171,7 @@ final class RateAppViewController: UIViewController { } private func showEmail() { - StepikAnalytics.shared.send(.rateAppNegativeStateWriteEmailTapped(parameters: self.defaultAnalyticsParams)) + self.analytics.send(.rateAppNegativeStateWriteEmailTapped(parameters: self.defaultAnalyticsParams)) if !MFMailComposeViewController.canSendMail() { return self.dismiss(animated: true, completion: nil) @@ -190,7 +192,7 @@ final class RateAppViewController: UIViewController { } private func showAppStore() { - StepikAnalytics.shared.send(.rateAppPositiveStateAppStoreTapped(parameters: self.defaultAnalyticsParams)) + self.analytics.send(.rateAppPositiveStateAppStoreTapped(parameters: self.defaultAnalyticsParams)) self.dismiss(animated: true, completion: { SKStoreReviewController.requestReview() }) @@ -207,9 +209,9 @@ final class RateAppViewController: UIViewController { switch self.buttonState! { case .appStore: - StepikAnalytics.shared.send(.rateAppPositiveStateLaterTapped(parameters: self.defaultAnalyticsParams)) + self.analytics.send(.rateAppPositiveStateLaterTapped(parameters: self.defaultAnalyticsParams)) case .email: - StepikAnalytics.shared.send(.rateAppNegativeStateLaterTapped(parameters: self.defaultAnalyticsParams)) + self.analytics.send(.rateAppNegativeStateLaterTapped(parameters: self.defaultAnalyticsParams)) } } @@ -238,9 +240,9 @@ extension RateAppViewController: MFMailComposeViewControllerDelegate { ) { switch result { case .cancelled, .failed, .saved: - StepikAnalytics.shared.send(.rateAppNegativeStateWriteEmailCancelled(parameters: self.defaultAnalyticsParams)) + self.analytics.send(.rateAppNegativeStateWriteEmailCancelled(parameters: self.defaultAnalyticsParams)) case .sent: - StepikAnalytics.shared.send(.rateAppNegativeStateWriteEmailSucceeded(parameters: self.defaultAnalyticsParams)) + self.analytics.send(.rateAppNegativeStateWriteEmailSucceeded(parameters: self.defaultAnalyticsParams)) @unknown default: break } diff --git a/Stepic/Legacy/Controllers/Certificates/CertificatesStoryboard.storyboard b/Stepic/Legacy/Controllers/Certificates/CertificatesStoryboard.storyboard index 3787085977..24917a880f 100644 --- a/Stepic/Legacy/Controllers/Certificates/CertificatesStoryboard.storyboard +++ b/Stepic/Legacy/Controllers/Certificates/CertificatesStoryboard.storyboard @@ -1,9 +1,9 @@ - + - + @@ -33,18 +33,17 @@ - + - + - diff --git a/Stepic/Legacy/Controllers/Certificates/CertificatesViewController.swift b/Stepic/Legacy/Controllers/Certificates/CertificatesViewController.swift index d76a36c967..2fbc3e7413 100644 --- a/Stepic/Legacy/Controllers/Certificates/CertificatesViewController.swift +++ b/Stepic/Legacy/Controllers/Certificates/CertificatesViewController.swift @@ -33,6 +33,7 @@ final class CertificatesLegacyAssembly: Assembly { certificatesPersistenceService: CertificatesPersistenceService(), view: certificatesVC ) + certificatesVC.analytics = StepikAnalytics.shared return certificatesVC } @@ -62,6 +63,8 @@ final class CertificatesViewController: UIViewController, ControllerWithStepikPl var presenter: CertificatesPresenter? var userID: User.IdType? + var analytics: Analytics? + private var certificates: [CertificateViewData] = [] private var showNextPageFooter = false @@ -70,6 +73,10 @@ final class CertificatesViewController: UIViewController, ControllerWithStepikPl tableView.delegate = self tableView.dataSource = self + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0 + } + let isMe = AuthInfo.shared.userId != nil && self.userID == AuthInfo.shared.userId if isMe { self.tableView.emptySetPlaceholder = StepikPlaceholder(.emptyCertificatesMe) { [weak self] in @@ -116,7 +123,7 @@ final class CertificatesViewController: UIViewController, ControllerWithStepikPl override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - StepikAnalytics.shared.send(.certificatesScreenOpened) + self.analytics?.send(.certificatesScreenOpened) } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { @@ -133,7 +140,7 @@ final class CertificatesViewController: UIViewController, ControllerWithStepikPl return } - StepikAnalytics.shared.send( + self.analytics?.send( .shareCertificateTapped(grade: certificate.grade, courseName: certificate.courseName ?? "") ) @@ -229,7 +236,7 @@ extension CertificatesViewController: UITableViewDelegate { return } - StepikAnalytics.shared.send( + self.analytics?.send( .certificateOpened( grade: certificates[indexPath.row].grade, courseName: certificates[indexPath.row].courseName ?? "" diff --git a/Stepic/Legacy/Controllers/Courses/SearchResults/SearchResultsPresenter.swift b/Stepic/Legacy/Controllers/Courses/SearchResults/SearchResultsPresenter.swift index d9da0d5d2f..dfd27335de 100644 --- a/Stepic/Legacy/Controllers/Courses/SearchResults/SearchResultsPresenter.swift +++ b/Stepic/Legacy/Controllers/Courses/SearchResults/SearchResultsPresenter.swift @@ -25,8 +25,11 @@ final class SearchResultsPresenter: SearchResultsModuleInputProtocol { var query: String = "" private var currentCourseListFilterQuery: CourseListFilterQuery? - init(view: SearchResultsView) { + private let analytics: Analytics + + init(view: SearchResultsView, analytics: Analytics) { self.view = view + self.analytics = analytics } func queryChanged(to query: String) { @@ -85,7 +88,7 @@ final class SearchResultsPresenter: SearchResultsModuleInputProtocol { extension SearchResultsPresenter: SearchQueriesViewControllerDelegate { func didSelectSuggestion(suggestion: String, position: Int) { - StepikAnalytics.shared.send( + self.analytics.send( .courseSearched( query: self.query.lowercased(), position: position, @@ -98,5 +101,7 @@ extension SearchResultsPresenter: SearchQueriesViewControllerDelegate { } enum CoursesSearchResultsState { - case waiting, suggestions, courses + case waiting + case suggestions + case courses } diff --git a/Stepic/Legacy/Controllers/Courses/SearchResults/SearchResultsViewController.swift b/Stepic/Legacy/Controllers/Courses/SearchResults/SearchResultsViewController.swift index 5dbf5d2807..10c8c479d8 100644 --- a/Stepic/Legacy/Controllers/Courses/SearchResults/SearchResultsViewController.swift +++ b/Stepic/Legacy/Controllers/Courses/SearchResults/SearchResultsViewController.swift @@ -33,7 +33,7 @@ final class SearchResultsAssembly: Assembly { fatalError("Failed to init module from storyboard") } - controller.presenter = SearchResultsPresenter(view: controller) + controller.presenter = SearchResultsPresenter(view: controller, analytics: StepikAnalytics.shared) controller.presenter?.updateQueryBlock = updateQueryBlock self.moduleInput = controller.presenter diff --git a/Stepic/Legacy/Controllers/Notifications/NotificationsPresenter.swift b/Stepic/Legacy/Controllers/Notifications/NotificationsPresenter.swift index c2fa277c06..7d96758389 100644 --- a/Stepic/Legacy/Controllers/Notifications/NotificationsPresenter.swift +++ b/Stepic/Legacy/Controllers/Notifications/NotificationsPresenter.swift @@ -353,17 +353,26 @@ final class NotificationsPresenter { } func markAllAsRead() { - view?.updateMarkAllAsReadButton(with: .loading) + self.view?.updateMarkAllAsReadButton(with: .loading) + + self.notificationsAPI.markAllAsRead().done { [weak self] _ in + guard let strongSelf = self else { + return + } - notificationsAPI.markAllAsRead().done { _ in Notification.markAllAsRead() - self.analytics.send(.markAllNotificationsAsReadTapped(badgeUnreadCount: self.badgeUnreadCount)) + strongSelf.analytics.send(.markAllNotificationsAsReadTapped(badgeUnreadCount: strongSelf.badgeUnreadCount)) - NotificationCenter.default.post(name: .allNotificationsMarkedAsRead, object: self, userInfo: ["section": self.section]) - self.view?.updateMarkAllAsReadButton(with: .normal) - }.catch { error in + NotificationCenter.default.post( + name: .allNotificationsMarkedAsRead, + object: strongSelf, + userInfo: ["section": strongSelf.section] + ) + + strongSelf.view?.updateMarkAllAsReadButton(with: .normal) + }.catch { [weak self] error in print("notifications: unable to mark all notifications as read, error = \(error)") - self.view?.updateMarkAllAsReadButton(with: .error) + self?.view?.updateMarkAllAsReadButton(with: .error) } } diff --git a/Stepic/Legacy/Controllers/Quizzes/BaseQuiz/QuizPresenter.swift b/Stepic/Legacy/Controllers/Quizzes/BaseQuiz/QuizPresenter.swift index 3efa17eeb2..6b2c6985d2 100644 --- a/Stepic/Legacy/Controllers/Quizzes/BaseQuiz/QuizPresenter.swift +++ b/Stepic/Legacy/Controllers/Quizzes/BaseQuiz/QuizPresenter.swift @@ -23,6 +23,8 @@ final class QuizPresenter { private let urlFactory: StepikURLFactory + private let analytics: Analytics + var state: QuizState = .nothing { didSet { view?.set(state: state) @@ -37,7 +39,8 @@ final class QuizPresenter { submissionsAPI: SubmissionsAPI, attemptsAPI: AttemptsAPI, userActivitiesAPI: UserActivitiesAPI, - urlFactory: StepikURLFactory + urlFactory: StepikURLFactory, + analytics: Analytics ) { self.view = view self.step = step @@ -47,6 +50,7 @@ final class QuizPresenter { self.userActivitiesAPI = userActivitiesAPI self.alwaysCreateNewAttemptOnRefresh = alwaysCreateNewAttemptOnRefresh self.urlFactory = urlFactory + self.analytics = analytics } convenience init( @@ -68,7 +72,8 @@ final class QuizPresenter { submissionsAPI: submissionsAPI, attemptsAPI: attemptsAPI, userActivitiesAPI: userActivitiesAPI, - urlFactory: urlFactory + urlFactory: urlFactory, + analytics: StepikAnalytics.shared ) self.streaksNotificationSuggestionManager = streaksNotificationSuggestionManager } @@ -321,7 +326,7 @@ final class QuizPresenter { private func submit() { //To view!!!!!!!! // submissionPressedBlock?() - StepikAnalytics.shared.send(.submitSubmissionTapped(parameters: self.view?.submissionAnalyticsParams)) + self.analytics.send(.submitSubmissionTapped(parameters: self.view?.submissionAnalyticsParams)) if let reply = self.dataSource?.getReply() { self.view?.showLoading(visible: true) submit(reply: reply, completion: { [weak self] in @@ -348,38 +353,43 @@ final class QuizPresenter { return } - _ = s.submissionsAPI.create(stepName: s.step.block.name, attemptId: id, reply: reply, success: { - [weak self] submission in - guard let strongSelf = self else { - return - } + _ = s.submissionsAPI.create( + stepName: s.step.block.name, + attemptId: id, + reply: reply, + success: { [weak self] submission in + guard let strongSelf = self else { + return + } - AnalyticsUserProperties.shared.incrementSubmissionsCount() - strongSelf.submissionsCount = (strongSelf.submissionsCount ?? 0) + 1 + AnalyticsUserProperties.shared.incrementSubmissionsCount() + strongSelf.submissionsCount = (strongSelf.submissionsCount ?? 0) + 1 - let isAdaptive: Bool? = { - if let course = LastStepGlobalContext.context.course { - return AdaptiveStorageManager().supportedInAdaptiveModeCoursesIDs.contains(course.id) - } - return nil - }() - let codeLanguageName = (reply as? CodeReply)?.languageName - StepikAnalytics.shared.send( - .submissionMade( - stepID: strongSelf.step.id, - submissionID: submission.id, - blockName: strongSelf.step.block.name, - isAdaptive: isAdaptive, - codeLanguageName: codeLanguageName + let isAdaptive: Bool? = { + if let course = LastStepGlobalContext.context.course { + return AdaptiveStorageManager().supportedInAdaptiveModeCoursesIDs.contains(course.id) + } + return nil + }() + let codeLanguageName = (reply as? CodeReply)?.languageName + strongSelf.analytics.send( + .submissionMade( + stepID: strongSelf.step.id, + submissionID: submission.id, + blockName: strongSelf.step.block.name, + isAdaptive: isAdaptive, + codeLanguageName: codeLanguageName + ) ) - ) - strongSelf.submission = submission - strongSelf.checkSubmission(submission.id, time: 0, completion: completion) - }, error: { error in - errorHandler(error) - //TODO: test this - }) + strongSelf.submission = submission + strongSelf.checkSubmission(submission.id, time: 0, completion: completion) + }, + error: { error in + errorHandler(error) + //TODO: test this + } + ) }, error: { [weak self] error in if error == PerformRequestError.noAccessToRefreshToken { self?.view?.logout { @@ -393,7 +403,7 @@ final class QuizPresenter { private func retrySubmission() { view?.showLoading(visible: true) - StepikAnalytics.shared.send(.generateNewAttemptTapped) + self.analytics.send(.generateNewAttemptTapped) self.delegate?.submissionDidRetry() diff --git a/Stepic/Legacy/Controllers/RegistrationSignIn/AuthEmail/EmailAuthViewController.swift b/Stepic/Legacy/Controllers/RegistrationSignIn/AuthEmail/EmailAuthViewController.swift index d9142bd607..a66bd1c896 100644 --- a/Stepic/Legacy/Controllers/RegistrationSignIn/AuthEmail/EmailAuthViewController.swift +++ b/Stepic/Legacy/Controllers/RegistrationSignIn/AuthEmail/EmailAuthViewController.swift @@ -40,6 +40,8 @@ final class EmailAuthViewController: UIViewController { private let urlFactory = StepikURLFactory() + private let analytics: Analytics = StepikAnalytics.shared + @IBOutlet weak var stepikLogoHeightConstraint: NSLayoutConstraint! @IBOutlet var alertLabelHeightConstraint: NSLayoutConstraint! @IBOutlet weak var alertBottomLabelConstraint: NSLayoutConstraint! @@ -114,7 +116,7 @@ final class EmailAuthViewController: UIViewController { @IBAction func onLogInClick(_ sender: Any) { view.endEditing(true) - StepikAnalytics.shared.send(.signInTapped(interactionType: .button)) + self.analytics.send(.signInTapped(interactionType: .button)) let email = emailTextField.text ?? "" let password = passwordTextField.text ?? "" @@ -129,14 +131,14 @@ final class EmailAuthViewController: UIViewController { } @IBAction func onSignInWithSocialClick(_ sender: Any) { - StepikAnalytics.shared.send(.tappedSignInOnEmailAuthScreen) + self.analytics.send(.tappedSignInOnEmailAuthScreen) if let navigationController = self.navigationController as? AuthNavigationViewController { navigationController.route(from: .email(email: nil), to: .social) } } @IBAction func onSignUpClick(_ sender: Any) { - StepikAnalytics.shared.send(.tappedSignUpOnEmailAuthScreen) + self.analytics.send(.tappedSignUpOnEmailAuthScreen) if let navigationController = self.navigationController as? AuthNavigationViewController { navigationController.route(from: .email(email: nil), to: .registration) } @@ -211,7 +213,7 @@ final class EmailAuthViewController: UIViewController { @objc private func textFieldDidChange(_ textField: UITextField) { - StepikAnalytics.shared.send(.loginTextFieldDidChange) + self.analytics.send(.loginTextFieldDidChange) state = .normal @@ -279,7 +281,7 @@ final class EmailAuthViewController: UIViewController { extension EmailAuthViewController: UITextFieldDelegate { func textFieldDidBeginEditing(_ textField: UITextField) { - StepikAnalytics.shared.send(.loginTextFieldTapped) + self.analytics.send(.loginTextFieldTapped) // 24 - default value in app (see AppDelegate), 60 - offset with button IQKeyboardManager.shared.keyboardDistanceFromTextField = textField == passwordTextField ? 60 : 24 } @@ -293,8 +295,8 @@ extension EmailAuthViewController: UITextFieldDelegate { if textField == passwordTextField { passwordTextField.resignFirstResponder() - StepikAnalytics.shared.send(.tappedSignInReturnKeyOnSignInScreen) - StepikAnalytics.shared.send(.signInTapped(interactionType: .ime)) + self.analytics.send(.tappedSignInReturnKeyOnSignInScreen) + self.analytics.send(.signInTapped(interactionType: .ime)) if logInButton.isEnabled { self.onLogInClick(logInButton!) diff --git a/Stepic/Legacy/Controllers/RegistrationSignIn/Registration/RegistrationViewController.swift b/Stepic/Legacy/Controllers/RegistrationSignIn/Registration/RegistrationViewController.swift index 06ab213cf2..121f669435 100644 --- a/Stepic/Legacy/Controllers/RegistrationSignIn/Registration/RegistrationViewController.swift +++ b/Stepic/Legacy/Controllers/RegistrationSignIn/Registration/RegistrationViewController.swift @@ -35,6 +35,8 @@ extension RegistrationViewController: RegistrationView { final class RegistrationViewController: UIViewController { var presenter: RegistrationPresenter? + private let analytics: Analytics = StepikAnalytics.shared + @IBOutlet weak var alertBottomLabelConstraint: NSLayoutConstraint! @IBOutlet var alertLabelHeightConstraint: NSLayoutConstraint! @IBOutlet weak var stepikLogoHeightConstraint: NSLayoutConstraint! @@ -111,7 +113,7 @@ final class RegistrationViewController: UIViewController { @IBAction func onRegisterClick(_ sender: Any) { view.endEditing(true) - StepikAnalytics.shared.send(.signUpTapped(interactionType: .button)) + self.analytics.send(.signUpTapped(interactionType: .button)) let name = nameTextField.text ?? "" let email = emailTextField.text ?? "" @@ -173,7 +175,7 @@ final class RegistrationViewController: UIViewController { @objc private func textFieldDidChange(_ textField: UITextField) { - StepikAnalytics.shared.send(.registrationTextFieldDidChange) + self.analytics.send(.registrationTextFieldDidChange) state = .normal @@ -275,7 +277,7 @@ extension RegistrationViewController: TTTAttributedLabelDelegate { extension RegistrationViewController: UITextFieldDelegate { func textFieldDidBeginEditing(_ textField: UITextField) { - StepikAnalytics.shared.send(.registrationTextFieldTapped) + self.analytics.send(.registrationTextFieldTapped) // 24 - default value in app (see AppDelegate), 64 - offset with button IQKeyboardManager.shared.keyboardDistanceFromTextField = textField == passwordTextField ? 64 : 24 } @@ -294,8 +296,8 @@ extension RegistrationViewController: UITextFieldDelegate { if textField == passwordTextField { passwordTextField.resignFirstResponder() - StepikAnalytics.shared.send(.tappedRegistrationSendIme) - StepikAnalytics.shared.send(.signUpTapped(interactionType: .ime)) + self.analytics.send(.tappedRegistrationSendIme) + self.analytics.send(.signUpTapped(interactionType: .ime)) if registerButton.isEnabled { self.onRegisterClick(registerButton!) diff --git a/Stepic/Legacy/Controllers/VideoPlayer/StepikVideoPlayerViewController/StepikVideoPlayerViewController.swift b/Stepic/Legacy/Controllers/VideoPlayer/StepikVideoPlayerViewController/StepikVideoPlayerViewController.swift index 332c3f9637..bb0b694213 100644 --- a/Stepic/Legacy/Controllers/VideoPlayer/StepikVideoPlayerViewController/StepikVideoPlayerViewController.swift +++ b/Stepic/Legacy/Controllers/VideoPlayer/StepikVideoPlayerViewController/StepikVideoPlayerViewController.swift @@ -700,11 +700,7 @@ final class StepikVideoPlayerViewController: UIViewController { } strongSelf.analytics.send( - .videoPlayerQualityChanged(source: strongSelf.currentVideoQuality, target: url.quality), - .videoPlayerDidChangeQuality( - quality: url.quality, - deviceModel: DeviceInfo.current.deviceModelString - ) + .videoPlayerQualityChanged(source: strongSelf.currentVideoQuality, target: url.quality) ) strongSelf.currentVideoQuality = url.quality @@ -728,11 +724,7 @@ final class StepikVideoPlayerViewController: UIViewController { } strongSelf.analytics.send( - .videoPlayerQualityChanged(source: strongSelf.currentVideoQuality, target: cachedQuality), - .videoPlayerDidChangeQuality( - quality: cachedQuality, - deviceModel: DeviceInfo.current.deviceModelString - ) + .videoPlayerQualityChanged(source: strongSelf.currentVideoQuality, target: cachedQuality) ) strongSelf.currentVideoQuality = cachedQuality diff --git a/Stepic/Legacy/Model/Entities/User/User+CoreDataProperties.swift b/Stepic/Legacy/Model/Entities/User/User+CoreDataProperties.swift index 8adbb4b28e..4f25b6905e 100644 --- a/Stepic/Legacy/Model/Entities/User/User+CoreDataProperties.swift +++ b/Stepic/Legacy/Model/Entities/User/User+CoreDataProperties.swift @@ -139,6 +139,11 @@ extension User { } } + var shortName: String { + let firstName = self.firstName.trimmed() + return firstName.isEmpty ? "User" : firstName + } + var fullName: String { "\(self.firstName) \(self.lastName)".trimmed() } diff --git a/Stepic/Legacy/Model/Entities/UserCourse/UserCourse+CoreDataProperties.swift b/Stepic/Legacy/Model/Entities/UserCourse/UserCourse+CoreDataProperties.swift index 502546badd..5347f44a8e 100644 --- a/Stepic/Legacy/Model/Entities/UserCourse/UserCourse+CoreDataProperties.swift +++ b/Stepic/Legacy/Model/Entities/UserCourse/UserCourse+CoreDataProperties.swift @@ -8,6 +8,7 @@ extension UserCourse { @NSManaged var managedIsPinned: NSNumber? @NSManaged var managedIsArchived: NSNumber? @NSManaged var managedLastViewed: Date? + @NSManaged var managedCanBeReviewed: NSNumber @NSManaged var managedUser: User? @NSManaged var managedCourse: Course? @@ -75,6 +76,15 @@ extension UserCourse { } } + var canBeReviewed: Bool { + get { + self.managedCanBeReviewed.boolValue + } + set { + self.managedCanBeReviewed = NSNumber(value: newValue) + } + } + var course: Course? { get { self.managedCourse diff --git a/Stepic/Legacy/Model/Entities/UserCourse/UserCourse.swift b/Stepic/Legacy/Model/Entities/UserCourse/UserCourse.swift index e41770c814..16468b634e 100644 --- a/Stepic/Legacy/Model/Entities/UserCourse/UserCourse.swift +++ b/Stepic/Legacy/Model/Entities/UserCourse/UserCourse.swift @@ -31,6 +31,7 @@ final class UserCourse: NSManagedObject, ManagedObject, IDFetchable { self.isFavorite = json[JSONKey.isFavorite.rawValue].boolValue self.isArchived = json[JSONKey.isArchived.rawValue].boolValue self.lastViewed = Parser.dateFromTimedateJSON(json[JSONKey.lastViewed.rawValue]) ?? Date() + self.canBeReviewed = json[JSONKey.canBeReviewed.rawValue].boolValue } func hasEqualId(json: JSON) -> Bool { @@ -57,6 +58,7 @@ final class UserCourse: NSManagedObject, ManagedObject, IDFetchable { case isFavorite = "is_favorite" case isArchived = "is_archived" case lastViewed = "last_viewed" + case canBeReviewed = "can_be_reviewed" } } diff --git a/Stepic/Legacy/Model/Model.xcdatamodeld/.xccurrentversion b/Stepic/Legacy/Model/Model.xcdatamodeld/.xccurrentversion index 4566737f1f..3ea438f5c2 100644 --- a/Stepic/Legacy/Model/Model.xcdatamodeld/.xccurrentversion +++ b/Stepic/Legacy/Model/Model.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - Model_announcements_v89.xcdatamodel + Model_user_course_can_be_reviewed_v90.xcdatamodel diff --git a/Stepic/Legacy/Model/Model.xcdatamodeld/Model_user_course_can_be_reviewed_v90.xcdatamodel/contents b/Stepic/Legacy/Model/Model.xcdatamodeld/Model_user_course_can_be_reviewed_v90.xcdatamodel/contents new file mode 100644 index 0000000000..5ec352d817 --- /dev/null +++ b/Stepic/Legacy/Model/Model.xcdatamodeld/Model_user_course_can_be_reviewed_v90.xcdatamodel/contents @@ -0,0 +1,636 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 049285e7e7..83dfb4d16b 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/UserCoursesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/UserCoursesAPI.swift @@ -26,7 +26,9 @@ final class UserCoursesAPI: APIEndpoint { func retrieve( page: Int = 1, isArchived: Bool? = nil, - isFavorite: Bool? = nil + isFavorite: Bool? = nil, + canBeReviewed: Bool? = nil, + isDraft: Bool? = nil ) -> Promise<([UserCourse], Meta)> { Promise { seal in var params = Parameters() @@ -38,6 +40,12 @@ final class UserCoursesAPI: APIEndpoint { if let isFavorite = isFavorite { params[UserCourse.JSONKey.isFavorite.rawValue] = String(isFavorite) } + if let canBeReviewed = canBeReviewed { + params[UserCourse.JSONKey.canBeReviewed.rawValue] = String(canBeReviewed) + } + if let isDraft = isDraft { + params["is_draft"] = String(isDraft) + } firstly { () -> Guarantee<[UserCourse]> in self.userCoursesPersistenceService.fetchAll() diff --git a/Stepic/Legacy/Model/PlainObjects/Announcements/AnnouncementPlainObject.swift b/Stepic/Legacy/Model/PlainObjects/Announcements/AnnouncementPlainObject.swift index 4d3d2554ee..2ea7952b2e 100644 --- a/Stepic/Legacy/Model/PlainObjects/Announcements/AnnouncementPlainObject.swift +++ b/Stepic/Legacy/Model/PlainObjects/Announcements/AnnouncementPlainObject.swift @@ -37,6 +37,17 @@ struct AnnouncementPlainObject: JSONSerializable { let estimatedStartDate: Date? let estimatedFinishDate: Date? let noticeDates: [Date] + + var isOneTimeEvent: Bool { !self.isInfinite && !self.onEnroll } + + var isActiveEvent: Bool { + self.onEnroll + || (self.isInfinite && (self.startDate == nil || self.startDate.require() < Date())) + } + + var displayedStartDate: Date? { self.estimatedStartDate ?? self.startDate } + + var displayedFinishDate: Date? { self.estimatedFinishDate ?? self.sentDate } } extension AnnouncementPlainObject { diff --git a/Stepic/Legacy/Model/PlainObjects/Announcements/AnnouncementStatus.swift b/Stepic/Legacy/Model/PlainObjects/Announcements/AnnouncementStatus.swift index 0f1e8752ac..909bac345d 100644 --- a/Stepic/Legacy/Model/PlainObjects/Announcements/AnnouncementStatus.swift +++ b/Stepic/Legacy/Model/PlainObjects/Announcements/AnnouncementStatus.swift @@ -13,24 +13,4 @@ enum AnnouncementStatus: String { let order: [AnnouncementStatus] = [.composing, .queueing, .queued, .sending, .scheduled, .sent, .aborted] return order.firstIndex(of: self) ?? 0 } - - var isReady: Bool { - let statuses: [AnnouncementStatus] = [.queueing, .queued, .sending, .sending] - return statuses.contains(self) - } - - var isInProgress: Bool { - let statuses: [AnnouncementStatus] = [.queueing, .queued, .sending] - return statuses.contains(self) - } - - var isQueuedOrSending: Bool { - let statuses: [AnnouncementStatus] = [.queued, .sending] - return statuses.contains(self) - } - - var isStopped: Bool { - let statuses: [AnnouncementStatus] = [.scheduled, .sent, .aborted] - return statuses.contains(self) - } } diff --git a/Stepic/Legacy/Model/Transformers/CatalogBlockContentValueTransformer.swift b/Stepic/Legacy/Model/Transformers/CatalogBlockContentValueTransformer.swift index adb89e6925..5b45d9eb2b 100644 --- a/Stepic/Legacy/Model/Transformers/CatalogBlockContentValueTransformer.swift +++ b/Stepic/Legacy/Model/Transformers/CatalogBlockContentValueTransformer.swift @@ -6,7 +6,7 @@ final class CatalogBlockContentValueTransformer: NSSecureUnarchiveFromDataTransf static let name = NSValueTransformerName(rawValue: String(describing: CatalogBlockContentValueTransformer.self)) override static var allowedTopLevelClasses: [AnyClass] { - [NSArray.self, CatalogBlockContentItem.self, NSDate.self] + [NSArray.self, CatalogBlockContentItem.self, NSDate.self, NSNumber.self, NSString.self] } static func register() { diff --git a/Stepic/Legacy/Model/Transformers/DatasetValueTransformer.swift b/Stepic/Legacy/Model/Transformers/DatasetValueTransformer.swift index d93ccec895..84db30d150 100644 --- a/Stepic/Legacy/Model/Transformers/DatasetValueTransformer.swift +++ b/Stepic/Legacy/Model/Transformers/DatasetValueTransformer.swift @@ -6,7 +6,7 @@ final class DatasetValueTransformer: NSSecureUnarchiveFromDataTransformer { static let name = NSValueTransformerName(rawValue: String(describing: DatasetValueTransformer.self)) override static var allowedTopLevelClasses: [AnyClass] { - [Dataset.self, FillBlanksComponent.self, NSArray.self] + [Dataset.self, FillBlanksComponent.self, NSArray.self, NSString.self] } static func register() { diff --git a/Stepic/Legacy/Model/Transformers/ReplyValueTransformer.swift b/Stepic/Legacy/Model/Transformers/ReplyValueTransformer.swift index e25902c1a8..8b9c13a172 100644 --- a/Stepic/Legacy/Model/Transformers/ReplyValueTransformer.swift +++ b/Stepic/Legacy/Model/Transformers/ReplyValueTransformer.swift @@ -6,7 +6,7 @@ final class ReplyValueTransformer: NSSecureUnarchiveFromDataTransformer { static let name = NSValueTransformerName(rawValue: String(describing: ReplyValueTransformer.self)) override static var allowedTopLevelClasses: [AnyClass] { - [Reply.self, NSArray.self, TableReplyChoice.self, TableReplyChoice.Column.self] + [Reply.self, NSArray.self, TableReplyChoice.self, TableReplyChoice.Column.self, NSNumber.self, NSString.self] } static func register() { diff --git a/Stepic/Legacy/Model/Transformers/SubmissionFeedbackValueTransformer.swift b/Stepic/Legacy/Model/Transformers/SubmissionFeedbackValueTransformer.swift index 9d6f8dd07a..25b875a8b6 100644 --- a/Stepic/Legacy/Model/Transformers/SubmissionFeedbackValueTransformer.swift +++ b/Stepic/Legacy/Model/Transformers/SubmissionFeedbackValueTransformer.swift @@ -6,7 +6,7 @@ final class SubmissionFeedbackValueTransformer: NSSecureUnarchiveFromDataTransfo static let name = NSValueTransformerName(rawValue: String(describing: SubmissionFeedbackValueTransformer.self)) override static var allowedTopLevelClasses: [AnyClass] { - [SubmissionFeedback.self, NSArray.self] + [SubmissionFeedback.self, NSArray.self, NSString.self] } static func register() { diff --git a/Stepic/Sources/Controllers/ReviewsAndWishlistContainerViewController.swift b/Stepic/Sources/Controllers/ReviewsAndWishlistContainerViewController.swift index 6c36285db1..41e1834f0d 100644 --- a/Stepic/Sources/Controllers/ReviewsAndWishlistContainerViewController.swift +++ b/Stepic/Sources/Controllers/ReviewsAndWishlistContainerViewController.swift @@ -10,9 +10,13 @@ extension ReviewsAndWishlistContainerViewController { } final class ReviewsAndWishlistContainerViewController: UIViewController { + private static let refreshDebounceInterval: TimeInterval = 1 + private let userCoursesReviewsWidgetAssembly: UserCoursesReviewsWidgetAssembly private let wishlistWidgetAssembly: WishlistWidgetAssembly + private let refreshDebouncer = Debouncer(delay: ReviewsAndWishlistContainerViewController.refreshDebounceInterval) + private lazy var stackView: UIStackView = { let stackView = UIStackView() stackView.axis = .horizontal @@ -40,8 +44,14 @@ final class ReviewsAndWishlistContainerViewController: UIViewController { // MARK: Public API func refreshSubmodules() { - self.userCoursesReviewsWidgetAssembly.moduleInput?.refreshReviews() - self.wishlistWidgetAssembly.moduleInput?.refreshWishlist() + self.refreshDebouncer.action = { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.userCoursesReviewsWidgetAssembly.moduleInput?.refreshReviews() + strongSelf.wishlistWidgetAssembly.moduleInput?.refreshWishlist() + } } // MARK: Private API diff --git a/Stepic/Sources/Controllers/StyledTabBarViewController.swift b/Stepic/Sources/Controllers/StyledTabBarViewController.swift index fb3abf8443..5303d28845 100644 --- a/Stepic/Sources/Controllers/StyledTabBarViewController.swift +++ b/Stepic/Sources/Controllers/StyledTabBarViewController.swift @@ -46,6 +46,14 @@ final class StyledTabBarViewController: UITabBarController { self.tabBar.unselectedItemTintColor = Appearance.unselectedItemTintColor self.tabBar.isTranslucent = false + if #available(iOS 15.0, *) { + let appearance = UITabBarAppearance() + appearance.configureWithOpaqueBackground() + appearance.backgroundColor = Appearance.barTintColor + self.tabBar.standardAppearance = appearance + self.tabBar.scrollEdgeAppearance = self.tabBar.standardAppearance + } + let tabBarViewControllers = self.items.map { tabBarItem -> UIViewController in let viewController = tabBarItem.controller viewController.tabBarItem = tabBarItem.makeTabBarItem() diff --git a/Stepic/Sources/Frameworks/Analytics/Analytics.swift b/Stepic/Sources/Frameworks/Analytics/Analytics.swift index e0277d0ae9..3918e474a8 100644 --- a/Stepic/Sources/Frameworks/Analytics/Analytics.swift +++ b/Stepic/Sources/Frameworks/Analytics/Analytics.swift @@ -37,7 +37,7 @@ final class StepikAnalytics: Analytics { } func send(_ event: AnalyticsEvent, forceSend: Bool) { - // Sends Amplitude events to Amplitude backend and also mirrors these events to AppMetrica. + // Sends Amplitude events to Amplitude backend and also mirrors these events to AppMetrica & FirebaseAnalytics. // Otherwise, sends the current event to AppMetrica and FirebaseAnalytics backends. if event is AmplitudeAnalyticsEvent { self.amplitudeAnalyticsEngine.sendAnalyticsEvent( @@ -50,6 +50,11 @@ final class StepikAnalytics: Analytics { parameters: event.parameters, forceSend: forceSend ) + self.firebaseAnalyticsEngine.sendAnalyticsEvent( + named: event.name, + parameters: event.parameters, + forceSend: forceSend + ) } else if event is StepikAnalyticsEvent { self.stepikAnalyticsEngine.sendAnalyticsEvent( named: event.name, diff --git a/Stepic/Sources/Frameworks/Analytics/AnalyticsEngine.swift b/Stepic/Sources/Frameworks/Analytics/AnalyticsEngine.swift index 42fae01353..19e1e3ef41 100644 --- a/Stepic/Sources/Frameworks/Analytics/AnalyticsEngine.swift +++ b/Stepic/Sources/Frameworks/Analytics/AnalyticsEngine.swift @@ -37,6 +37,10 @@ final class FirebaseAnalyticsEngine: AnalyticsEngine { init() {} func sendAnalyticsEvent(named name: String, parameters: [String: Any]?, forceSend: Bool) { + guard name != "Course card seen" else { + return + } + let processedName = self.processString(name) let processedParameters: [String: Any]? = { guard let parameters = parameters else { diff --git a/Stepic/Sources/Frameworks/ContentProcessor/Processing/ContentProcessingRule.swift b/Stepic/Sources/Frameworks/ContentProcessor/Processing/ContentProcessingRule.swift index 162c66181c..ddcb8357da 100644 --- a/Stepic/Sources/Frameworks/ContentProcessor/Processing/ContentProcessingRule.swift +++ b/Stepic/Sources/Frameworks/ContentProcessor/Processing/ContentProcessingRule.swift @@ -173,15 +173,25 @@ final class AlwaysOpenedDetailsDisclosureBoxRule: ContentProcessingRule { } final class ReplaceTemplateUsernameRule: ContentProcessingRule { - private let username: String + private let shortName: String + private let fullName: String - init(username: String) { - self.username = username + init(shortName: String, fullName: String) { + self.shortName = shortName + self.fullName = fullName } func process(content: String) -> String { - content - .replacingOccurrences(of: "{{ user_name }}", with: self.username) - .replacingOccurrences(of: "{{user_name}}", with: self.username) + guard let shortRegex = try? Regex(string: "\\{\\{\\s*user_name\\s*\\}\\}", options: [.ignoreCase]), + let fullRegex = try? Regex(string: "\\{\\{\\s*user_full_name\\s*\\}\\}", options: [.ignoreCase]) else { + return content + } + + var content = content + + content.replaceAll(matching: shortRegex, with: self.shortName) + content.replaceAll(matching: fullRegex, with: self.fullName) + + return content } } diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabInfo/Views/Blocks/CourseInfoTabInfoInstructorsBlockView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabInfo/Views/Blocks/CourseInfoTabInfoInstructorsBlockView.swift index 1820918346..0a4a49e027 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabInfo/Views/Blocks/CourseInfoTabInfoInstructorsBlockView.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabInfo/Views/Blocks/CourseInfoTabInfoInstructorsBlockView.swift @@ -45,9 +45,7 @@ final class CourseInfoTabInfoInstructorsBlockView: UIView { self.headerView.icon = CourseInfoTabInfoView.Block.instructors.icon self.headerView.title = CourseInfoTabInfoView.Block.instructors.title - if !self.stackView.arrangedSubviews.isEmpty { - self.stackView.removeAllArrangedSubviews() - } + self.stackView.removeAllArrangedSubviews() instructors.forEach { instructor in let view = CourseInfoTabInfoInstructorView() diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/CourseInfoTabNewsPresenter.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/CourseInfoTabNewsPresenter.swift index 58c54f6a40..de8edb8a25 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/CourseInfoTabNewsPresenter.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/CourseInfoTabNewsPresenter.swift @@ -12,7 +12,11 @@ final class CourseInfoTabNewsPresenter: CourseInfoTabNewsPresenterProtocol { func presentCourseNews(response: CourseInfoTabNews.NewsLoad.Response) { switch response.result { case .success(let data): - self.makeNewsViewModels(data.announcements, currentUser: data.currentUser).done { viewModels in + self.makeNewsViewModels( + data.announcements, + course: data.course, + currentUser: data.currentUser + ).done { viewModels in let data = CourseInfoTabNews.NewsResultData(news: viewModels, hasNextPage: data.hasNextPage) self.viewController?.displayCourseNews(viewModel: .init(state: .result(data: data))) } @@ -24,7 +28,11 @@ final class CourseInfoTabNewsPresenter: CourseInfoTabNewsPresenterProtocol { func presentNextCourseNews(response: CourseInfoTabNews.NextNewsLoad.Response) { switch response.result { case .success(let data): - self.makeNewsViewModels(data.announcements, currentUser: data.currentUser).done { viewModels in + self.makeNewsViewModels( + data.announcements, + course: data.course, + currentUser: data.currentUser + ).done { viewModels in let data = CourseInfoTabNews.NewsResultData(news: viewModels, hasNextPage: data.hasNextPage) self.viewController?.displayNextCourseNews(viewModel: .init(state: .result(data: data))) } @@ -37,12 +45,15 @@ final class CourseInfoTabNewsPresenter: CourseInfoTabNewsPresenterProtocol { private func makeNewsViewModels( _ announcements: [AnnouncementPlainObject], + course: Course, currentUser: User? ) -> Guarantee<[CourseInfoTabNewsViewModel]> { Guarantee { seal in DispatchQueue.global(qos: .userInitiated).async { let contentProcessor = self.makeContentProcessor(currentUser: currentUser) - let viewModels = announcements.map { self.makeViewModel($0, contentProcessor: contentProcessor) } + let viewModels = announcements.map { + self.makeViewModel($0, course: course, contentProcessor: contentProcessor) + } DispatchQueue.main.async { seal(viewModels) @@ -53,24 +64,156 @@ final class CourseInfoTabNewsPresenter: CourseInfoTabNewsPresenterProtocol { private func makeViewModel( _ announcement: AnnouncementPlainObject, + course: Course, contentProcessor: ContentProcessor ) -> CourseInfoTabNewsViewModel { - let formattedDate = FormatterHelper.dateStringWithFullMonthAndYear(announcement.sentDate ?? Date()) - + let formattedDate = self.makeDateRepresentation(announcement: announcement, course: course) let processedContent = contentProcessor.processContent(announcement.text) + let badgeViewModel = self.makeBadgeViewModel(announcement: announcement, course: course) + let statisticsViewModel = self.makeStatisticsViewModel(announcement: announcement, course: course) return CourseInfoTabNewsViewModel( uniqueIdentifier: "\(announcement.id)", formattedDate: formattedDate, subject: announcement.subject.trimmed(), - processedContent: processedContent + processedContent: processedContent, + badge: badgeViewModel, + statistics: statisticsViewModel + ) + } + + private func makeDateRepresentation(announcement: AnnouncementPlainObject, course: Course) -> String { + let defaultDate = (announcement.sentDate ?? announcement.createDate) ?? Date() + + guard let status = announcement.status else { + return FormatterHelper.dateStringWithFullMonthAndYear(defaultDate) + } + + if !course.canCreateAnnouncements { + if announcement.isActiveEvent, let noticeDate = announcement.noticeDates.first { + return FormatterHelper.dateStringWithFullMonthAndYear(noticeDate) + } + } else { + switch status { + case .composing: + if let displayedStartDate = announcement.displayedStartDate { + let formattedStartDate = FormatterHelper.dateStringWithFullMonthAndYear(displayedStartDate) + if announcement.isActiveEvent { + return String( + format: NSLocalizedString("CourseInfoTabNewsDateOnEventComposing", comment: ""), + arguments: [formattedStartDate] + ) + } else { + return formattedStartDate + } + } + case .scheduled: + if let displayedStartDate = announcement.displayedStartDate { + let formattedStartDate = FormatterHelper.dateStringWithFullMonthAndYear(displayedStartDate) + if announcement.isActiveEvent { + return String( + format: NSLocalizedString("CourseInfoTabNewsDateOnEventSending", comment: ""), + arguments: [formattedStartDate] + ) + } else { + return String( + format: NSLocalizedString("CourseInfoTabNewsDateOneTimeScheduled", comment: ""), + arguments: [FormatterHelper.dateStringWithFullMonthAndYear(displayedStartDate)] + ) + } + } + case .queueing, .queued, .sending: + if let displayedStartDate = announcement.displayedStartDate { + let formattedStartDate = FormatterHelper.dateStringWithFullMonthAndYear(displayedStartDate) + if announcement.isActiveEvent { + return String( + format: NSLocalizedString("CourseInfoTabNewsDateOnEventSending", comment: ""), + arguments: [formattedStartDate] + ) + } else { + return String( + format: NSLocalizedString("CourseInfoTabNewsDateOneTimeSending", comment: ""), + arguments: [formattedStartDate] + ) + } + } + case .sent, .aborted: + if let displayedStartDate = announcement.displayedStartDate { + let formattedStartDate = FormatterHelper.dateStringWithFullMonthAndYear(displayedStartDate) + if announcement.isActiveEvent { + if let displayedFinishDate = announcement.displayedFinishDate { + return String( + format: NSLocalizedString("CourseInfoTabNewsDateOnEventSent", comment: ""), + arguments: [ + formattedStartDate, + FormatterHelper.dateStringWithFullMonthAndYear(displayedFinishDate) + ] + ) + } + } else { + return formattedStartDate + } + } + } + } + + return FormatterHelper.dateStringWithFullMonthAndYear(defaultDate) + } + + private func makeBadgeViewModel( + announcement: AnnouncementPlainObject, + course: Course + ) -> CourseInfoTabNewsBadgeViewModel? { + guard course.canCreateAnnouncements, + let status = announcement.status else { + return nil + } + + return CourseInfoTabNewsBadgeViewModel( + status: status, + isOneTimeEvent: announcement.isOneTimeEvent, + isActiveEvent: announcement.isActiveEvent + ) + } + + private func makeStatisticsViewModel( + announcement: AnnouncementPlainObject, + course: Course + ) -> CourseInfoTabNewsStatisticsViewModel? { + guard course.canCreateAnnouncements, + let status = announcement.status else { + return nil + } + + switch status { + case .composing: + return nil + case .scheduled: + if !announcement.isActiveEvent { + return nil + } + case .queueing, .queued, .sending, .sent, .aborted: + break + } + + return CourseInfoTabNewsStatisticsViewModel( + publishCount: announcement.publishCount ?? 0, + queueCount: announcement.queueCount ?? 0, + sentCount: announcement.sentCount ?? 0, + openCount: announcement.openCount ?? 0, + clickCount: announcement.clickCount ?? 0 ) } private func makeContentProcessor(currentUser: User?) -> ContentProcessor { var rules = ContentProcessor.defaultRules if let currentUser = currentUser, !currentUser.fullName.isEmpty { - rules.append(ReplaceTemplateUsernameRule(username: currentUser.fullName)) + rules.append( + ReplaceTemplateUsernameRule( + shortName: currentUser.shortName, + fullName: FormatterHelper.username(currentUser) + ) + ) } var injections = ContentProcessor.defaultInjections diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/CourseInfoTabNewsViewModel.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/CourseInfoTabNewsViewModel.swift index 8ffc414cd3..10b16cae26 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/CourseInfoTabNewsViewModel.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/CourseInfoTabNewsViewModel.swift @@ -6,4 +6,21 @@ struct CourseInfoTabNewsViewModel: UniqueIdentifiable { let formattedDate: String let subject: String let processedContent: ProcessedContent + + var badge: CourseInfoTabNewsBadgeViewModel? + var statistics: CourseInfoTabNewsStatisticsViewModel? +} + +struct CourseInfoTabNewsBadgeViewModel { + let status: AnnouncementStatus + let isOneTimeEvent: Bool + let isActiveEvent: Bool +} + +struct CourseInfoTabNewsStatisticsViewModel { + let publishCount: Int + let queueCount: Int + let sentCount: Int + let openCount: Int + let clickCount: Int } diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/Views/Cell/Badge/CourseInfoTabNewsBadgeView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/Views/Cell/Badge/CourseInfoTabNewsBadgeView.swift new file mode 100644 index 0000000000..e5fbdff2ba --- /dev/null +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/Views/Cell/Badge/CourseInfoTabNewsBadgeView.swift @@ -0,0 +1,180 @@ +import SnapKit +import UIKit + +extension CourseInfoTabNewsBadgeView { + struct Appearance { + let iconSize = CGSize(width: 12, height: 12) + + let textFont = Typography.caption2Font + + let stackViewSpacing: CGFloat = 4 + let stackViewInsets = LayoutInsets(top: 4, left: 8, bottom: 4, right: 8) + } +} + +final class CourseInfoTabNewsBadgeView: UIView { + let appearance: Appearance + + private lazy var iconImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + return imageView + }() + + private lazy var textLabel: UILabel = { + let label = UILabel() + label.font = self.appearance.textFont + label.textAlignment = .left + label.numberOfLines = 1 + return label + }() + + private lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.distribution = .fill + stackView.alignment = .center + stackView.spacing = self.appearance.stackViewSpacing + return stackView + }() + + override var intrinsicContentSize: CGSize { + let textLabelIntrinsicContentSize = self.textLabel.intrinsicContentSize + + let width = self.appearance.stackViewInsets.left + + (self.iconImageView.isHidden ? 0 : self.appearance.iconSize.width) + + (self.iconImageView.isHidden ? 0 : self.appearance.stackViewSpacing) + + textLabelIntrinsicContentSize.width + + self.appearance.stackViewInsets.right + + let height = self.appearance.stackViewInsets.top + + max(self.appearance.iconSize.height, textLabelIntrinsicContentSize.height) + + self.appearance.stackViewInsets.bottom + + return CGSize(width: width, height: height) + } + + init( + frame: CGRect = .zero, + appearance: Appearance = Appearance() + ) { + self.appearance = appearance + super.init(frame: frame) + + self.addSubviews() + self.makeConstraints() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + self.setRoundedCorners(cornerRadius: self.intrinsicContentSize.height / 2) + } + + func configure(type: BadgeType) { + self.stackView.removeAllArrangedSubviews() + + self.stackView.addArrangedSubview(self.iconImageView) + self.stackView.addArrangedSubview(self.textLabel) + + self.iconImageView.image = type.icon + self.iconImageView.tintColor = type.tintColor + self.iconImageView.isHidden = type.icon == nil + + self.textLabel.text = type.title + self.textLabel.textColor = type.tintColor + + self.backgroundColor = type.backgroundColor + + self.invalidateIntrinsicContentSize() + } + + enum BadgeType { + case composing + case scheduled + case sending + case sent + case onEvent + case oneTime + + fileprivate var icon: UIImage? { + switch self { + case .composing: + return UIImage(named: "course-info-news-badge-eye-off")?.withRenderingMode(.alwaysTemplate) + case .scheduled: + return UIImage(named: "course-info-news-badge-timer")?.withRenderingMode(.alwaysTemplate) + case .sending: + return UIImage(named: "course-info-news-badge-mail")?.withRenderingMode(.alwaysTemplate) + case .sent: + return UIImage(named: "course-info-news-badge-correct")?.withRenderingMode(.alwaysTemplate) + case .onEvent, .oneTime: + return nil + } + } + + fileprivate var title: String { + switch self { + case .composing: + return NSLocalizedString("CourseInfoTabNewsBadgeComposing", comment: "") + case .scheduled: + return NSLocalizedString("CourseInfoTabNewsBadgeScheduled", comment: "") + case .sending: + return NSLocalizedString("CourseInfoTabNewsBadgeSending", comment: "") + case .sent: + return NSLocalizedString("CourseInfoTabNewsBadgeSent", comment: "") + case .onEvent: + return NSLocalizedString("CourseInfoTabNewsBadgeOnEvent", comment: "") + case .oneTime: + return NSLocalizedString("CourseInfoTabNewsBadgeOneTime", comment: "") + } + } + + fileprivate var tintColor: UIColor { + switch self { + case .composing: + return .stepikMaterialSecondaryText + case .scheduled, .sending: + return .dynamic(light: .stepikVioletFixed, dark: .stepikViolet05Fixed) + case .sent: + return .stepikGreen + case .onEvent, .oneTime: + return .stepikOverlayBlue + } + } + + fileprivate var backgroundColor: UIColor { + switch self { + case .composing: + return .stepikOverlayOnSurfaceBackground + case .scheduled, .sending: + return .stepikOverlayVioletBackground + case .sent: + return .stepikOverlayGreenBackground + case .onEvent, .oneTime: + return .stepikOverlayBlueBackground + } + } + } +} + +extension CourseInfoTabNewsBadgeView: ProgrammaticallyInitializableViewProtocol { + func addSubviews() { + self.addSubview(self.stackView) + } + + func makeConstraints() { + self.iconImageView.translatesAutoresizingMaskIntoConstraints = false + self.iconImageView.snp.makeConstraints { make in + make.size.equalTo(self.appearance.iconSize) + } + + self.stackView.translatesAutoresizingMaskIntoConstraints = false + self.stackView.snp.makeConstraints { make in + make.edges.equalToSuperview().inset(self.appearance.stackViewInsets.edgeInsets) + } + } +} diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/Views/Cell/Badge/CourseInfoTabNewsBadgesView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/Views/Cell/Badge/CourseInfoTabNewsBadgesView.swift new file mode 100644 index 0000000000..d3885dba98 --- /dev/null +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/Views/Cell/Badge/CourseInfoTabNewsBadgesView.swift @@ -0,0 +1,80 @@ +import SnapKit +import UIKit + +extension CourseInfoTabNewsBadgesView { + struct Appearance { + let stackViewSpacing: CGFloat = 8 + } +} + +final class CourseInfoTabNewsBadgesView: UIView { + let appearance: Appearance + + private lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.spacing = self.appearance.stackViewSpacing + return stackView + }() + + override var intrinsicContentSize: CGSize { + self.stackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + } + + init( + frame: CGRect = .zero, + appearance: Appearance = Appearance() + ) { + self.appearance = appearance + super.init(frame: frame) + + self.addSubviews() + self.makeConstraints() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure(viewModel: CourseInfoTabNewsBadgeViewModel) { + self.stackView.removeAllArrangedSubviews() + + let statusBadgeType: CourseInfoTabNewsBadgeView.BadgeType = { + switch viewModel.status { + case .composing: + return .composing + case .scheduled: + return viewModel.isActiveEvent ? .sending : .scheduled + case .queueing, .queued, .sending: + return .sending + case .sent, .aborted: + return .sent + } + }() + let statusBadge = CourseInfoTabNewsBadgeView() + statusBadge.configure(type: statusBadgeType) + + let eventTypeBadge = CourseInfoTabNewsBadgeView() + eventTypeBadge.configure(type: viewModel.isOneTimeEvent ? .oneTime : .onEvent) + + self.stackView.addArrangedSubview(statusBadge) + self.stackView.addArrangedSubview(eventTypeBadge) + + self.invalidateIntrinsicContentSize() + } +} + +extension CourseInfoTabNewsBadgesView: ProgrammaticallyInitializableViewProtocol { + func addSubviews() { + self.addSubview(self.stackView) + } + + func makeConstraints() { + self.stackView.translatesAutoresizingMaskIntoConstraints = false + self.stackView.snp.makeConstraints { make in + make.top.leading.bottom.equalToSuperview() + make.trailing.lessThanOrEqualToSuperview() + } + } +} diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/Views/Cell/CourseInfoTabNewsCellView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/Views/Cell/CourseInfoTabNewsCellView.swift index fc9367af6a..25a91891c3 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/Views/Cell/CourseInfoTabNewsCellView.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/Views/Cell/CourseInfoTabNewsCellView.swift @@ -14,12 +14,20 @@ extension CourseInfoTabNewsCellView { let contentStackViewInsets = LayoutInsets(top: 20, left: 16, bottom: 16, right: 16) let contentStackViewSpacing: CGFloat = 8 + + let statisticsViewInsets = LayoutInsets(top: 8) } } final class CourseInfoTabNewsCellView: UIView { let appearance: Appearance + private lazy var badgesView: CourseInfoTabNewsBadgesView = { + let view = CourseInfoTabNewsBadgesView() + view.isHidden = true + return view + }() + private lazy var dateLabel: UILabel = { let label = UILabel() label.font = self.appearance.dateLabelFont @@ -60,6 +68,14 @@ final class CourseInfoTabNewsCellView: UIView { return processedContentView }() + private lazy var statisticsView = CourseInfoTabNewsStatisticsView() + + private lazy var statisticsContainerView: UIView = { + let view = UIView() + view.isHidden = true + return view + }() + private lazy var contentStackView: UIStackView = { let stackView = UIStackView() stackView.axis = .vertical @@ -80,7 +96,6 @@ final class CourseInfoTabNewsCellView: UIView { self.appearance = appearance super.init(frame: frame) - self.setupView() self.addSubviews() self.makeConstraints() } @@ -104,11 +119,16 @@ final class CourseInfoTabNewsCellView: UIView { let height = ( self.appearance.contentStackViewInsets.top + + (self.badgesView.isHidden ? 0 : self.badgesView.intrinsicContentSize.height) + + (self.badgesView.isHidden ? 0 : self.appearance.contentStackViewSpacing) + self.dateLabel.intrinsicContentSize.height + (self.subjectLabel.isHidden ? 0 : self.appearance.contentStackViewSpacing) + (self.subjectLabel.isHidden ? 0 : self.subjectLabel.intrinsicContentSize.height) + self.appearance.contentStackViewSpacing + textContentHeight + + (self.statisticsContainerView.isHidden ? 0 : self.appearance.contentStackViewSpacing) + + (self.statisticsContainerView.isHidden ? 0 : self.appearance.statisticsViewInsets.top) + + (self.statisticsContainerView.isHidden ? 0 : self.statisticsView.intrinsicContentSize.height) + self.appearance.contentStackViewInsets.bottom ).rounded(.up) @@ -117,30 +137,48 @@ final class CourseInfoTabNewsCellView: UIView { func configure(viewModel: CourseInfoTabNewsViewModel?) { guard let viewModel = viewModel else { + self.badgesView.isHidden = true self.dateLabel.text = nil self.subjectLabel.text = nil self.processedContentView.setText(nil) + self.statisticsContainerView.isHidden = true return } + if let badgeViewModel = viewModel.badge { + self.badgesView.isHidden = false + self.badgesView.configure(viewModel: badgeViewModel) + } else { + self.badgesView.isHidden = true + } + self.dateLabel.text = viewModel.formattedDate self.subjectLabel.text = viewModel.subject self.subjectLabel.isHidden = viewModel.subject.isEmpty self.processedContentView.processedContent = viewModel.processedContent + + if let statisticsViewModel = viewModel.statistics { + self.statisticsContainerView.isHidden = false + self.statisticsView.configure(viewModel: statisticsViewModel) + } else { + self.statisticsContainerView.isHidden = true + } } } extension CourseInfoTabNewsCellView: ProgrammaticallyInitializableViewProtocol { - func setupView() {} - func addSubviews() { self.addSubview(self.contentStackView) + self.contentStackView.addArrangedSubview(self.badgesView) self.contentStackView.addArrangedSubview(self.dateLabel) self.contentStackView.addArrangedSubview(self.subjectLabel) self.contentStackView.addArrangedSubview(self.processedContentView) + self.contentStackView.addArrangedSubview(self.statisticsContainerView) + + self.statisticsContainerView.addSubview(self.statisticsView) } func makeConstraints() { @@ -148,6 +186,11 @@ extension CourseInfoTabNewsCellView: ProgrammaticallyInitializableViewProtocol { self.contentStackView.snp.makeConstraints { make in make.edges.equalToSuperview().inset(self.appearance.contentStackViewInsets.edgeInsets) } + + self.statisticsView.translatesAutoresizingMaskIntoConstraints = false + self.statisticsView.snp.makeConstraints { make in + make.edges.equalToSuperview().inset(self.appearance.statisticsViewInsets.edgeInsets) + } } } diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/Views/Cell/CourseInfoTabNewsStatisticsView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/Views/Cell/CourseInfoTabNewsStatisticsView.swift new file mode 100644 index 0000000000..aefb33989d --- /dev/null +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/Views/Cell/CourseInfoTabNewsStatisticsView.swift @@ -0,0 +1,125 @@ +import SnapKit +import UIKit + +extension CourseInfoTabNewsStatisticsView { + struct Appearance { + let stackViewInsets = LayoutInsets.default + let stackViewSpacing: CGFloat = 4 + + let titleFont = UIFont.systemFont(ofSize: 15) + let titleTextColor = UIColor.stepikMaterialPrimaryText + + let countTitleFont = UIFont.systemFont(ofSize: 16, weight: .semibold) + let countTitleTextColor = UIColor.stepikMaterialPrimaryText + + let backgroundColor = UIColor.stepikOverlayOnSurfaceBackground + let cornerRadius: CGFloat = 8 + } +} + +final class CourseInfoTabNewsStatisticsView: UIView { + let appearance: Appearance + + private lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = self.appearance.stackViewSpacing + return stackView + }() + + override var intrinsicContentSize: CGSize { + let stackViewIntrinsicContentSize = self.stackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + return CGSize( + width: UIView.noIntrinsicMetric, + height: self.appearance.stackViewInsets.top + + stackViewIntrinsicContentSize.height + + self.appearance.stackViewInsets.bottom + ) + } + + 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") + } + + func configure(viewModel: CourseInfoTabNewsStatisticsViewModel) { + self.stackView.removeAllArrangedSubviews() + + let data = [ + (NSLocalizedString("CourseInfoTabNewsPublishCountTitle", comment: ""), viewModel.publishCount), + (NSLocalizedString("CourseInfoTabNewsQueueCountTitle", comment: ""), viewModel.queueCount), + (NSLocalizedString("CourseInfoTabNewsSentCountTitle", comment: ""), viewModel.sentCount), + (NSLocalizedString("CourseInfoTabNewsOpenCountTitle", comment: ""), viewModel.openCount), + (NSLocalizedString("CourseInfoTabNewsClickCountTitle", comment: ""), viewModel.clickCount) + ] + + data.forEach { title, count in + let label = self.makeLabel(title: title, count: count) + self.stackView.addArrangedSubview(label) + } + + self.invalidateIntrinsicContentSize() + } + + private func makeLabel(title: String, count: Int) -> UILabel { + let label = UILabel() + label.numberOfLines = 1 + label.textAlignment = .left + + let countStringValue = "\(count)" + let formattedTitle = "\(title): \(countStringValue)" + + let attributedTitle = NSMutableAttributedString( + string: formattedTitle, + attributes: [ + .font: self.appearance.titleFont, + .foregroundColor: self.appearance.titleTextColor + ] + ) + + if let countLocation = formattedTitle.indexOf(countStringValue) { + attributedTitle.addAttributes( + [ + .font: self.appearance.countTitleFont, + .foregroundColor: self.appearance.countTitleTextColor, + .baselineOffset: -0.75 + ], + range: NSRange(location: countLocation, length: countStringValue.count) + ) + } + + label.attributedText = attributedTitle + + return label + } +} + +extension CourseInfoTabNewsStatisticsView: ProgrammaticallyInitializableViewProtocol { + func setupView() { + self.backgroundColor = self.appearance.backgroundColor + self.setRoundedCorners(cornerRadius: self.appearance.cornerRadius) + } + + func addSubviews() { + self.addSubview(self.stackView) + } + + func makeConstraints() { + self.stackView.translatesAutoresizingMaskIntoConstraints = false + self.stackView.snp.makeConstraints { make in + make.edges.equalToSuperview().inset(self.appearance.stackViewInsets.edgeInsets) + } + } +} diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/Views/Cell/CourseInfoTabNewsTableViewCell.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/Views/Cell/CourseInfoTabNewsTableViewCell.swift index ac99fc5fd8..da24a5e1db 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/Views/Cell/CourseInfoTabNewsTableViewCell.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabNews/Views/Cell/CourseInfoTabNewsTableViewCell.swift @@ -3,7 +3,7 @@ import UIKit extension CourseInfoTabNewsTableViewCell { enum Appearance { static let separatorHeight: CGFloat = 4 - static let separatorColor = UIColor.onSurface.withAlphaComponent(0.04) + static let separatorColor = UIColor.stepikOverlayOnSurfaceBackground } } diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/Views/Section/CourseInfoTabSyllabusSectionBadgesView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/Views/Section/CourseInfoTabSyllabusSectionBadgesView.swift index 58fb9c22a5..29ccfbfc67 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/Views/Section/CourseInfoTabSyllabusSectionBadgesView.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/Views/Section/CourseInfoTabSyllabusSectionBadgesView.swift @@ -46,9 +46,7 @@ final class CourseInfoTabSyllabusSectionBadgesView: UIView { } func configure(viewModel: CourseInfoTabSyllabusSectionViewModel) { - if !self.stackView.arrangedSubviews.isEmpty { - self.stackView.removeAllArrangedSubviews() - } + self.stackView.removeAllArrangedSubviews() guard let examViewModel = viewModel.exam else { return @@ -143,9 +141,9 @@ final class CourseInfoTabSyllabusSectionBadgesView: UIView { var backgroundColor: UIColor { switch self { case .violet: - return .stepikOverlayViolet + return .stepikOverlayVioletBackground case .green: - return UIColor.stepikGreen.withAlphaComponent(0.12) + return .stepikOverlayGreenBackground case .greenWhite: return .stepikGreenFixed case .violetWhite: diff --git a/Stepic/Sources/Modules/CourseList/Views/CourseListView.swift b/Stepic/Sources/Modules/CourseList/Views/CourseListView.swift index 7ba52d0b20..92ad0e7c4c 100644 --- a/Stepic/Sources/Modules/CourseList/Views/CourseListView.swift +++ b/Stepic/Sources/Modules/CourseList/Views/CourseListView.swift @@ -20,6 +20,8 @@ extension CourseListView { } } +// MARK: - CourseListView: UIView - + class CourseListView: UIView { let appearance: Appearance let colorMode: CourseListColorMode @@ -138,7 +140,7 @@ class CourseListView: UIView { return (columnsCount, columnWidth) } - // MARK: - ColorMode + // MARK: ColorMode private func getBackgroundColor(for colorMode: CourseListColorMode) -> UIColor { switch colorMode { @@ -151,7 +153,7 @@ class CourseListView: UIView { } } - // MARK: - Loading state + // MARK: Loading state func showLoading() { self.collectionView.skeleton.viewBuilder = { @@ -233,7 +235,7 @@ extension CourseListView: ProgrammaticallyInitializableViewProtocol { } } -// Subclasses for two orientations +// MARK: - VerticalCourseListView: CourseListView, UICollectionViewDelegate, UICollectionViewDataSource - final class VerticalCourseListView: CourseListView, UICollectionViewDelegate, UICollectionViewDataSource { private var gridSize: CourseListGridSize { @@ -445,6 +447,8 @@ final class VerticalCourseListView: CourseListView, UICollectionViewDelegate, UI } } +// MARK: - HorizontalCourseListView: CourseListView, UICollectionViewDelegate, UICollectionViewDataSource - + final class HorizontalCourseListView: CourseListView, UICollectionViewDelegate, UICollectionViewDataSource { private var gridSize: CourseListGridSize { didSet { @@ -514,6 +518,8 @@ final class HorizontalCourseListView: CourseListView, UICollectionViewDelegate, self.collectionView.delegate = self self.collectionView.dataSource = self + self.collectionView.contentInsetAdjustmentBehavior = .never + self.collectionView.showsVerticalScrollIndicator = false // Make scroll faster self.collectionView.decelerationRate = .fast } @@ -643,6 +649,8 @@ final class HorizontalCourseListView: CourseListView, UICollectionViewDelegate, // Wrapper for reusable views +// MARK: - CollectionViewReusableView: UICollectionReusableView, Reusable - + final class CollectionViewReusableView: UICollectionReusableView, Reusable { private var subview: UIView? @@ -662,6 +670,8 @@ final class CollectionViewReusableView: UICollectionReusableView, Reusable { } } +// MARK: - CourseListCollectionViewCell subclasses - + // Cause we can't init cell with custom initializer let's use custom classes private class LightCourseListCollectionViewCell: CourseListCollectionViewCell { diff --git a/Stepic/Sources/Modules/CourseRevenue/Views/CourseRevenueTabSegmentedControlView.swift b/Stepic/Sources/Modules/CourseRevenue/Views/CourseRevenueTabSegmentedControlView.swift index 035de5153c..1fa26d9d35 100644 --- a/Stepic/Sources/Modules/CourseRevenue/Views/CourseRevenueTabSegmentedControlView.swift +++ b/Stepic/Sources/Modules/CourseRevenue/Views/CourseRevenueTabSegmentedControlView.swift @@ -3,10 +3,7 @@ import UIKit extension CourseRevenueTabSegmentedControlView { struct Appearance { - let separatorBackgroundColor = UIColor.dynamic( - light: .onSurface.withAlphaComponent(0.04), - dark: .stepikSeparator - ) + let separatorBackgroundColor = UIColor.stepikOverlayOnSurfaceBackground let separatorViewHeight: CGFloat = 1 let insets = LayoutInsets.default diff --git a/Stepic/Sources/Modules/CourseRevenue/Views/IncomeCard/CourseRevenueIncomeView.swift b/Stepic/Sources/Modules/CourseRevenue/Views/IncomeCard/CourseRevenueIncomeView.swift index bcee348c73..12a8c7fec2 100644 --- a/Stepic/Sources/Modules/CourseRevenue/Views/IncomeCard/CourseRevenueIncomeView.swift +++ b/Stepic/Sources/Modules/CourseRevenue/Views/IncomeCard/CourseRevenueIncomeView.swift @@ -11,10 +11,7 @@ extension CourseRevenueIncomeView { let shadowRadius: CGFloat = 4.0 let shadowOpacity: Float = 0.1 - let separatorBackgroundColor = UIColor.dynamic( - light: .onSurface.withAlphaComponent(0.04), - dark: .stepikSeparator - ) + let separatorBackgroundColor = UIColor.stepikOverlayOnSurfaceBackground let separatorViewHeight: CGFloat = 1 } diff --git a/Stepic/Sources/Modules/CourseRevenueSubmodules/CourseRevenueTabMonthly/Views/Cell/CourseRevenueTabMonthlyTableViewCell.swift b/Stepic/Sources/Modules/CourseRevenueSubmodules/CourseRevenueTabMonthly/Views/Cell/CourseRevenueTabMonthlyTableViewCell.swift index 5672916fb7..5e06e43390 100644 --- a/Stepic/Sources/Modules/CourseRevenueSubmodules/CourseRevenueTabMonthly/Views/Cell/CourseRevenueTabMonthlyTableViewCell.swift +++ b/Stepic/Sources/Modules/CourseRevenueSubmodules/CourseRevenueTabMonthly/Views/Cell/CourseRevenueTabMonthlyTableViewCell.swift @@ -3,10 +3,7 @@ import UIKit final class CourseRevenueTabMonthlyTableViewCell: UITableViewCell, Reusable { enum Appearance { static let separatorHeight: CGFloat = 1 - static let separatorColor = UIColor.dynamic( - light: .onSurface.withAlphaComponent(0.04), - dark: .stepikSeparator - ) + static let separatorColor = UIColor.stepikOverlayOnSurfaceBackground } private lazy var cellView = CourseRevenueTabMonthlyCellView() diff --git a/Stepic/Sources/Modules/CourseRevenueSubmodules/CourseRevenueTabPurchases/Views/Cell/CourseRevenueTabPurchasesTableViewCell.swift b/Stepic/Sources/Modules/CourseRevenueSubmodules/CourseRevenueTabPurchases/Views/Cell/CourseRevenueTabPurchasesTableViewCell.swift index 21ecd2dc01..2269cb23b4 100644 --- a/Stepic/Sources/Modules/CourseRevenueSubmodules/CourseRevenueTabPurchases/Views/Cell/CourseRevenueTabPurchasesTableViewCell.swift +++ b/Stepic/Sources/Modules/CourseRevenueSubmodules/CourseRevenueTabPurchases/Views/Cell/CourseRevenueTabPurchasesTableViewCell.swift @@ -3,10 +3,7 @@ import UIKit final class CourseRevenueTabPurchasesTableViewCell: UITableViewCell, Reusable { enum Appearance { static let separatorHeight: CGFloat = 1 - static let separatorColor = UIColor.dynamic( - light: .onSurface.withAlphaComponent(0.04), - dark: .stepikSeparator - ) + static let separatorColor = UIColor.stepikOverlayOnSurfaceBackground } private lazy var cellView = CourseRevenueTabPurchasesCellView() diff --git a/Stepic/Sources/Modules/CourseSearch/CourseSearchInteractor.swift b/Stepic/Sources/Modules/CourseSearch/CourseSearchInteractor.swift index e0e5a98d82..0eeec4135a 100644 --- a/Stepic/Sources/Modules/CourseSearch/CourseSearchInteractor.swift +++ b/Stepic/Sources/Modules/CourseSearch/CourseSearchInteractor.swift @@ -99,7 +99,7 @@ final class CourseSearchInteractor: CourseSearchInteractorProtocol { func doSearchResultsLoad(request: CourseSearch.SearchResultsLoad.Request) { defer { self.analytics.send( - .сourseContentSearched( + .courseContentSearched( id: self.courseID, title: self.currentCourse?.title, query: self.currentQuery, diff --git a/Stepic/Sources/Modules/CourseSearch/Views/SearchResults/Cell/CourseSearchResultTableViewCell.swift b/Stepic/Sources/Modules/CourseSearch/Views/SearchResults/Cell/CourseSearchResultTableViewCell.swift index 3fa83fae4c..0f5438f41c 100644 --- a/Stepic/Sources/Modules/CourseSearch/Views/SearchResults/Cell/CourseSearchResultTableViewCell.swift +++ b/Stepic/Sources/Modules/CourseSearch/Views/SearchResults/Cell/CourseSearchResultTableViewCell.swift @@ -4,10 +4,7 @@ import UIKit final class CourseSearchResultTableViewCell: UITableViewCell, Reusable { enum Appearance { static let separatorHeight: CGFloat = 4 - static let separatorBackgroundColor = UIColor.dynamic( - light: .onSurface.withAlphaComponent(0.04), - dark: .stepikSeparator - ) + static let separatorBackgroundColor = UIColor.stepikOverlayOnSurfaceBackground } private lazy var cellView = CourseSearchResultTableCellView() diff --git a/Stepic/Sources/Modules/ExploreSubmodules/CatalogBlocksSubmodules/SimpleCourseList/Views/Grid/Cell/GridSimpleCourseListWidgetView.swift b/Stepic/Sources/Modules/ExploreSubmodules/CatalogBlocksSubmodules/SimpleCourseList/Views/Grid/Cell/GridSimpleCourseListWidgetView.swift index 817b411742..feeecf9d61 100644 --- a/Stepic/Sources/Modules/ExploreSubmodules/CatalogBlocksSubmodules/SimpleCourseList/Views/Grid/Cell/GridSimpleCourseListWidgetView.swift +++ b/Stepic/Sources/Modules/ExploreSubmodules/CatalogBlocksSubmodules/SimpleCourseList/Views/Grid/Cell/GridSimpleCourseListWidgetView.swift @@ -7,7 +7,7 @@ extension GridSimpleCourseListWidgetView { let titleLabelTextColor = UIColor.dynamic(light: .stepikVioletFixed, dark: .stepikViolet05Fixed) let titleLabelInsets = LayoutInsets(top: 16, left: 16, bottom: 16, right: 16) - let backgroundColor = UIColor.stepikOverlayViolet + let backgroundColor = UIColor.stepikOverlayVioletBackground } } diff --git a/Stepic/Sources/Modules/Home/HomeViewController.swift b/Stepic/Sources/Modules/Home/HomeViewController.swift index d1fac48bd0..9c48350163 100644 --- a/Stepic/Sources/Modules/Home/HomeViewController.swift +++ b/Stepic/Sources/Modules/Home/HomeViewController.swift @@ -31,6 +31,8 @@ final class HomeViewController: BaseExploreViewController { private lazy var streakView = StreakActivityView() private lazy var homeInteractor = self.interactor as? HomeInteractorProtocol + private var isFirstTimeViewDidAppear = true + init(interactor: HomeInteractorProtocol, analytics: Analytics) { super.init(interactor: interactor, analytics: analytics) @@ -54,19 +56,23 @@ final class HomeViewController: BaseExploreViewController { self.analytics.send(.homeScreenOpened) self.homeInteractor?.doStreakActivityLoad(request: .init()) - DispatchQueue.main.asyncAfter(deadline: .now() + Animation.modulesRefreshDelay) { [weak self] in - guard let strongSelf = self else { - return - } + if self.isFirstTimeViewDidAppear { + self.isFirstTimeViewDidAppear = false + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + Animation.modulesRefreshDelay) { [weak self] in + guard let strongSelf = self else { + return + } - if strongSelf.currentEnrolledCourseListState == .empty { - strongSelf.refreshStateForEnrolledCourses(state: .normal) - } - if strongSelf.currentReviewsAndWishlistState == .shown { - strongSelf.refreshReviewsAndWishlist(state: .shown) - } + if strongSelf.currentEnrolledCourseListState == .empty { + strongSelf.refreshStateForEnrolledCourses(state: .normal) + } + if strongSelf.currentReviewsAndWishlistState == .shown { + strongSelf.refreshReviewsAndWishlist(state: .shown) + } - strongSelf.refreshStateForVisitedCourses(state: .shown) + strongSelf.refreshStateForVisitedCourses(state: .shown) + } } } @@ -303,6 +309,8 @@ final class HomeViewController: BaseExploreViewController { type: submoduleType ) ) + + containerViewController.refreshSubmodules() } case .hidden: if let submodule = self.getSubmodule(type: submoduleType) { diff --git a/Stepic/Sources/Modules/HomeSubmodules/UserCoursesReviewsWidget/UserCoursesReviewsWidgetAssembly.swift b/Stepic/Sources/Modules/HomeSubmodules/UserCoursesReviewsWidget/UserCoursesReviewsWidgetAssembly.swift index 2d043f88be..c4c874f48f 100644 --- a/Stepic/Sources/Modules/HomeSubmodules/UserCoursesReviewsWidget/UserCoursesReviewsWidgetAssembly.swift +++ b/Stepic/Sources/Modules/HomeSubmodules/UserCoursesReviewsWidget/UserCoursesReviewsWidgetAssembly.swift @@ -9,7 +9,9 @@ final class UserCoursesReviewsWidgetAssembly: Assembly { courseReviewsNetworkService: CourseReviewsNetworkService(courseReviewsAPI: CourseReviewsAPI()), courseReviewsPersistenceService: CourseReviewsPersistenceService(), coursesNetworkService: CoursesNetworkService(coursesAPI: CoursesAPI()), - coursesPersistenceService: CoursesPersistenceService() + coursesPersistenceService: CoursesPersistenceService(), + userCoursesNetworkService: UserCoursesNetworkService(userCoursesAPI: UserCoursesAPI()), + userCoursesPersistenceService: UserCoursesPersistenceService() ) let provider = UserCoursesReviewsWidgetProvider(userCoursesReviewsProvider: userCoursesReviewsProvider) diff --git a/Stepic/Sources/Modules/HomeSubmodules/UserCoursesReviewsWidget/UserCoursesReviewsWidgetInteractor.swift b/Stepic/Sources/Modules/HomeSubmodules/UserCoursesReviewsWidget/UserCoursesReviewsWidgetInteractor.swift index aa4954dd55..a103e77ae1 100644 --- a/Stepic/Sources/Modules/HomeSubmodules/UserCoursesReviewsWidget/UserCoursesReviewsWidgetInteractor.swift +++ b/Stepic/Sources/Modules/HomeSubmodules/UserCoursesReviewsWidget/UserCoursesReviewsWidgetInteractor.swift @@ -56,7 +56,11 @@ final class UserCoursesReviewsWidgetInteractor: UserCoursesReviewsWidgetInteract ? self.provider.fetchLeavedCourseReviewsFromRemote() : self.provider.fetchLeavedCourseReviewsFromCache() }.then { leavedCourseReviews -> Promise<([Course], [CourseReview])> in - self.provider.fetchPossibleCoursesFromCache().map { ($0, leavedCourseReviews) } + ( + self.didLoadFromCache + ? self.provider.fetchPossibleCoursesFromRemote() + : self.provider.fetchPossibleCoursesFromCache() + ).map { ($0, leavedCourseReviews) } }.done { possibleCourses, leavedCourseReviews in let filteredPossibleCourses = possibleCourses.filter { course in !leavedCourseReviews.contains(where: { $0.courseID == course.id }) diff --git a/Stepic/Sources/Modules/HomeSubmodules/UserCoursesReviewsWidget/UserCoursesReviewsWidgetProvider.swift b/Stepic/Sources/Modules/HomeSubmodules/UserCoursesReviewsWidget/UserCoursesReviewsWidgetProvider.swift index 4f3b3efbe8..015c181253 100644 --- a/Stepic/Sources/Modules/HomeSubmodules/UserCoursesReviewsWidget/UserCoursesReviewsWidgetProvider.swift +++ b/Stepic/Sources/Modules/HomeSubmodules/UserCoursesReviewsWidget/UserCoursesReviewsWidgetProvider.swift @@ -22,6 +22,10 @@ final class UserCoursesReviewsWidgetProvider: UserCoursesReviewsWidgetProviderPr self.userCoursesReviewsProvider.fetchPossibleCoursesFromCache() } + func fetchPossibleCoursesFromRemote() -> Promise<[Course]> { + self.userCoursesReviewsProvider.fetchPossibleCoursesFromRemote() + } + func deleteCourseReview(id: CourseReview.IdType) -> Promise { self.userCoursesReviewsProvider.deleteCourseReview(id: id) } diff --git a/Stepic/Sources/Modules/Quizzes/CodeQuiz/Views/CodeDetails/CodeDetailsContentView.swift b/Stepic/Sources/Modules/Quizzes/CodeQuiz/Views/CodeDetails/CodeDetailsContentView.swift index af0922fe51..fa5315a963 100644 --- a/Stepic/Sources/Modules/Quizzes/CodeQuiz/Views/CodeDetails/CodeDetailsContentView.swift +++ b/Stepic/Sources/Modules/Quizzes/CodeQuiz/Views/CodeDetails/CodeDetailsContentView.swift @@ -32,9 +32,7 @@ final class CodeDetailsContentView: UIView { } func configure(samples: [CodeSamplePlainObject], limit: CodeLimitPlainObject) { - if !self.stackView.arrangedSubviews.isEmpty { - self.stackView.removeAllArrangedSubviews() - } + self.stackView.removeAllArrangedSubviews() self.makeCodeSampleViews(samples).forEach { self.stackView.addArrangedSubview($0) } self.makeCodeLimitViews(limit).forEach { self.stackView.addArrangedSubview($0) } diff --git a/Stepic/Sources/Modules/StepQuizReview/Views/StepQuizReviewExpandQuizView.swift b/Stepic/Sources/Modules/StepQuizReview/Views/StepQuizReviewExpandQuizView.swift index ccdc377ece..70305c50d9 100644 --- a/Stepic/Sources/Modules/StepQuizReview/Views/StepQuizReviewExpandQuizView.swift +++ b/Stepic/Sources/Modules/StepQuizReview/Views/StepQuizReviewExpandQuizView.swift @@ -4,7 +4,7 @@ import UIKit extension StepQuizReviewExpandQuizView { struct Appearance { let primaryColor = UIColor.stepikPrimaryText - let backgroundColor = UIColor.onSurface.withAlphaComponent(0.04) + let backgroundColor = UIColor.stepikOverlayOnSurfaceBackground let titleFont = Typography.bodyFont let titleInsets = LayoutInsets.default diff --git a/Stepic/Sources/Modules/StepQuizReview/Views/StepQuizReviewMessageView.swift b/Stepic/Sources/Modules/StepQuizReview/Views/StepQuizReviewMessageView.swift index 2d3fd342f6..0f9f0e81a4 100644 --- a/Stepic/Sources/Modules/StepQuizReview/Views/StepQuizReviewMessageView.swift +++ b/Stepic/Sources/Modules/StepQuizReview/Views/StepQuizReviewMessageView.swift @@ -3,7 +3,7 @@ import UIKit extension StepQuizReviewMessageView { struct Appearance { - let backgroundColor = UIColor.stepikOverlayViolet + let backgroundColor = UIColor.stepikOverlayVioletBackground let cornerRadius: CGFloat = 6 let tintColor = UIColor.dynamic(light: .stepikVioletFixed, dark: .stepikViolet05Fixed) let insets = LayoutInsets.default diff --git a/Stepic/Sources/Modules/Submissions/Views/Cell/SubmissionsReviewView.swift b/Stepic/Sources/Modules/Submissions/Views/Cell/SubmissionsReviewView.swift index 3150d82fd3..4c2091fd8f 100644 --- a/Stepic/Sources/Modules/Submissions/Views/Cell/SubmissionsReviewView.swift +++ b/Stepic/Sources/Modules/Submissions/Views/Cell/SubmissionsReviewView.swift @@ -4,7 +4,7 @@ import UIKit extension SubmissionsReviewView { struct Appearance { let separatorHeight: CGFloat = 1 - let separatorColor = UIColor.onSurface.withAlphaComponent(0.04) + let separatorColor = UIColor.stepikOverlayOnSurfaceBackground let titleFont = Typography.caption1Font let titleTextColor = UIColor.stepikMaterialSecondaryText diff --git a/Stepic/Sources/Modules/Submissions/Views/Cell/SubmissionsSelectionView.swift b/Stepic/Sources/Modules/Submissions/Views/Cell/SubmissionsSelectionView.swift index 9e4f231da4..a193ba2b67 100644 --- a/Stepic/Sources/Modules/Submissions/Views/Cell/SubmissionsSelectionView.swift +++ b/Stepic/Sources/Modules/Submissions/Views/Cell/SubmissionsSelectionView.swift @@ -4,7 +4,7 @@ import UIKit extension SubmissionsSelectionView { struct Appearance { let separatorHeight: CGFloat = 1 - let separatorColor = UIColor.onSurface.withAlphaComponent(0.04) + let separatorColor = UIColor.stepikOverlayOnSurfaceBackground let titleFont = Typography.bodyFont let titleTextColor = UIColor.stepikVioletFixed diff --git a/Stepic/Sources/Modules/Submissions/Views/Cell/SubmissionsTableViewCell.swift b/Stepic/Sources/Modules/Submissions/Views/Cell/SubmissionsTableViewCell.swift index dde11ec889..38e350e1ab 100644 --- a/Stepic/Sources/Modules/Submissions/Views/Cell/SubmissionsTableViewCell.swift +++ b/Stepic/Sources/Modules/Submissions/Views/Cell/SubmissionsTableViewCell.swift @@ -4,7 +4,7 @@ import UIKit final class SubmissionsTableViewCell: UITableViewCell, Reusable { enum Appearance { static let separatorHeight: CGFloat = 4 - static let separatorBackgroundColor = UIColor.onSurface.withAlphaComponent(0.04) + static let separatorBackgroundColor = UIColor.stepikOverlayOnSurfaceBackground } private lazy var cellView = SubmissionsCellView() diff --git a/Stepic/Sources/Modules/UserCoursesReviews/UserCoursesReviewsAssembly.swift b/Stepic/Sources/Modules/UserCoursesReviews/UserCoursesReviewsAssembly.swift index 3516048fd7..057e4e4b26 100644 --- a/Stepic/Sources/Modules/UserCoursesReviews/UserCoursesReviewsAssembly.swift +++ b/Stepic/Sources/Modules/UserCoursesReviews/UserCoursesReviewsAssembly.swift @@ -13,7 +13,9 @@ final class UserCoursesReviewsAssembly: Assembly { courseReviewsNetworkService: CourseReviewsNetworkService(courseReviewsAPI: CourseReviewsAPI()), courseReviewsPersistenceService: CourseReviewsPersistenceService(), coursesNetworkService: CoursesNetworkService(coursesAPI: CoursesAPI()), - coursesPersistenceService: CoursesPersistenceService() + coursesPersistenceService: CoursesPersistenceService(), + userCoursesNetworkService: UserCoursesNetworkService(userCoursesAPI: UserCoursesAPI()), + userCoursesPersistenceService: UserCoursesPersistenceService() ) let presenter = UserCoursesReviewsPresenter() let interactor = UserCoursesReviewsInteractor( diff --git a/Stepic/Sources/Modules/UserCoursesReviews/UserCoursesReviewsInteractor.swift b/Stepic/Sources/Modules/UserCoursesReviews/UserCoursesReviewsInteractor.swift index 7137c949bb..b5d3a82359 100644 --- a/Stepic/Sources/Modules/UserCoursesReviews/UserCoursesReviewsInteractor.swift +++ b/Stepic/Sources/Modules/UserCoursesReviews/UserCoursesReviewsInteractor.swift @@ -235,7 +235,11 @@ final class UserCoursesReviewsInteractor: UserCoursesReviewsInteractorProtocol { ? self.provider.fetchLeavedCourseReviewsFromRemote() : self.provider.fetchLeavedCourseReviewsFromCache() }.then { leavedCourseReviews -> Promise<([Course], [CourseReview])> in - self.provider.fetchPossibleCoursesFromCache().map { ($0, leavedCourseReviews) } + ( + self.didLoadFromCache + ? self.provider.fetchPossibleCoursesFromRemote() + : self.provider.fetchPossibleCoursesFromCache() + ).map { ($0, leavedCourseReviews) } }.done { possibleCourses, leavedCourseReviews in self.currentLeavedCourseReviews = leavedCourseReviews diff --git a/Stepic/Sources/Modules/UserCoursesReviews/UserCoursesReviewsProvider.swift b/Stepic/Sources/Modules/UserCoursesReviews/UserCoursesReviewsProvider.swift index b392336529..8703161b2f 100644 --- a/Stepic/Sources/Modules/UserCoursesReviews/UserCoursesReviewsProvider.swift +++ b/Stepic/Sources/Modules/UserCoursesReviews/UserCoursesReviewsProvider.swift @@ -6,6 +6,7 @@ protocol UserCoursesReviewsProviderProtocol { func fetchLeavedCourseReviewsFromRemote() -> Promise<[CourseReview]> func fetchPossibleCoursesFromCache() -> Promise<[Course]> + func fetchPossibleCoursesFromRemote() -> Promise<[Course]> func deleteCourseReview(id: CourseReview.IdType) -> Promise } @@ -19,6 +20,9 @@ final class UserCoursesReviewsProvider: UserCoursesReviewsProviderProtocol { private let coursesNetworkService: CoursesNetworkServiceProtocol private let coursesPersistenceService: CoursesPersistenceServiceProtocol + private let userCoursesNetworkService: UserCoursesNetworkServiceProtocol + private let userCoursesPersistenceService: UserCoursesPersistenceServiceProtocol + private var currentUserID: User.IdType? { self.userAccountService.currentUserID } init( @@ -26,13 +30,17 @@ final class UserCoursesReviewsProvider: UserCoursesReviewsProviderProtocol { courseReviewsNetworkService: CourseReviewsNetworkServiceProtocol, courseReviewsPersistenceService: CourseReviewsPersistenceServiceProtocol, coursesNetworkService: CoursesNetworkServiceProtocol, - coursesPersistenceService: CoursesPersistenceServiceProtocol + coursesPersistenceService: CoursesPersistenceServiceProtocol, + userCoursesNetworkService: UserCoursesNetworkServiceProtocol, + userCoursesPersistenceService: UserCoursesPersistenceServiceProtocol ) { self.userAccountService = userAccountService self.courseReviewsNetworkService = courseReviewsNetworkService self.courseReviewsPersistenceService = courseReviewsPersistenceService self.coursesNetworkService = coursesNetworkService self.coursesPersistenceService = coursesPersistenceService + self.userCoursesNetworkService = userCoursesNetworkService + self.userCoursesPersistenceService = userCoursesPersistenceService } func fetchLeavedCourseReviewsFromCache() -> Promise<[CourseReview]> { @@ -71,19 +79,63 @@ final class UserCoursesReviewsProvider: UserCoursesReviewsProviderProtocol { func fetchPossibleCoursesFromCache() -> Promise<[Course]> { Promise { seal in - self.coursesPersistenceService.fetchEnrolled().done { courses in - let filteredCourses = courses.filter(\.canWriteReview) + self.userCoursesPersistenceService.fetchCanBeReviewed().done { userCourses in + var uniqueUserCourses = [UserCourse]() + for userCourse in userCourses { + if !uniqueUserCourses.contains(where: { $0.id == userCourse.id }) { + uniqueUserCourses.append(userCourse) + } + } - var uniqueCourses = [Course]() - for course in filteredCourses { - if !uniqueCourses.contains(where: { $0.id == course.id }) { - uniqueCourses.append(course) + let relationshipsCourses = uniqueUserCourses.compactMap(\.course) + if uniqueUserCourses.count == relationshipsCourses.count { + return seal.fulfill(relationshipsCourses) + } + + self.coursesPersistenceService.fetch(ids: uniqueUserCourses.map(\.courseID)).done { courses in + CoreDataHelper.shared.context.performChanges { + let coursesMap = Dictionary( + courses.map({ ($0.id, $0) }), + uniquingKeysWith: { first, _ in first } + ) + let resultCourses = uniqueUserCourses.compactMap { userCourse -> Course? in + if let course = coursesMap[userCourse.courseID] { + userCourse.course = course + return course + } + return nil + } + seal.fulfill(resultCourses) } + }.catch { _ in + seal.reject(Error.persistenceFetchFailed) } + } + } + } - let result = uniqueCourses.reordered(order: courses.map(\.id), transform: { $0.id }) + func fetchPossibleCoursesFromRemote() -> Promise<[Course]> { + Promise { seal in + self.userCoursesNetworkService.fetchAllCanBeReviewedPages().then { + userCourses -> Promise<([UserCourse], [Course])> in + self.coursesNetworkService + .fetch(ids: userCourses.map(\.courseID)) + .map { (userCourses, $0) } + }.done { userCourses, courses in + let orderedCourses = courses.reordered(order: userCourses.map(\.id), transform: \.id) + let userCoursesMap = Dictionary(uniqueKeysWithValues: userCourses.map({ ($0.courseID, $0) })) + + CoreDataHelper.shared.context.performChanges { + for course in orderedCourses { + if let userCourse = userCoursesMap[course.id] { + userCourse.course = course + } + } - seal.fulfill(result) + seal.fulfill(orderedCourses) + } + }.catch { _ in + seal.reject(Error.networkFetchFailed) } } } diff --git a/Stepic/Sources/Modules/UserCoursesReviews/View/Cell/Leaved/UserCoursesReviewsLeavedReviewTableViewCell.swift b/Stepic/Sources/Modules/UserCoursesReviews/View/Cell/Leaved/UserCoursesReviewsLeavedReviewTableViewCell.swift index 8bc158b1e4..1b2fd6027d 100644 --- a/Stepic/Sources/Modules/UserCoursesReviews/View/Cell/Leaved/UserCoursesReviewsLeavedReviewTableViewCell.swift +++ b/Stepic/Sources/Modules/UserCoursesReviews/View/Cell/Leaved/UserCoursesReviewsLeavedReviewTableViewCell.swift @@ -4,10 +4,7 @@ import UIKit extension UserCoursesReviewsLeavedReviewTableViewCell { enum Appearance { static let separatorHeight: CGFloat = 4 - static let separatorBackgroundColor = UIColor.dynamic( - light: .onSurface.withAlphaComponent(0.04), - dark: .stepikSeparator - ) + static let separatorBackgroundColor = UIColor.stepikOverlayOnSurfaceBackground } } diff --git a/Stepic/Sources/Modules/UserCoursesReviews/View/Cell/Possible/UserCoursesReviewsPossibleReviewTableViewCell.swift b/Stepic/Sources/Modules/UserCoursesReviews/View/Cell/Possible/UserCoursesReviewsPossibleReviewTableViewCell.swift index bd7b35ce7c..c5670c259a 100644 --- a/Stepic/Sources/Modules/UserCoursesReviews/View/Cell/Possible/UserCoursesReviewsPossibleReviewTableViewCell.swift +++ b/Stepic/Sources/Modules/UserCoursesReviews/View/Cell/Possible/UserCoursesReviewsPossibleReviewTableViewCell.swift @@ -4,10 +4,7 @@ import UIKit extension UserCoursesReviewsPossibleReviewTableViewCell { enum Appearance { static let separatorHeight: CGFloat = 4 - static let separatorBackgroundColor = UIColor.dynamic( - light: .onSurface.withAlphaComponent(0.04), - dark: .stepikSeparator - ) + static let separatorBackgroundColor = UIColor.stepikOverlayOnSurfaceBackground } } diff --git a/Stepic/Sources/Modules/UserCoursesReviews/View/UserCoursesReviewsView.swift b/Stepic/Sources/Modules/UserCoursesReviews/View/UserCoursesReviewsView.swift index 833dc0e014..756bb56019 100644 --- a/Stepic/Sources/Modules/UserCoursesReviews/View/UserCoursesReviewsView.swift +++ b/Stepic/Sources/Modules/UserCoursesReviews/View/UserCoursesReviewsView.swift @@ -25,6 +25,10 @@ final class UserCoursesReviewsView: UIView { tableView.separatorStyle = .none tableView.keyboardDismissMode = .interactive + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0 + } + tableView.refreshControl = self.refreshControl self.refreshControl.addTarget(self, action: #selector(self.refreshControlDidChangeValue), for: .valueChanged) diff --git a/Stepic/Sources/Services/Models/Network/UserCoursesNetworkService.swift b/Stepic/Sources/Services/Models/Network/UserCoursesNetworkService.swift index 95548ee48c..533e02dd1f 100644 --- a/Stepic/Sources/Services/Models/Network/UserCoursesNetworkService.swift +++ b/Stepic/Sources/Services/Models/Network/UserCoursesNetworkService.swift @@ -4,6 +4,8 @@ import PromiseKit protocol UserCoursesNetworkServiceProtocol: AnyObject { func fetch(page: Int) -> Promise<([UserCourse], Meta)> func fetch(courseID: Course.IdType) -> Promise + func fetchCanBeReviewed(page: Int) -> Promise<([UserCourse], Meta)> + func fetchAllCanBeReviewedPages() -> Promise<[UserCourse]> func update(userCourse: UserCourse) -> Promise } @@ -12,6 +14,10 @@ extension UserCoursesNetworkServiceProtocol { func fetch() -> Promise<([UserCourse], Meta)> { self.fetch(page: 1) } + + func fetchCanBeReviewed() -> Promise<([UserCourse], Meta)> { + self.fetchCanBeReviewed(page: 1) + } } final class UserCoursesNetworkService: UserCoursesNetworkServiceProtocol { @@ -41,6 +47,43 @@ final class UserCoursesNetworkService: UserCoursesNetworkServiceProtocol { } } + func fetchCanBeReviewed(page: Int) -> Promise<([UserCourse], Meta)> { + Promise { seal in + self.userCoursesAPI.retrieve(page: page, canBeReviewed: true, isDraft: false).done { userCourses, meta in + seal.fulfill((userCourses, meta)) + }.catch { _ in + seal.reject(Error.fetchFailed) + } + } + } + + func fetchAllCanBeReviewedPages() -> Promise<[UserCourse]> { + var allUserCourses = [UserCourse]() + + func load(page: Int) -> Guarantee { + Guarantee { seal in + self.fetchCanBeReviewed(page: page).done { userCourses, meta in + allUserCourses.append(contentsOf: userCourses) + seal(meta.hasNext) + }.catch { _ in + seal(false) + } + } + } + + func collect(page: Int) -> Promise<[UserCourse]> { + load(page: page).then { hasNext -> Promise<[UserCourse]> in + if hasNext { + return collect(page: page + 1) + } else { + return .value(allUserCourses) + } + } + } + + return collect(page: 1) + } + func update(userCourse: UserCourse) -> Promise { Promise { seal in self.userCoursesAPI.update(userCourse).done { userCourse in diff --git a/Stepic/Sources/Services/Models/Persistence/UserCoursesPersistenceService.swift b/Stepic/Sources/Services/Models/Persistence/UserCoursesPersistenceService.swift index 92ea7b10e8..9c2d478190 100644 --- a/Stepic/Sources/Services/Models/Persistence/UserCoursesPersistenceService.swift +++ b/Stepic/Sources/Services/Models/Persistence/UserCoursesPersistenceService.swift @@ -4,6 +4,7 @@ import PromiseKit protocol UserCoursesPersistenceServiceProtocol: AnyObject { func fetch(ids: [UserCourse.IdType]) -> Guarantee<[UserCourse]> func fetch(courseID: Course.IdType) -> Guarantee<[UserCourse]> + func fetchCanBeReviewed() -> Guarantee<[UserCourse]> func fetchAll() -> Guarantee<[UserCourse]> func deleteAll() -> Promise @@ -29,4 +30,24 @@ final class UserCoursesPersistenceService: BasePersistenceService, U } } } + + func fetchCanBeReviewed() -> Guarantee<[UserCourse]> { + Guarantee { seal in + let request = UserCourse.sortedFetchRequest + request.predicate = NSPredicate( + format: "%K == %@", + #keyPath(UserCourse.managedCanBeReviewed), + NSNumber(value: true) + ) + request.returnsObjectsAsFaults = false + + do { + let userCourses = try self.managedObjectContext.fetch(request) + seal(userCourses) + } catch { + print("UserCoursesPersistenceService :: failed fetch canBeReviewed with error = \(error)") + seal([]) + } + } + } } diff --git a/Stepic/Theme/ColorPalette.swift b/Stepic/Theme/ColorPalette.swift index 945fc8d297..01b052f264 100644 --- a/Stepic/Theme/ColorPalette.swift +++ b/Stepic/Theme/ColorPalette.swift @@ -105,6 +105,14 @@ extension UIColor { ) } + /// Adaptable color with base hex value #2F80ED (colorOverlayBlue). + static var stepikOverlayBlue: UIColor { + .dynamic( + light: ColorPalette.blueLightColorOverlay, + dark: ColorPalette.blueDarkColorOverlay + ) + } + /// A non adaptable color with hex value #4485ED (blue05). static let stepikBlueFixed = ColorPalette.blue600 /// A non adaptable color with hex value #56A4FF (blue03). @@ -535,13 +543,22 @@ extension UIColor { } /// The color to use for the content overlay with violet. - static var stepikOverlayViolet: UIColor { + static var stepikOverlayVioletBackground: UIColor { .dynamic( light: UIColor.stepikVioletFixed.withAlphaComponent(0.12), dark: ColorPalette.violet05.withAlphaComponent(0.12) ) } + /// The color to use for the content overlay with green. + static var stepikOverlayGreenBackground: UIColor { .stepikGreen.withAlphaComponent(0.12) } + + /// The color to use for the content overlay with blue. + static var stepikOverlayBlueBackground: UIColor { .stepikOverlayBlue.withAlphaComponent(0.12) } + + /// The color to use for the content overlay with onSurface. + static var stepikOverlayOnSurfaceBackground: UIColor { .onSurface.withAlphaComponent(0.04) } + // MARK: - Skeleton Gradient - static var skeletonGradientFirst: UIColor { @@ -726,6 +743,9 @@ private enum ColorPalette { // MARK: - Blue - + static let blueLightColorOverlay = UIColor(hex6: 0x2F80ED) + static let blueDarkColorOverlay = UIColor(hex6: 0x91C7FF) + // MARK: Normal /// Color to use in light/unspecified mode and with a high contrast level. diff --git a/Stepic/en.lproj/Localizable.strings b/Stepic/en.lproj/Localizable.strings index f922c0028d..eb90b5b1a3 100644 --- a/Stepic/en.lproj/Localizable.strings +++ b/Stepic/en.lproj/Localizable.strings @@ -509,6 +509,22 @@ CourseInfoTitleCertificate = "Certificate"; CourseInfoTitleCertificateDetails = "Certificate details"; CourseInfoTabNews = "News"; CourseInfoTabNewsEmptyNews = "No News"; +CourseInfoTabNewsPublishCountTitle = "Published"; +CourseInfoTabNewsQueueCountTitle = "Queued"; +CourseInfoTabNewsSentCountTitle = "Sent"; +CourseInfoTabNewsOpenCountTitle = "Opened"; +CourseInfoTabNewsClickCountTitle = "Clicked"; +CourseInfoTabNewsBadgeComposing = "Draft"; +CourseInfoTabNewsBadgeScheduled = "Scheduled"; +CourseInfoTabNewsBadgeSending = "Active"; +CourseInfoTabNewsBadgeSent = "Sent"; +CourseInfoTabNewsBadgeOnEvent = "By Event"; +CourseInfoTabNewsBadgeOneTime = "One-time"; +CourseInfoTabNewsDateOnEventComposing = "From %@"; +CourseInfoTabNewsDateOnEventSending = "From %@"; +CourseInfoTabNewsDateOnEventSent = "%@ - %@"; +CourseInfoTabNewsDateOneTimeScheduled = "Scheduled for %@"; +CourseInfoTabNewsDateOneTimeSending = "From %@"; /* Course revenue */ CourseRevenueTitle = "Revenue"; diff --git a/Stepic/ru.lproj/Localizable.strings b/Stepic/ru.lproj/Localizable.strings index 00b6bb46d2..96ac50faed 100644 --- a/Stepic/ru.lproj/Localizable.strings +++ b/Stepic/ru.lproj/Localizable.strings @@ -511,6 +511,22 @@ CourseInfoTitleCertificate = "Сертификат"; CourseInfoTitleCertificateDetails = "Подробнее о сертификате"; CourseInfoTabNews = "Новости"; CourseInfoTabNewsEmptyNews = "Нет новостей"; +CourseInfoTabNewsPublishCountTitle = "Рассылок сделано"; +CourseInfoTabNewsQueueCountTitle = "Видят на странице"; +CourseInfoTabNewsSentCountTitle = "Получили на почту"; +CourseInfoTabNewsOpenCountTitle = "Открыли письмо"; +CourseInfoTabNewsClickCountTitle = "Кликнули по ссылкам"; +CourseInfoTabNewsBadgeComposing = "Черновик"; +CourseInfoTabNewsBadgeScheduled = "Запланирована"; +CourseInfoTabNewsBadgeSending = "Рассылается"; +CourseInfoTabNewsBadgeSent = "Отправлена"; +CourseInfoTabNewsBadgeOnEvent = "По событию"; +CourseInfoTabNewsBadgeOneTime = "Разовая"; +CourseInfoTabNewsDateOnEventComposing = "С %@"; +CourseInfoTabNewsDateOnEventSending = "С %@"; +CourseInfoTabNewsDateOnEventSent = "%@ - %@"; +CourseInfoTabNewsDateOneTimeScheduled = "Запланирована на %@"; +CourseInfoTabNewsDateOneTimeSending = "С %@"; /* Course revenue */ CourseRevenueTitle = "Доходы"; diff --git a/StepicTests/Info-Develop.plist b/StepicTests/Info-Develop.plist index 62e53236a0..4c15f76948 100644 --- a/StepicTests/Info-Develop.plist +++ b/StepicTests/Info-Develop.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.191-develop + 1.192-develop CFBundleSignature ???? CFBundleVersion - 375 + 378 diff --git a/StepicTests/Info-Production.plist b/StepicTests/Info-Production.plist index fb4a51cdeb..3740e13afb 100644 --- a/StepicTests/Info-Production.plist +++ b/StepicTests/Info-Production.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.191 + 1.192 CFBundleSignature ???? CFBundleVersion - 375 + 378 diff --git a/StepicTests/Info-Release.plist b/StepicTests/Info-Release.plist index 37bfc36b19..c695033d85 100644 --- a/StepicTests/Info-Release.plist +++ b/StepicTests/Info-Release.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.191-release + 1.192-release CFBundleSignature ???? CFBundleVersion - 375 + 378 diff --git a/StepicUITests/Info-Develop.plist b/StepicUITests/Info-Develop.plist index be8cec6f85..f74584d2be 100644 --- a/StepicUITests/Info-Develop.plist +++ b/StepicUITests/Info-Develop.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.191-develop + 1.192-develop CFBundleVersion - 375 + 378 diff --git a/StepicUITests/Info-Production.plist b/StepicUITests/Info-Production.plist index 13a605d20b..2f54aeddf5 100644 --- a/StepicUITests/Info-Production.plist +++ b/StepicUITests/Info-Production.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.191 + 1.192 CFBundleVersion - 375 + 378 diff --git a/StepicUITests/Info-Release.plist b/StepicUITests/Info-Release.plist index e1b9f62a7c..8c0a523a4e 100644 --- a/StepicUITests/Info-Release.plist +++ b/StepicUITests/Info-Release.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.191-release + 1.192-release CFBundleVersion - 375 + 378 diff --git a/StepicWidget/Info-Develop.plist b/StepicWidget/Info-Develop.plist index 42bf267608..959a6ba20f 100644 --- a/StepicWidget/Info-Develop.plist +++ b/StepicWidget/Info-Develop.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.191-develop + 1.192-develop CFBundleVersion - 375 + 378 NSExtension NSExtensionPointIdentifier diff --git a/StepicWidget/Info-Production.plist b/StepicWidget/Info-Production.plist index 7b92d53042..aa43812050 100644 --- a/StepicWidget/Info-Production.plist +++ b/StepicWidget/Info-Production.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.191 + 1.192 CFBundleVersion - 375 + 378 NSExtension NSExtensionPointIdentifier diff --git a/StepicWidget/Info-Release.plist b/StepicWidget/Info-Release.plist index 98af5c7aab..4ef0bbbdec 100644 --- a/StepicWidget/Info-Release.plist +++ b/StepicWidget/Info-Release.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.191-release + 1.192-release CFBundleVersion - 375 + 378 NSExtension NSExtensionPointIdentifier diff --git a/StickerPackExtension/Info-Develop.plist b/StickerPackExtension/Info-Develop.plist index 9f04544cdf..0f277e23fa 100644 --- a/StickerPackExtension/Info-Develop.plist +++ b/StickerPackExtension/Info-Develop.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.191-develop + 1.192-develop CFBundleVersion - 375 + 378 NSExtension NSExtensionPointIdentifier diff --git a/StickerPackExtension/Info-Production.plist b/StickerPackExtension/Info-Production.plist index b2506dae01..54afa03019 100644 --- a/StickerPackExtension/Info-Production.plist +++ b/StickerPackExtension/Info-Production.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.191 + 1.192 CFBundleVersion - 375 + 378 NSExtension NSExtensionPointIdentifier diff --git a/StickerPackExtension/Info-Release.plist b/StickerPackExtension/Info-Release.plist index 5934574ada..5804940c43 100644 --- a/StickerPackExtension/Info-Release.plist +++ b/StickerPackExtension/Info-Release.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.191-release + 1.192-release CFBundleVersion - 375 + 378 NSExtension NSExtensionPointIdentifier diff --git a/fastlane/release-notes.txt b/fastlane/release-notes.txt index aa330bb75f..4677b71558 100644 --- a/fastlane/release-notes.txt +++ b/fastlane/release-notes.txt @@ -1,8 +1,13 @@ Что тестировать: -- Новости курса / UI APPS-3423 -- Сделать описание подборок в приложении читабельным APPS-3435 +- Поддержка iOS 15 SDK APPS-3406 +- Новости курса / UI для преподавателей APPS-3461 +- Экран отзывов пользователя / Оптимизация запросов APPS-3449 - Рецензирование: * Отображение шагов для преподавателей APPS-3397 * UI контейнера APPS-3400 * Логика контейнера APPS-3402 - * Поддержка квизов APPS-3403 \ No newline at end of file + * Поддержка квизов APPS-3403 +- Доходы по курсу: + * Контейнер экрана доходов по курсу APPS-3337 + * UI списка транзакций и экран с детальной информацией о транзакции APPS-3369 + * UI экрана доходов о курсе с группировкой по месяцам APPS-3365 \ No newline at end of file