From c194ec7f1cac822eb2ac8de2b0241bf732cb1f04 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Wed, 29 Jan 2020 14:00:50 +0300 Subject: [PATCH 01/12] Remove about course A/B (#634) * Remove a/b test --- Stepic.xcodeproj/project.pbxproj | 4 ---- .../AboutCourseStringSplitTest.swift | 23 ------------------- .../ActiveSplitTestsContainer.swift | 2 +- .../CourseInfoTabInfoPresenter.swift | 15 +++--------- 4 files changed, 4 insertions(+), 40 deletions(-) delete mode 100644 Stepic/Analytics/SplitTests/ActiveTests/AboutCourseStringSplitTest.swift diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index 833aa5fcbf..ab4cda42e0 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -641,7 +641,6 @@ 2CC0754720177A2E004A6005 /* AdaptiveStatsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC0754620177A2E004A6005 /* AdaptiveStatsViewController.swift */; }; 2CC16BA923875DE30000EF36 /* DiscussionsSkeletonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC16BA823875DE30000EF36 /* DiscussionsSkeletonView.swift */; }; 2CC2A78E235DE26700B2DC44 /* DiscussionsLoadMoreTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC2A78D235DE26700B2DC44 /* DiscussionsLoadMoreTableViewCell.swift */; }; - 2CC3093D23CF2740005F49C1 /* AboutCourseStringSplitTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC3093C23CF273F005F49C1 /* AboutCourseStringSplitTest.swift */; }; 2CC3518A1F682A02004255B6 /* SocialAuthCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC351881F682A02004255B6 /* SocialAuthCollectionViewCell.swift */; }; 2CC3518B1F682A02004255B6 /* SocialAuthCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CC351891F682A02004255B6 /* SocialAuthCollectionViewCell.xib */; }; 2CC3518F1F682B6C004255B6 /* SocialAuthHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC3518E1F682B6C004255B6 /* SocialAuthHeaderView.swift */; }; @@ -1798,7 +1797,6 @@ 2CC0754620177A2E004A6005 /* AdaptiveStatsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveStatsViewController.swift; sourceTree = ""; }; 2CC16BA823875DE30000EF36 /* DiscussionsSkeletonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscussionsSkeletonView.swift; sourceTree = ""; }; 2CC2A78D235DE26700B2DC44 /* DiscussionsLoadMoreTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscussionsLoadMoreTableViewCell.swift; sourceTree = ""; }; - 2CC3093C23CF273F005F49C1 /* AboutCourseStringSplitTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutCourseStringSplitTest.swift; sourceTree = ""; }; 2CC351851F6827BE004255B6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Auth.storyboard; sourceTree = ""; }; 2CC351881F682A02004255B6 /* SocialAuthCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocialAuthCollectionViewCell.swift; sourceTree = ""; }; 2CC351891F682A02004255B6 /* SocialAuthCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SocialAuthCollectionViewCell.xib; sourceTree = ""; }; @@ -3762,7 +3760,6 @@ 2C37968421ABED9500BC7E62 /* ActiveTests */ = { isa = PBXGroup; children = ( - 2CC3093C23CF273F005F49C1 /* AboutCourseStringSplitTest.swift */, 08421BCA21764FC400E8A81B /* ActiveSplitTestsContainer.swift */, ); path = ActiveTests; @@ -6763,7 +6760,6 @@ 2CE8391320C8102300FE3672 /* AchievementBadgeView.swift in Sources */, 08F4859A1C57868E000165AA /* TextReply.swift in Sources */, 2C4BE7E0221328E700AEAC34 /* CourseReview+CoreDataProperties.swift in Sources */, - 2CC3093D23CF2740005F49C1 /* AboutCourseStringSplitTest.swift in Sources */, 080E1ACC212571D9006B58A9 /* StoryPartViewFactory.swift in Sources */, 0828FF831BC800C0000AFEA7 /* JSONSerializable.swift in Sources */, 086D5B3F20127A25000F7715 /* Tooltip.swift in Sources */, diff --git a/Stepic/Analytics/SplitTests/ActiveTests/AboutCourseStringSplitTest.swift b/Stepic/Analytics/SplitTests/ActiveTests/AboutCourseStringSplitTest.swift deleted file mode 100644 index d275ae7558..0000000000 --- a/Stepic/Analytics/SplitTests/ActiveTests/AboutCourseStringSplitTest.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation - -final class AboutCourseStringSplitTest: SplitTestProtocol { - typealias GroupType = Group - - static let identifier = "about_course_string" - static let minParticipatingStartVersion = "1.109" - - var currentGroup: Group - var analytics: ABAnalyticsServiceProtocol - - init(currentGroup: Group, analytics: ABAnalyticsServiceProtocol) { - self.currentGroup = currentGroup - self.analytics = analytics - } - - enum Group: String, SplitTestGroupProtocol { - case control - case test - - static var groups: [Group] = [.control, .test] - } -} diff --git a/Stepic/Analytics/SplitTests/ActiveTests/ActiveSplitTestsContainer.swift b/Stepic/Analytics/SplitTests/ActiveTests/ActiveSplitTestsContainer.swift index a2e47bccf4..51e54c3fa8 100644 --- a/Stepic/Analytics/SplitTests/ActiveTests/ActiveSplitTestsContainer.swift +++ b/Stepic/Analytics/SplitTests/ActiveTests/ActiveSplitTestsContainer.swift @@ -15,6 +15,6 @@ final class ActiveSplitTestsContainer { ) static func setActiveTestsGroups() { - self.splitTestingService.fetchSplitTest(AboutCourseStringSplitTest.self).setSplitTestGroup() + // There are no A/B tests now } } diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabInfo/CourseInfoTabInfoPresenter.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabInfo/CourseInfoTabInfoPresenter.swift index d04355928b..3560b7af7e 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabInfo/CourseInfoTabInfoPresenter.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabInfo/CourseInfoTabInfoPresenter.swift @@ -7,11 +7,6 @@ protocol CourseInfoTabInfoPresenterProtocol { final class CourseInfoTabInfoPresenter: CourseInfoTabInfoPresenterProtocol { weak var viewController: CourseInfoTabInfoViewControllerProtocol? - private let splitTestingService = SplitTestingService( - analyticsService: AnalyticsUserProperties(), - storage: UserDefaults.standard - ) - func presentCourseInfo(response: CourseInfoTabInfo.InfoLoad.Response) { var viewModel: CourseInfoTabInfo.InfoLoad.ViewModel @@ -50,13 +45,9 @@ final class CourseInfoTabInfoPresenter: CourseInfoTabInfoPresenterProtocol { : nil let aboutText: String = { - var text = course.summary - if AboutCourseStringSplitTest.shouldParticipate { - let splitTest = self.splitTestingService.fetchSplitTest(AboutCourseStringSplitTest.self) - if case .test = splitTest.currentGroup { - text = course.courseDescription - } - } + let text = course.courseDescription.isEmpty + ? course.summary + : course.courseDescription return text.trimmingCharacters(in: .whitespaces) }() From e3f3716572d0a4b4c5c08a373a95ada11c0b12f0 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Wed, 29 Jan 2020 14:18:37 +0300 Subject: [PATCH 02/12] Update dependencies 1.111 (#636) * Bump SDWebImage from 5.5.1 to 5.5.2 Bumps [SDWebImage](https://github.com/SDWebImage/SDWebImage) from 5.5.1 to 5.5.2. - [Release notes](https://github.com/SDWebImage/SDWebImage/releases/tag/5.5.2) - [Commits](https://github.com/SDWebImage/SDWebImage/compare/5.5.1...5.5.2) * Bump Agrume from 5.6.0 to 5.6.1 Bumps [Agrume](https://github.com/JanGorman/Agrume) from 5.6.0 to 5.6.1. - [Release notes](https://github.com/JanGorman/Agrume/releases/tag/5.6.1) - [Commits](https://github.com/JanGorman/Agrume/compare/5.6.0...5.6.1) --- Podfile | 4 ++-- Podfile.lock | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Podfile b/Podfile index 00dfb631a9..191f38d5f6 100644 --- a/Podfile +++ b/Podfile @@ -8,7 +8,7 @@ def shared_pods pod 'Alamofire', '4.9.1' pod 'Atributika', '4.9.4' pod 'SwiftyJSON', '5.0.0' - pod 'SDWebImage', '5.5.1' + pod 'SDWebImage', '5.5.2' pod 'SVGKit', :git => 'https://github.com/SVGKit/SVGKit.git', :branch => '2.x' pod 'Logging', '1.2.0' pod 'Fabric', '1.10.2' @@ -54,7 +54,7 @@ def all_pods pod 'Presentr', '1.9' - pod 'Agrume', '5.6.0' + pod 'Agrume', '5.6.1' pod 'Highlightr', '2.1.0' pod 'TTTAttributedLabel', '2.0.0' pod 'lottie-ios', '2.5.3' diff --git a/Podfile.lock b/Podfile.lock index 2bc94d48f4..54a16f4bff 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,6 +1,6 @@ PODS: - ActionSheetPicker-3.0 (2.4.0) - - Agrume (5.6.0): + - Agrume (5.6.1): - SwiftyGif - Alamofire (4.9.1) - Amplitude-iOS (4.9.3) @@ -158,9 +158,9 @@ PODS: - Protobuf (3.11.2) - Quick (2.2.0) - Reveal-SDK (24) - - SDWebImage (5.5.1): - - SDWebImage/Core (= 5.5.1) - - SDWebImage/Core (5.5.1) + - SDWebImage (5.5.2): + - SDWebImage/Core (= 5.5.2) + - SDWebImage/Core (5.5.2) - SnapKit (5.0.1) - STRegex (2.1.0) - SVGKit (2.1.0): @@ -188,7 +188,7 @@ PODS: DEPENDENCIES: - ActionSheetPicker-3.0 (= 2.4.0) - - Agrume (= 5.6.0) + - Agrume (= 5.6.1) - Alamofire (= 4.9.1) - Amplitude-iOS (= 4.9.3) - Atributika (= 4.9.4) @@ -220,7 +220,7 @@ DEPENDENCIES: - PromiseKit (= 6.13.0) - Quick (= 2.2.0) - Reveal-SDK - - SDWebImage (= 5.5.1) + - SDWebImage (= 5.5.2) - SnapKit (= 5.0.1) - STRegex (= 2.1.0) - SVGKit (from `https://github.com/SVGKit/SVGKit.git`, branch `2.x`) @@ -319,7 +319,7 @@ CHECKOUT OPTIONS: SPEC CHECKSUMS: ActionSheetPicker-3.0: c68e1f8355828b4e1c823fa87185aacfef5e6fe3 - Agrume: c3e5858b50bbfb32611898624d7da017f992f1db + Agrume: 101c9874313f9e33b70c477e094e40a2f7e56910 Alamofire: 85e8a02c69d6020a0d734f6054870d7ecb75cf18 Amplitude-iOS: 122e026c44db8460e5efcf84859aa290a0ae9786 Atributika: 643a248e2dd8b4d74b23b53ea31393cab0a01d94 @@ -368,7 +368,7 @@ SPEC CHECKSUMS: Protobuf: dd1aaea7140debfe4dd0683fb8ef208e527ae153 Quick: 7fb19e13be07b5dfb3b90d4f9824c855a11af40e Reveal-SDK: 5d7e56b8f018c0a88b3a2c10bf68d598bbd3b071 - SDWebImage: 1245d058b7b8f59adef7a6da6bbafd4f1ab67041 + SDWebImage: 4d5c027c935438f341ed33dbac53ff9f479922ca SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb STRegex: dfa420d93d8c1402956233b3879ec1fc14b45fbe SVGKit: 8a2fc74258bdb2abb54d3b65f3dd68b0277a9c4d @@ -385,6 +385,6 @@ SPEC CHECKSUMS: VK-ios-sdk: 62a10b6571fbcda0657f455fedce7fedf55b4cd0 YandexMobileMetrica: edb00e8af2903290e142ba4c488adf8d394e828a -PODFILE CHECKSUM: ddd8fc897562a54318454d89332f656049ae9093 +PODFILE CHECKSUM: c2be7fc1245e747fb75a579749c1bc0cb6c2b054 COCOAPODS: 1.8.4 From 0c7701eb6a4a7bb0ff969cb230eb15200e0cbe6b Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Wed, 29 Jan 2020 16:29:47 +0300 Subject: [PATCH 03/12] Continue playback after video quality changed (#635) * Continue playing after video quality changed * handle autoplay on playback did end and when in background --- Stepic/StepikVideoPlayerViewController.swift | 48 ++++++++++++++++---- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/Stepic/StepikVideoPlayerViewController.swift b/Stepic/StepikVideoPlayerViewController.swift index 145ce9d68e..ba4f10af31 100644 --- a/Stepic/StepikVideoPlayerViewController.swift +++ b/Stepic/StepikVideoPlayerViewController.swift @@ -207,6 +207,9 @@ final class StepikVideoPlayerViewController: UIViewController { private var isPlayerControlsVisible = true private var hidePlayerControlsTimer: Timer? + /// This property will be set to `true` when player playback did end and device, not in the foreground state. + private var shouldShowAutoplayOnPlayerReady = false + private var videoInBackgroundTooltip: Tooltip? override var prefersStatusBarHidden: Bool { true } @@ -241,6 +244,11 @@ final class StepikVideoPlayerViewController: UIViewController { } } + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.saveCurrentPlayerTime() + } + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) @@ -258,7 +266,6 @@ final class StepikVideoPlayerViewController: UIViewController { MPRemoteCommandCenter.shared().togglePlayPauseCommand.removeTarget(self) NotificationCenter.default.removeObserver(self) Self.logger.info("StepikVideoPlayerViewController :: did deinit") - self.saveCurrentPlayerTime() self.hidePlayerControlsTimer?.invalidate() } @@ -599,9 +606,11 @@ final class StepikVideoPlayerViewController: UIViewController { } private func saveCurrentPlayerTime() { - let time = self.player.currentTime != self.player.maximumDuration ? self.player.currentTime : 0.0 - self.video.playTime = time - CoreDataHelper.shared.save() + DispatchQueue.main.async { + let time = self.player.currentTime != self.player.maximumDuration ? self.player.currentTime : 0.0 + self.video.playTime = max(0, time) + CoreDataHelper.shared.save() + } } @IBAction @@ -773,13 +782,27 @@ final class StepikVideoPlayerViewController: UIViewController { extension StepikVideoPlayerViewController: PlayerDelegate { func playerReady(_ player: Player) { - guard player.playbackState == .stopped || !self.isPlayerPassedReadyState else { + Self.logger.info("StepikVideoPlayerViewController :: player is ready to display") + + if self.shouldShowAutoplayOnPlayerReady { + self.shouldShowAutoplayOnPlayerReady = false + + self.setAutoplayControlsHidden(false) + if self.autoplayPreferenceSwitch.isOn { + self.startAutoplayCountdown() + } + return } - self.isPlayerPassedReadyState = true + let isPlayerFirstTimeReady = player.playbackState == .stopped || !self.isPlayerPassedReadyState + let isPlayerReadyAfterVideoQualityChanged = player.playbackState == .paused && self.isPlayerPassedReadyState - Self.logger.info("StepikVideoPlayerViewController :: player is ready to display") + guard isPlayerFirstTimeReady || isPlayerReadyAfterVideoQualityChanged else { + return + } + + self.isPlayerPassedReadyState = true self.activityIndicator.isHidden = true self.setTimeParametersAfterPlayerIsReady() @@ -813,9 +836,13 @@ extension StepikVideoPlayerViewController: PlayerDelegate { Self.logger.info("StepikVideoPlayerViewController :: player playback state changed to \(player.playbackState)") } - func playerBufferingStateDidChange(_ player: Player) { } + func playerCurrentTimeDidChange(_ player: Player) { + if player.playbackState == .playing { + self.playerStartTime = max(0, player.currentTime) + } - func playerPlaybackWillStartFromBeginning(_ player: Player) { } + Self.logger.info("StepikVideoPlayerViewController :: player current time changed to \(player.currentTime)") + } func playerPlaybackDidEnd(_ player: Player) { self.setButtonPlaying(true) @@ -830,6 +857,9 @@ extension StepikVideoPlayerViewController: PlayerDelegate { if self.autoplayPreferenceSwitch.isOn { self.startAutoplayCountdown() } + self.shouldShowAutoplayOnPlayerReady = false + } else { + self.shouldShowAutoplayOnPlayerReady = true } } From bd5688ed9852b90cfb5f1574bd864e0bc6789cc5 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Wed, 29 Jan 2020 16:44:35 +0300 Subject: [PATCH 04/12] Bump YandexMobileMetrica from 3.8.2 to 3.9.2 (#637) Bumps [YandexMobileMetrica](https://github.com/yandexmobile/metrica-sdk-ios) from 3.8.2 to 3.9.2 - [Release notes](https://github.com/yandexmobile/metrica-sdk-ios/releases/tag/3.9.2) --- Podfile | 2 +- Podfile.lock | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Podfile b/Podfile index 191f38d5f6..eeb6cfa741 100644 --- a/Podfile +++ b/Podfile @@ -35,7 +35,7 @@ def all_pods pod 'Firebase/Analytics' , '6.14.0' pod 'Firebase/RemoteConfig', '6.14.0' - pod 'YandexMobileMetrica/Dynamic', '3.8.2' + pod 'YandexMobileMetrica/Dynamic', '3.9.2' pod 'Amplitude-iOS', '4.9.3' pod 'Branch', '0.31.0' diff --git a/Podfile.lock b/Podfile.lock index 54a16f4bff..2c5506b231 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -179,11 +179,11 @@ PODS: - TUSafariActivity (1.0.4) - URITemplate (3.0.0) - VK-ios-sdk (1.5.1) - - YandexMobileMetrica/Dynamic (3.8.2): - - YandexMobileMetrica/Dynamic/Core (= 3.8.2) - - YandexMobileMetrica/Dynamic/Crashes (= 3.8.2) - - YandexMobileMetrica/Dynamic/Core (3.8.2) - - YandexMobileMetrica/Dynamic/Crashes (3.8.2): + - YandexMobileMetrica/Dynamic (3.9.2): + - YandexMobileMetrica/Dynamic/Core (= 3.9.2) + - YandexMobileMetrica/Dynamic/Crashes (= 3.9.2) + - YandexMobileMetrica/Dynamic/Core (3.9.2) + - YandexMobileMetrica/Dynamic/Crashes (3.9.2): - YandexMobileMetrica/Dynamic/Core DEPENDENCIES: @@ -233,7 +233,7 @@ DEPENDENCIES: - TTTAttributedLabel (= 2.0.0) - TUSafariActivity (= 1.0.4) - VK-ios-sdk (= 1.5.1) - - YandexMobileMetrica/Dynamic (= 3.8.2) + - YandexMobileMetrica/Dynamic (= 3.9.2) SPEC REPOS: https://github.com/CocoaPods/Specs.git: @@ -383,8 +383,8 @@ SPEC CHECKSUMS: TUSafariActivity: afc55a00965377939107ce4fdc7f951f62454546 URITemplate: 58e0d47f967006c5d59888af5356c4a8ed3b197d VK-ios-sdk: 62a10b6571fbcda0657f455fedce7fedf55b4cd0 - YandexMobileMetrica: edb00e8af2903290e142ba4c488adf8d394e828a + YandexMobileMetrica: ea82087806f175981c9eccbb8e35f1d4ee35d788 -PODFILE CHECKSUM: c2be7fc1245e747fb75a579749c1bc0cb6c2b054 +PODFILE CHECKSUM: cd262647e966daed303a0675002ffac8f97cf697 COCOAPODS: 1.8.4 From e1c735fe95c0c9dcfa810a286bbdccccf41b3384 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Wed, 29 Jan 2020 18:08:35 +0300 Subject: [PATCH 05/12] Refactor rename modules with New prefix (#638) * Refactor rename NewLesson -> Lesson * Refactor rename NewStep -> Step --- Stepic.xcodeproj/project.pbxproj | 148 +++++++++--------- Stepic/LastStepRouter.swift | 2 +- .../Services/DeepLinks/DeepLinkRouter.swift | 2 +- .../StepsControllerDeepLinkRouter.swift | 4 +- .../CourseInfo/CourseInfoViewController.swift | 2 +- .../LessonAssembly.swift} | 16 +- .../LessonDataFlow.swift} | 10 +- .../LessonInteractor.swift} | 40 ++--- .../LessonPresenter.swift} | 62 ++++---- .../LessonProvider.swift} | 4 +- .../LessonViewController.swift} | 78 +++++---- .../LessonViewModel.swift} | 2 +- .../Views/LessonInfoTooltipView.swift | 0 .../Views/TabBar/StepTabBarButton.swift | 0 .../Quizzes/BaseQuiz/BaseQuizAssembly.swift | 2 +- .../Quizzes/BaseQuiz/BaseQuizPresenter.swift | 18 +-- .../ChildProtocols/QuizAssembly.swift | 2 +- .../Solution/SolutionViewController.swift | 2 +- .../InputOutput/StepInputProtocol.swift} | 2 +- .../InputOutput/StepOutputProtocol.swift} | 2 +- .../StepAssembly.swift} | 16 +- .../StepDataFlow.swift} | 4 +- .../StepInteractor.swift} | 59 +++---- .../StepPresenter.swift} | 52 +++--- .../StepProvider.swift} | 10 +- .../NewStepView.swift => Step/StepView.swift} | 50 +++--- .../StepViewController.swift} | 99 ++++++------ .../StepViewModel.swift} | 8 +- .../Views/StepControlsView.swift | 0 .../Views/StepDiscussionsButton.swift | 0 .../Views/StepNavigationButton.swift | 0 .../Views/StepStatisticsView.swift | 0 .../Views/StepVideoPreviewView.swift | 0 .../NewStepViewControllerTests.swift | 28 ++-- 34 files changed, 360 insertions(+), 364 deletions(-) rename Stepic/Sources/Modules/{NewLesson/NewLessonAssembly.swift => Lesson/LessonAssembly.swift} (84%) rename Stepic/Sources/Modules/{NewLesson/NewLessonDataFlow.swift => Lesson/LessonDataFlow.swift} (95%) rename Stepic/Sources/Modules/{NewLesson/NewLessonInteractor.swift => Lesson/LessonInteractor.swift} (89%) rename Stepic/Sources/Modules/{NewLesson/NewLessonPresenter.swift => Lesson/LessonPresenter.swift} (68%) rename Stepic/Sources/Modules/{NewLesson/NewLessonProvider.swift => Lesson/LessonProvider.swift} (99%) rename Stepic/Sources/Modules/{NewLesson/NewLessonViewController.swift => Lesson/LessonViewController.swift} (85%) rename Stepic/Sources/Modules/{NewLesson/NewLessonViewModel.swift => Lesson/LessonViewModel.swift} (91%) rename Stepic/Sources/Modules/{NewLesson => Lesson}/Views/LessonInfoTooltipView.swift (100%) rename Stepic/Sources/Modules/{NewLesson => Lesson}/Views/TabBar/StepTabBarButton.swift (100%) rename Stepic/Sources/Modules/{NewStep/InputOutput/NewStepInputProtocol.swift => Step/InputOutput/StepInputProtocol.swift} (82%) rename Stepic/Sources/Modules/{NewStep/InputOutput/NewStepOutputProtocol.swift => Step/InputOutput/StepOutputProtocol.swift} (86%) rename Stepic/Sources/Modules/{NewStep/NewStepAssembly.swift => Step/StepAssembly.swift} (59%) rename Stepic/Sources/Modules/{NewStep/NewStepDataFlow.swift => Step/StepDataFlow.swift} (98%) rename Stepic/Sources/Modules/{NewStep/NewStepInteractor.swift => Step/StepInteractor.swift} (74%) rename Stepic/Sources/Modules/{NewStep/NewStepPresenter.swift => Step/StepPresenter.swift} (75%) rename Stepic/Sources/Modules/{NewStep/NewStepProvider.swift => Step/StepProvider.swift} (95%) rename Stepic/Sources/Modules/{NewStep/NewStepView.swift => Step/StepView.swift} (83%) rename Stepic/Sources/Modules/{NewStep/NewStepViewController.swift => Step/StepViewController.swift} (76%) rename Stepic/Sources/Modules/{NewStep/NewStepViewModel.swift => Step/StepViewModel.swift} (79%) rename Stepic/Sources/Modules/{NewStep => Step}/Views/StepControlsView.swift (100%) rename Stepic/Sources/Modules/{NewStep => Step}/Views/StepDiscussionsButton.swift (100%) rename Stepic/Sources/Modules/{NewStep => Step}/Views/StepNavigationButton.swift (100%) rename Stepic/Sources/Modules/{NewStep => Step}/Views/StepStatisticsView.swift (100%) rename Stepic/Sources/Modules/{NewStep => Step}/Views/StepVideoPreviewView.swift (100%) diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index ab4cda42e0..dec603f528 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -465,7 +465,7 @@ 2C47A164206284B1003E87EC /* NotificationRequestAlertContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C47A163206284B1003E87EC /* NotificationRequestAlertContext.swift */; }; 2C48D5FA228D7D2C00739477 /* ProcessedContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C48D5F9228D7D2C00739477 /* ProcessedContentTextView.swift */; }; 2C48D5FC228D865E00739477 /* Guarantee+ThenableWithFallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C48D5FB228D865E00739477 /* Guarantee+ThenableWithFallback.swift */; }; - 2C48D5FE228ECF3E00739477 /* NewStepViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C48D5FD228ECF3D00739477 /* NewStepViewModel.swift */; }; + 2C48D5FE228ECF3E00739477 /* StepViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C48D5FD228ECF3D00739477 /* StepViewModel.swift */; }; 2C48D601228F0C6000739477 /* ContentProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C48D600228F0C5F00739477 /* ContentProcessor.swift */; }; 2C48D604228F0EF700739477 /* ContentProcessingRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C48D603228F0EF700739477 /* ContentProcessingRule.swift */; }; 2C48D606228F114400739477 /* HTMLExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C48D605228F114400739477 /* HTMLExtractor.swift */; }; @@ -704,7 +704,7 @@ 2CF24F642390040F002DBD0F /* BlockTypeSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CF24F632390040E002DBD0F /* BlockTypeSpec.swift */; }; 2CF24F67239004D8002DBD0F /* NewStepViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CF24F66239004D8002DBD0F /* NewStepViewControllerTests.swift */; }; 2CFC5ABC228ADFC400B5248A /* StepsPersistenceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFC5ABB228ADFC400B5248A /* StepsPersistenceService.swift */; }; - 2CFC5ABE228AF0B400B5248A /* NewLessonViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFC5ABD228AF0B400B5248A /* NewLessonViewModel.swift */; }; + 2CFC5ABE228AF0B400B5248A /* LessonViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFC5ABD228AF0B400B5248A /* LessonViewModel.swift */; }; 2CFDB1241F559F9A00B8035C /* AvatarImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFDB1231F559F9A00B8035C /* AvatarImageView.swift */; }; 2D6AB32933F1C93FCF849056 /* WriteCommentProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77A40A485EDD8A3E4AA0157 /* WriteCommentProvider.swift */; }; 3203AD6A1594995EDE114EA0 /* Pods_StepicTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 938533EAAD61D57EF139C60C /* Pods_StepicTests.framework */; }; @@ -714,26 +714,26 @@ 34577A9694176BFA7CAC950C /* ProfileEditDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9CD16032427B60FC4C99EF /* ProfileEditDataFlow.swift */; }; 348FD1B42836EEF4AFD23E9F /* NewMatchingQuizView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404D37E2105ABA9C150864A6 /* NewMatchingQuizView.swift */; }; 3609433482973280A509095A /* SolutionDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57CF9578B2EDF50A2193B2AB /* SolutionDataFlow.swift */; }; - 36D02A00641C747F6D18DB60 /* NewStepPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4737058E519D801DAB2131 /* NewStepPresenter.swift */; }; + 36D02A00641C747F6D18DB60 /* StepPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4737058E519D801DAB2131 /* StepPresenter.swift */; }; 36D7748CC6CAC2ACEFCEDC82 /* DownloadsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 878DD0046D63D70DBA7F0FE3 /* DownloadsDataFlow.swift */; }; 3A0DB3026F3DE2D4983186A3 /* DiscussionsAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = A458B834FD5BF8E1E16E5A0A /* DiscussionsAssembly.swift */; }; - 3CE76179FE70EA84C4627AF6 /* NewStepOutputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32A31B642B26D8A51B139F01 /* NewStepOutputProtocol.swift */; }; + 3CE76179FE70EA84C4627AF6 /* StepOutputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32A31B642B26D8A51B139F01 /* StepOutputProtocol.swift */; }; 3E2FBE43761DC0D89BD35ED1 /* NewSortingQuizInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6520F5CFAE56B9CFBA963C1D /* NewSortingQuizInteractor.swift */; }; 4152AF14D57F1AB90FB62C08 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DAE94A371813F1B0854FADE /* SettingsViewController.swift */; }; 46DDCD0E0E352C61FA053980 /* DownloadsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63EF797C1E4918832BBB1BD6 /* DownloadsView.swift */; }; 47AA3FAF72E7AD067ABF2804 /* NewFreeAnswerQuizDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF50F304BF0016F5B477EEC8 /* NewFreeAnswerQuizDataFlow.swift */; }; 49D8E231DE76283BD37FFD3B /* WriteCommentPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44BBB1022CD4FB280EB7DFB /* WriteCommentPresenter.swift */; }; - 4AC49732BD397C516D132297 /* NewLessonPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFC0A2EAEB9E30B54695D44A /* NewLessonPresenter.swift */; }; + 4AC49732BD397C516D132297 /* LessonPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFC0A2EAEB9E30B54695D44A /* LessonPresenter.swift */; }; 4E3A4636F1A4E8E38E0AB66B /* NewChoiceQuizDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAECD143E5E804E55A9DE096 /* NewChoiceQuizDataFlow.swift */; }; 528D865B4EA9962D11C7678B /* DownloadsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AEE8626753C6B246F063A92 /* DownloadsViewController.swift */; }; 5406504928626E0C7222456D /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C46910C13E674FBC565EA68 /* SettingsView.swift */; }; 580F18AF73EFFCBF1EDED227 /* NewStringQuizViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06D70A098825966D4584016F /* NewStringQuizViewController.swift */; }; 582E484FCF140403A5F7739B /* ProfileEditViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B304C7487F9B47AE32F4190D /* ProfileEditViewController.swift */; }; - 5DABE0AA5285D92AE83E5C3A /* NewLessonProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E8045E8645E07DAAA62D82 /* NewLessonProvider.swift */; }; - 5E6DBFD6BE7121D6E6C6C0D5 /* NewStepInputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21A89AE8D9541C3D75F0D343 /* NewStepInputProtocol.swift */; }; + 5DABE0AA5285D92AE83E5C3A /* LessonProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E8045E8645E07DAAA62D82 /* LessonProvider.swift */; }; + 5E6DBFD6BE7121D6E6C6C0D5 /* StepInputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21A89AE8D9541C3D75F0D343 /* StepInputProtocol.swift */; }; 5EA0648A0D1E82FCFECF6D4C /* NewCodeQuizFullscreenDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F285E88F0E449B536C9DE9 /* NewCodeQuizFullscreenDataFlow.swift */; }; 5F3549C23D2669F6C8A699B3 /* UnsupportedQuizView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ABB1CAB6E2FC592DAE34A68 /* UnsupportedQuizView.swift */; }; - 60B1FF909EC34FCFD6D2502D /* NewStepViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88372B078904014335CC5A4F /* NewStepViewController.swift */; }; + 60B1FF909EC34FCFD6D2502D /* StepViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88372B078904014335CC5A4F /* StepViewController.swift */; }; 612E40603ADA04D4496A2F6E /* NewCodeQuizFullscreenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F47B9DDB10472D2B2542B4 /* NewCodeQuizFullscreenViewController.swift */; }; 619E7185C28F57A5932341E6 /* WriteCourseReviewOutputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90E18232B2A8EC99F6ACBEF3 /* WriteCourseReviewOutputProtocol.swift */; }; 6286EAC5F762C008E9CD672C /* NewCodeQuizFullscreenAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FBB4E2D74ED45195B3EF372 /* NewCodeQuizFullscreenAssembly.swift */; }; @@ -1001,7 +1001,7 @@ 7614F02D6A4C7326419CB1EA /* SettingsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AEDAC6E765B0DE89099982A /* SettingsInteractor.swift */; }; 7AC15207BB66E845FDBEE234 /* NewCodeQuizFullscreenInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3025F71BBEA83B2F2D4EE286 /* NewCodeQuizFullscreenInteractor.swift */; }; 7D334A7F2599276A266E17AE /* WriteCommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C65429C78822944952072FB /* WriteCommentView.swift */; }; - 7FBB25F3FD251734F7E049DD /* NewStepDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49034C40838BCF58896D0118 /* NewStepDataFlow.swift */; }; + 7FBB25F3FD251734F7E049DD /* StepDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49034C40838BCF58896D0118 /* StepDataFlow.swift */; }; 81AA1C99009AE54671EA94AA /* EditStepPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ABF7F0EEB3C23FDB077812B /* EditStepPresenter.swift */; }; 857E1FD24A70F87BCA6EBABA /* NewStringQuizInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2111290D2E9D752A22AFC024 /* NewStringQuizInteractor.swift */; }; 85CD3504769C1B6C8C21661A /* NewMatchingQuizAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DC73A992112910D33A82457 /* NewMatchingQuizAssembly.swift */; }; @@ -1027,13 +1027,13 @@ AC5F57E43EC0844613551EFF /* EditStepAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80DF178AB92B327825602F51 /* EditStepAssembly.swift */; }; ADE0C828C6BE8588EAEA3BFC /* SettingsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29E3A2305742B76FF18CD6A /* SettingsDataFlow.swift */; }; B05BCD4B9FBDCF94AF7DB650 /* DiscussionsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B671AACF375B3ED49120185 /* DiscussionsPresenter.swift */; }; - B0D23296AB3FAC5BC6F45029 /* NewLessonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC193B8E01FF4A61441ABF13 /* NewLessonViewController.swift */; }; - B8042A238380ACC1F32082E1 /* NewStepAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D1AB142BF62815DF8939656 /* NewStepAssembly.swift */; }; + B0D23296AB3FAC5BC6F45029 /* LessonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC193B8E01FF4A61441ABF13 /* LessonViewController.swift */; }; + B8042A238380ACC1F32082E1 /* StepAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D1AB142BF62815DF8939656 /* StepAssembly.swift */; }; BD98999E70D8AE972B458DB5 /* DiscussionsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F646AB1EA500AD2EB797350 /* DiscussionsProvider.swift */; }; BF44B6E974F78C7738EA81A2 /* DiscussionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF03A1A429714E7C4A2EBA60 /* DiscussionsViewController.swift */; }; C01B4688B65BD5EC465F2322 /* SolutionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6B4AE0E203E899C885C05BC /* SolutionPresenter.swift */; }; C2937BCC91387BCF0B550CFB /* SettingsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D245891C69A7F666098C4F79 /* SettingsPresenter.swift */; }; - C31127DFC0924AFC68D0187C /* NewStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3FE676A699B1C33D4D0F5CC /* NewStepView.swift */; }; + C31127DFC0924AFC68D0187C /* StepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3FE676A699B1C33D4D0F5CC /* StepView.swift */; }; C45AFCD5C2566D6E63CF28EC /* ProfileEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 831CC7AB7FC20E336DB6813F /* ProfileEditView.swift */; }; C5C3CEDDFA57BFF4EDF0BBA0 /* ProfileEditPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0FAD2AD12244A99DAD7BE6 /* ProfileEditPresenter.swift */; }; C72AAB2E66F9618214B2B3CD /* NewStringQuizPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E495D6D30976CAD27AE1EB /* NewStringQuizPresenter.swift */; }; @@ -1050,18 +1050,18 @@ E2ED35FB098051B42A544BD9 /* NewSortingQuizViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76D90D632B5CFB603F1295 /* NewSortingQuizViewController.swift */; }; E31985EE51C589E6F169D77C /* ProfileEditProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B871095B7511E41F19281EE9 /* ProfileEditProvider.swift */; }; E36410A645B2D9008B9FD40B /* Pods_Stepic.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F98579F526E5A4D162C3356 /* Pods_Stepic.framework */; }; - E68F5C66A733F2B9F2C18920 /* NewStepProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 362684BA69196E87B5919671 /* NewStepProvider.swift */; }; + E68F5C66A733F2B9F2C18920 /* StepProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 362684BA69196E87B5919671 /* StepProvider.swift */; }; EBF9068859AF1AFB10EE4364 /* NewChoiceQuizAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD36917F532D0F83798B2F2 /* NewChoiceQuizAssembly.swift */; }; ECDAA11F9E600B3A7164DE22 /* UnsupportedQuizDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18C7F49ED17D7E9DF8A5A6D /* UnsupportedQuizDataFlow.swift */; }; EEE23D861EC13465CD0211C0 /* NewStringQuizView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79467CCB416E6C3CC9613912 /* NewStringQuizView.swift */; }; - F4FC5DFF1A9B86DE2724F95F /* NewLessonAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132A23540B83F9E4658F740C /* NewLessonAssembly.swift */; }; - F5EB81E67462BA3FD6B6CCCB /* NewLessonDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC5B5698955B5FF2B7EFB6F4 /* NewLessonDataFlow.swift */; }; + F4FC5DFF1A9B86DE2724F95F /* LessonAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132A23540B83F9E4658F740C /* LessonAssembly.swift */; }; + F5EB81E67462BA3FD6B6CCCB /* LessonDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC5B5698955B5FF2B7EFB6F4 /* LessonDataFlow.swift */; }; F81E6692BE2CD3711D84A5DF /* SolutionInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A729151A18944E2513E4B28 /* SolutionInteractor.swift */; }; FA91CB5A2F90947FEC242887 /* WriteCourseReviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB28C8741D8E5CBF35B3437 /* WriteCourseReviewViewController.swift */; }; FD4DF3653C56C80E65B7678B /* NewStringQuizDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACDDB66D0848F4DCD752795B /* NewStringQuizDataFlow.swift */; }; - FD78F24BD455BFB5E7D3C18F /* NewLessonInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 230630B59E23EF2D82208F85 /* NewLessonInteractor.swift */; }; + FD78F24BD455BFB5E7D3C18F /* LessonInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 230630B59E23EF2D82208F85 /* LessonInteractor.swift */; }; FE3A91E790CEB767447AC16A /* NewMatchingQuizViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7398B256EE198D1F6A6AF959 /* NewMatchingQuizViewController.swift */; }; - FE43FE17249BB2B4F6890F8D /* NewStepInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84763D78E2F327874DA51576 /* NewStepInteractor.swift */; }; + FE43FE17249BB2B4F6890F8D /* StepInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84763D78E2F327874DA51576 /* StepInteractor.swift */; }; FED534F473711C52863B4B7E /* NewFreeAnswerQuizInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 232D0A4BD87325BE0E568BF0 /* NewFreeAnswerQuizInteractor.swift */; }; FF6724819335FB28269A43B7 /* NewStringQuizAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 063F920E9867D0DB4C2CBA6A /* NewStringQuizAssembly.swift */; }; /* End PBXBuildFile section */ @@ -1501,14 +1501,14 @@ 08FEFC1B1F117257005CA0FB /* CodeSuggestionTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CodeSuggestionTableViewCell.xib; sourceTree = ""; }; 08FEFC201F127470005CA0FB /* AutocompleteWords.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutocompleteWords.swift; sourceTree = ""; }; 114BFE84178287AE99EAACEF /* SettingsProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsProvider.swift; sourceTree = ""; }; - 132A23540B83F9E4658F740C /* NewLessonAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewLessonAssembly.swift; sourceTree = ""; }; + 132A23540B83F9E4658F740C /* LessonAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LessonAssembly.swift; sourceTree = ""; }; 1C65429C78822944952072FB /* WriteCommentView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WriteCommentView.swift; sourceTree = ""; }; 20DD1856CD25464BAC75EE10 /* DiscussionsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DiscussionsInteractor.swift; sourceTree = ""; }; 2111290D2E9D752A22AFC024 /* NewStringQuizInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewStringQuizInteractor.swift; sourceTree = ""; }; - 21A89AE8D9541C3D75F0D343 /* NewStepInputProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewStepInputProtocol.swift; sourceTree = ""; }; + 21A89AE8D9541C3D75F0D343 /* StepInputProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StepInputProtocol.swift; sourceTree = ""; }; 227BB01DDF94CCF5E5EBBB43 /* NewFreeAnswerQuizViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewFreeAnswerQuizViewController.swift; sourceTree = ""; }; 22D74D3F78768D42D919852E /* WriteCourseReviewInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WriteCourseReviewInteractor.swift; sourceTree = ""; }; - 230630B59E23EF2D82208F85 /* NewLessonInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewLessonInteractor.swift; sourceTree = ""; }; + 230630B59E23EF2D82208F85 /* LessonInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LessonInteractor.swift; sourceTree = ""; }; 232D0A4BD87325BE0E568BF0 /* NewFreeAnswerQuizInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewFreeAnswerQuizInteractor.swift; sourceTree = ""; }; 233DE469779653213327B9C5 /* WriteCourseReviewView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WriteCourseReviewView.swift; sourceTree = ""; }; 278FE0A80C243768DA5EBD30 /* SolutionProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SolutionProvider.swift; sourceTree = ""; }; @@ -1618,7 +1618,7 @@ 2C47A163206284B1003E87EC /* NotificationRequestAlertContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationRequestAlertContext.swift; sourceTree = ""; }; 2C48D5F9228D7D2C00739477 /* ProcessedContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessedContentTextView.swift; sourceTree = ""; }; 2C48D5FB228D865E00739477 /* Guarantee+ThenableWithFallback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Guarantee+ThenableWithFallback.swift"; sourceTree = ""; }; - 2C48D5FD228ECF3D00739477 /* NewStepViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewStepViewModel.swift; sourceTree = ""; }; + 2C48D5FD228ECF3D00739477 /* StepViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepViewModel.swift; sourceTree = ""; }; 2C48D600228F0C5F00739477 /* ContentProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentProcessor.swift; sourceTree = ""; }; 2C48D603228F0EF700739477 /* ContentProcessingRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentProcessingRule.swift; sourceTree = ""; }; 2C48D605228F114400739477 /* HTMLExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLExtractor.swift; sourceTree = ""; }; @@ -1863,15 +1863,15 @@ 2CF425302024C114002D7305 /* en */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = en; path = en.lproj/overlay_simple.png; sourceTree = ""; }; 2CF6649122F307A500F2A899 /* Model_user_ is_organization.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Model_user_ is_organization.xcdatamodel"; sourceTree = ""; }; 2CFC5ABB228ADFC400B5248A /* StepsPersistenceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepsPersistenceService.swift; sourceTree = ""; }; - 2CFC5ABD228AF0B400B5248A /* NewLessonViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewLessonViewModel.swift; sourceTree = ""; }; + 2CFC5ABD228AF0B400B5248A /* LessonViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LessonViewModel.swift; sourceTree = ""; }; 2CFDA8181FBB3CFD0098A441 /* Model_sections_with_course_id_v21.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_sections_with_course_id_v21.xcdatamodel; sourceTree = ""; }; 2CFDB1231F559F9A00B8035C /* AvatarImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarImageView.swift; sourceTree = ""; }; 2D76D90D632B5CFB603F1295 /* NewSortingQuizViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewSortingQuizViewController.swift; sourceTree = ""; }; 3025F71BBEA83B2F2D4EE286 /* NewCodeQuizFullscreenInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewCodeQuizFullscreenInteractor.swift; sourceTree = ""; }; - 32A31B642B26D8A51B139F01 /* NewStepOutputProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewStepOutputProtocol.swift; sourceTree = ""; }; + 32A31B642B26D8A51B139F01 /* StepOutputProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StepOutputProtocol.swift; sourceTree = ""; }; 34BBD3EFC6CF6DE0ADC84018 /* NewSortingQuizPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewSortingQuizPresenter.swift; sourceTree = ""; }; 35F47B9DDB10472D2B2542B4 /* NewCodeQuizFullscreenViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewCodeQuizFullscreenViewController.swift; sourceTree = ""; }; - 362684BA69196E87B5919671 /* NewStepProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewStepProvider.swift; sourceTree = ""; }; + 362684BA69196E87B5919671 /* StepProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StepProvider.swift; sourceTree = ""; }; 36E041388C85D190467E74A5 /* NewChoiceQuizViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewChoiceQuizViewController.swift; sourceTree = ""; }; 36F53BF6D2A309CFC55A72D5 /* WriteCommentAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WriteCommentAssembly.swift; sourceTree = ""; }; 3750E7500C08250CD7F6FC2B /* EditStepInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditStepInteractor.swift; sourceTree = ""; }; @@ -1882,7 +1882,7 @@ 3E6741032B8AF7696CAC7126 /* NewFreeAnswerQuizAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewFreeAnswerQuizAssembly.swift; sourceTree = ""; }; 404D37E2105ABA9C150864A6 /* NewMatchingQuizView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewMatchingQuizView.swift; sourceTree = ""; }; 43C0A53DD377C21215A19DEB /* UnsupportedQuizAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UnsupportedQuizAssembly.swift; sourceTree = ""; }; - 49034C40838BCF58896D0118 /* NewStepDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewStepDataFlow.swift; sourceTree = ""; }; + 49034C40838BCF58896D0118 /* StepDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StepDataFlow.swift; sourceTree = ""; }; 49B8797DC84D64C5BAA84E76 /* Pods-Stepic.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Stepic.debug.xcconfig"; path = "Target Support Files/Pods-Stepic/Pods-Stepic.debug.xcconfig"; sourceTree = ""; }; 4A4D51541BF7AE1FCD2865C0 /* ProfileEditAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ProfileEditAssembly.swift; sourceTree = ""; }; 4ABF7F0EEB3C23FDB077812B /* EditStepPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditStepPresenter.swift; sourceTree = ""; }; @@ -2158,9 +2158,9 @@ 79467CCB416E6C3CC9613912 /* NewStringQuizView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewStringQuizView.swift; sourceTree = ""; }; 7D3417B44078A2AB57A0E63F /* NewCodeQuizFullscreenPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewCodeQuizFullscreenPresenter.swift; sourceTree = ""; }; 80DF178AB92B327825602F51 /* EditStepAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditStepAssembly.swift; sourceTree = ""; }; - 80E8045E8645E07DAAA62D82 /* NewLessonProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewLessonProvider.swift; sourceTree = ""; }; + 80E8045E8645E07DAAA62D82 /* LessonProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LessonProvider.swift; sourceTree = ""; }; 831CC7AB7FC20E336DB6813F /* ProfileEditView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ProfileEditView.swift; sourceTree = ""; }; - 84763D78E2F327874DA51576 /* NewStepInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewStepInteractor.swift; sourceTree = ""; }; + 84763D78E2F327874DA51576 /* StepInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StepInteractor.swift; sourceTree = ""; }; 861B96361FE1DF7F00773EDA /* CAGradientLayer+Init.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CAGradientLayer+Init.swift"; sourceTree = ""; }; 8622056A2055561F00F14255 /* PinsMapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinsMapView.swift; sourceTree = ""; }; 86624A721FC76578008E7E6C /* NotificationStatusesAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationStatusesAPI.swift; sourceTree = ""; }; @@ -2173,11 +2173,11 @@ 86BB7C05201953AF00063538 /* CongratulationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CongratulationViewController.swift; sourceTree = ""; }; 86BB7C06201953AF00063538 /* CongratulationViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CongratulationViewController.xib; sourceTree = ""; }; 878DD0046D63D70DBA7F0FE3 /* DownloadsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DownloadsDataFlow.swift; sourceTree = ""; }; - 88372B078904014335CC5A4F /* NewStepViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewStepViewController.swift; sourceTree = ""; }; + 88372B078904014335CC5A4F /* StepViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StepViewController.swift; sourceTree = ""; }; 899A181C0230E6359625710F /* DiscussionsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DiscussionsDataFlow.swift; sourceTree = ""; }; 8A729151A18944E2513E4B28 /* SolutionInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SolutionInteractor.swift; sourceTree = ""; }; 8C485A2D511BCFEA6E85B22F /* WriteCourseReviewDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WriteCourseReviewDataFlow.swift; sourceTree = ""; }; - 8D1AB142BF62815DF8939656 /* NewStepAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewStepAssembly.swift; sourceTree = ""; }; + 8D1AB142BF62815DF8939656 /* StepAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StepAssembly.swift; sourceTree = ""; }; 8DC73A992112910D33A82457 /* NewMatchingQuizAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewMatchingQuizAssembly.swift; sourceTree = ""; }; 90E18232B2A8EC99F6ACBEF3 /* WriteCourseReviewOutputProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WriteCourseReviewOutputProtocol.swift; sourceTree = ""; }; 938533EAAD61D57EF139C60C /* Pods_StepicTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_StepicTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -2190,7 +2190,7 @@ A484FA9C0CDF08A193A71381 /* NewMatchingQuizInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewMatchingQuizInteractor.swift; sourceTree = ""; }; A715786928D212A9475B378B /* EditStepDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditStepDataFlow.swift; sourceTree = ""; }; AACBF39DAC4A4A599F8949F2 /* WriteCommentInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WriteCommentInteractor.swift; sourceTree = ""; }; - AC5B5698955B5FF2B7EFB6F4 /* NewLessonDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewLessonDataFlow.swift; sourceTree = ""; }; + AC5B5698955B5FF2B7EFB6F4 /* LessonDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LessonDataFlow.swift; sourceTree = ""; }; ACDDB66D0848F4DCD752795B /* NewStringQuizDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewStringQuizDataFlow.swift; sourceTree = ""; }; AF89927A843447A9AE4C0AF4 /* NewChoiceQuizPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewChoiceQuizPresenter.swift; sourceTree = ""; }; B304C7487F9B47AE32F4190D /* ProfileEditViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ProfileEditViewController.swift; sourceTree = ""; }; @@ -2215,14 +2215,14 @@ D44BBB1022CD4FB280EB7DFB /* WriteCommentPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WriteCommentPresenter.swift; sourceTree = ""; }; D779F899F835A1724F0C1186 /* NewMatchingQuizPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewMatchingQuizPresenter.swift; sourceTree = ""; }; D7DBF5A43025E726B6D61E1E /* WriteCommentDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WriteCommentDataFlow.swift; sourceTree = ""; }; - DA4737058E519D801DAB2131 /* NewStepPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewStepPresenter.swift; sourceTree = ""; }; - DC193B8E01FF4A61441ABF13 /* NewLessonViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewLessonViewController.swift; sourceTree = ""; }; + DA4737058E519D801DAB2131 /* StepPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StepPresenter.swift; sourceTree = ""; }; + DC193B8E01FF4A61441ABF13 /* LessonViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LessonViewController.swift; sourceTree = ""; }; DCCE000FB8689122DC202C3E /* NewCodeQuizFullscreenOutputProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewCodeQuizFullscreenOutputProtocol.swift; sourceTree = ""; }; DF03A1A429714E7C4A2EBA60 /* DiscussionsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DiscussionsViewController.swift; sourceTree = ""; }; - DFC0A2EAEB9E30B54695D44A /* NewLessonPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewLessonPresenter.swift; sourceTree = ""; }; + DFC0A2EAEB9E30B54695D44A /* LessonPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LessonPresenter.swift; sourceTree = ""; }; E18C7F49ED17D7E9DF8A5A6D /* UnsupportedQuizDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UnsupportedQuizDataFlow.swift; sourceTree = ""; }; E29E3A2305742B76FF18CD6A /* SettingsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsDataFlow.swift; sourceTree = ""; }; - E3FE676A699B1C33D4D0F5CC /* NewStepView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewStepView.swift; sourceTree = ""; }; + E3FE676A699B1C33D4D0F5CC /* StepView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StepView.swift; sourceTree = ""; }; E6D840A9E95DE4D1CE6CEA7A /* SolutionViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SolutionViewController.swift; sourceTree = ""; }; E836B14C65BADD59C0CAA65B /* NewSortingQuizView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewSortingQuizView.swift; sourceTree = ""; }; ECD9168AF529D0D527B1DCDB /* Pods-StepicTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-StepicTests.debug.xcconfig"; path = "Target Support Files/Pods-StepicTests/Pods-StepicTests.debug.xcconfig"; sourceTree = ""; }; @@ -2257,19 +2257,19 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 04261228D8CE67BAF558EC33 /* NewLesson */ = { + 04261228D8CE67BAF558EC33 /* Lesson */ = { isa = PBXGroup; children = ( - 132A23540B83F9E4658F740C /* NewLessonAssembly.swift */, - AC5B5698955B5FF2B7EFB6F4 /* NewLessonDataFlow.swift */, - 230630B59E23EF2D82208F85 /* NewLessonInteractor.swift */, - DFC0A2EAEB9E30B54695D44A /* NewLessonPresenter.swift */, - 80E8045E8645E07DAAA62D82 /* NewLessonProvider.swift */, - DC193B8E01FF4A61441ABF13 /* NewLessonViewController.swift */, - 2CFC5ABD228AF0B400B5248A /* NewLessonViewModel.swift */, + 132A23540B83F9E4658F740C /* LessonAssembly.swift */, + AC5B5698955B5FF2B7EFB6F4 /* LessonDataFlow.swift */, + 230630B59E23EF2D82208F85 /* LessonInteractor.swift */, + DFC0A2EAEB9E30B54695D44A /* LessonPresenter.swift */, + 80E8045E8645E07DAAA62D82 /* LessonProvider.swift */, + DC193B8E01FF4A61441ABF13 /* LessonViewController.swift */, + 2CFC5ABD228AF0B400B5248A /* LessonViewModel.swift */, 2C7F782622708A520089FDD7 /* Views */, ); - path = NewLesson; + path = Lesson; sourceTree = ""; }; 0805FE3E1F0D38C5001226B4 /* CodeAnalysis */ = { @@ -4887,12 +4887,12 @@ 62E98AD1A0A46256AE28CD6C /* ExploreSubmodules */, 62E98DED2155646F770C5AAD /* FullscreenCourseList */, 62E98538C54252C3BFD3D836 /* Home */, - 04261228D8CE67BAF558EC33 /* NewLesson */, - FE8AFFAEB370EB0581BEA460 /* NewStep */, + 04261228D8CE67BAF558EC33 /* Lesson */, 685E26390C2E037B1CD1C8A1 /* ProfileEdit */, 2CB273FE22C4E25C0078CA2F /* Quizzes */, 9F614F892653F49424E614FB /* Settings */, 69F476480675022F8C3F54A6 /* Solution */, + FE8AFFAEB370EB0581BEA460 /* Step */, 257195BBF7205219EE894C00 /* WriteComment */, 6770E38E55D13B80C3F16A99 /* WriteCourseReview */, ); @@ -5688,8 +5688,8 @@ C029726DE3288A13E774C8C3 /* InputOutput */ = { isa = PBXGroup; children = ( - 21A89AE8D9541C3D75F0D343 /* NewStepInputProtocol.swift */, - 32A31B642B26D8A51B139F01 /* NewStepOutputProtocol.swift */, + 21A89AE8D9541C3D75F0D343 /* StepInputProtocol.swift */, + 32A31B642B26D8A51B139F01 /* StepOutputProtocol.swift */, ); path = InputOutput; sourceTree = ""; @@ -5741,21 +5741,21 @@ path = NewStringQuiz; sourceTree = ""; }; - FE8AFFAEB370EB0581BEA460 /* NewStep */ = { + FE8AFFAEB370EB0581BEA460 /* Step */ = { isa = PBXGroup; children = ( - 8D1AB142BF62815DF8939656 /* NewStepAssembly.swift */, - 49034C40838BCF58896D0118 /* NewStepDataFlow.swift */, - 84763D78E2F327874DA51576 /* NewStepInteractor.swift */, - DA4737058E519D801DAB2131 /* NewStepPresenter.swift */, - 362684BA69196E87B5919671 /* NewStepProvider.swift */, - E3FE676A699B1C33D4D0F5CC /* NewStepView.swift */, - 88372B078904014335CC5A4F /* NewStepViewController.swift */, - 2C48D5FD228ECF3D00739477 /* NewStepViewModel.swift */, + 8D1AB142BF62815DF8939656 /* StepAssembly.swift */, + 49034C40838BCF58896D0118 /* StepDataFlow.swift */, + 84763D78E2F327874DA51576 /* StepInteractor.swift */, + DA4737058E519D801DAB2131 /* StepPresenter.swift */, + 362684BA69196E87B5919671 /* StepProvider.swift */, + E3FE676A699B1C33D4D0F5CC /* StepView.swift */, + 88372B078904014335CC5A4F /* StepViewController.swift */, + 2C48D5FD228ECF3D00739477 /* StepViewModel.swift */, C029726DE3288A13E774C8C3 /* InputOutput */, 2CB5B1572292E0C200D7F706 /* Views */, ); - path = NewStep; + path = Step; sourceTree = ""; }; /* End PBXGroup section */ @@ -6894,7 +6894,7 @@ 62E987CB113F6D20B108D070 /* AchievementPopupAlertManager.swift in Sources */, 08484F0A211AF4320006266F /* Story.swift in Sources */, 2CD87014230DF663003D9F1A /* NewMatchingQuizTitleView.swift in Sources */, - 2CFC5ABE228AF0B400B5248A /* NewLessonViewModel.swift in Sources */, + 2CFC5ABE228AF0B400B5248A /* LessonViewModel.swift in Sources */, 2CF1B33F2163BE770008DA0C /* StoriesPresenter.swift in Sources */, 62E98F602ED9E5CEA4E02D0A /* UIStackView+RemoveAllArrangedSubviews.swift in Sources */, 62E98447CF53F0402668F488 /* ContinueLastStepView.swift in Sources */, @@ -6964,7 +6964,7 @@ 62E983B1F742209528DC222A /* ContentLanguageService.swift in Sources */, 2C7F782B2270B0910089FDD7 /* LessonInfoTooltipView.swift in Sources */, 62E9826EBCAB31DA927E56FE /* Reusable.swift in Sources */, - 2C48D5FE228ECF3E00739477 /* NewStepViewModel.swift in Sources */, + 2C48D5FE228ECF3E00739477 /* StepViewModel.swift in Sources */, 62E98E916518CC0446F2DCDF /* ProgrammaticallyInitializableViewProtocol.swift in Sources */, 62E9854D204FADAEDD044CAD /* NibLoadable.swift in Sources */, 62E9824265673F973F160C0A /* ProgressCircleImage.swift in Sources */, @@ -7173,25 +7173,25 @@ 62E9852B2621211F8DEA7CBE /* SettingsTableViewCell.swift in Sources */, 2CD71454237F033A00013774 /* DownloadsTableViewCell.swift in Sources */, 62E98AF1D0D810811FAC3CCA /* SettingsInputTableViewCell.swift in Sources */, - F4FC5DFF1A9B86DE2724F95F /* NewLessonAssembly.swift in Sources */, - B0D23296AB3FAC5BC6F45029 /* NewLessonViewController.swift in Sources */, - FD78F24BD455BFB5E7D3C18F /* NewLessonInteractor.swift in Sources */, - 4AC49732BD397C516D132297 /* NewLessonPresenter.swift in Sources */, - F5EB81E67462BA3FD6B6CCCB /* NewLessonDataFlow.swift in Sources */, + F4FC5DFF1A9B86DE2724F95F /* LessonAssembly.swift in Sources */, + B0D23296AB3FAC5BC6F45029 /* LessonViewController.swift in Sources */, + FD78F24BD455BFB5E7D3C18F /* LessonInteractor.swift in Sources */, + 4AC49732BD397C516D132297 /* LessonPresenter.swift in Sources */, + F5EB81E67462BA3FD6B6CCCB /* LessonDataFlow.swift in Sources */, 2C8F3ADD23CCAB7C004D113A /* DownloadVideoQualityStorageManager.swift in Sources */, - 5DABE0AA5285D92AE83E5C3A /* NewLessonProvider.swift in Sources */, + 5DABE0AA5285D92AE83E5C3A /* LessonProvider.swift in Sources */, 2CDAD30A229EC81A00AA9EF5 /* PersistenceQueuesService.swift in Sources */, 2CFC5ABC228ADFC400B5248A /* StepsPersistenceService.swift in Sources */, - B8042A238380ACC1F32082E1 /* NewStepAssembly.swift in Sources */, - 60B1FF909EC34FCFD6D2502D /* NewStepViewController.swift in Sources */, - FE43FE17249BB2B4F6890F8D /* NewStepInteractor.swift in Sources */, + B8042A238380ACC1F32082E1 /* StepAssembly.swift in Sources */, + 60B1FF909EC34FCFD6D2502D /* StepViewController.swift in Sources */, + FE43FE17249BB2B4F6890F8D /* StepInteractor.swift in Sources */, 2C9A8D2E22D348A5009434DB /* String+HTMLEscape.swift in Sources */, - 36D02A00641C747F6D18DB60 /* NewStepPresenter.swift in Sources */, - 7FBB25F3FD251734F7E049DD /* NewStepDataFlow.swift in Sources */, - E68F5C66A733F2B9F2C18920 /* NewStepProvider.swift in Sources */, - C31127DFC0924AFC68D0187C /* NewStepView.swift in Sources */, - 5E6DBFD6BE7121D6E6C6C0D5 /* NewStepInputProtocol.swift in Sources */, - 3CE76179FE70EA84C4627AF6 /* NewStepOutputProtocol.swift in Sources */, + 36D02A00641C747F6D18DB60 /* StepPresenter.swift in Sources */, + 7FBB25F3FD251734F7E049DD /* StepDataFlow.swift in Sources */, + E68F5C66A733F2B9F2C18920 /* StepProvider.swift in Sources */, + C31127DFC0924AFC68D0187C /* StepView.swift in Sources */, + 5E6DBFD6BE7121D6E6C6C0D5 /* StepInputProtocol.swift in Sources */, + 3CE76179FE70EA84C4627AF6 /* StepOutputProtocol.swift in Sources */, FF6724819335FB28269A43B7 /* NewStringQuizAssembly.swift in Sources */, 580F18AF73EFFCBF1EDED227 /* NewStringQuizViewController.swift in Sources */, 2C20C85B22F8D74F0052E9BF /* NewCodeQuizProvider.swift in Sources */, diff --git a/Stepic/LastStepRouter.swift b/Stepic/LastStepRouter.swift index d2cc51e715..e990a7b332 100644 --- a/Stepic/LastStepRouter.swift +++ b/Stepic/LastStepRouter.swift @@ -115,7 +115,7 @@ final class LastStepRouter { } stepIdPromise.done { targetStepId in - let lessonAssembly = NewLessonAssembly( + let lessonAssembly = LessonAssembly( initialContext: .unit(id: unit.id), startStep: .id(targetStepId) ) diff --git a/Stepic/Services/DeepLinks/DeepLinkRouter.swift b/Stepic/Services/DeepLinks/DeepLinkRouter.swift index d23a500448..6ed2d1bcd6 100644 --- a/Stepic/Services/DeepLinks/DeepLinkRouter.swift +++ b/Stepic/Services/DeepLinks/DeepLinkRouter.swift @@ -329,7 +329,7 @@ final class DeepLinkRouter { completion: @escaping ([UIViewController]) -> Void ) { DeepLinkRouter.routeToStepWithId(stepID, lessonId: lessonID, unitID: unitID) { viewControllers in - guard let _ = viewControllers.last as? NewLessonViewController else { + guard let _ = viewControllers.last as? LessonViewController else { completion([]) return } diff --git a/Stepic/Services/DeepLinks/StepsControllerDeepLinkRouter.swift b/Stepic/Services/DeepLinks/StepsControllerDeepLinkRouter.swift index 3fece08803..1bf5ff1304 100644 --- a/Stepic/Services/DeepLinks/StepsControllerDeepLinkRouter.swift +++ b/Stepic/Services/DeepLinks/StepsControllerDeepLinkRouter.swift @@ -141,7 +141,7 @@ final class StepsControllerDeepLinkRouter: NSObject { fetchOrLoadCourse(for: section) }.done { course in if lesson.isPublic || course.enrolled { - let lessonAssemblyWithoutUnit = NewLessonAssembly( + let lessonAssemblyWithoutUnit = LessonAssembly( initialContext: .lesson(id: lesson.id), startStep: .index(stepId - 1) ) @@ -152,7 +152,7 @@ final class StepsControllerDeepLinkRouter: NSObject { controllersStack.append(CourseInfoAssembly(courseID: course.id, initialTab: .syllabus).makeModule()) if let unit = currentUnit { - let lessonAssembly = NewLessonAssembly( + let lessonAssembly = LessonAssembly( initialContext: .unit(id: unit.id), startStep: .index(stepId - 1) ) diff --git a/Stepic/Sources/Modules/CourseInfo/CourseInfoViewController.swift b/Stepic/Sources/Modules/CourseInfo/CourseInfoViewController.swift index 6189f6addd..4ea0123607 100644 --- a/Stepic/Sources/Modules/CourseInfo/CourseInfoViewController.swift +++ b/Stepic/Sources/Modules/CourseInfo/CourseInfoViewController.swift @@ -407,7 +407,7 @@ extension CourseInfoViewController: CourseInfoViewControllerProtocol { } func displayLesson(viewModel: CourseInfo.LessonPresentation.ViewModel) { - let assembly = NewLessonAssembly(initialContext: .unit(id: viewModel.unitID)) + let assembly = LessonAssembly(initialContext: .unit(id: viewModel.unitID)) self.push(module: assembly.makeModule()) } diff --git a/Stepic/Sources/Modules/NewLesson/NewLessonAssembly.swift b/Stepic/Sources/Modules/Lesson/LessonAssembly.swift similarity index 84% rename from Stepic/Sources/Modules/NewLesson/NewLessonAssembly.swift rename to Stepic/Sources/Modules/Lesson/LessonAssembly.swift index 604e91e0ba..b6de816a7e 100644 --- a/Stepic/Sources/Modules/NewLesson/NewLessonAssembly.swift +++ b/Stepic/Sources/Modules/Lesson/LessonAssembly.swift @@ -1,10 +1,10 @@ import UIKit -final class NewLessonAssembly: Assembly { - private var initialContext: NewLesson.Context - private var startStep: NewLesson.StartStep? +final class LessonAssembly: Assembly { + private var initialContext: LessonDataFlow.Context + private var startStep: LessonDataFlow.StartStep? - init(initialContext: NewLesson.Context, startStep: NewLesson.StartStep? = nil) { + init(initialContext: LessonDataFlow.Context, startStep: LessonDataFlow.StartStep? = nil) { self.initialContext = initialContext self.startStep = startStep } @@ -26,7 +26,7 @@ final class NewLessonAssembly: Assembly { progressesNetworkService: ProgressesNetworkService(progressesAPI: ProgressesAPI()) ) - let provider = NewLessonProvider( + let provider = LessonProvider( lessonsPersistenceService: LessonsPersistenceService(), lessonsNetworkService: LessonsNetworkService(lessonsAPI: LessonsAPI()), unitsPersistenceService: UnitsPersistenceService(), @@ -39,8 +39,8 @@ final class NewLessonAssembly: Assembly { progressesNetworkService: ProgressesNetworkService(progressesAPI: ProgressesAPI()), viewsNetworkService: ViewsNetworkService(viewsAPI: ViewsAPI()) ) - let presenter = NewLessonPresenter() - let interactor = NewLessonInteractor( + let presenter = LessonPresenter() + let interactor = LessonInteractor( initialContext: self.initialContext, startStep: self.startStep, presenter: presenter, @@ -49,7 +49,7 @@ final class NewLessonAssembly: Assembly { persistenceQueuesService: PersistenceQueuesService(), dataBackUpdateService: dataBackUpdateService ) - let viewController = NewLessonViewController(interactor: interactor) + let viewController = LessonViewController(interactor: interactor) viewController.hidesBottomBarWhenPushed = true presenter.viewController = viewController diff --git a/Stepic/Sources/Modules/NewLesson/NewLessonDataFlow.swift b/Stepic/Sources/Modules/Lesson/LessonDataFlow.swift similarity index 95% rename from Stepic/Sources/Modules/NewLesson/NewLessonDataFlow.swift rename to Stepic/Sources/Modules/Lesson/LessonDataFlow.swift index 26a123aca4..79daafd952 100644 --- a/Stepic/Sources/Modules/NewLesson/NewLessonDataFlow.swift +++ b/Stepic/Sources/Modules/Lesson/LessonDataFlow.swift @@ -1,11 +1,11 @@ import Foundation -enum NewLesson { +enum LessonDataFlow { // MARK: Data flow /// Load lesson content enum LessonLoad { - struct Request { } + struct Request {} struct Data { let lesson: Lesson @@ -62,9 +62,9 @@ enum NewLesson { /// Autoplay current step enum CurrentStepAutoplay { - struct Response { } + struct Response {} - struct ViewModel { } + struct ViewModel {} } /// Load lesson tooltip info content @@ -137,7 +137,7 @@ enum NewLesson { enum ViewControllerState { case loading - case result(data: NewLessonViewModel) + case result(data: LessonViewModel) case error } diff --git a/Stepic/Sources/Modules/NewLesson/NewLessonInteractor.swift b/Stepic/Sources/Modules/Lesson/LessonInteractor.swift similarity index 89% rename from Stepic/Sources/Modules/NewLesson/NewLessonInteractor.swift rename to Stepic/Sources/Modules/Lesson/LessonInteractor.swift index 30ca26ac36..ff29b3ef0c 100644 --- a/Stepic/Sources/Modules/NewLesson/NewLessonInteractor.swift +++ b/Stepic/Sources/Modules/Lesson/LessonInteractor.swift @@ -1,14 +1,14 @@ import Foundation import PromiseKit -protocol NewLessonInteractorProtocol { - func doLessonLoad(request: NewLesson.LessonLoad.Request) - func doEditStepPresentation(request: NewLesson.EditStepPresentation.Request) +protocol LessonInteractorProtocol { + func doLessonLoad(request: LessonDataFlow.LessonLoad.Request) + func doEditStepPresentation(request: LessonDataFlow.EditStepPresentation.Request) } -final class NewLessonInteractor: NewLessonInteractorProtocol { - private let presenter: NewLessonPresenterProtocol - private let provider: NewLessonProviderProtocol +final class LessonInteractor: LessonInteractorProtocol { + private let presenter: LessonPresenterProtocol + private let provider: LessonProviderProtocol private let unitNavigationService: UnitNavigationServiceProtocol private let persistenceQueuesService: PersistenceQueuesServiceProtocol private let dataBackUpdateService: DataBackUpdateServiceProtocol @@ -24,13 +24,13 @@ final class NewLessonInteractor: NewLessonInteractorProtocol { private var nextUnit: Unit? private var assignmentsForCurrentSteps: [Step.IdType: Assignment.IdType] = [:] - private var lastLoadState: (context: NewLesson.Context, startStep: NewLesson.StartStep?) + private var lastLoadState: (context: LessonDataFlow.Context, startStep: LessonDataFlow.StartStep?) init( - initialContext: NewLesson.Context, - startStep: NewLesson.StartStep?, - presenter: NewLessonPresenterProtocol, - provider: NewLessonProviderProtocol, + initialContext: LessonDataFlow.Context, + startStep: LessonDataFlow.StartStep?, + presenter: LessonPresenterProtocol, + provider: LessonProviderProtocol, unitNavigationService: UnitNavigationServiceProtocol, persistenceQueuesService: PersistenceQueuesServiceProtocol, dataBackUpdateService: DataBackUpdateServiceProtocol @@ -45,14 +45,14 @@ final class NewLessonInteractor: NewLessonInteractorProtocol { // MARK: Public API - func doLessonLoad(request: NewLesson.LessonLoad.Request) { + func doLessonLoad(request: LessonDataFlow.LessonLoad.Request) { self.refresh( context: self.lastLoadState.context, startStep: self.lastLoadState.startStep ).cauterize() } - func doEditStepPresentation(request: NewLesson.EditStepPresentation.Request) { + func doEditStepPresentation(request: LessonDataFlow.EditStepPresentation.Request) { guard let lesson = self.currentLesson, let stepID = lesson.stepsArray[safe: request.index] else { return @@ -63,7 +63,7 @@ final class NewLessonInteractor: NewLessonInteractorProtocol { // MARK: Private API - private func refresh(context: NewLesson.Context, startStep: NewLesson.StartStep? = nil) -> Promise { + private func refresh(context: LessonDataFlow.Context, startStep: LessonDataFlow.StartStep? = nil) -> Promise { Promise { seal in self.previousUnit = nil self.nextUnit = nil @@ -89,7 +89,7 @@ final class NewLessonInteractor: NewLessonInteractorProtocol { } } - private func loadData(context: NewLesson.Context, startStep: NewLesson.StartStep) -> Promise { + private func loadData(context: LessonDataFlow.Context, startStep: LessonDataFlow.StartStep) -> Promise { firstly { () -> Promise<(Lesson?, Unit?)> in switch context { case .lesson(let lessonID): @@ -142,7 +142,7 @@ final class NewLessonInteractor: NewLessonInteractorProtocol { steps.forEach { $0.lesson = lesson } - let data = NewLesson.LessonLoad.Data( + let data = LessonDataFlow.LessonLoad.Data( lesson: lesson, steps: steps, progresses: progresses, @@ -217,9 +217,9 @@ final class NewLessonInteractor: NewLessonInteractorProtocol { } } -// MARK: - NewLessonInteractor: NewStepOutputProtocol - +// MARK: - LessonInteractor: StepOutputProtocol - -extension NewLessonInteractor: NewStepOutputProtocol { +extension LessonInteractor: StepOutputProtocol { private static let autoplayDelay: TimeInterval = 0.33 func handleStepView(id: Step.IdType) { @@ -311,9 +311,9 @@ extension NewLessonInteractor: NewStepOutputProtocol { } } -// MARK: - NewLessonInteractor: EditStepOutputProtocol - +// MARK: - LessonInteractor: EditStepOutputProtocol - -extension NewLessonInteractor: EditStepOutputProtocol { +extension LessonInteractor: EditStepOutputProtocol { func handleStepSourceUpdated(_ stepSource: StepSource) { guard let lesson = self.currentLesson, let stepIndex = lesson.stepsArray.firstIndex(where: { $0 == stepSource.id }) else { diff --git a/Stepic/Sources/Modules/NewLesson/NewLessonPresenter.swift b/Stepic/Sources/Modules/Lesson/LessonPresenter.swift similarity index 68% rename from Stepic/Sources/Modules/NewLesson/NewLessonPresenter.swift rename to Stepic/Sources/Modules/Lesson/LessonPresenter.swift index 9c27dc3030..d4ea50a8cc 100644 --- a/Stepic/Sources/Modules/NewLesson/NewLessonPresenter.swift +++ b/Stepic/Sources/Modules/Lesson/LessonPresenter.swift @@ -1,23 +1,23 @@ import UIKit -protocol NewLessonPresenterProtocol { - func presentLesson(response: NewLesson.LessonLoad.Response) - func presentLessonNavigation(response: NewLesson.LessonNavigationLoad.Response) - func presentLessonTooltipInfo(response: NewLesson.LessonTooltipInfoLoad.Response) - func presentStepTooltipInfoUpdate(response: NewLesson.StepTooltipInfoUpdate.Response) - func presentStepPassedStatusUpdate(response: NewLesson.StepPassedStatusUpdate.Response) - func presentCurrentStepUpdate(response: NewLesson.CurrentStepUpdate.Response) - func presentCurrentStepAutoplay(response: NewLesson.CurrentStepAutoplay.Response) - func presentEditStep(response: NewLesson.EditStepPresentation.Response) - func presentStepTextUpdate(response: NewLesson.StepTextUpdate.Response) - func presentWaitingState(response: NewLesson.BlockingWaitingIndicatorUpdate.Response) +protocol LessonPresenterProtocol { + func presentLesson(response: LessonDataFlow.LessonLoad.Response) + func presentLessonNavigation(response: LessonDataFlow.LessonNavigationLoad.Response) + func presentLessonTooltipInfo(response: LessonDataFlow.LessonTooltipInfoLoad.Response) + func presentStepTooltipInfoUpdate(response: LessonDataFlow.StepTooltipInfoUpdate.Response) + func presentStepPassedStatusUpdate(response: LessonDataFlow.StepPassedStatusUpdate.Response) + func presentCurrentStepUpdate(response: LessonDataFlow.CurrentStepUpdate.Response) + func presentCurrentStepAutoplay(response: LessonDataFlow.CurrentStepAutoplay.Response) + func presentEditStep(response: LessonDataFlow.EditStepPresentation.Response) + func presentStepTextUpdate(response: LessonDataFlow.StepTextUpdate.Response) + func presentWaitingState(response: LessonDataFlow.BlockingWaitingIndicatorUpdate.Response) } -final class NewLessonPresenter: NewLessonPresenterProtocol { - weak var viewController: NewLessonViewControllerProtocol? +final class LessonPresenter: LessonPresenterProtocol { + weak var viewController: LessonViewControllerProtocol? - func presentLesson(response: NewLesson.LessonLoad.Response) { - let viewModel: NewLesson.LessonLoad.ViewModel + func presentLesson(response: LessonDataFlow.LessonLoad.Response) { + let viewModel: LessonDataFlow.LessonLoad.ViewModel switch response.state { case .failure: @@ -39,8 +39,8 @@ final class NewLessonPresenter: NewLessonPresenterProtocol { self.viewController?.displayLesson(viewModel: viewModel) } - func presentLessonNavigation(response: NewLesson.LessonNavigationLoad.Response) { - let viewModel = NewLesson.LessonNavigationLoad.ViewModel( + func presentLessonNavigation(response: LessonDataFlow.LessonNavigationLoad.Response) { + let viewModel = LessonDataFlow.LessonNavigationLoad.ViewModel( hasPreviousUnit: response.hasPreviousUnit, hasNextUnit: response.hasNextUnit ) @@ -48,8 +48,8 @@ final class NewLessonPresenter: NewLessonPresenterProtocol { self.viewController?.displayLessonNavigation(viewModel: viewModel) } - func presentLessonTooltipInfo(response: NewLesson.LessonTooltipInfoLoad.Response) { - var data: [Step.IdType: [NewLesson.TooltipInfo]] = [:] + func presentLessonTooltipInfo(response: LessonDataFlow.LessonTooltipInfoLoad.Response) { + var data: [Step.IdType: [LessonDataFlow.TooltipInfo]] = [:] zip(response.steps, response.progresses).forEach { step, progress in data[step.id] = self.makeTooltipInfoViewModel(lesson: response.lesson, progress: progress) } @@ -57,7 +57,7 @@ final class NewLessonPresenter: NewLessonPresenterProtocol { self.viewController?.displayLessonTooltipInfo(viewModel: .init(data: data)) } - func presentStepTooltipInfoUpdate(response: NewLesson.StepTooltipInfoUpdate.Response) { + func presentStepTooltipInfoUpdate(response: LessonDataFlow.StepTooltipInfoUpdate.Response) { self.viewController?.displayStepTooltipInfoUpdate( viewModel: .init( stepID: response.step.id, @@ -66,29 +66,29 @@ final class NewLessonPresenter: NewLessonPresenterProtocol { ) } - func presentStepPassedStatusUpdate(response: NewLesson.StepPassedStatusUpdate.Response) { + func presentStepPassedStatusUpdate(response: LessonDataFlow.StepPassedStatusUpdate.Response) { self.viewController?.displayStepPassedStatusUpdate(viewModel: .init(stepID: response.stepID)) } - func presentCurrentStepUpdate(response: NewLesson.CurrentStepUpdate.Response) { + func presentCurrentStepUpdate(response: LessonDataFlow.CurrentStepUpdate.Response) { self.viewController?.displayCurrentStepUpdate(viewModel: .init(index: response.index)) } - func presentCurrentStepAutoplay(response: NewLesson.CurrentStepAutoplay.Response) { + func presentCurrentStepAutoplay(response: LessonDataFlow.CurrentStepAutoplay.Response) { self.viewController?.displayCurrentStepAutoplay(viewModel: .init()) } - func presentStepTextUpdate(response: NewLesson.StepTextUpdate.Response) { + func presentStepTextUpdate(response: LessonDataFlow.StepTextUpdate.Response) { self.viewController?.displayStepTextUpdate( viewModel: .init(index: response.index, text: response.stepSource.text) ) } - func presentEditStep(response: NewLesson.EditStepPresentation.Response) { + func presentEditStep(response: LessonDataFlow.EditStepPresentation.Response) { self.viewController?.displayEditStep(viewModel: .init(stepID: response.stepID)) } - func presentWaitingState(response: NewLesson.BlockingWaitingIndicatorUpdate.Response) { + func presentWaitingState(response: LessonDataFlow.BlockingWaitingIndicatorUpdate.Response) { self.viewController?.displayBlockingLoadingIndicator(viewModel: .init(shouldDismiss: response.shouldDismiss)) } @@ -100,9 +100,9 @@ final class NewLessonPresenter: NewLessonPresenterProtocol { progresses: [Progress], startStepIndex: Int, canEdit: Bool - ) -> NewLessonViewModel { + ) -> LessonViewModel { let lessonTitle = lesson.title - let steps: [NewLessonViewModel.StepDescription] = steps.enumerated().map { index, step in + let steps: [LessonViewModel.StepDescription] = steps.enumerated().map { index, step in let iconImage: UIImage? = { if step.hasReview { return UIImage(named: "ic_peer_review")?.withRenderingMode(.alwaysTemplate) @@ -126,7 +126,7 @@ final class NewLessonPresenter: NewLessonPresenterProtocol { canEdit: canEdit && step.block.type != .video ) } - return NewLessonViewModel( + return LessonViewModel( lessonTitle: lessonTitle, steps: steps, stepLinkMaker: { @@ -136,8 +136,8 @@ final class NewLessonPresenter: NewLessonPresenterProtocol { ) } - private func makeTooltipInfoViewModel(lesson: Lesson, progress: Progress) -> [NewLesson.TooltipInfo] { - var viewModel: [NewLesson.TooltipInfo] = [] + private func makeTooltipInfoViewModel(lesson: Lesson, progress: Progress) -> [LessonDataFlow.TooltipInfo] { + var viewModel: [LessonDataFlow.TooltipInfo] = [] if progress.score > 0 { let text = String( diff --git a/Stepic/Sources/Modules/NewLesson/NewLessonProvider.swift b/Stepic/Sources/Modules/Lesson/LessonProvider.swift similarity index 99% rename from Stepic/Sources/Modules/NewLesson/NewLessonProvider.swift rename to Stepic/Sources/Modules/Lesson/LessonProvider.swift index 94b998d219..61640e45fd 100644 --- a/Stepic/Sources/Modules/NewLesson/NewLessonProvider.swift +++ b/Stepic/Sources/Modules/Lesson/LessonProvider.swift @@ -1,7 +1,7 @@ import Foundation import PromiseKit -protocol NewLessonProviderProtocol { +protocol LessonProviderProtocol { func fetchLesson(id: Lesson.IdType) -> Promise> func fetchLessonAndUnit(unitID: Unit.IdType) -> Promise<(FetchResult, FetchResult)> func fetchSteps(ids: [Step.IdType]) -> Promise> @@ -11,7 +11,7 @@ protocol NewLessonProviderProtocol { func createView(stepID: Step.IdType, assignmentID: Assignment.IdType?) -> Promise } -final class NewLessonProvider: NewLessonProviderProtocol { +final class LessonProvider: LessonProviderProtocol { private let lessonsPersistenceService: LessonsPersistenceServiceProtocol private let lessonsNetworkService: LessonsNetworkServiceProtocol private let unitsPersistenceService: UnitsPersistenceServiceProtocol diff --git a/Stepic/Sources/Modules/NewLesson/NewLessonViewController.swift b/Stepic/Sources/Modules/Lesson/LessonViewController.swift similarity index 85% rename from Stepic/Sources/Modules/NewLesson/NewLessonViewController.swift rename to Stepic/Sources/Modules/Lesson/LessonViewController.swift index 24a96cb002..cd9e78a311 100644 --- a/Stepic/Sources/Modules/NewLesson/NewLessonViewController.swift +++ b/Stepic/Sources/Modules/Lesson/LessonViewController.swift @@ -5,24 +5,22 @@ import SVProgressHUD import Tabman import UIKit -// MARK: NewLessonViewControllerProtocol: class - - -protocol NewLessonViewControllerProtocol: AnyObject { - func displayLesson(viewModel: NewLesson.LessonLoad.ViewModel) - func displayLessonNavigation(viewModel: NewLesson.LessonNavigationLoad.ViewModel) - func displayLessonTooltipInfo(viewModel: NewLesson.LessonTooltipInfoLoad.ViewModel) - func displayStepTooltipInfoUpdate(viewModel: NewLesson.StepTooltipInfoUpdate.ViewModel) - func displayStepPassedStatusUpdate(viewModel: NewLesson.StepPassedStatusUpdate.ViewModel) - func displayCurrentStepUpdate(viewModel: NewLesson.CurrentStepUpdate.ViewModel) - func displayCurrentStepAutoplay(viewModel: NewLesson.CurrentStepAutoplay.ViewModel) - func displayEditStep(viewModel: NewLesson.EditStepPresentation.ViewModel) - func displayStepTextUpdate(viewModel: NewLesson.StepTextUpdate.ViewModel) - func displayBlockingLoadingIndicator(viewModel: NewLesson.BlockingWaitingIndicatorUpdate.ViewModel) +protocol LessonViewControllerProtocol: AnyObject { + func displayLesson(viewModel: LessonDataFlow.LessonLoad.ViewModel) + func displayLessonNavigation(viewModel: LessonDataFlow.LessonNavigationLoad.ViewModel) + func displayLessonTooltipInfo(viewModel: LessonDataFlow.LessonTooltipInfoLoad.ViewModel) + func displayStepTooltipInfoUpdate(viewModel: LessonDataFlow.StepTooltipInfoUpdate.ViewModel) + func displayStepPassedStatusUpdate(viewModel: LessonDataFlow.StepPassedStatusUpdate.ViewModel) + func displayCurrentStepUpdate(viewModel: LessonDataFlow.CurrentStepUpdate.ViewModel) + func displayCurrentStepAutoplay(viewModel: LessonDataFlow.CurrentStepAutoplay.ViewModel) + func displayEditStep(viewModel: LessonDataFlow.EditStepPresentation.ViewModel) + func displayStepTextUpdate(viewModel: LessonDataFlow.StepTextUpdate.ViewModel) + func displayBlockingLoadingIndicator(viewModel: LessonDataFlow.BlockingWaitingIndicatorUpdate.ViewModel) } -// MARK: - NewLessonViewController: TabmanViewController, ControllerWithStepikPlaceholder - +// MARK: - LessonViewController: TabmanViewController, ControllerWithStepikPlaceholder - -final class NewLessonViewController: TabmanViewController, ControllerWithStepikPlaceholder { +final class LessonViewController: TabmanViewController, ControllerWithStepikPlaceholder { private static let animationDuration: TimeInterval = 0.25 enum Appearance { @@ -35,7 +33,7 @@ final class NewLessonViewController: TabmanViewController, ControllerWithStepikP static let tooltipHorizontalSpacing: CGFloat = 16 } - private let interactor: NewLessonInteractorProtocol + private let interactor: LessonInteractorProtocol private lazy var infoBarButtonItem: UIBarButtonItem = { let image: UIImage? @@ -93,10 +91,10 @@ final class NewLessonViewController: TabmanViewController, ControllerWithStepikP private var tooltipView: EasyTipView? private var isTooltipVisible = false - private var tooltipInfos: [Step.IdType: [NewLesson.TooltipInfo]] = [:] + private var tooltipInfos: [Step.IdType: [LessonDataFlow.TooltipInfo]] = [:] private var stepControllers: [UIViewController?] = [] - private var stepModulesInputs: [NewStepInputProtocol?] = [] + private var stepModulesInputs: [StepInputProtocol?] = [] private var hasNavigationToPreviousUnit = false private var hasNavigationToNextUnit = false @@ -122,7 +120,7 @@ final class NewLessonViewController: TabmanViewController, ControllerWithStepikP return bar }() - private var state: NewLesson.ViewControllerState { + private var state: LessonDataFlow.ViewControllerState { didSet { self.updateState() } @@ -130,7 +128,7 @@ final class NewLessonViewController: TabmanViewController, ControllerWithStepikP var placeholderContainer = StepikPlaceholderControllerContainer() - init(interactor: NewLessonInteractorProtocol) { + init(interactor: LessonInteractorProtocol) { self.interactor = interactor self.state = .loading super.init(nibName: nil, bundle: nil) @@ -237,7 +235,7 @@ final class NewLessonViewController: TabmanViewController, ControllerWithStepikP self.reloadData() self.addBar(self.tabBarView, dataSource: self, at: .top) - UIView.animate(withDuration: NewLessonViewController.animationDuration) { + UIView.animate(withDuration: Self.animationDuration) { self.overlayView.alpha = 0.0 } } @@ -266,7 +264,7 @@ final class NewLessonViewController: TabmanViewController, ControllerWithStepikP return controller } - let assembly = NewStepAssembly(stepID: step.id, output: self.interactor as? NewStepOutputProtocol) + let assembly = StepAssembly(stepID: step.id, output: self.interactor as? StepOutputProtocol) let controller = assembly.makeModule() self.stepControllers[index] = controller self.stepModulesInputs[index] = assembly.moduleInput @@ -436,9 +434,9 @@ final class NewLessonViewController: TabmanViewController, ControllerWithStepikP } } -// MARK: - NewLessonViewController: PageboyViewControllerDataSource - +// MARK: - LessonViewController: PageboyViewControllerDataSource - -extension NewLessonViewController: PageboyViewControllerDataSource { +extension LessonViewController: PageboyViewControllerDataSource { func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int { if case .result(let data) = self.state { return data.steps.count @@ -472,9 +470,9 @@ extension NewLessonViewController: PageboyViewControllerDataSource { } } -// MARK: - NewLessonViewController: TMBarDataSource - +// MARK: - LessonViewController: TMBarDataSource - -extension NewLessonViewController: TMBarDataSource { +extension LessonViewController: TMBarDataSource { func barItem(for bar: TMBar, at index: Int) -> TMBarItemable { guard case .result(let data) = self.state, let stepDescription = data.steps[safe: index] else { fatalError("Step not found") @@ -487,14 +485,14 @@ extension NewLessonViewController: TMBarDataSource { } } -// MARK: - NewLessonViewController: NewLessonViewControllerProtocol - +// MARK: - LessonViewController: LessonViewControllerProtocol - -extension NewLessonViewController: NewLessonViewControllerProtocol { - func displayLesson(viewModel: NewLesson.LessonLoad.ViewModel) { +extension LessonViewController: LessonViewControllerProtocol { + func displayLesson(viewModel: LessonDataFlow.LessonLoad.ViewModel) { self.state = viewModel.state } - func displayLessonNavigation(viewModel: NewLesson.LessonNavigationLoad.ViewModel) { + func displayLessonNavigation(viewModel: LessonDataFlow.LessonNavigationLoad.ViewModel) { self.hasNavigationToNextUnit = viewModel.hasNextUnit self.hasNavigationToPreviousUnit = viewModel.hasPreviousUnit @@ -504,17 +502,17 @@ extension NewLessonViewController: NewLessonViewControllerProtocol { ) } - func displayLessonTooltipInfo(viewModel: NewLesson.LessonTooltipInfoLoad.ViewModel) { + func displayLessonTooltipInfo(viewModel: LessonDataFlow.LessonTooltipInfoLoad.ViewModel) { self.tooltipInfos = viewModel.data self.updateInfoBarButtonItem() } - func displayStepTooltipInfoUpdate(viewModel: NewLesson.StepTooltipInfoUpdate.ViewModel) { + func displayStepTooltipInfoUpdate(viewModel: LessonDataFlow.StepTooltipInfoUpdate.ViewModel) { self.tooltipInfos[viewModel.stepID] = viewModel.info self.updateInfoBarButtonItem() } - func displayStepPassedStatusUpdate(viewModel: NewLesson.StepPassedStatusUpdate.ViewModel) { + func displayStepPassedStatusUpdate(viewModel: LessonDataFlow.StepPassedStatusUpdate.ViewModel) { let tabIdentifier = "\(viewModel.stepID)" NotificationCenter.default.post( @@ -524,11 +522,11 @@ extension NewLessonViewController: NewLessonViewControllerProtocol { ) } - func displayCurrentStepUpdate(viewModel: NewLesson.CurrentStepUpdate.ViewModel) { + func displayCurrentStepUpdate(viewModel: LessonDataFlow.CurrentStepUpdate.ViewModel) { self.scrollToPage(.at(index: viewModel.index), animated: true) } - func displayCurrentStepAutoplay(viewModel: NewLesson.CurrentStepAutoplay.ViewModel) { + func displayCurrentStepAutoplay(viewModel: LessonDataFlow.CurrentStepAutoplay.ViewModel) { guard let currentIndex = self.currentIndex, let stepModuleInput = self.stepModulesInputs[safe: currentIndex] else { return @@ -537,7 +535,7 @@ extension NewLessonViewController: NewLessonViewControllerProtocol { stepModuleInput?.play() } - func displayEditStep(viewModel: NewLesson.EditStepPresentation.ViewModel) { + func displayEditStep(viewModel: LessonDataFlow.EditStepPresentation.ViewModel) { let (modalPresentationStyle, navigationBarAppearance) = { () -> (UIModalPresentationStyle, StyledNavigationController.NavigationBarAppearanceState) in if #available(iOS 13.0, *) { @@ -567,7 +565,7 @@ extension NewLessonViewController: NewLessonViewControllerProtocol { ) } - func displayStepTextUpdate(viewModel: NewLesson.StepTextUpdate.ViewModel) { + func displayStepTextUpdate(viewModel: LessonDataFlow.StepTextUpdate.ViewModel) { guard let stepModuleInput = self.stepModulesInputs[safe: viewModel.index] else { return } @@ -575,7 +573,7 @@ extension NewLessonViewController: NewLessonViewControllerProtocol { stepModuleInput?.updateStepText(viewModel.text) } - func displayBlockingLoadingIndicator(viewModel: NewLesson.BlockingWaitingIndicatorUpdate.ViewModel) { + func displayBlockingLoadingIndicator(viewModel: LessonDataFlow.BlockingWaitingIndicatorUpdate.ViewModel) { if viewModel.shouldDismiss { SVProgressHUD.dismiss() } else { @@ -584,9 +582,9 @@ extension NewLessonViewController: NewLessonViewControllerProtocol { } } -// MARK: - NewLessonViewController: EasyTipViewDelegate - +// MARK: - LessonViewController: EasyTipViewDelegate - -extension NewLessonViewController: EasyTipViewDelegate { +extension LessonViewController: EasyTipViewDelegate { func easyTipViewDidDismiss(_ tipView: EasyTipView) { self.isTooltipVisible = false self.tooltipView = nil diff --git a/Stepic/Sources/Modules/NewLesson/NewLessonViewModel.swift b/Stepic/Sources/Modules/Lesson/LessonViewModel.swift similarity index 91% rename from Stepic/Sources/Modules/NewLesson/NewLessonViewModel.swift rename to Stepic/Sources/Modules/Lesson/LessonViewModel.swift index add2e0326d..1f74d75a21 100644 --- a/Stepic/Sources/Modules/NewLesson/NewLessonViewModel.swift +++ b/Stepic/Sources/Modules/Lesson/LessonViewModel.swift @@ -1,6 +1,6 @@ import Foundation -struct NewLessonViewModel { +struct LessonViewModel { struct StepDescription { let id: Step.IdType let iconImage: UIImage diff --git a/Stepic/Sources/Modules/NewLesson/Views/LessonInfoTooltipView.swift b/Stepic/Sources/Modules/Lesson/Views/LessonInfoTooltipView.swift similarity index 100% rename from Stepic/Sources/Modules/NewLesson/Views/LessonInfoTooltipView.swift rename to Stepic/Sources/Modules/Lesson/Views/LessonInfoTooltipView.swift diff --git a/Stepic/Sources/Modules/NewLesson/Views/TabBar/StepTabBarButton.swift b/Stepic/Sources/Modules/Lesson/Views/TabBar/StepTabBarButton.swift similarity index 100% rename from Stepic/Sources/Modules/NewLesson/Views/TabBar/StepTabBarButton.swift rename to Stepic/Sources/Modules/Lesson/Views/TabBar/StepTabBarButton.swift diff --git a/Stepic/Sources/Modules/Quizzes/BaseQuiz/BaseQuizAssembly.swift b/Stepic/Sources/Modules/Quizzes/BaseQuiz/BaseQuizAssembly.swift index 7532e0eb7e..dd6e775d17 100644 --- a/Stepic/Sources/Modules/Quizzes/BaseQuiz/BaseQuizAssembly.swift +++ b/Stepic/Sources/Modules/Quizzes/BaseQuiz/BaseQuizAssembly.swift @@ -29,7 +29,7 @@ final class BaseQuizAssembly: Assembly { ) let viewController = BaseQuizViewController( interactor: interactor, - quizAssembly: QuizAssemblyFactory().make(for: NewStep.QuizType(blockName: self.step.block.name)) + quizAssembly: QuizAssemblyFactory().make(for: StepDataFlow.QuizType(blockName: self.step.block.name)) ) presenter.viewController = viewController diff --git a/Stepic/Sources/Modules/Quizzes/BaseQuiz/BaseQuizPresenter.swift b/Stepic/Sources/Modules/Quizzes/BaseQuiz/BaseQuizPresenter.swift index 92dc09ed7d..990591fefc 100644 --- a/Stepic/Sources/Modules/Quizzes/BaseQuiz/BaseQuizPresenter.swift +++ b/Stepic/Sources/Modules/Quizzes/BaseQuiz/BaseQuizPresenter.swift @@ -60,14 +60,14 @@ final class BaseQuizPresenter: BaseQuizPresenterProtocol { // The following quizzes can be retried w/o new attempt let isQuizNotNeededNewAttempt = [ - NewStep.QuizType.string, - NewStep.QuizType.number, - NewStep.QuizType.math, - NewStep.QuizType.freeAnswer, - NewStep.QuizType.code, - NewStep.QuizType.sorting, - NewStep.QuizType.matching - ].contains(NewStep.QuizType(blockName: step.block.name)) + StepDataFlow.QuizType.string, + StepDataFlow.QuizType.number, + StepDataFlow.QuizType.math, + StepDataFlow.QuizType.freeAnswer, + StepDataFlow.QuizType.code, + StepDataFlow.QuizType.sorting, + StepDataFlow.QuizType.matching + ].contains(StepDataFlow.QuizType(blockName: step.block.name)) // 1. if quiz is not needed new attempt and status == wrong // => retry not needed (by quiz design or we've clean attempt) @@ -194,7 +194,7 @@ final class BaseQuizPresenter: BaseQuizPresenterProtocol { if step.hasReview { return NSLocalizedString("PeerReviewFeedbackTitle", comment: "") } - if case .freeAnswer = NewStep.QuizType(blockName: step.block.name) { + if case .freeAnswer = StepDataFlow.QuizType(blockName: step.block.name) { return NSLocalizedString("CorrectFeedbackTitleFreeAnswer", comment: "") } return correctTitles.randomElement() ?? NSLocalizedString("Correct", comment: "") diff --git a/Stepic/Sources/Modules/Quizzes/BaseQuiz/ChildProtocols/QuizAssembly.swift b/Stepic/Sources/Modules/Quizzes/BaseQuiz/ChildProtocols/QuizAssembly.swift index 0d6ec3815a..a53fb793a6 100644 --- a/Stepic/Sources/Modules/Quizzes/BaseQuiz/ChildProtocols/QuizAssembly.swift +++ b/Stepic/Sources/Modules/Quizzes/BaseQuiz/ChildProtocols/QuizAssembly.swift @@ -6,7 +6,7 @@ protocol QuizAssembly: Assembly { } final class QuizAssemblyFactory { - func make(for type: NewStep.QuizType) -> QuizAssembly { + func make(for type: StepDataFlow.QuizType) -> QuizAssembly { switch type { case .string: return NewStringQuizAssembly(type: .string) diff --git a/Stepic/Sources/Modules/Solution/SolutionViewController.swift b/Stepic/Sources/Modules/Solution/SolutionViewController.swift index a187acdb96..d8cbcbe41e 100644 --- a/Stepic/Sources/Modules/Solution/SolutionViewController.swift +++ b/Stepic/Sources/Modules/Solution/SolutionViewController.swift @@ -78,7 +78,7 @@ final class SolutionViewController: UIViewController, ControllerWithStepikPlaceh self.solutionURL = data.solutionURL - let quizType = NewStep.QuizType(blockName: data.step.block.name) + let quizType = StepDataFlow.QuizType(blockName: data.step.block.name) if case .unknown = quizType { self.solutionView?.actionTitle = NSLocalizedString("UnsupportedSolutionActionTitle", comment: "") diff --git a/Stepic/Sources/Modules/NewStep/InputOutput/NewStepInputProtocol.swift b/Stepic/Sources/Modules/Step/InputOutput/StepInputProtocol.swift similarity index 82% rename from Stepic/Sources/Modules/NewStep/InputOutput/NewStepInputProtocol.swift rename to Stepic/Sources/Modules/Step/InputOutput/StepInputProtocol.swift index b2347b558c..9cb82dc661 100644 --- a/Stepic/Sources/Modules/NewStep/InputOutput/NewStepInputProtocol.swift +++ b/Stepic/Sources/Modules/Step/InputOutput/StepInputProtocol.swift @@ -1,6 +1,6 @@ import Foundation -protocol NewStepInputProtocol: AnyObject { +protocol StepInputProtocol: AnyObject { func updateStepNavigation(canNavigateToPreviousUnit: Bool, canNavigateToNextUnit: Bool, canNavigateToNextStep: Bool) func updateStepText(_ text: String) func play() diff --git a/Stepic/Sources/Modules/NewStep/InputOutput/NewStepOutputProtocol.swift b/Stepic/Sources/Modules/Step/InputOutput/StepOutputProtocol.swift similarity index 86% rename from Stepic/Sources/Modules/NewStep/InputOutput/NewStepOutputProtocol.swift rename to Stepic/Sources/Modules/Step/InputOutput/StepOutputProtocol.swift index 06325781a1..cbbdc50990 100644 --- a/Stepic/Sources/Modules/NewStep/InputOutput/NewStepOutputProtocol.swift +++ b/Stepic/Sources/Modules/Step/InputOutput/StepOutputProtocol.swift @@ -1,6 +1,6 @@ import Foundation -protocol NewStepOutputProtocol: AnyObject { +protocol StepOutputProtocol: AnyObject { func handleStepView(id: Step.IdType) func handleStepDone(id: Step.IdType) func handlePreviousUnitNavigation() diff --git a/Stepic/Sources/Modules/NewStep/NewStepAssembly.swift b/Stepic/Sources/Modules/Step/StepAssembly.swift similarity index 59% rename from Stepic/Sources/Modules/NewStep/NewStepAssembly.swift rename to Stepic/Sources/Modules/Step/StepAssembly.swift index db4202a0a0..b662cbf5ad 100644 --- a/Stepic/Sources/Modules/NewStep/NewStepAssembly.swift +++ b/Stepic/Sources/Modules/Step/StepAssembly.swift @@ -1,26 +1,26 @@ import UIKit -final class NewStepAssembly: Assembly { - var moduleInput: NewStepInputProtocol? +final class StepAssembly: Assembly { + var moduleInput: StepInputProtocol? private let stepID: Step.IdType - private weak var moduleOutput: NewStepOutputProtocol? + private weak var moduleOutput: StepOutputProtocol? - init(stepID: Step.IdType, output: NewStepOutputProtocol? = nil) { + init(stepID: Step.IdType, output: StepOutputProtocol? = nil) { self.stepID = stepID self.moduleOutput = output } func makeModule() -> UIViewController { - let provider = NewStepProvider( + let provider = StepProvider( stepsPersistenceService: StepsPersistenceService(), stepsNetworkService: StepsNetworkService(stepsAPI: StepsAPI()), stepFontSizeStorageManager: StepFontSizeStorageManager(), imageStoredFileManager: StoredFileManagerFactory.makeStoredFileManager(type: .image) ) - let presenter = NewStepPresenter() - let interactor = NewStepInteractor(stepID: self.stepID, presenter: presenter, provider: provider) - let viewController = NewStepViewController(interactor: interactor) + let presenter = StepPresenter() + let interactor = StepInteractor(stepID: self.stepID, presenter: presenter, provider: provider) + let viewController = StepViewController(interactor: interactor) presenter.viewController = viewController self.moduleInput = interactor diff --git a/Stepic/Sources/Modules/NewStep/NewStepDataFlow.swift b/Stepic/Sources/Modules/Step/StepDataFlow.swift similarity index 98% rename from Stepic/Sources/Modules/NewStep/NewStepDataFlow.swift rename to Stepic/Sources/Modules/Step/StepDataFlow.swift index 917a391d37..541f68100a 100644 --- a/Stepic/Sources/Modules/NewStep/NewStepDataFlow.swift +++ b/Stepic/Sources/Modules/Step/StepDataFlow.swift @@ -1,6 +1,6 @@ import Foundation -enum NewStep { +enum StepDataFlow { /// Load step content enum StepLoad { struct Request {} @@ -135,7 +135,7 @@ enum NewStep { enum ViewControllerState { case loading case error - case result(data: NewStepViewModel) + case result(data: StepViewModel) } enum QuizType: Equatable { diff --git a/Stepic/Sources/Modules/NewStep/NewStepInteractor.swift b/Stepic/Sources/Modules/Step/StepInteractor.swift similarity index 74% rename from Stepic/Sources/Modules/NewStep/NewStepInteractor.swift rename to Stepic/Sources/Modules/Step/StepInteractor.swift index 15f81ee010..cb9bace527 100644 --- a/Stepic/Sources/Modules/NewStep/NewStepInteractor.swift +++ b/Stepic/Sources/Modules/Step/StepInteractor.swift @@ -1,22 +1,22 @@ import Foundation import PromiseKit -protocol NewStepInteractorProtocol { - func doStepLoad(request: NewStep.StepLoad.Request) - func doLessonNavigationRequest(request: NewStep.LessonNavigationRequest.Request) - func doStepNavigationRequest(request: NewStep.StepNavigationRequest.Request) - func doAutoplayNavigationRequest(request: NewStep.AutoplayNavigationRequest.Request) - func doStepViewRequest(request: NewStep.StepViewRequest.Request) - func doStepDoneRequest(request: NewStep.StepDoneRequest.Request) - func doDiscussionsButtonUpdate(request: NewStep.DiscussionsButtonUpdate.Request) - func doDiscussionsPresentation(request: NewStep.DiscussionsPresentation.Request) +protocol StepInteractorProtocol { + func doStepLoad(request: StepDataFlow.StepLoad.Request) + func doLessonNavigationRequest(request: StepDataFlow.LessonNavigationRequest.Request) + func doStepNavigationRequest(request: StepDataFlow.StepNavigationRequest.Request) + func doAutoplayNavigationRequest(request: StepDataFlow.AutoplayNavigationRequest.Request) + func doStepViewRequest(request: StepDataFlow.StepViewRequest.Request) + func doStepDoneRequest(request: StepDataFlow.StepDoneRequest.Request) + func doDiscussionsButtonUpdate(request: StepDataFlow.DiscussionsButtonUpdate.Request) + func doDiscussionsPresentation(request: StepDataFlow.DiscussionsPresentation.Request) } -final class NewStepInteractor: NewStepInteractorProtocol { - weak var moduleOutput: NewStepOutputProtocol? +final class StepInteractor: StepInteractorProtocol { + weak var moduleOutput: StepOutputProtocol? - private let presenter: NewStepPresenterProtocol - private let provider: NewStepProviderProtocol + private let presenter: StepPresenterProtocol + private let provider: StepProviderProtocol private let stepID: Step.IdType private var didAnalyticsSend = false @@ -26,8 +26,8 @@ final class NewStepInteractor: NewStepInteractorProtocol { init( stepID: Step.IdType, - presenter: NewStepPresenterProtocol, - provider: NewStepProviderProtocol + presenter: StepPresenterProtocol, + provider: StepProviderProtocol ) { self.presenter = presenter self.provider = provider @@ -35,7 +35,7 @@ final class NewStepInteractor: NewStepInteractorProtocol { self.stepID = stepID } - func doStepLoad(request: NewStep.StepLoad.Request) { + func doStepLoad(request: StepDataFlow.StepLoad.Request) { firstly { self.provider.fetchStep(id: self.stepID) }.then(on: .global(qos: .userInitiated)) { @@ -45,18 +45,19 @@ final class NewStepInteractor: NewStepInteractorProtocol { } return when( - fulfilled: self.provider.fetchCurrentFontSize(), self.provider.fetchStoredImages(id: step.id) + fulfilled: self.provider.fetchCurrentFontSize(), + self.provider.fetchStoredImages(id: step.id) ).map { ($0, $1, step) } }.done(on: .global(qos: .userInitiated)) { fontSize, storedImages, step in self.currentStepIndex = step.position - 1 DispatchQueue.main.async { [weak self] in - let data = NewStep.StepLoad.Data( + let data = StepDataFlow.StepLoad.Data( step: step, fontSize: fontSize, storedImages: storedImages.compactMap { imageURL, storedFile in if let imageData = storedFile.data { - return NewStep.StoredImage(url: imageURL, data: imageData) + return StepDataFlow.StoredImage(url: imageURL, data: imageData) } return nil } @@ -105,7 +106,7 @@ final class NewStepInteractor: NewStepInteractorProtocol { } } - func doStepNavigationRequest(request: NewStep.StepNavigationRequest.Request) { + func doStepNavigationRequest(request: StepDataFlow.StepNavigationRequest.Request) { switch request.direction { case .index(let stepIndex): self.moduleOutput?.handleStepNavigation(to: stepIndex) @@ -118,7 +119,7 @@ final class NewStepInteractor: NewStepInteractorProtocol { } } - func doLessonNavigationRequest(request: NewStep.LessonNavigationRequest.Request) { + func doLessonNavigationRequest(request: StepDataFlow.LessonNavigationRequest.Request) { switch request.direction { case .previous: self.moduleOutput?.handlePreviousUnitNavigation() @@ -127,7 +128,7 @@ final class NewStepInteractor: NewStepInteractorProtocol { } } - func doAutoplayNavigationRequest(request: NewStep.AutoplayNavigationRequest.Request) { + func doAutoplayNavigationRequest(request: StepDataFlow.AutoplayNavigationRequest.Request) { guard let currentStepIndex = self.currentStepIndex else { return } @@ -135,15 +136,15 @@ final class NewStepInteractor: NewStepInteractorProtocol { self.moduleOutput?.handleAutoplayNavigation(from: currentStepIndex) } - func doStepViewRequest(request: NewStep.StepViewRequest.Request) { + func doStepViewRequest(request: StepDataFlow.StepViewRequest.Request) { self.moduleOutput?.handleStepView(id: self.stepID) } - func doStepDoneRequest(request: NewStep.StepDoneRequest.Request) { + func doStepDoneRequest(request: StepDataFlow.StepDoneRequest.Request) { self.moduleOutput?.handleStepDone(id: self.stepID) } - func doDiscussionsButtonUpdate(request: NewStep.DiscussionsButtonUpdate.Request) { + func doDiscussionsButtonUpdate(request: StepDataFlow.DiscussionsButtonUpdate.Request) { self.provider.fetchCachedStep(id: self.stepID).done { cachedStep in if let cachedStep = cachedStep { self.presenter.presentDiscussionsButtonUpdate(response: .init(step: cachedStep)) @@ -151,7 +152,7 @@ final class NewStepInteractor: NewStepInteractorProtocol { }.cauterize() } - func doDiscussionsPresentation(request: NewStep.DiscussionsPresentation.Request) { + func doDiscussionsPresentation(request: StepDataFlow.DiscussionsPresentation.Request) { self.provider.fetchCachedStep(id: self.stepID).done { cachedStep in if let cachedStep = cachedStep { self.presenter.presentDiscussions(response: .init(step: cachedStep)) @@ -166,9 +167,9 @@ final class NewStepInteractor: NewStepInteractorProtocol { } } -// MARK: - NewStepInteractor: NewStepInputProtocol - +// MARK: - StepInteractor: StepInputProtocol - -extension NewStepInteractor: NewStepInputProtocol { +extension StepInteractor: StepInputProtocol { func updateStepNavigation( canNavigateToPreviousUnit: Bool, canNavigateToNextUnit: Bool, @@ -193,7 +194,7 @@ extension NewStepInteractor: NewStepInputProtocol { fontSize: fetchResult.0, storedImages: fetchResult.1.compactMap { imageURL, storedFile in if let imageData = storedFile.data { - return NewStep.StoredImage(url: imageURL, data: imageData) + return StepDataFlow.StoredImage(url: imageURL, data: imageData) } return nil } diff --git a/Stepic/Sources/Modules/NewStep/NewStepPresenter.swift b/Stepic/Sources/Modules/Step/StepPresenter.swift similarity index 75% rename from Stepic/Sources/Modules/NewStep/NewStepPresenter.swift rename to Stepic/Sources/Modules/Step/StepPresenter.swift index 27f75de281..686615cfb3 100644 --- a/Stepic/Sources/Modules/NewStep/NewStepPresenter.swift +++ b/Stepic/Sources/Modules/Step/StepPresenter.swift @@ -1,19 +1,19 @@ import PromiseKit import UIKit -protocol NewStepPresenterProtocol { - func presentStep(response: NewStep.StepLoad.Response) - func presentStepTextUpdate(response: NewStep.StepTextUpdate.Response) - func presentPlayStep(response: NewStep.PlayStep.Response) - func presentControlsUpdate(response: NewStep.ControlsUpdate.Response) - func presentDiscussionsButtonUpdate(response: NewStep.DiscussionsButtonUpdate.Response) - func presentDiscussions(response: NewStep.DiscussionsPresentation.Response) +protocol StepPresenterProtocol { + func presentStep(response: StepDataFlow.StepLoad.Response) + func presentStepTextUpdate(response: StepDataFlow.StepTextUpdate.Response) + func presentPlayStep(response: StepDataFlow.PlayStep.Response) + func presentControlsUpdate(response: StepDataFlow.ControlsUpdate.Response) + func presentDiscussionsButtonUpdate(response: StepDataFlow.DiscussionsButtonUpdate.Response) + func presentDiscussions(response: StepDataFlow.DiscussionsPresentation.Response) } -final class NewStepPresenter: NewStepPresenterProtocol { - weak var viewController: NewStepViewControllerProtocol? +final class StepPresenter: StepPresenterProtocol { + weak var viewController: StepViewControllerProtocol? - func presentStep(response: NewStep.StepLoad.Response) { + func presentStep(response: StepDataFlow.StepLoad.Response) { if case .success(let data) = response.result { self.makeViewModel( step: data.step, @@ -22,7 +22,7 @@ final class NewStepPresenter: NewStepPresenterProtocol { ).done(on: .global(qos: .userInitiated)) { viewModel in DispatchQueue.main.async { [weak self] in self?.viewController?.displayStep( - viewModel: NewStep.StepLoad.ViewModel(state: .result(data: viewModel)) + viewModel: StepDataFlow.StepLoad.ViewModel(state: .result(data: viewModel)) ) } } @@ -31,11 +31,11 @@ final class NewStepPresenter: NewStepPresenterProtocol { } if case .failure = response.result { - self.viewController?.displayStep(viewModel: NewStep.StepLoad.ViewModel(state: .error)) + self.viewController?.displayStep(viewModel: StepDataFlow.StepLoad.ViewModel(state: .error)) } } - func presentStepTextUpdate(response: NewStep.StepTextUpdate.Response) { + func presentStepTextUpdate(response: StepDataFlow.StepTextUpdate.Response) { let htmlString = self.makeProcessedContentHTMLString( response.text, fontSize: response.fontSize, @@ -45,12 +45,12 @@ final class NewStepPresenter: NewStepPresenterProtocol { self.viewController?.displayStepTextUpdate(viewModel: .init(htmlText: htmlString)) } - func presentPlayStep(response: NewStep.PlayStep.Response) { + func presentPlayStep(response: StepDataFlow.PlayStep.Response) { self.viewController?.displayPlayStep(viewModel: .init()) } - func presentControlsUpdate(response: NewStep.ControlsUpdate.Response) { - let viewModel = NewStep.ControlsUpdate.ViewModel( + func presentControlsUpdate(response: StepDataFlow.ControlsUpdate.Response) { + let viewModel = StepDataFlow.ControlsUpdate.ViewModel( canNavigateToPreviousUnit: response.canNavigateToPreviousUnit, canNavigateToNextUnit: response.canNavigateToNextUnit, canNavigateToNextStep: response.canNavigateToNextStep @@ -59,7 +59,7 @@ final class NewStepPresenter: NewStepPresenterProtocol { self.viewController?.displayControlsUpdate(viewModel: viewModel) } - func presentDiscussionsButtonUpdate(response: NewStep.DiscussionsButtonUpdate.Response) { + func presentDiscussionsButtonUpdate(response: StepDataFlow.DiscussionsButtonUpdate.Response) { self.viewController?.displayDiscussionsButtonUpdate( viewModel: .init( title: self.makeDiscussionsLabelTitle(step: response.step), @@ -68,7 +68,7 @@ final class NewStepPresenter: NewStepPresenterProtocol { ) } - func presentDiscussions(response: NewStep.DiscussionsPresentation.Response) { + func presentDiscussions(response: StepDataFlow.DiscussionsPresentation.Response) { guard let discussionProxyID = response.step.discussionProxyID else { return } @@ -87,14 +87,14 @@ final class NewStepPresenter: NewStepPresenterProtocol { private func makeViewModel( step: Step, fontSize: StepFontSize, - storedImages: [NewStep.StoredImage] - ) -> Guarantee { + storedImages: [StepDataFlow.StoredImage] + ) -> Guarantee { Guarantee { seal in - let contentType: NewStepViewModel.ContentType = { + let contentType: StepViewModel.ContentType = { switch step.block.type { case .video: if let video = step.block.video { - let viewModel = NewStepVideoViewModel( + let viewModel = StepVideoViewModel( video: video, videoThumbnailImageURL: URL(string: video.thumbnailURL) ) @@ -111,12 +111,12 @@ final class NewStepPresenter: NewStepPresenterProtocol { } }() - let quizType: NewStep.QuizType? + let quizType: StepDataFlow.QuizType? switch step.block.type { case .text, .video: quizType = nil default: - quizType = NewStep.QuizType(blockName: step.block.name) + quizType = StepDataFlow.QuizType(blockName: step.block.name) } let shouldShowStepStatistics: Bool = { @@ -132,7 +132,7 @@ final class NewStepPresenter: NewStepPresenterProtocol { let discussionsLabelTitle = self.makeDiscussionsLabelTitle(step: step) let urlPath = "\(StepicApplicationsInfo.stepicURL)/lesson/\(step.lessonID)/step/\(step.position)?from_mobile_app=true" - let viewModel = NewStepViewModel( + let viewModel = StepViewModel( content: contentType, quizType: quizType, discussionsLabelTitle: discussionsLabelTitle, @@ -167,7 +167,7 @@ final class NewStepPresenter: NewStepPresenterProtocol { private func makeProcessedContentHTMLString( _ text: String, fontSize: StepFontSize, - storedImages: [NewStep.StoredImage] + storedImages: [StepDataFlow.StoredImage] ) -> String { let base64EncodedStringByImageURL = Dictionary( uniqueKeysWithValues: storedImages.map { ($0.url, $0.data.base64EncodedString()) } diff --git a/Stepic/Sources/Modules/NewStep/NewStepProvider.swift b/Stepic/Sources/Modules/Step/StepProvider.swift similarity index 95% rename from Stepic/Sources/Modules/NewStep/NewStepProvider.swift rename to Stepic/Sources/Modules/Step/StepProvider.swift index 77a7f88393..aa5d7b1002 100644 --- a/Stepic/Sources/Modules/NewStep/NewStepProvider.swift +++ b/Stepic/Sources/Modules/Step/StepProvider.swift @@ -1,18 +1,14 @@ import Foundation import PromiseKit -// MARK: NewStepProviderProtocol - - -protocol NewStepProviderProtocol { +protocol StepProviderProtocol { func fetchStep(id: Step.IdType) -> Promise> func fetchCachedStep(id: Step.IdType) -> Promise func fetchStoredImages(id: Step.IdType) -> Guarantee<[(imageURL: URL, storedFile: StoredFileProtocol)]> func fetchCurrentFontSize() -> Guarantee } -// MARK: - NewStepProvider: NewStepProviderProtocol - - -final class NewStepProvider: NewStepProviderProtocol { +final class StepProvider: StepProviderProtocol { private let stepsPersistenceService: StepsPersistenceServiceProtocol private let stepsNetworkService: StepsNetworkServiceProtocol private let stepFontSizeStorageManager: StepFontSizeStorageManagerProtocol @@ -30,6 +26,8 @@ final class NewStepProvider: NewStepProviderProtocol { self.imageStoredFileManager = imageStoredFileManager } + // MARK: Protocol Conforming + func fetchStep(id: Step.IdType) -> Promise> { let persistenceServicePromise = Guarantee(self.stepsPersistenceService.fetch(ids: [id]), fallback: nil) let networkServicePromise = Guarantee(self.stepsNetworkService.fetch(ids: [id]), fallback: nil) diff --git a/Stepic/Sources/Modules/NewStep/NewStepView.swift b/Stepic/Sources/Modules/Step/StepView.swift similarity index 83% rename from Stepic/Sources/Modules/NewStep/NewStepView.swift rename to Stepic/Sources/Modules/Step/StepView.swift index aae5487416..8ac382b906 100644 --- a/Stepic/Sources/Modules/NewStep/NewStepView.swift +++ b/Stepic/Sources/Modules/Step/StepView.swift @@ -1,19 +1,19 @@ import SnapKit import UIKit -protocol NewStepViewDelegate: AnyObject { - func newStepViewDidRequestVideo(_ view: NewStepView) - func newStepViewDidRequestPrevious(_ view: NewStepView) - func newStepViewDidRequestNext(_ view: NewStepView) - func newStepViewDidRequestDiscussions(_ view: NewStepView) - func newStepViewDidLoadContent(_ view: NewStepView) - - func newStepView(_ view: NewStepView, didRequestFullscreenImage url: URL) - func newStepView(_ view: NewStepView, didRequestFullscreenImage image: UIImage) - func newStepView(_ view: NewStepView, didRequestOpenURL url: URL) +protocol StepViewDelegate: AnyObject { + func stepViewDidRequestVideo(_ view: StepView) + func stepViewDidRequestPrevious(_ view: StepView) + func stepViewDidRequestNext(_ view: StepView) + func stepViewDidRequestDiscussions(_ view: StepView) + func stepViewDidLoadContent(_ view: StepView) + + func stepView(_ view: StepView, didRequestFullscreenImage url: URL) + func stepView(_ view: StepView, didRequestFullscreenImage image: UIImage) + func stepView(_ view: StepView, didRequestOpenURL url: URL) } -extension NewStepView { +extension StepView { struct Appearance { let loadingIndicatorColor = UIColor.mainDark } @@ -24,9 +24,9 @@ extension NewStepView { } } -final class NewStepView: UIView { +final class StepView: UIView { let appearance: Appearance - weak var delegate: NewStepViewDelegate? + weak var delegate: StepViewDelegate? private lazy var loadingIndicatorView: UIActivityIndicatorView = { let loadingIndicatorView = UIActivityIndicatorView(style: .whiteLarge) @@ -56,7 +56,7 @@ final class NewStepView: UIView { guard let strongSelf = self else { return } - strongSelf.delegate?.newStepViewDidRequestVideo(strongSelf) + strongSelf.delegate?.stepViewDidRequestVideo(strongSelf) } return view }() @@ -70,19 +70,19 @@ final class NewStepView: UIView { guard let strongSelf = self else { return } - strongSelf.delegate?.newStepViewDidRequestPrevious(strongSelf) + strongSelf.delegate?.stepViewDidRequestPrevious(strongSelf) } view.onNextButtonClick = { [weak self] in guard let strongSelf = self else { return } - strongSelf.delegate?.newStepViewDidRequestNext(strongSelf) + strongSelf.delegate?.stepViewDidRequestNext(strongSelf) } view.onDiscussionsButtonClick = { [weak self] in guard let strongSelf = self else { return } - strongSelf.delegate?.newStepViewDidRequestDiscussions(strongSelf) + strongSelf.delegate?.stepViewDidRequestDiscussions(strongSelf) } return view }() @@ -131,12 +131,12 @@ final class NewStepView: UIView { } } - func configure(viewModel: NewStepViewModel, quizView: UIView?) { + func configure(viewModel: StepViewModel, quizView: UIView?) { switch viewModel.content { case .video(let viewModel): self.scrollableStackView.insertArrangedView(self.stepVideoPreviewContainerView, at: 0) self.stepVideoPreviewView.thumbnailImageURL = viewModel?.videoThumbnailImageURL - self.delegate?.newStepViewDidLoadContent(self) + self.delegate?.stepViewDidLoadContent(self) case .text(let htmlString): self.scrollableStackView.insertArrangedView(self.stepTextView, at: 0) self.stepTextView.loadHTMLText(htmlString) @@ -207,7 +207,7 @@ final class NewStepView: UIView { } } -extension NewStepView: ProgrammaticallyInitializableViewProtocol { +extension StepView: ProgrammaticallyInitializableViewProtocol { func addSubviews() { self.stepVideoPreviewContainerView.addSubview(self.stepVideoPreviewView) @@ -236,20 +236,20 @@ extension NewStepView: ProgrammaticallyInitializableViewProtocol { } } -extension NewStepView: ProcessedContentTextViewDelegate { +extension StepView: ProcessedContentTextViewDelegate { func processedContentTextView(_ view: ProcessedContentTextView, didOpenLink url: URL) { - self.delegate?.newStepView(self, didRequestOpenURL: url) + self.delegate?.stepView(self, didRequestOpenURL: url) } func processedContentTextView(_ view: ProcessedContentTextView, didOpenImageURL url: URL) { - self.delegate?.newStepView(self, didRequestFullscreenImage: url) + self.delegate?.stepView(self, didRequestFullscreenImage: url) } func processedContentTextView(_ view: ProcessedContentTextView, didOpenImage image: UIImage) { - self.delegate?.newStepView(self, didRequestFullscreenImage: image) + self.delegate?.stepView(self, didRequestFullscreenImage: image) } func processedContentTextViewDidLoadContent(_ view: ProcessedContentTextView) { - self.delegate?.newStepViewDidLoadContent(self) + self.delegate?.stepViewDidLoadContent(self) } } diff --git a/Stepic/Sources/Modules/NewStep/NewStepViewController.swift b/Stepic/Sources/Modules/Step/StepViewController.swift similarity index 76% rename from Stepic/Sources/Modules/NewStep/NewStepViewController.swift rename to Stepic/Sources/Modules/Step/StepViewController.swift index a8be235794..fa7daf68a5 100644 --- a/Stepic/Sources/Modules/NewStep/NewStepViewController.swift +++ b/Stepic/Sources/Modules/Step/StepViewController.swift @@ -1,36 +1,35 @@ import Agrume import UIKit -// MARK: NewStepViewControllerProtocol: class - - -protocol NewStepViewControllerProtocol: AnyObject { - func displayStep(viewModel: NewStep.StepLoad.ViewModel) - func displayStepTextUpdate(viewModel: NewStep.StepTextUpdate.ViewModel) - func displayPlayStep(viewModel: NewStep.PlayStep.ViewModel) - func displayControlsUpdate(viewModel: NewStep.ControlsUpdate.ViewModel) - func displayDiscussionsButtonUpdate(viewModel: NewStep.DiscussionsButtonUpdate.ViewModel) - func displayDiscussions(viewModel: NewStep.DiscussionsPresentation.ViewModel) +protocol StepViewControllerProtocol: AnyObject { + func displayStep(viewModel: StepDataFlow.StepLoad.ViewModel) + func displayStepTextUpdate(viewModel: StepDataFlow.StepTextUpdate.ViewModel) + func displayPlayStep(viewModel: StepDataFlow.PlayStep.ViewModel) + func displayControlsUpdate(viewModel: StepDataFlow.ControlsUpdate.ViewModel) + func displayDiscussionsButtonUpdate(viewModel: StepDataFlow.DiscussionsButtonUpdate.ViewModel) + func displayDiscussions(viewModel: StepDataFlow.DiscussionsPresentation.ViewModel) } -// MARK: - NewStepViewController (Animation) - -extension NewStepViewController { +// MARK: - StepViewController (Animation) - + +extension StepViewController { enum Animation { static let autoplayVideoPlayerPresentationDelay: TimeInterval = 0.75 } } -// MARK: - NewStepViewController: UIViewController, ControllerWithStepikPlaceholder - +// MARK: - StepViewController: UIViewController, ControllerWithStepikPlaceholder - -final class NewStepViewController: UIViewController, ControllerWithStepikPlaceholder { +final class StepViewController: UIViewController, ControllerWithStepikPlaceholder { private static let stepPassedDelay: TimeInterval = 1.0 - lazy var newStepView = self.view as? NewStepView + lazy var stepView = self.view as? StepView var placeholderContainer = StepikPlaceholderControllerContainer() - private let interactor: NewStepInteractorProtocol + private let interactor: StepInteractorProtocol - private var state: NewStep.ViewControllerState { + private var state: StepDataFlow.ViewControllerState { didSet { self.updateState() } @@ -45,7 +44,7 @@ final class NewStepViewController: UIViewController, ControllerWithStepikPlaceho /// Keeps track of need to autoplay the step or not. private var shouldRequestAutoplay = false - init(interactor: NewStepInteractorProtocol) { + init(interactor: StepInteractorProtocol) { self.interactor = interactor self.state = .loading super.init(nibName: nil, bundle: nil) @@ -57,7 +56,7 @@ final class NewStepViewController: UIViewController, ControllerWithStepikPlaceho } override func loadView() { - self.view = NewStepView(frame: UIScreen.main.bounds) + self.view = StepView(frame: UIScreen.main.bounds) } override func viewDidLoad() { @@ -73,7 +72,7 @@ final class NewStepViewController: UIViewController, ControllerWithStepikPlaceho for: .connectionError ) - self.newStepView?.delegate = self + self.stepView?.delegate = self // Enter group, leave when content did load & in view did appear self.sendStepDidPassedGroup?.enter() @@ -122,7 +121,7 @@ final class NewStepViewController: UIViewController, ControllerWithStepikPlaceho self.showContent() case .loading: self.isPlaceholderShown = false - self.newStepView?.startLoading() + self.stepView?.startLoading() case .error: self.showPlaceholder(for: .connectionError) } @@ -139,7 +138,7 @@ final class NewStepViewController: UIViewController, ControllerWithStepikPlaceho return } - DispatchQueue.main.asyncAfter(deadline: .now() + NewStepViewController.stepPassedDelay) { [weak self] in + DispatchQueue.main.asyncAfter(deadline: .now() + StepViewController.stepPassedDelay) { [weak self] in self?.interactor.doStepDoneRequest(request: .init()) } } @@ -156,7 +155,7 @@ final class NewStepViewController: UIViewController, ControllerWithStepikPlaceho guard let quizType = viewModel.quizType else { // Video & text steps - self.newStepView?.configure(viewModel: viewModel, quizView: nil) + self.stepView?.configure(viewModel: viewModel, quizView: nil) self.requestAutoplayIfNeeded() return } @@ -177,12 +176,12 @@ final class NewStepViewController: UIViewController, ControllerWithStepikPlaceho if let controller = quizController { self.addChild(controller) - self.newStepView?.configure(viewModel: viewModel, quizView: controller.view) + self.stepView?.configure(viewModel: viewModel, quizView: controller.view) } else { let assembly = UnsupportedQuizAssembly(stepURLPath: viewModel.stepURLPath) let viewController = assembly.makeModule() self.addChild(viewController) - self.newStepView?.configure(viewModel: viewModel, quizView: viewController.view) + self.stepView?.configure(viewModel: viewModel, quizView: viewController.view) } } @@ -200,35 +199,35 @@ final class NewStepViewController: UIViewController, ControllerWithStepikPlaceho } } -// MARK: - NewStepViewController: NewStepViewControllerProtocol - +// MARK: - StepViewController: StepViewControllerProtocol - -extension NewStepViewController: NewStepViewControllerProtocol { - func displayStep(viewModel: NewStep.StepLoad.ViewModel) { +extension StepViewController: StepViewControllerProtocol { + func displayStep(viewModel: StepDataFlow.StepLoad.ViewModel) { self.state = viewModel.state } - func displayStepTextUpdate(viewModel: NewStep.StepTextUpdate.ViewModel) { - self.newStepView?.updateText(viewModel.htmlText) + func displayStepTextUpdate(viewModel: StepDataFlow.StepTextUpdate.ViewModel) { + self.stepView?.updateText(viewModel.htmlText) } - func displayPlayStep(viewModel: NewStep.PlayStep.ViewModel) { + func displayPlayStep(viewModel: StepDataFlow.PlayStep.ViewModel) { self.shouldRequestAutoplay = true self.requestAutoplayIfNeeded() } - func displayControlsUpdate(viewModel: NewStep.ControlsUpdate.ViewModel) { - self.newStepView?.updateNavigationButtons( + func displayControlsUpdate(viewModel: StepDataFlow.ControlsUpdate.ViewModel) { + self.stepView?.updateNavigationButtons( hasPreviousButton: viewModel.canNavigateToPreviousUnit, hasNextButton: viewModel.canNavigateToNextUnit ) self.canNavigateToNextStep = viewModel.canNavigateToNextStep } - func displayDiscussionsButtonUpdate(viewModel: NewStep.DiscussionsButtonUpdate.ViewModel) { - self.newStepView?.updateDiscussionButton(title: viewModel.title, isEnabled: viewModel.isEnabled) + func displayDiscussionsButtonUpdate(viewModel: StepDataFlow.DiscussionsButtonUpdate.ViewModel) { + self.stepView?.updateDiscussionButton(title: viewModel.title, isEnabled: viewModel.isEnabled) } - func displayDiscussions(viewModel: NewStep.DiscussionsPresentation.ViewModel) { + func displayDiscussions(viewModel: StepDataFlow.DiscussionsPresentation.ViewModel) { let discussionsAssembly = DiscussionsAssembly( discussionProxyID: viewModel.discussionProxyID, stepID: viewModel.stepID @@ -284,26 +283,26 @@ extension NewStepViewController: NewStepViewControllerProtocol { } } -// MARK: - NewStepViewController: NewStepViewDelegate - +// MARK: - StepViewController: StepViewDelegate - -extension NewStepViewController: NewStepViewDelegate { - func newStepViewDidRequestVideo(_ view: NewStepView) { +extension StepViewController: StepViewDelegate { + func stepViewDidRequestVideo(_ view: StepView) { self.presentVideoPlayer() } - func newStepViewDidRequestPrevious(_ view: NewStepView) { + func stepViewDidRequestPrevious(_ view: StepView) { self.interactor.doLessonNavigationRequest(request: .init(direction: .previous)) } - func newStepViewDidRequestNext(_ view: NewStepView) { + func stepViewDidRequestNext(_ view: StepView) { self.interactor.doLessonNavigationRequest(request: .init(direction: .next)) } - func newStepViewDidRequestDiscussions(_ view: NewStepView) { + func stepViewDidRequestDiscussions(_ view: StepView) { self.interactor.doDiscussionsPresentation(request: .init()) } - func newStepView(_ view: NewStepView, didRequestOpenURL url: URL) { + func stepView(_ view: StepView, didRequestOpenURL url: URL) { guard case .result(let viewModel) = self.state else { return } @@ -331,18 +330,18 @@ extension NewStepViewController: NewStepViewDelegate { ) } - func newStepView(_ view: NewStepView, didRequestFullscreenImage url: URL) { + func stepView(_ view: StepView, didRequestFullscreenImage url: URL) { let agrume = Agrume(url: url) agrume.show(from: self) } - func newStepView(_ view: NewStepView, didRequestFullscreenImage image: UIImage) { + func stepView(_ view: StepView, didRequestFullscreenImage image: UIImage) { let agrume = Agrume(image: image) agrume.show(from: self) } - func newStepViewDidLoadContent(_ view: NewStepView) { - self.newStepView?.endLoading() + func stepViewDidLoadContent(_ view: StepView) { + self.stepView?.endLoading() } // MARK: Private helpers @@ -374,9 +373,9 @@ extension NewStepViewController: NewStepViewDelegate { } } -// MARK: - NewStepViewController: BaseQuizOutputProtocol - +// MARK: - StepViewController: BaseQuizOutputProtocol - -extension NewStepViewController: BaseQuizOutputProtocol { +extension StepViewController: BaseQuizOutputProtocol { func handleCorrectSubmission() { self.interactor.doStepDoneRequest(request: .init()) } @@ -386,9 +385,9 @@ extension NewStepViewController: BaseQuizOutputProtocol { } } -// MARK: - NewStepViewController: StepikVideoPlayerViewControllerDelegate - +// MARK: - StepViewController: StepikVideoPlayerViewControllerDelegate - -extension NewStepViewController: StepikVideoPlayerViewControllerDelegate { +extension StepViewController: StepikVideoPlayerViewControllerDelegate { func stepikVideoPlayerViewControllerDidRequestAutoplay() { self.dismiss( animated: true, diff --git a/Stepic/Sources/Modules/NewStep/NewStepViewModel.swift b/Stepic/Sources/Modules/Step/StepViewModel.swift similarity index 79% rename from Stepic/Sources/Modules/NewStep/NewStepViewModel.swift rename to Stepic/Sources/Modules/Step/StepViewModel.swift index 608bb4b263..f9adde6ad5 100644 --- a/Stepic/Sources/Modules/NewStep/NewStepViewModel.swift +++ b/Stepic/Sources/Modules/Step/StepViewModel.swift @@ -1,8 +1,8 @@ import Foundation -struct NewStepViewModel { +struct StepViewModel { let content: ContentType - let quizType: NewStep.QuizType? + let quizType: StepDataFlow.QuizType? let discussionsLabelTitle: String let isDiscussionsEnabled: Bool let discussionProxyID: DiscussionProxy.IdType? @@ -16,11 +16,11 @@ struct NewStepViewModel { enum ContentType { case text(htmlString: String) - case video(viewModel: NewStepVideoViewModel?) + case video(viewModel: StepVideoViewModel?) } } -struct NewStepVideoViewModel { +struct StepVideoViewModel { @available(*, deprecated, message: "Deprecated initialization") let video: Video let videoThumbnailImageURL: URL? diff --git a/Stepic/Sources/Modules/NewStep/Views/StepControlsView.swift b/Stepic/Sources/Modules/Step/Views/StepControlsView.swift similarity index 100% rename from Stepic/Sources/Modules/NewStep/Views/StepControlsView.swift rename to Stepic/Sources/Modules/Step/Views/StepControlsView.swift diff --git a/Stepic/Sources/Modules/NewStep/Views/StepDiscussionsButton.swift b/Stepic/Sources/Modules/Step/Views/StepDiscussionsButton.swift similarity index 100% rename from Stepic/Sources/Modules/NewStep/Views/StepDiscussionsButton.swift rename to Stepic/Sources/Modules/Step/Views/StepDiscussionsButton.swift diff --git a/Stepic/Sources/Modules/NewStep/Views/StepNavigationButton.swift b/Stepic/Sources/Modules/Step/Views/StepNavigationButton.swift similarity index 100% rename from Stepic/Sources/Modules/NewStep/Views/StepNavigationButton.swift rename to Stepic/Sources/Modules/Step/Views/StepNavigationButton.swift diff --git a/Stepic/Sources/Modules/NewStep/Views/StepStatisticsView.swift b/Stepic/Sources/Modules/Step/Views/StepStatisticsView.swift similarity index 100% rename from Stepic/Sources/Modules/NewStep/Views/StepStatisticsView.swift rename to Stepic/Sources/Modules/Step/Views/StepStatisticsView.swift diff --git a/Stepic/Sources/Modules/NewStep/Views/StepVideoPreviewView.swift b/Stepic/Sources/Modules/Step/Views/StepVideoPreviewView.swift similarity index 100% rename from Stepic/Sources/Modules/NewStep/Views/StepVideoPreviewView.swift rename to Stepic/Sources/Modules/Step/Views/StepVideoPreviewView.swift diff --git a/StepicTests/ModulesTests/NewStepViewControllerTests.swift b/StepicTests/ModulesTests/NewStepViewControllerTests.swift index 7feab58248..b9addc807c 100644 --- a/StepicTests/ModulesTests/NewStepViewControllerTests.swift +++ b/StepicTests/ModulesTests/NewStepViewControllerTests.swift @@ -5,34 +5,34 @@ import Quick import SwiftyJSON import XCTest -private final class NewStepViewControllerMock: NewStepViewControllerProtocol { - var didSetViewModelCompletion: ((NewStepViewModel) -> Void)? +private final class NewStepViewControllerMock: StepViewControllerProtocol { + var didSetViewModelCompletion: ((StepViewModel) -> Void)? - func displayStep(viewModel: NewStep.StepLoad.ViewModel) { + func displayStep(viewModel: StepDataFlow.StepLoad.ViewModel) { if case .result(let data) = viewModel.state { self.didSetViewModelCompletion?(data) } } - func displayStepTextUpdate(viewModel: NewStep.StepTextUpdate.ViewModel) { } + func displayStepTextUpdate(viewModel: StepDataFlow.StepTextUpdate.ViewModel) { } - func displayPlayStep(viewModel: NewStep.PlayStep.ViewModel) { } + func displayPlayStep(viewModel: StepDataFlow.PlayStep.ViewModel) { } - func displayControlsUpdate(viewModel: NewStep.ControlsUpdate.ViewModel) { } + func displayControlsUpdate(viewModel: StepDataFlow.ControlsUpdate.ViewModel) { } - func displayDiscussionsButtonUpdate(viewModel: NewStep.DiscussionsButtonUpdate.ViewModel) { } + func displayDiscussionsButtonUpdate(viewModel: StepDataFlow.DiscussionsButtonUpdate.ViewModel) { } - func displayDiscussions(viewModel: NewStep.DiscussionsPresentation.ViewModel) { } + func displayDiscussions(viewModel: StepDataFlow.DiscussionsPresentation.ViewModel) { } } class NewStepViewControllerSpec: QuickSpec { override func spec() { - var presenter: NewStepPresenterProtocol! + var presenter: StepPresenterProtocol! var viewController: NewStepViewControllerMock! beforeEach { viewController = NewStepViewControllerMock() - let newStepPresenter = NewStepPresenter() + let newStepPresenter = StepPresenter() newStepPresenter.viewController = viewController presenter = newStepPresenter } @@ -58,7 +58,7 @@ class NewStepViewControllerSpec: QuickSpec { presenter.presentStep( response: .init( - result: .success(NewStep.StepLoad.Data(step: step, fontSize: .small, storedImages: [])) + result: .success(StepDataFlow.StepLoad.Data(step: step, fontSize: .small, storedImages: [])) ) ) @@ -127,7 +127,7 @@ class NewStepViewControllerSpec: QuickSpec { presenter.presentStep( response: .init( - result: .success(NewStep.StepLoad.Data(step: step, fontSize: .small, storedImages: [])) + result: .success(StepDataFlow.StepLoad.Data(step: step, fontSize: .small, storedImages: [])) ) ) @@ -147,7 +147,7 @@ class NewStepViewControllerSpec: QuickSpec { } it("has correct quiz type") { - let blockNameWithQuizTypePairs: [(String, NewStep.QuizType)] = [ + let blockNameWithQuizTypePairs: [(String, StepDataFlow.QuizType)] = [ ("choice", .choice), ("code", .code), ("free-answer", .freeAnswer), @@ -196,7 +196,7 @@ class NewStepViewControllerSpec: QuickSpec { waitUntil { done in presenter.presentStep( response: .init( - result: .success(NewStep.StepLoad.Data(step: step, fontSize: .small, storedImages: [])) + result: .success(StepDataFlow.StepLoad.Data(step: step, fontSize: .small, storedImages: [])) ) ) From 9997f5691d05bf66b69be142afbc48356a32aa57 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Mon, 3 Feb 2020 01:53:07 +0300 Subject: [PATCH 06/12] Solutions forum (read only) (#639) * Add DiscussionThread model * Add DiscussionThreads API * Fix API call * Solutions button * Add DiscussionsSortTypeStorageManager * Prefer Self over class * Present solutions * Remove back button title for discussions top controller * Update navigation item * Solution view model * Show solutions * Refactor rename stepicURL -> stepikURL * Refactor rename StepicToken -> StepikToken * Clean up replies * Show only cached submissions * Show solution alert action * Deep link * Empty state * Disable editable actions * Test submission parsing --- Stepic.xcodeproj/project.pbxproj | 86 ++++- Stepic/AchievementBadgeView.swift | 2 +- Stepic/ApiDataDownloader.swift | 1 + Stepic/AppDelegate.swift | 6 +- Stepic/Attempt.swift | 61 ++-- Stepic/AttemptsAPI.swift | 4 +- Stepic/AuthAPI.swift | 40 +-- Stepic/AuthInfo.swift | 6 +- Stepic/Block.swift | 4 +- Stepic/CardStepViewController.swift | 2 +- Stepic/CardsStepsViewController.swift | 6 +- Stepic/ChoiceReply.swift | 40 ++- Stepic/CodeReply.swift | 52 +-- Stepic/CommentsAPI.swift | 38 +- Stepic/CongratulationViewController.swift | 2 +- Stepic/ContinueActionButton.swift | 2 +- Stepic/Course.swift | 2 +- Stepic/CreateRequestMaker.swift | 2 +- Stepic/DeleteRequestMaker.swift | 2 +- Stepic/DevicesAPI.swift | 10 +- .../DiscussionThread+CoreDataProperties.swift | 64 ++++ Stepic/DiscussionThread.swift | 50 +++ Stepic/DiscussionThreadsAPI.swift | 34 ++ Stepic/Discussions/Comment.swift | 37 +- Stepic/Discussions/Vote.swift | 6 + Stepic/EmailAuthViewController.swift | 2 +- Stepic/EnrollmentsAPI.swift | 4 +- Stepic/Extensions/UIColorExtensions.swift | 6 +- Stepic/FreeAnswerReply.swift | 47 ++- Stepic/HTMLProcessor.swift | 2 +- .../quiz-feedback-correct.pdf | Bin 3902 -> 3930 bytes .../solutions-icon.imageset/Contents.json | 15 + .../solutions-icon.pdf | Bin 0 -> 1525 bytes Stepic/MatchingReply.swift | 37 +- Stepic/MathReply.swift | 40 ++- Stepic/Model.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 316 ++++++++++++++++ Stepic/NotificationDataExtractor.swift | 2 +- Stepic/NotificationStatusButton.swift | 2 +- Stepic/NotificationSuggestionManager.swift | 2 +- Stepic/NotificationsAPI.swift | 2 +- Stepic/NumberReply.swift | 40 ++- .../PersistentUserTokenRecoveryManager.swift | 8 +- Stepic/ProfileViewController.swift | 2 +- Stepic/QuizPresenter.swift | 2 +- Stepic/QuizViewController.swift | 2 +- Stepic/RateAppManager.swift | 2 +- Stepic/RateAppViewController.swift | 2 +- Stepic/RecommendationsAPI.swift | 4 +- Stepic/RegistrationPresenter.swift | 2 +- Stepic/RegistrationViewController.swift | 2 +- Stepic/RemoteConfig.swift | 8 +- Stepic/RemoteVersionManager.swift | 2 +- Stepic/Reply.swift | 8 - Stepic/RetrieveRequestMaker.swift | 8 +- Stepic/SQLReply.swift | 35 +- Stepic/SearchResultsAPI.swift | 2 +- Stepic/Services/DeepLinks/DeepLinkRoute.swift | 15 + .../Services/DeepLinks/DeepLinkRouter.swift | 52 ++- .../DeepLinks/DeepLinkRoutingService.swift | 15 +- .../NotificationsRegistrationService.swift | 2 +- Stepic/Session.swift | 8 +- Stepic/SocialAuthPresenter.swift | 2 +- Stepic/SocialAuthProviders.swift | 12 +- Stepic/SocialAuthViewController.swift | 2 +- Stepic/SortingReply.swift | 40 ++- .../StyledTabBarViewController.swift | 2 +- .../ContentProcessingRule.swift | 2 +- .../CourseInfo/CourseInfoInteractor.swift | 4 +- .../Cell/CourseInfoTabSyllabusCellView.swift | 2 +- .../CourseInfoTabSyllabusSectionView.swift | 2 +- .../Views/CourseListColorMode.swift | 8 +- .../Views/Widget/CourseWidgetButton.swift | 4 +- .../Discussions/DiscussionsAssembly.swift | 8 +- .../Discussions/DiscussionsDataFlow.swift | 38 +- .../Discussions/DiscussionsInteractor.swift | 151 ++++---- .../Discussions/DiscussionsPresenter.swift | 53 ++- .../Discussions/DiscussionsProvider.swift | 26 +- .../DiscussionsViewController.swift | 131 ++++--- .../Discussions/DiscussionsViewModel.swift | 7 + .../Views/Cell/DiscussionsCellView.swift | 79 +++- .../Views/Cell/DiscussionsTableViewCell.swift | 4 + .../Views/DiscussionsSolutionControl.swift | 150 ++++++++ .../DiscussionsTableViewDataSource.swift | 34 +- .../Modules/Lesson/LessonPresenter.swift | 2 +- .../Quizzes/BaseQuiz/BaseQuizInteractor.swift | 2 + .../Quizzes/BaseQuiz/BaseQuizPresenter.swift | 2 +- .../Quizzes/BaseQuiz/BaseQuizView.swift | 2 +- .../BaseQuizOutputProtocol.swift | 1 + ...CodeQuizFullscreenCodeViewController.swift | 2 +- .../Modules/Solution/SolutionAssembly.swift | 17 +- .../Modules/Solution/SolutionDataFlow.swift | 2 +- .../Modules/Solution/SolutionInteractor.swift | 30 +- .../Modules/Solution/SolutionPresenter.swift | 26 +- .../Modules/Solution/SolutionProvider.swift | 24 +- .../Modules/Solution/SolutionView.swift | 2 +- .../Solution/SolutionViewController.swift | 48 ++- .../Sources/Modules/Step/StepAssembly.swift | 6 +- .../Sources/Modules/Step/StepDataFlow.swift | 44 ++- .../Sources/Modules/Step/StepInteractor.swift | 82 ++++- .../Sources/Modules/Step/StepPresenter.swift | 68 +++- .../Sources/Modules/Step/StepProvider.swift | 84 ++++- Stepic/Sources/Modules/Step/StepView.swift | 12 + .../Modules/Step/StepViewController.swift | 38 +- .../Modules/Step/Views/StepControlsView.swift | 109 +++++- ...swift => StepDiscussionThreadButton.swift} | 42 ++- .../Step/Views/StepNavigationButton.swift | 4 +- .../DiscussionsSortTypeStorageManager.swift | 25 ++ .../Network/CommentsNetworkService.swift | 6 +- .../DiscussionThreadsNetworkService.swift | 39 ++ .../DiscussionThreadsPersistenceService.swift | 17 + .../Services/NetworkReachabilityService.swift | 2 +- .../Sources/Views/DownloadControlView.swift | 2 +- .../Views/PlayNextCircleControlView.swift | 2 +- Stepic/Step+CoreDataProperties.swift | 32 +- Stepic/Step.swift | 9 +- Stepic/StepicsAPI.swift | 2 +- ...nfo.swift => StepikApplicationsInfo.swift} | 82 +++-- Stepic/StepikButton.swift | 4 +- .../StepikPlaceholderStyle+Placeholders.swift | 6 + .../{StepicToken.swift => StepikToken.swift} | 27 +- Stepic/StoriesPresenter.swift | 2 +- Stepic/Submission.swift | 111 ++++-- Stepic/SubmissionsAPI.swift | 27 +- Stepic/TextReply.swift | 40 ++- Stepic/UpdateRequestMaker.swift | 2 +- Stepic/VKSocialSDKProvider.swift | 2 +- Stepic/ViewsAPI.swift | 2 +- Stepic/WebControllerManager.swift | 4 +- Stepic/en.lproj/Localizable.strings | 6 + Stepic/ru.lproj/Localizable.strings | 6 + StepicTests/DeepLinkRouteTests.swift | 40 +++ StepicTests/Model/SubmissionTests.swift | 336 ++++++++++++++++++ .../NewStepViewControllerTests.swift | 16 +- 134 files changed, 2879 insertions(+), 639 deletions(-) create mode 100644 Stepic/DiscussionThread+CoreDataProperties.swift create mode 100644 Stepic/DiscussionThread.swift create mode 100644 Stepic/DiscussionThreadsAPI.swift create mode 100644 Stepic/Images.xcassets/solutions-icon.imageset/Contents.json create mode 100644 Stepic/Images.xcassets/solutions-icon.imageset/solutions-icon.pdf create mode 100644 Stepic/Model.xcdatamodeld/Model_ discussion_threads.xcdatamodel/contents create mode 100644 Stepic/Sources/Modules/Discussions/Views/DiscussionsSolutionControl.swift rename Stepic/Sources/Modules/Quizzes/BaseQuiz/{ => InputOutput}/BaseQuizOutputProtocol.swift (78%) rename Stepic/Sources/Modules/Step/Views/{StepDiscussionsButton.swift => StepDiscussionThreadButton.swift} (71%) create mode 100644 Stepic/Sources/Services/Managers/DiscussionsSortTypeStorageManager.swift create mode 100644 Stepic/Sources/Services/Models/Network/DiscussionThreadsNetworkService.swift create mode 100644 Stepic/Sources/Services/Models/Persistence/DiscussionThreadsPersistenceService.swift rename Stepic/{StepicApplicationsInfo.swift => StepikApplicationsInfo.swift} (50%) rename Stepic/{StepicToken.swift => StepikToken.swift} (57%) create mode 100644 StepicTests/Model/SubmissionTests.swift diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index dec603f528..58c71b4513 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -44,7 +44,7 @@ 080EBA331EA64BC000C43C93 /* PresentationContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080EBA321EA64BC000C43C93 /* PresentationContainer.swift */; }; 080EBA371EA64C0C00C43C93 /* CertificatesPresentationContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080EBA361EA64C0C00C43C93 /* CertificatesPresentationContainer.swift */; }; 080F211B2034DA2500A1204C /* LocalProgressLastViewedUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080F211A2034DA2500A1204C /* LocalProgressLastViewedUpdater.swift */; }; - 080F31DD1BA7162C00F356A0 /* StepicToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080F31DC1BA7162C00F356A0 /* StepicToken.swift */; }; + 080F31DD1BA7162C00F356A0 /* StepikToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080F31DC1BA7162C00F356A0 /* StepikToken.swift */; }; 081387E11D7AF7700092E05D /* StyledTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081387E01D7AF7700092E05D /* StyledTabBarViewController.swift */; }; 0813EEA61BFE5A5400DB4B83 /* Assignment+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0813EEA41BFE5A5400DB4B83 /* Assignment+CoreDataProperties.swift */; }; 0813EEA71BFE5A5400DB4B83 /* Assignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0813EEA51BFE5A5400DB4B83 /* Assignment.swift */; }; @@ -100,7 +100,7 @@ 083AE48120BD6DCC00102FE4 /* PersonalDeadlineLocalStorageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083AE48020BD6DCC00102FE4 /* PersonalDeadlineLocalStorageManager.swift */; }; 083AE48320BD72CA00102FE4 /* PersonalDeadlinesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083AE48220BD72CA00102FE4 /* PersonalDeadlinesService.swift */; }; 083B164D1C2AF27700250B37 /* JSQWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083B164C1C2AF27700250B37 /* JSQWebViewController.swift */; }; - 083D649C1C172015003222F0 /* StepicApplicationsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083D649B1C172015003222F0 /* StepicApplicationsInfo.swift */; }; + 083D649C1C172015003222F0 /* StepikApplicationsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083D649B1C172015003222F0 /* StepikApplicationsInfo.swift */; }; 083D64AF1C19BDB2003222F0 /* ControllerHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083D64AE1C19BDB2003222F0 /* ControllerHelper.swift */; }; 083E1DA61C96E9F100B305E4 /* ApplicationInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083E1DA51C96E9F100B305E4 /* ApplicationInfo.swift */; }; 083E49DD2072B684004896C0 /* IDFetchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083E49DC2072B684004896C0 /* IDFetchable.swift */; }; @@ -470,6 +470,11 @@ 2C48D604228F0EF700739477 /* ContentProcessingRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C48D603228F0EF700739477 /* ContentProcessingRule.swift */; }; 2C48D606228F114400739477 /* HTMLExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C48D605228F114400739477 /* HTMLExtractor.swift */; }; 2C48D608228F1E0800739477 /* ContentProcessingInjection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C48D607228F1E0800739477 /* ContentProcessingInjection.swift */; }; + 2C4AD01123E2F3CD0049B7B0 /* DiscussionThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4AD01023E2F3CD0049B7B0 /* DiscussionThread.swift */; }; + 2C4AD01323E2F3EC0049B7B0 /* DiscussionThread+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4AD01223E2F3EC0049B7B0 /* DiscussionThread+CoreDataProperties.swift */; }; + 2C4AD01523E301C50049B7B0 /* DiscussionThreadsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4AD01423E301C50049B7B0 /* DiscussionThreadsAPI.swift */; }; + 2C4AD01923E304140049B7B0 /* DiscussionThreadsNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4AD01823E304140049B7B0 /* DiscussionThreadsNetworkService.swift */; }; + 2C4AD01B23E305FA0049B7B0 /* DiscussionThreadsPersistenceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4AD01A23E305FA0049B7B0 /* DiscussionThreadsPersistenceService.swift */; }; 2C4BBF13203DC668000A4250 /* plyr.js in Resources */ = {isa = PBXBuildFile; fileRef = 2C4BBF10203DC668000A4250 /* plyr.js */; }; 2C4BBF15203DC668000A4250 /* plyr.css in Resources */ = {isa = PBXBuildFile; fileRef = 2C4BBF12203DC668000A4250 /* plyr.css */; }; 2C4BE7DE221325FB00AEAC34 /* CourseReview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4BE7DD221325F400AEAC34 /* CourseReview.swift */; }; @@ -488,6 +493,7 @@ 2C546C1323DA02AB00352F27 /* String+SafeSubscript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C546C1223DA02AB00352F27 /* String+SafeSubscript.swift */; }; 2C546C1523DA05F300352F27 /* StringExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C546C1423DA05F300352F27 /* StringExtensionsTests.swift */; }; 2C558780217521E8009E1BDE /* LocalNotificationsMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C55877F217521E8009E1BDE /* LocalNotificationsMigrator.swift */; }; + 2C5967EB23E7828800072800 /* SubmissionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5967EA23E7828800072800 /* SubmissionTests.swift */; }; 2C5BE9DC233C0A110098EB2F /* katex in Resources */ = {isa = PBXBuildFile; fileRef = 2C5BE9DB233C0A100098EB2F /* katex */; }; 2C5D51592024653B00B9D932 /* BaseCardsStepsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5D51582024653B00B9D932 /* BaseCardsStepsViewController.swift */; }; 2C5DF1391FEBDC8C003B1177 /* CardsStepsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5DF1381FEBDC8C003B1177 /* CardsStepsPresenter.swift */; }; @@ -611,7 +617,7 @@ 2CB51F6B204FC6220008431C /* UserActivitySpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB51F6A204FC6220008431C /* UserActivitySpec.swift */; }; 2CB5B1592292E0D700D7F706 /* StepControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB5B1582292E0D700D7F706 /* StepControlsView.swift */; }; 2CB5B15B2292E65400D7F706 /* StepNavigationButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB5B15A2292E65400D7F706 /* StepNavigationButton.swift */; }; - 2CB5B15D22941D7000D7F706 /* StepDiscussionsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB5B15C22941D7000D7F706 /* StepDiscussionsButton.swift */; }; + 2CB5B15D22941D7000D7F706 /* StepDiscussionThreadButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB5B15C22941D7000D7F706 /* StepDiscussionThreadButton.swift */; }; 2CB5B1612297F2FB00D7F706 /* StepVideoPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB5B1602297F2FA00D7F706 /* StepVideoPreviewView.swift */; }; 2CB62BDB2019ECB800B5E336 /* OnboardingCardStepViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB62BDA2019ECB800B5E336 /* OnboardingCardStepViewController.swift */; }; 2CB62BE42019FD8500B5E336 /* step3.html in Resources */ = {isa = PBXBuildFile; fileRef = 2CB62BDC2019FD8400B5E336 /* step3.html */; }; @@ -754,6 +760,7 @@ 62E98116576B12451E5418F4 /* CourseCoverImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E9888DC7DFE8CFE5C220B6 /* CourseCoverImageView.swift */; }; 62E98121031477DEACF5CF28 /* CourseInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98A86AF422B9248692C03 /* CourseInfoViewController.swift */; }; 62E98144B8A52511F4EEB63F /* CourseWidgetSkeletonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98A399A313681138734B4 /* CourseWidgetSkeletonView.swift */; }; + 62E9814EEFA8DC1D8D59C63C /* DiscussionsSortTypeStorageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E981C57D5BCF81714F344C /* DiscussionsSortTypeStorageManager.swift */; }; 62E981544EDEBEB2E69B7CC3 /* CourseListOutputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98E42354E7C07E4355066 /* CourseListOutputProtocol.swift */; }; 62E9815A6AAC6C12E9B9D862 /* ProfileHeaderInfoView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 62E98801330B94B34FA0D7B9 /* ProfileHeaderInfoView.xib */; }; 62E98176284407D3D2A0E271 /* Achievement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98708CFECD3F1D0AD4120 /* Achievement.swift */; }; @@ -866,6 +873,7 @@ 62E987F6149525BC5F7CFDE7 /* CourseListColorMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E986C27C3371C7E8B38566 /* CourseListColorMode.swift */; }; 62E987F624FBDAB64FB38E5A /* ExploreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E986DA4B69983947DA9687 /* ExploreViewController.swift */; }; 62E9881CB604CB84623EFF03 /* TagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E983D4436B935F9B27AF98 /* TagsView.swift */; }; + 62E9882879C618BA36F5A1C9 /* DiscussionsSolutionControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E9824274BDDA2082AA6570 /* DiscussionsSolutionControl.swift */; }; 62E98844F3BEE9CC807B462B /* CourseListPersistenceStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98B46D64F254F0A835EEB /* CourseListPersistenceStorage.swift */; }; 62E9884EE92275DE964EFB37 /* CourseInfoDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E982F96FE46CDC2F032E31 /* CourseInfoDataFlow.swift */; }; 62E9886837CEF83D626D9536 /* StreakActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E986D38DE1E4890415F9F7 /* StreakActivityView.swift */; }; @@ -1143,7 +1151,7 @@ 080EBA321EA64BC000C43C93 /* PresentationContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationContainer.swift; sourceTree = ""; }; 080EBA361EA64C0C00C43C93 /* CertificatesPresentationContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CertificatesPresentationContainer.swift; sourceTree = ""; }; 080F211A2034DA2500A1204C /* LocalProgressLastViewedUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalProgressLastViewedUpdater.swift; sourceTree = ""; }; - 080F31DC1BA7162C00F356A0 /* StepicToken.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StepicToken.swift; sourceTree = ""; }; + 080F31DC1BA7162C00F356A0 /* StepikToken.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StepikToken.swift; sourceTree = ""; }; 081387E01D7AF7700092E05D /* StyledTabBarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StyledTabBarViewController.swift; sourceTree = ""; }; 0813EEA41BFE5A5400DB4B83 /* Assignment+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Assignment+CoreDataProperties.swift"; sourceTree = ""; }; 0813EEA51BFE5A5400DB4B83 /* Assignment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Assignment.swift; sourceTree = ""; }; @@ -1210,7 +1218,7 @@ 083AE48220BD72CA00102FE4 /* PersonalDeadlinesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonalDeadlinesService.swift; sourceTree = ""; }; 083B164C1C2AF27700250B37 /* JSQWebViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSQWebViewController.swift; sourceTree = ""; }; 083C20BC1CB2998900C9966E /* Model_v3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_v3.xcdatamodel; sourceTree = ""; }; - 083D649B1C172015003222F0 /* StepicApplicationsInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StepicApplicationsInfo.swift; sourceTree = ""; }; + 083D649B1C172015003222F0 /* StepikApplicationsInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StepikApplicationsInfo.swift; sourceTree = ""; }; 083D64AE1C19BDB2003222F0 /* ControllerHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControllerHelper.swift; sourceTree = ""; }; 083E1DA51C96E9F100B305E4 /* ApplicationInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApplicationInfo.swift; sourceTree = ""; }; 083E49DC2072B684004896C0 /* IDFetchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IDFetchable.swift; sourceTree = ""; }; @@ -1623,6 +1631,12 @@ 2C48D603228F0EF700739477 /* ContentProcessingRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentProcessingRule.swift; sourceTree = ""; }; 2C48D605228F114400739477 /* HTMLExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLExtractor.swift; sourceTree = ""; }; 2C48D607228F1E0800739477 /* ContentProcessingInjection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentProcessingInjection.swift; sourceTree = ""; }; + 2C4AD00E23E2E6550049B7B0 /* Model_ discussion_threads.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Model_ discussion_threads.xcdatamodel"; sourceTree = ""; }; + 2C4AD01023E2F3CD0049B7B0 /* DiscussionThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscussionThread.swift; sourceTree = ""; }; + 2C4AD01223E2F3EC0049B7B0 /* DiscussionThread+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiscussionThread+CoreDataProperties.swift"; sourceTree = ""; }; + 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 = ""; }; 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 = ""; }; @@ -1642,6 +1656,7 @@ 2C546C1223DA02AB00352F27 /* String+SafeSubscript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+SafeSubscript.swift"; sourceTree = ""; }; 2C546C1423DA05F300352F27 /* StringExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensionsTests.swift; sourceTree = ""; }; 2C55877F217521E8009E1BDE /* LocalNotificationsMigrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotificationsMigrator.swift; sourceTree = ""; }; + 2C5967EA23E7828800072800 /* SubmissionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmissionTests.swift; sourceTree = ""; }; 2C5AB2B122F9BC78005E7AA0 /* Model_step_options_limits.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_step_options_limits.xcdatamodel; sourceTree = ""; }; 2C5BE9DB233C0A100098EB2F /* katex */ = {isa = PBXFileReference; lastKnownFileType = folder; path = katex; sourceTree = ""; }; 2C5D51582024653B00B9D932 /* BaseCardsStepsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseCardsStepsViewController.swift; sourceTree = ""; }; @@ -1762,7 +1777,7 @@ 2CB51F6A204FC6220008431C /* UserActivitySpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivitySpec.swift; sourceTree = ""; }; 2CB5B1582292E0D700D7F706 /* StepControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepControlsView.swift; sourceTree = ""; }; 2CB5B15A2292E65400D7F706 /* StepNavigationButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepNavigationButton.swift; sourceTree = ""; }; - 2CB5B15C22941D7000D7F706 /* StepDiscussionsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepDiscussionsButton.swift; sourceTree = ""; }; + 2CB5B15C22941D7000D7F706 /* StepDiscussionThreadButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepDiscussionThreadButton.swift; sourceTree = ""; }; 2CB5B1602297F2FA00D7F706 /* StepVideoPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepVideoPreviewView.swift; sourceTree = ""; }; 2CB62BDA2019ECB800B5E336 /* OnboardingCardStepViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingCardStepViewController.swift; sourceTree = ""; }; 2CB62BDD2019FD8400B5E336 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = ru; path = OnboardingContent/ru.lproj/step3.html; sourceTree = ""; }; @@ -1925,6 +1940,7 @@ 62E9819721AE70E0BCAD1375 /* FullscreenCourseListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullscreenCourseListViewController.swift; sourceTree = ""; }; 62E981A02A8E071C5BA610E7 /* FullscreenCourseListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullscreenCourseListView.swift; sourceTree = ""; }; 62E981BE70619C07A3F72EC9 /* UnitsPersistenceService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnitsPersistenceService.swift; sourceTree = ""; }; + 62E981C57D5BCF81714F344C /* DiscussionsSortTypeStorageManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscussionsSortTypeStorageManager.swift; sourceTree = ""; }; 62E981C952A9DD6055CBCBF0 /* SettingsTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsTableViewCell.swift; sourceTree = ""; }; 62E981CAF11255059CB09BF1 /* FormatterHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormatterHelper.swift; sourceTree = ""; }; 62E981F1C2013A08AB3104DA /* ContentLanguageSwitchAvailabilityService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentLanguageSwitchAvailabilityService.swift; sourceTree = ""; }; @@ -1932,6 +1948,7 @@ 62E98202B63DCB3B6908745E /* CourseInfoTabSyllabusViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseInfoTabSyllabusViewController.swift; sourceTree = ""; }; 62E9821FBC36908E1F53173E /* NewCodeQuizViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewCodeQuizViewModel.swift; sourceTree = ""; }; 62E9823CEE80A194D6C304F4 /* CourseListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseListViewController.swift; sourceTree = ""; }; + 62E9824274BDDA2082AA6570 /* DiscussionsSolutionControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscussionsSolutionControl.swift; sourceTree = ""; }; 62E98256FF0135D7A7B59A54 /* ContentLanguageService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentLanguageService.swift; sourceTree = ""; }; 62E9826CE6F076640792E2C0 /* CourseInfoTabSyllabusTableViewDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseInfoTabSyllabusTableViewDataSource.swift; sourceTree = ""; }; 62E98270EC373087E18B51FE /* CourseInfoTabSyllabusTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseInfoTabSyllabusTableViewCell.swift; sourceTree = ""; }; @@ -2377,7 +2394,7 @@ children = ( 0885F8571BAAD43300F2A188 /* AuthInfo.swift */, 08AB82371D74C44100FDEADE /* Session.swift */, - 080F31DC1BA7162C00F356A0 /* StepicToken.swift */, + 080F31DC1BA7162C00F356A0 /* StepikToken.swift */, ); name = Stepic; sourceTree = ""; @@ -2413,6 +2430,7 @@ 087585AC1FB51D590047A269 /* CourseList */, 2C4BE7DC221325E800AEAC34 /* CourseReview */, 08D5F5751F7DA6C1007C1634 /* CourseReviewSummary */, + 2C4AD00F23E2F3BA0049B7B0 /* DiscussionThread */, 2CD6E257234E0A0C00F49303 /* EmailAddress */, 2C5E907E2333CF2700288BE3 /* LastCodeLanguage */, 0846B1001EDDECE600D64D77 /* LastStep */, @@ -2741,6 +2759,7 @@ 080CE1421E955D300089A27F /* CoursesAPI.swift */, 08AEC3A21CCA69C700FFF29E /* DevicesAPI.swift */, 0800B81E1D06FDE4006C987E /* DiscussionProxiesAPI.swift */, + 2C4AD01423E301C50049B7B0 /* DiscussionThreadsAPI.swift */, 2CD6E255234E042800F49303 /* EmailAddressesAPI.swift */, 2C8CB0E21FB48F39008CB1AC /* EnrollmentsAPI.swift */, 084F7AAC1E775A210088368A /* LastStepsAPI.swift */, @@ -2794,6 +2813,7 @@ 2CB51F6A204FC6220008431C /* UserActivitySpec.swift */, 62E987B9D418CC2175A64EC2 /* UserAgentTests.swift */, 2C546C1623DA0E6100352F27 /* ExtensionsTests */, + 2C5967EC23E7828E00072800 /* Model */, 2CF24F65239004AC002DBD0F /* ModulesTests */, ); path = StepicTests; @@ -3768,6 +3788,7 @@ 2C3A24552359DA9200E2405F /* Views */ = { isa = PBXGroup; children = ( + 62E9824274BDDA2082AA6570 /* DiscussionsSolutionControl.swift */, 2C9E78B22374211B00880459 /* DiscussionsTableViewDataSource.swift */, 2C9E78B4237423CA00880459 /* DiscussionsView.swift */, 2C3A2459235A1ACA00E2405F /* Cell */, @@ -3815,6 +3836,15 @@ path = ContentProcessor; sourceTree = ""; }; + 2C4AD00F23E2F3BA0049B7B0 /* DiscussionThread */ = { + isa = PBXGroup; + children = ( + 2C4AD01023E2F3CD0049B7B0 /* DiscussionThread.swift */, + 2C4AD01223E2F3EC0049B7B0 /* DiscussionThread+CoreDataProperties.swift */, + ); + name = DiscussionThread; + sourceTree = ""; + }; 2C4BBF0B203DC5C9000A4250 /* Plyr */ = { isa = PBXGroup; children = ( @@ -3880,6 +3910,14 @@ name = BaseQuiz; sourceTree = ""; }; + 2C5967EC23E7828E00072800 /* Model */ = { + isa = PBXGroup; + children = ( + 2C5967EA23E7828800072800 /* SubmissionTests.swift */, + ); + path = Model; + sourceTree = ""; + }; 2C5DF1371FEBDC50003B1177 /* AdaptiveSteps */ = { isa = PBXGroup; children = ( @@ -3965,7 +4003,7 @@ isa = PBXGroup; children = ( 083E1DA51C96E9F100B305E4 /* ApplicationInfo.swift */, - 083D649B1C172015003222F0 /* StepicApplicationsInfo.swift */, + 083D649B1C172015003222F0 /* StepikApplicationsInfo.swift */, ); name = ApplicationInfo; sourceTree = ""; @@ -4031,6 +4069,14 @@ name = CodeEditorPreview; sourceTree = ""; }; + 2C6076AC23E357550077DFDD /* InputOutput */ = { + isa = PBXGroup; + children = ( + 2C20778722BCED1600D44DC0 /* BaseQuizOutputProtocol.swift */, + ); + path = InputOutput; + sourceTree = ""; + }; 2C63CDF023B26A0A00B4D6FE /* InputOutput */ = { isa = PBXGroup; children = ( @@ -4081,13 +4127,13 @@ 2C6BBBAA22B261E200889A45 /* BaseQuizAssembly.swift */, 2C6BBBA622B261E200889A45 /* BaseQuizDataFlow.swift */, 2C6BBBAF22B261E200889A45 /* BaseQuizInteractor.swift */, - 2C20778722BCED1600D44DC0 /* BaseQuizOutputProtocol.swift */, 2C6BBBAE22B261E200889A45 /* BaseQuizPresenter.swift */, 2C6BBBA722B261E200889A45 /* BaseQuizProvider.swift */, 2C6BBBA822B261E200889A45 /* BaseQuizView.swift */, 2C6BBBA922B261E200889A45 /* BaseQuizViewController.swift */, 2C20778122BB956900D44DC0 /* BaseQuizViewModel.swift */, 2C6BBBAB22B261E200889A45 /* ChildProtocols */, + 2C6076AC23E357550077DFDD /* InputOutput */, 2C6BBBB022B261E200889A45 /* Views */, ); path = BaseQuiz; @@ -4421,7 +4467,7 @@ isa = PBXGroup; children = ( 2CB5B1582292E0D700D7F706 /* StepControlsView.swift */, - 2CB5B15C22941D7000D7F706 /* StepDiscussionsButton.swift */, + 2CB5B15C22941D7000D7F706 /* StepDiscussionThreadButton.swift */, 2CB5B15A2292E65400D7F706 /* StepNavigationButton.swift */, 2C07DD8023913F5500286CAA /* StepStatisticsView.swift */, 2CB5B1602297F2FA00D7F706 /* StepVideoPreviewView.swift */, @@ -4842,6 +4888,7 @@ 62E98B4C083CFD4E93597D32 /* CourseReviewsPersistenceService.swift */, 62E985BDDC6349F80D395284 /* CourseReviewSummariesPersistenceService.swift */, 62E98B706335B4BAC35D3C6D /* CoursesPersistenceService.swift */, + 2C4AD01A23E305FA0049B7B0 /* DiscussionThreadsPersistenceService.swift */, 2CD6E25C234E388B00F49303 /* EmailAddressesPersistenceService.swift */, 62E985EA7905BE734BB77FBD /* LessonsPersistenceService.swift */, 62E9803E0D93EBBD439FFEDD /* ProgressesPersistenceService.swift */, @@ -4965,6 +5012,7 @@ 62E9860B3CAE0438AD4A29E7 /* CourseReviewSummariesNetworkService.swift */, 62E98F431EF3B10C63AE0575 /* CoursesNetworkService.swift */, 62E983E08309F9779A0691DC /* DiscussionProxiesNetworkService.swift */, + 2C4AD01823E304140049B7B0 /* DiscussionThreadsNetworkService.swift */, 2CD6E25E234E392900F49303 /* EmailAddressesNetworkService.swift */, 62E98B1D8C5FD917DC6E6053 /* LessonsNetworkService.swift */, 2C06E094223FF46500AF4DA2 /* ProfilesNetworkService.swift */, @@ -5102,6 +5150,7 @@ isa = PBXGroup; children = ( 2C7F641D23B12208006C7648 /* AutoplayStorageManager.swift */, + 62E981C57D5BCF81714F344C /* DiscussionsSortTypeStorageManager.swift */, 2C3DAB8C233D71B100453B1C /* StepFontSizeStorageManager.swift */, 62E9881CFD7892350EA31213 /* TooltipStorageManager.swift */, 2C8F3ADE23CCAB88004D113A /* Video */, @@ -6346,6 +6395,7 @@ 2C2485492101EE3E006F8858 /* DownloaderTests.swift in Sources */, 080AA2371EA05CD20079272F /* TestConfig.swift in Sources */, 2CF24F67239004D8002DBD0F /* NewStepViewControllerTests.swift in Sources */, + 2C5967EB23E7828800072800 /* SubmissionTests.swift in Sources */, 62E98ABEDEB0D955D6F3A951 /* UserAgentTests.swift in Sources */, 62E98CA7BEE2A5954DD4FB4C /* DeepLinkRouteTests.swift in Sources */, ); @@ -6467,7 +6517,7 @@ 2C06E095223FF46500AF4DA2 /* ProfilesNetworkService.swift in Sources */, 2CC3519D1F683E7C004255B6 /* AuthNavigationViewController.swift in Sources */, 081D741A211DB4930086F6F8 /* OpenedStoriesPresenter.swift in Sources */, - 083D649C1C172015003222F0 /* StepicApplicationsInfo.swift in Sources */, + 083D649C1C172015003222F0 /* StepikApplicationsInfo.swift in Sources */, 0800B8221D07029B006C987E /* CommentsAPI.swift in Sources */, 2C23C5E41F6BF52700FC2B7C /* AuthButton.swift in Sources */, 0819856C20BF273800897BBA /* PersonalDeadlineTableViewCell.swift in Sources */, @@ -6503,6 +6553,7 @@ 2CAD8B9D2170CDB4003F420B /* LocalNotificationContentProvider.swift in Sources */, 86B457031E9F984800D31850 /* RecommendationsAPI.swift in Sources */, 08DF1D921BDAB93900BA35EA /* StringExtensions.swift in Sources */, + 2C4AD01923E304140049B7B0 /* DiscussionThreadsNetworkService.swift in Sources */, 08FA62222121BEF900F00275 /* GrowPresentAnimationController.swift in Sources */, 083F2B2A1E9EC17F00714173 /* LoadingPaginationView.swift in Sources */, 084070871D64DC7500308FC1 /* CyrillicURLActivityItemSource.swift in Sources */, @@ -6729,7 +6780,7 @@ 2C3A24572359DD2E00E2405F /* DiscussionsViewModel.swift in Sources */, 2C10100B239EFAD700440651 /* DiscountingPolicy.swift in Sources */, 082E35B220B5F1E4006E28F9 /* StorageRecordsAPI.swift in Sources */, - 2CB5B15D22941D7000D7F706 /* StepDiscussionsButton.swift in Sources */, + 2CB5B15D22941D7000D7F706 /* StepDiscussionThreadButton.swift in Sources */, 08F485B11C58EB0D000165AA /* SortingReply.swift in Sources */, 084156931BCBFFBD006B8C73 /* Block+CoreDataProperties.swift in Sources */, 2C6BBBBB22B261E700889A45 /* QuizFeedbackView.swift in Sources */, @@ -6778,7 +6829,7 @@ 089877B0214047EE0065DFA2 /* UserDefaults+StorageServiceProtocol.swift in Sources */, 083F2B171E9D8F1D00714173 /* CertificatesView.swift in Sources */, 2C01BB68233CD92C00C8DCF0 /* Require.swift in Sources */, - 080F31DD1BA7162C00F356A0 /* StepicToken.swift in Sources */, + 080F31DD1BA7162C00F356A0 /* StepikToken.swift in Sources */, 080CE14F1E9562F30089A27F /* StepsAPI.swift in Sources */, 08E3B9671EEA16DC0072995B /* CodeReply.swift in Sources */, 2CA9D9852010EEA2007AA743 /* AdaptiveRatingsAPI.swift in Sources */, @@ -6863,6 +6914,7 @@ 62E98C1377148A52D15AAEBF /* CodeEditorPreferencesContainer.swift in Sources */, 62E98B94B2E3FE4ABB80AE41 /* ProfileHeaderInfoView.swift in Sources */, 2CE8390E20C8096400FE3672 /* ProfileAchievementsPresenter.swift in Sources */, + 2C4AD01B23E305FA0049B7B0 /* DiscussionThreadsPersistenceService.swift in Sources */, 62E983C7E7B1194D9C809522 /* ProfileInfoPresenter.swift in Sources */, 62E983397736699787D8DD35 /* UIView+FromNib.swift in Sources */, 2CAD8B9B217096AB003F420B /* LocalNotificationsService.swift in Sources */, @@ -6896,8 +6948,10 @@ 2CD87014230DF663003D9F1A /* NewMatchingQuizTitleView.swift in Sources */, 2CFC5ABE228AF0B400B5248A /* LessonViewModel.swift in Sources */, 2CF1B33F2163BE770008DA0C /* StoriesPresenter.swift in Sources */, + 2C4AD01123E2F3CD0049B7B0 /* DiscussionThread.swift in Sources */, 62E98F602ED9E5CEA4E02D0A /* UIStackView+RemoveAllArrangedSubviews.swift in Sources */, 62E98447CF53F0402668F488 /* ContinueLastStepView.swift in Sources */, + 2C4AD01323E2F3EC0049B7B0 /* DiscussionThread+CoreDataProperties.swift in Sources */, 62E9886837CEF83D626D9536 /* StreakActivityView.swift in Sources */, 2C7F641E23B12208006C7648 /* AutoplayStorageManager.swift in Sources */, 62E98905245F33B16D020586 /* ContinueActionButton.swift in Sources */, @@ -6923,6 +6977,7 @@ 62E987560B7A0EFE8D4753CB /* RetentionLocalNotificationProvider.swift in Sources */, 62E985181538017E088C38D9 /* DataBackUpdateService.swift in Sources */, 62E980E2B82515489059C986 /* UnitNavigationService.swift in Sources */, + 2C4AD01523E301C50049B7B0 /* DiscussionThreadsAPI.swift in Sources */, 62E98F49F6018A67DEB616C5 /* CourseReviewsNetworkService.swift in Sources */, 62E9852B56E55E5C79AEC91A /* CourseReviewsPersistenceService.swift in Sources */, 62E988F1582D22B8EAAB6E55 /* CourseInfoTabReviewsView.swift in Sources */, @@ -7300,6 +7355,8 @@ 04F11B91B2F6C39A7A8EE8CB /* SolutionProvider.swift in Sources */, 6DB937552C1A7809BBB08CA3 /* SolutionView.swift in Sources */, 62E98FF5201E3C988EB0FA7F /* SolutionViewModel.swift in Sources */, + 62E9814EEFA8DC1D8D59C63C /* DiscussionsSortTypeStorageManager.swift in Sources */, + 62E9882879C618BA36F5A1C9 /* DiscussionsSolutionControl.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -7744,6 +7801,7 @@ 08D1EF6E1BB5618700BE84E6 /* Model.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + 2C4AD00E23E2E6550049B7B0 /* Model_ discussion_threads.xcdatamodel */, 2CBF593423C8A61D00C366A1 /* Model_is_certificate_issued.xcdatamodel */, 2C8B6E0323A27D2D0078A4B3 /* Model_required_section.xcdatamodel */, 2C101009239E7A1E00440651 /* Model_discount_policy.xcdatamodel */, @@ -7792,7 +7850,7 @@ 0802AC531C7222B200C4F3E6 /* Model_v2.xcdatamodel */, 08D1EF6F1BB5618700BE84E6 /* Model.xcdatamodel */, ); - currentVersion = 2CBF593423C8A61D00C366A1 /* Model_is_certificate_issued.xcdatamodel */; + currentVersion = 2C4AD00E23E2E6550049B7B0 /* Model_ discussion_threads.xcdatamodel */; path = Model.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/Stepic/AchievementBadgeView.swift b/Stepic/AchievementBadgeView.swift index ab99a1567d..c3fc60fcd1 100644 --- a/Stepic/AchievementBadgeView.swift +++ b/Stepic/AchievementBadgeView.swift @@ -140,7 +140,7 @@ final class AchievementBadgeView: UIView { circleProgressLayer.path = circlePath.cgPath circleProgressLayer.fillColor = nil - circleProgressLayer.strokeColor = UIColor.stepicGreen.cgColor + circleProgressLayer.strokeColor = UIColor.stepikGreen.cgColor circleProgressLayer.lineWidth = progressWidth circleView.layer.addSublayer(circleProgressLayer) diff --git a/Stepic/ApiDataDownloader.swift b/Stepic/ApiDataDownloader.swift index e59b339455..de3d919810 100644 --- a/Stepic/ApiDataDownloader.swift +++ b/Stepic/ApiDataDownloader.swift @@ -20,6 +20,7 @@ final class ApiDataDownloader { static let courseReviewSummaries = CourseReviewSummariesAPI() static let courses = CoursesAPI() static let discussionProxies = DiscussionProxiesAPI() + static let discussionThreads = DiscussionThreadsAPI() static let enrollments = EnrollmentsAPI() static let lastSteps = LastStepsAPI() static let lessons = LessonsAPI() diff --git a/Stepic/AppDelegate.swift b/Stepic/AppDelegate.swift index 4ccc49beec..2778352803 100644 --- a/Stepic/AppDelegate.swift +++ b/Stepic/AppDelegate.swift @@ -96,7 +96,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { AmplitudeAnalyticsEvents.Launch.firstTime.send() } - if StepicApplicationsInfo.inAppUpdatesAvailable { + if StepikApplicationsInfo.inAppUpdatesAvailable { self.checkForUpdates() } @@ -240,8 +240,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { if ApplicationDelegate.shared.application(app, open: url, options: options) { return true } - if url.scheme == "vk\(StepicApplicationsInfo.SocialInfo.AppIds.vk)" - || url.scheme == "fb\(StepicApplicationsInfo.SocialInfo.AppIds.facebook)" { + if url.scheme == "vk\(StepikApplicationsInfo.SocialInfo.AppIds.vk)" + || url.scheme == "fb\(StepikApplicationsInfo.SocialInfo.AppIds.facebook)" { return true } diff --git a/Stepic/Attempt.swift b/Stepic/Attempt.swift index 9c554ee7c3..1612cb46a3 100644 --- a/Stepic/Attempt.swift +++ b/Stepic/Attempt.swift @@ -21,17 +21,25 @@ final class Attempt: JSONSerializable { var timeLeft: String? var user: Int? + var json: JSON { + [ + JSONKey.step.rawValue: step + ] + } + func update(json: JSON) { - id = json["id"].intValue - datasetUrl = json["dataset_url"].string - time = json["time"].string - status = json["status"].string - step = json["step"].intValue - timeLeft = json["time_left"].string - user = json["user"].int + self.id = json[JSONKey.id.rawValue].intValue + self.datasetUrl = json[JSONKey.datasetURL.rawValue].string + self.time = json[JSONKey.time.rawValue].string + self.status = json[JSONKey.status.rawValue].string + self.step = json[JSONKey.step.rawValue].intValue + self.timeLeft = json[JSONKey.timeLeft.rawValue].string + self.user = json[JSONKey.user.rawValue].int } - func hasEqualId(json: JSON) -> Bool { id == json["id"].int } + func hasEqualId(json: JSON) -> Bool { + self.id == json[JSONKey.id.rawValue].int + } init(step: Int) { self.step = step @@ -42,30 +50,28 @@ final class Attempt: JSONSerializable { } func initDataset(json: JSON, stepName: String) { - dataset = getDatasetFromJSON(json, stepName: stepName) + self.dataset = self.getDatasetFromJSON(json, stepName: stepName) } init(json: JSON, stepName: String) { - id = json["id"].intValue - dataset = nil - datasetUrl = json["dataset_url"].string - time = json["time"].string - status = json["status"].string - step = json["step"].intValue - timeLeft = json["time_left"].string - user = json["user"].int - dataset = getDatasetFromJSON(json["dataset"], stepName: stepName) + self.id = json[JSONKey.id.rawValue].intValue + self.dataset = nil + self.datasetUrl = json[JSONKey.datasetURL.rawValue].string + self.time = json[JSONKey.time.rawValue].string + self.status = json[JSONKey.status.rawValue].string + self.step = json[JSONKey.step.rawValue].intValue + self.timeLeft = json[JSONKey.timeLeft.rawValue].string + self.user = json[JSONKey.user.rawValue].int + self.dataset = self.getDatasetFromJSON(json[JSONKey.dataset.rawValue], stepName: stepName) } - var json: JSON { ["step": step] } - private func getDatasetFromJSON(_ json: JSON, stepName: String) -> Dataset? { switch stepName { - case "choice" : + case "choice": return ChoiceDataset(json: json) case "math", "string", "number", "code", "sql": return String(json: json) - case "sorting" : + case "sorting": return SortingDataset(json: json) case "free-answer": return FreeAnswerDataset(json: json) @@ -75,4 +81,15 @@ final class Attempt: JSONSerializable { return nil } } + + enum JSONKey: String { + case id + case datasetURL = "dataset_url" + case time + case status + case step + case timeLeft = "time_left" + case user + case dataset + } } diff --git a/Stepic/AttemptsAPI.swift b/Stepic/AttemptsAPI.swift index e9f345e5e9..6b0b74a115 100644 --- a/Stepic/AttemptsAPI.swift +++ b/Stepic/AttemptsAPI.swift @@ -28,7 +28,7 @@ final class AttemptsAPI: APIEndpoint { return Promise { seal in self.manager.request( - "\(StepicApplicationsInfo.apiURL)/\(self.name)", + "\(StepikApplicationsInfo.apiURL)/\(self.name)", method: .get, parameters: parameters, encoding: URLEncoding.default, @@ -98,7 +98,7 @@ final class AttemptsAPI: APIEndpoint { } return self.manager.request( - "\(StepicApplicationsInfo.apiURL)/attempts", + "\(StepikApplicationsInfo.apiURL)/attempts", method: .get, parameters: params, encoding: URLEncoding.default, diff --git a/Stepic/AuthAPI.swift b/Stepic/AuthAPI.swift index ab1cadb3bf..a26972e3f1 100644 --- a/Stepic/AuthAPI.swift +++ b/Stepic/AuthAPI.swift @@ -45,9 +45,9 @@ final class AuthAPI { manager = Alamofire.SessionManager(configuration: configuration) } - func signInWithCode(_ code: String) -> Promise<(StepicToken, AuthorizationType)> { + func signInWithCode(_ code: String) -> Promise<(StepikToken, AuthorizationType)> { Promise { seal in - guard let socialInfo = StepicApplicationsInfo.social else { + guard let socialInfo = StepikApplicationsInfo.social else { throw SignInError.noAppWithCredentials } @@ -62,7 +62,7 @@ final class AuthAPI { "redirect_uri": socialInfo.redirectUri ] - manager.request("\(StepicApplicationsInfo.oauthURL)/token/", method: .post, parameters: params, headers: headers).responseSwiftyJSON { response in + manager.request("\(StepikApplicationsInfo.oauthURL)/token/", method: .post, parameters: params, headers: headers).responseSwiftyJSON { response in switch response.result { case .failure(let error): if let typedError = error as? URLError { @@ -76,16 +76,16 @@ final class AuthAPI { seal.reject(SignInError.other(error: error, code: nil, message: nil)) } case .success(let json): - let token = StepicToken(json: json) + let token = StepikToken(json: json) seal.fulfill((token, AuthorizationType.code)) } } } } - func signInWithAccount(email: String, password: String) -> Promise<(StepicToken, AuthorizationType)> { + func signInWithAccount(email: String, password: String) -> Promise<(StepikToken, AuthorizationType)> { Promise { seal in - guard let passwordInfo = StepicApplicationsInfo.password else { + guard let passwordInfo = StepikApplicationsInfo.password else { throw SignInError.noAppWithCredentials } @@ -100,7 +100,7 @@ final class AuthAPI { "username": email ] - manager.request("\(StepicApplicationsInfo.oauthURL)/token/", method: .post, parameters: params, headers: headers).responseSwiftyJSON { response in + manager.request("\(StepikApplicationsInfo.oauthURL)/token/", method: .post, parameters: params, headers: headers).responseSwiftyJSON { response in switch response.result { case .failure(let error): if let typedError = error as? URLError { @@ -126,14 +126,14 @@ final class AuthAPI { } } - let token = StepicToken(json: json) + let token = StepikToken(json: json) seal.fulfill((token, AuthorizationType.password)) } } } } - func refreshToken(with refresh_token: String, authorizationType: AuthorizationType) -> Promise { + func refreshToken(with refresh_token: String, authorizationType: AuthorizationType) -> Promise { func logRefreshError(statusCode: Int?, message: String?) { var parameters: [String: String] = [:] if let code = statusCode { parameters["code"] = "\(code)" } @@ -147,12 +147,12 @@ final class AuthAPI { case .none: throw TokenRefreshError.other case .code: - guard let socialInfo = StepicApplicationsInfo.social else { + guard let socialInfo = StepikApplicationsInfo.social else { throw TokenRefreshError.noAppWithCredentials } credentials = socialInfo.credentials case .password: - guard let passwordInfo = StepicApplicationsInfo.password else { + guard let passwordInfo = StepikApplicationsInfo.password else { throw TokenRefreshError.noAppWithCredentials } credentials = passwordInfo.credentials @@ -168,13 +168,13 @@ final class AuthAPI { "refresh_token": refresh_token ] - manager.request("\(StepicApplicationsInfo.oauthURL)/token/", method: .post, parameters: params, headers: headers).responseSwiftyJSON { response in + manager.request("\(StepikApplicationsInfo.oauthURL)/token/", method: .post, parameters: params, headers: headers).responseSwiftyJSON { response in switch response.result { case .failure(let error): logRefreshError(statusCode: response.response?.statusCode, message: "Error \(error.localizedDescription) while refreshing") seal.reject(TokenRefreshError.other) case .success(let json): - let token = StepicToken(json: json) + let token = StepikToken(json: json) if token.accessToken.isEmpty { logRefreshError(statusCode: response.response?.statusCode, message: "Error after getting empty access token") if response.response?.statusCode == 401 { @@ -204,7 +204,7 @@ final class AuthAPI { ] ] - manager.request("\(StepicApplicationsInfo.apiURL)/users", method: .post, parameters: params, encoding: JSONEncoding.default, headers: headers).responseSwiftyJSON { response in + manager.request("\(StepikApplicationsInfo.apiURL)/users", method: .post, parameters: params, encoding: JSONEncoding.default, headers: headers).responseSwiftyJSON { response in switch response.result { case .failure(let error): seal.reject(SignUpError.other(error: error, code: nil, message: nil)) @@ -225,9 +225,9 @@ final class AuthAPI { } } - func signUpWithToken(socialToken: String, email: String?, provider: String) -> Promise<(StepicToken, AuthorizationType)> { + func signUpWithToken(socialToken: String, email: String?, provider: String) -> Promise<(StepikToken, AuthorizationType)> { Promise { seal in - guard let socialInfo = StepicApplicationsInfo.social else { + guard let socialInfo = StepikApplicationsInfo.social else { throw SignInError.noAppWithCredentials } @@ -247,7 +247,7 @@ final class AuthAPI { "Authorization": "Basic \(socialInfo.credentials)" ] - manager.request("\(StepicApplicationsInfo.oauthURL)/social-token/", method: .post, parameters: params, encoding: URLEncoding.default, headers: headers).responseSwiftyJSON { response in + manager.request("\(StepikApplicationsInfo.oauthURL)/social-token/", method: .post, parameters: params, encoding: URLEncoding.default, headers: headers).responseSwiftyJSON { response in switch response.result { case .failure(let error): if let typedError = error as? URLError { @@ -272,7 +272,7 @@ final class AuthAPI { } } - let token = StepicToken(json: json) + let token = StepikToken(json: json) seal.fulfill((token, AuthorizationType.code)) } } @@ -284,7 +284,7 @@ final class AuthAPI { // TODO: remove this extension after global refactoring extension AuthAPI { @available(*, deprecated, message: "Legacy method with callbacks") - @discardableResult func logInWithUsername(_ username: String, password: String, success : @escaping (_ token: StepicToken) -> Void, failure : @escaping (_ error: SignInError) -> Void) -> Request? { + @discardableResult func logInWithUsername(_ username: String, password: String, success : @escaping (_ token: StepikToken) -> Void, failure : @escaping (_ error: SignInError) -> Void) -> Request? { signInWithAccount(email: username, password: password).done { token, authorizationType in AuthInfo.shared.authorizationType = authorizationType success(token) @@ -299,7 +299,7 @@ extension AuthAPI { } @available(*, deprecated, message: "Legacy method with callbacks") - @discardableResult func refreshTokenWith(_ refresh_token: String, success : @escaping (_ token: StepicToken) -> Void, failure : @escaping (_ error: TokenRefreshError) -> Void) -> Request? { + @discardableResult func refreshTokenWith(_ refresh_token: String, success : @escaping (_ token: StepikToken) -> Void, failure : @escaping (_ error: TokenRefreshError) -> Void) -> Request? { refreshToken(with: refresh_token, authorizationType: AuthInfo.shared.authorizationType).done { token in success(token) }.catch { error in diff --git a/Stepic/AuthInfo.swift b/Stepic/AuthInfo.swift index 4ce3bd5c0d..5f48688a29 100644 --- a/Stepic/AuthInfo.swift +++ b/Stepic/AuthInfo.swift @@ -33,7 +33,7 @@ final class AuthInfo: NSObject { } } - private func setTokenValue(_ newToken: StepicToken?) { + private func setTokenValue(_ newToken: StepikToken?) { defaults.setValue(newToken?.accessToken, forKey: "access_token") defaults.setValue(newToken?.refreshToken, forKey: "refresh_token") defaults.setValue(newToken?.tokenType, forKey: "token_type") @@ -41,7 +41,7 @@ final class AuthInfo: NSObject { defaults.synchronize() } - var token: StepicToken? { + var token: StepikToken? { set(newToken) { if newToken == nil || newToken?.accessToken == "" { print("\nsetting new token to nil\n") @@ -97,7 +97,7 @@ final class AuthInfo: NSObject { let tokenType = defaults.value(forKey: "token_type") as? String { // print("got accessToken \(accessToken)") let expireDate = Date(timeIntervalSince1970: defaults.value(forKey: "expire_date") as? TimeInterval ?? 0.0) - return StepicToken(accessToken: accessToken, refreshToken: refreshToken, tokenType: tokenType, expireDate: expireDate) + return StepikToken(accessToken: accessToken, refreshToken: refreshToken, tokenType: tokenType, expireDate: expireDate) } else { return nil } diff --git a/Stepic/Block.swift b/Stepic/Block.swift index 6b49c4a757..2c2a32a9fe 100644 --- a/Stepic/Block.swift +++ b/Stepic/Block.swift @@ -84,7 +84,9 @@ final class Block: NSManagedObject { case randomTasks = "random-tasks" case manualScore = "manual-score" - var isTheory: Bool { [BlockType.text, BlockType.video].contains(self) } + static var theoryTypes: [BlockType] { [.text, .video] } + + var isTheory: Bool { Self.theoryTypes.contains(self) } } enum JSONKey: String { diff --git a/Stepic/CardStepViewController.swift b/Stepic/CardStepViewController.swift index 31d41dad20..e25a3bfae3 100644 --- a/Stepic/CardStepViewController.swift +++ b/Stepic/CardStepViewController.swift @@ -192,7 +192,7 @@ extension CardStepViewController: WKNavigationDelegate { // Disable WebKit callout on long press var jsCode = "document.documentElement.style.webkitTouchCallout='none';" // Change color for audio control - jsCode += "document.body.style.setProperty('--actionColor', '#\(UIColor.stepicGreen.hexString)');" + jsCode += "document.body.style.setProperty('--actionColor', '#\(UIColor.stepikGreen.hexString)');" // Center images jsCode += "var imgs = document.getElementsByTagName('img');" jsCode += "for (var i = 0; i < imgs.length; i++){ imgs[i].style.marginLeft = (document.body.clientWidth / 2) - (imgs[i].clientWidth / 2) - 8 }" diff --git a/Stepic/CardsStepsViewController.swift b/Stepic/CardsStepsViewController.swift index bc01d5d648..ef29e3a38a 100644 --- a/Stepic/CardsStepsViewController.swift +++ b/Stepic/CardsStepsViewController.swift @@ -98,7 +98,11 @@ class CardsStepsViewController: UIViewController, CardsStepsView, ControllerWith } func presentDiscussions(stepId: Int, discussionProxyId: String) { - let assembly = DiscussionsAssembly(discussionProxyID: discussionProxyId, stepID: stepId) + let assembly = DiscussionsAssembly( + discussionThreadType: .default, + discussionProxyID: discussionProxyId, + stepID: stepId + ) self.push(module: assembly.makeModule()) } diff --git a/Stepic/ChoiceReply.swift b/Stepic/ChoiceReply.swift index 6d6c2e3779..3b714aadfc 100644 --- a/Stepic/ChoiceReply.swift +++ b/Stepic/ChoiceReply.swift @@ -1,25 +1,39 @@ -// -// ChoiceReply.swift -// Stepic -// -// Created by Alexander Karpov on 20.01.16. -// Copyright © 2016 Alex Karpov. All rights reserved. -// - import SwiftyJSON -import UIKit +import Foundation -final class ChoiceReply: NSObject, Reply { +final class ChoiceReply: Reply { var choices: [Bool] + var dictValue: [String: Any] { + [JSONKey.choices.rawValue: self.choices] + } + + var description: String { + "ChoiceReply(choices: \(self.choices))" + } + init(choices: [Bool]) { self.choices = choices } required init(json: JSON) { - choices = json["choices"].arrayValue.map({ $0.boolValue }) - super.init() + self.choices = json[JSONKey.choices.rawValue].arrayValue.map { $0.boolValue } } - var dictValue: [String: Any] { ["choices": choices] } + enum JSONKey: String { + case choices + } +} + +extension ChoiceReply: Hashable { + static func == (lhs: ChoiceReply, rhs: ChoiceReply) -> Bool { + if lhs === rhs { return true } + if type(of: lhs) != type(of: rhs) { return false } + if lhs.choices != rhs.choices { return false } + return true + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.choices) + } } diff --git a/Stepic/CodeReply.swift b/Stepic/CodeReply.swift index c478d1ed8b..80d515ae9b 100644 --- a/Stepic/CodeReply.swift +++ b/Stepic/CodeReply.swift @@ -1,45 +1,57 @@ -// -// CodeReply.swift -// Stepic -// -// Created by Ostrenkiy on 09.06.17. -// Copyright © 2017 Alex Karpov. All rights reserved. -// - import Foundation import SwiftyJSON final class CodeReply: Reply { var code: String - var language: CodeLanguage? var languageName: String + var language: CodeLanguage? { + CodeLanguage(rawValue: self.languageName) + } + + var dictValue: [String: Any] { + [ + JSONKey.code.rawValue: self.code, + JSONKey.language.rawValue: self.languageName + ] + } + var description: String { - "CodeReply(code: \(self.code), languageName: \(self.languageName))" + "CodeReply(code: \(self.code), language: \(self.languageName))" } init(code: String, languageName: String) { self.code = code - self.language = CodeLanguage(rawValue: languageName) self.languageName = languageName } init(code: String, language: CodeLanguage) { self.code = code - self.language = language self.languageName = language.rawValue } required init(json: JSON) { - code = json["code"].stringValue - languageName = json["language"].stringValue - language = CodeLanguage(rawValue: languageName) + self.code = json[JSONKey.code.rawValue].stringValue + self.languageName = json[JSONKey.language.rawValue].stringValue } - var dictValue: [String: Any] { - [ - "code": code, - "language": languageName - ] + enum JSONKey: String { + case code + case language + } +} + +extension CodeReply: Hashable { + static func == (lhs: CodeReply, rhs: CodeReply) -> Bool { + if lhs === rhs { return true } + if type(of: lhs) != type(of: rhs) { return false } + if lhs.code != rhs.code { return false } + if lhs.languageName != rhs.languageName { return false } + return true + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.code) + hasher.combine(self.languageName) } } diff --git a/Stepic/CommentsAPI.swift b/Stepic/CommentsAPI.swift index 9414004524..e19f6f25cd 100644 --- a/Stepic/CommentsAPI.swift +++ b/Stepic/CommentsAPI.swift @@ -14,7 +14,12 @@ import SwiftyJSON final class CommentsAPI: APIEndpoint { override var name: String { "comments" } - func retrieve(ids: [Comment.IdType]) -> Promise<[Comment]> { + /// Get comments by ids. + /// + /// - Parameter ids: The identifiers array of the comments to fetch. + /// - Parameter blockName: The name of the step's block (see Block.BlockType) for parsing reply and dataset. + /// - Returns: A promise with an array comments. + func retrieve(ids: [Comment.IdType], blockName: String?) -> Promise<[Comment]> { Promise { seal in self.retrieve.request( requestEndpoint: self.name, @@ -23,10 +28,10 @@ final class CommentsAPI: APIEndpoint { updating: [Comment](), withManager: self.manager ).done { comments, json in - var userInfoByID = [Int: UserInfo]() + var userInfoByUserID = [User.IdType: UserInfo]() json[Comment.JSONKey.users.rawValue].arrayValue.forEach { let user = UserInfo(json: $0) - userInfoByID[user.id] = user + userInfoByUserID[user.id] = user } var voteByID = [Vote.IdType: Vote]() @@ -35,9 +40,34 @@ final class CommentsAPI: APIEndpoint { voteByID[vote.id] = vote } + let isBlockNameProvided = !(blockName?.isEmpty ?? true) + + let attempts = json[Comment.JSONKey.attempts.rawValue].arrayValue.map { + isBlockNameProvided + ? Attempt(json: $0, stepName: blockName ?? "") + : Attempt(json: $0) + } + + var submissionByID = [Submission.IdType: Submission]() + json[Comment.JSONKey.submissions.rawValue].arrayValue.forEach { + let submission = isBlockNameProvided + ? Submission(json: $0, stepName: blockName ?? "") + : Submission(json: $0) + + if let attempt = attempts.first(where: { $0.id == submission.attemptID }) { + submission.attempt = attempt + } + + submissionByID[submission.id] = submission + } + for comment in comments { - comment.userInfo = userInfoByID[comment.userID] + comment.userInfo = userInfoByUserID[comment.userID] comment.vote = voteByID[comment.voteID] + + if let submissionID = comment.submissionID { + comment.submission = submissionByID[submissionID] + } } seal.fulfill(comments) diff --git a/Stepic/CongratulationViewController.swift b/Stepic/CongratulationViewController.swift index 201bad174f..47c3cf556b 100644 --- a/Stepic/CongratulationViewController.swift +++ b/Stepic/CongratulationViewController.swift @@ -52,7 +52,7 @@ final class CongratulationViewController: UIViewController { @IBOutlet weak var textLabel: UILabel! @IBAction func onShareButtonClick(_ sender: Any) { - guard let url = URL(string: "https://itunes.apple.com/app/id\(StepicApplicationsInfo.appId)") else { + guard let url = URL(string: "https://itunes.apple.com/app/id\(StepikApplicationsInfo.appId)") else { return } diff --git a/Stepic/ContinueActionButton.swift b/Stepic/ContinueActionButton.swift index ba39b9ae5c..dc1927e9b2 100644 --- a/Stepic/ContinueActionButton.swift +++ b/Stepic/ContinueActionButton.swift @@ -16,7 +16,7 @@ extension ContinueActionButton { let defaultBackgroundColor = UIColor.white let defaultTitleColor = UIColor.mainDark - let callToActionBackgroundColor = UIColor.stepicGreen + let callToActionBackgroundColor = UIColor.stepikGreen let callToActionTitleColor = UIColor.white } } diff --git a/Stepic/Course.swift b/Stepic/Course.swift index de4ec50258..c54f9795cd 100644 --- a/Stepic/Course.swift +++ b/Stepic/Course.swift @@ -88,7 +88,7 @@ final class Course: NSManagedObject, IDFetchable { self.id = json[JSONKey.id.rawValue].intValue self.title = json[JSONKey.title.rawValue].stringValue self.courseDescription = json[JSONKey.description.rawValue].stringValue - self.coverURLString = "\(StepicApplicationsInfo.stepicURL)" + json[JSONKey.cover.rawValue].stringValue + self.coverURLString = "\(StepikApplicationsInfo.stepikURL)" + json[JSONKey.cover.rawValue].stringValue self.beginDate = Parser.shared.dateFromTimedateJSON(json[JSONKey.beginDateSource.rawValue]) self.endDate = Parser.shared.dateFromTimedateJSON(json[JSONKey.lastDeadline.rawValue]) diff --git a/Stepic/CreateRequestMaker.swift b/Stepic/CreateRequestMaker.swift index df6f2941d7..925ce63a40 100644 --- a/Stepic/CreateRequestMaker.swift +++ b/Stepic/CreateRequestMaker.swift @@ -25,7 +25,7 @@ final class CreateRequestMaker { checkToken().done { manager.request( - "\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)", + "\(StepikApplicationsInfo.apiURL)/\(requestEndpoint)", method: .post, parameters: params, encoding: JSONEncoding.default diff --git a/Stepic/DeleteRequestMaker.swift b/Stepic/DeleteRequestMaker.swift index dbf31be325..718b162622 100644 --- a/Stepic/DeleteRequestMaker.swift +++ b/Stepic/DeleteRequestMaker.swift @@ -19,7 +19,7 @@ final class DeleteRequestMaker { Promise { seal in checkToken().done { manager.request( - "\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)/\(deletingId)", + "\(StepikApplicationsInfo.apiURL)/\(requestEndpoint)/\(deletingId)", method: .delete, encoding: JSONEncoding.default ).validate().responseSwiftyJSON { response in diff --git a/Stepic/DevicesAPI.swift b/Stepic/DevicesAPI.swift index 230b00727d..11234c23a2 100644 --- a/Stepic/DevicesAPI.swift +++ b/Stepic/DevicesAPI.swift @@ -32,7 +32,7 @@ final class DevicesAPI: APIEndpoint { func retrieve(deviceId: Int, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders) -> Promise { Promise { seal in self.manager.request( - "\(StepicApplicationsInfo.apiURL)/\(self.name)/\(deviceId)", + "\(StepikApplicationsInfo.apiURL)/\(self.name)/\(deviceId)", parameters: [:], headers: headers ).responseSwiftyJSON { response in @@ -70,7 +70,7 @@ final class DevicesAPI: APIEndpoint { ] self.manager.request( - "\(StepicApplicationsInfo.apiURL)/\(self.name)/\(deviceId)", + "\(StepikApplicationsInfo.apiURL)/\(self.name)/\(deviceId)", method: .put, parameters: params, encoding: JSONEncoding.default, @@ -102,7 +102,7 @@ final class DevicesAPI: APIEndpoint { let params = ["device": device.json] return Promise { seal in - manager.request("\(StepicApplicationsInfo.apiURL)/devices", method: .post, parameters: params, encoding: JSONEncoding.default, headers: headers).responseSwiftyJSON { response in + manager.request("\(StepikApplicationsInfo.apiURL)/devices", method: .post, parameters: params, encoding: JSONEncoding.default, headers: headers).responseSwiftyJSON { response in switch response.result { case .failure(let error): seal.reject(error) @@ -127,7 +127,7 @@ final class DevicesAPI: APIEndpoint { func delete(_ deviceId: Int, headers: [String: String] = APIDefaults.headers.bearer) -> Promise { Promise { seal in self.manager.request( - "\(StepicApplicationsInfo.apiURL)/devices/\(deviceId)", + "\(StepikApplicationsInfo.apiURL)/devices/\(deviceId)", method: .delete, headers: headers ).responseSwiftyJSON { response in @@ -158,7 +158,7 @@ final class DevicesAPI: APIEndpoint { ) -> Promise<(Meta, [Device])> { Promise { seal in self.manager.request( - "\(StepicApplicationsInfo.apiURL)/\(self.name)", + "\(StepikApplicationsInfo.apiURL)/\(self.name)", parameters: params, headers: headers ).responseSwiftyJSON { response in diff --git a/Stepic/DiscussionThread+CoreDataProperties.swift b/Stepic/DiscussionThread+CoreDataProperties.swift new file mode 100644 index 0000000000..43ebded1e2 --- /dev/null +++ b/Stepic/DiscussionThread+CoreDataProperties.swift @@ -0,0 +1,64 @@ +import CoreData +import Foundation + +extension DiscussionThread { + @NSManaged var managedId: String? + @NSManaged var managedThread: String? + @NSManaged var managedDiscussionsCount: NSNumber? + @NSManaged var managedDiscussionProxy: String? + + @NSManaged var managedStep: Step? + + static var oldEntity: NSEntityDescription { + NSEntityDescription.entity(forEntityName: "DiscussionThread", in: CoreDataHelper.shared.context)! + } + + convenience init() { + self.init(entity: DiscussionThread.oldEntity, insertInto: CoreDataHelper.shared.context) + } + + var id: String { + get { + self.managedId ?? "" + } + set { + self.managedId = newValue + } + } + + var thread: String { + get { + self.managedThread ?? "" + } + set { + self.managedThread = newValue + } + } + + var discussionsCount: Int { + get { + self.managedDiscussionsCount?.intValue ?? 0 + } + set { + self.managedDiscussionsCount = newValue as NSNumber? + } + } + + var discussionProxy: String { + get { + self.managedDiscussionProxy ?? "" + } + set { + self.managedDiscussionProxy = newValue + } + } + + var step: Step? { + get { + self.managedStep + } + set { + self.managedStep = newValue + } + } +} diff --git a/Stepic/DiscussionThread.swift b/Stepic/DiscussionThread.swift new file mode 100644 index 0000000000..a1d46ee09f --- /dev/null +++ b/Stepic/DiscussionThread.swift @@ -0,0 +1,50 @@ +import CoreData +import Foundation +import SwiftyJSON + +final class DiscussionThread: NSManagedObject, JSONSerializable, IDFetchable { + typealias IdType = String + + var json: JSON { + [ + JSONKey.id.rawValue: self.id, + JSONKey.thread.rawValue: self.thread, + JSONKey.discussionsCount.rawValue: self.discussionsCount, + JSONKey.discussionProxy.rawValue: self.discussionProxy + ] + } + + var threadType: ThreadType? { + ThreadType(rawValue: self.thread) + } + + required convenience init(json: JSON) { + self.init() + self.initialize(json) + } + + func initialize(_ json: JSON) { + self.id = json[JSONKey.id.rawValue].stringValue + self.thread = json[JSONKey.thread.rawValue].stringValue + self.discussionsCount = json[JSONKey.discussionsCount.rawValue].intValue + self.discussionProxy = json[JSONKey.discussionProxy.rawValue].stringValue + } + + func update(json: JSON) { + self.initialize(json) + } + + // MARK: Enums + + enum JSONKey: String { + case id + case thread + case discussionsCount = "discussions_count" + case discussionProxy = "discussion_proxy" + } + + enum ThreadType: String { + case `default` + case solutions + } +} diff --git a/Stepic/DiscussionThreadsAPI.swift b/Stepic/DiscussionThreadsAPI.swift new file mode 100644 index 0000000000..5939ec91a6 --- /dev/null +++ b/Stepic/DiscussionThreadsAPI.swift @@ -0,0 +1,34 @@ +import Alamofire +import Foundation +import PromiseKit +import SwiftyJSON + +final class DiscussionThreadsAPI: APIEndpoint { + override var name: String { "discussion-threads" } + + /// Get discussion threads by ids. + func retrieve(ids: [DiscussionThread.IdType], page: Int = 1) -> Promise<([DiscussionThread], Meta)> { + Promise { seal in + let params: Parameters = [ + "ids": ids, + "page": page + ] + + firstly { + DiscussionThread.fetchAsync(ids: ids) + }.then { cachedDiscussionThreads -> Promise<([DiscussionThread], Meta, JSON)> in + self.retrieve.request( + requestEndpoint: self.name, + paramName: self.name, + params: params, + updatingObjects: cachedDiscussionThreads, + withManager: self.manager + ) + }.done { discussionThreads, meta, _ in + seal.fulfill((discussionThreads, meta)) + }.catch { error in + seal.reject(error) + } + } + } +} diff --git a/Stepic/Discussions/Comment.swift b/Stepic/Discussions/Comment.swift index 8e25f5951b..c76733e173 100644 --- a/Stepic/Discussions/Comment.swift +++ b/Stepic/Discussions/Comment.swift @@ -17,7 +17,7 @@ enum UserRole: String { final class Comment: JSONSerializable { var id: Int = -1 - var parentID: Comment.IdType? + var parentID: IdType? var userID: User.IdType = 0 var userRole: UserRole = .student var time = Date() @@ -32,9 +32,11 @@ final class Comment: JSONSerializable { var epicCount: Int = 0 var abuseCount: Int = 0 var actions: [Action] = [] + var submissionID: Submission.IdType? var userInfo: UserInfo! var vote: Vote! + var submission: Submission? var json: JSON { var dict: JSON = [ @@ -79,6 +81,7 @@ final class Comment: JSONSerializable { self.voteID = json[JSONKey.vote.rawValue].stringValue self.epicCount = json[JSONKey.epicCount.rawValue].intValue self.abuseCount = json[JSONKey.abuseCount.rawValue].intValue + self.submissionID = json[JSONKey.submission.rawValue].int self.actions.removeAll(keepingCapacity: true) for (actionKey, value) in json[JSONKey.actions.rawValue].dictionaryValue { @@ -119,5 +122,37 @@ final class Comment: JSONSerializable { case actions case users case votes + case submission + case submissions + case attempts + } +} + +// MARK: - Comment: CustomDebugStringConvertible - + +extension Comment: CustomDebugStringConvertible { + var debugDescription: String { + """ + Comment(id: \(id), \ + parentID: \(parentID ??? "nil"), \ + userID: \(userID), \ + userRole: \(userRole), \ + time: \(time), \ + lastTime: \(lastTime), \ + text: \(text), \ + replyCount: \(replyCount), \ + isDeleted: \(isDeleted), \ + targetStepID: \(targetStepID), \ + repliesIDs: \(repliesIDs), \ + isPinned: \(isPinned), \ + voteID: \(voteID), \ + epicCount: \(epicCount), \ + abuseCount: \(abuseCount), \ + actions: \(actions), \ + submissionID: \(submissionID ??? "nil"), \ + userInfo: \(userInfo ??? "nil"), \ + vote: \(vote ??? "nil"), \ + submission: \(submission ??? "nil")) + """ } } diff --git a/Stepic/Discussions/Vote.swift b/Stepic/Discussions/Vote.swift index b2982d5b55..b6720f10bb 100644 --- a/Stepic/Discussions/Vote.swift +++ b/Stepic/Discussions/Vote.swift @@ -51,3 +51,9 @@ final class Vote: JSONSerializable { case value } } + +extension Vote: CustomDebugStringConvertible { + var debugDescription: String { + "Vote(id: \(id), value: \(value?.rawValue ?? "nil"))" + } +} \ No newline at end of file diff --git a/Stepic/EmailAuthViewController.swift b/Stepic/EmailAuthViewController.swift index 2493d4aa9e..82fd991bea 100644 --- a/Stepic/EmailAuthViewController.swift +++ b/Stepic/EmailAuthViewController.swift @@ -135,7 +135,7 @@ final class EmailAuthViewController: UIViewController { } @IBAction func onRemindPasswordClick(_ sender: Any) { - WebControllerManager.sharedManager.presentWebControllerWithURLString("\(StepicApplicationsInfo.stepicURL)/accounts/password/reset/", inController: self, withKey: "reset password", allowsSafari: true, backButtonStyle: BackButtonStyle.done) + WebControllerManager.sharedManager.presentWebControllerWithURLString("\(StepikApplicationsInfo.stepikURL)/accounts/password/reset/", inController: self, withKey: "reset password", allowsSafari: true, backButtonStyle: BackButtonStyle.done) } override func viewDidLoad() { diff --git a/Stepic/EnrollmentsAPI.swift b/Stepic/EnrollmentsAPI.swift index 93a9ea272b..64a2ca7a7f 100644 --- a/Stepic/EnrollmentsAPI.swift +++ b/Stepic/EnrollmentsAPI.swift @@ -48,7 +48,7 @@ extension EnrollmentsAPI { if !delete { return self.manager.request( - "\(StepicApplicationsInfo.apiURL)/\(name)", + "\(StepikApplicationsInfo.apiURL)/\(name)", method: .post, parameters: params, encoding: JSONEncoding.default, @@ -82,7 +82,7 @@ extension EnrollmentsAPI { }) } else { return self.manager.request( - "\(StepicApplicationsInfo.apiURL)/enrollments/\(course.id)", + "\(StepikApplicationsInfo.apiURL)/enrollments/\(course.id)", method: .delete, parameters: params, encoding: URLEncoding.default, diff --git a/Stepic/Extensions/UIColorExtensions.swift b/Stepic/Extensions/UIColorExtensions.swift index 4d32cd7805..03844c7edd 100644 --- a/Stepic/Extensions/UIColorExtensions.swift +++ b/Stepic/Extensions/UIColorExtensions.swift @@ -40,13 +40,13 @@ extension UIColor { static let lightBlue = UIColor(hex: 0x45B0FF) - static var stepicGreen: UIColor { StepicApplicationsInfo.Colors.mainGreen } + static var stepikGreen: UIColor { StepikApplicationsInfo.Colors.mainGreen } static let mainLight = UIColor(hex: 0xf6f6f6) - static var mainDark: UIColor { StepicApplicationsInfo.Colors.mainDark } + static var mainDark: UIColor { StepikApplicationsInfo.Colors.mainDark } - static var mainText: UIColor { return StepicApplicationsInfo.Colors.mainText } + static var mainText: UIColor { return StepikApplicationsInfo.Colors.mainText } static let thirdColor = UIColor(hex: 0x54a2ff) diff --git a/Stepic/FreeAnswerReply.swift b/Stepic/FreeAnswerReply.swift index 04dc7efb55..1f0a5accf3 100644 --- a/Stepic/FreeAnswerReply.swift +++ b/Stepic/FreeAnswerReply.swift @@ -1,30 +1,43 @@ -// -// FreeAnswerReply.swift -// Stepic -// -// Created by Alexander Karpov on 26.01.16. -// Copyright © 2016 Alex Karpov. All rights reserved. -// - +import Foundation import SwiftyJSON -import UIKit -final class FreeAnswerReply: NSObject, Reply { +final class FreeAnswerReply: Reply { var text: String + var dictValue: [String: Any] { + [ + JSONKey.text.rawValue: self.text, + JSONKey.attachments.rawValue: [] + ] + } + + var description: String { + "FreeAnswerReply(text: \(self.text))" + } + init(text: String) { self.text = text } required init(json: JSON) { - text = json["text"].stringValue - super.init() + self.text = json[JSONKey.text.rawValue].stringValue } - var dictValue: [String: Any] { - [ - "text": text as NSObject, - "attachments": [] - ] + enum JSONKey: String { + case text + case attachments + } +} + +extension FreeAnswerReply: Hashable { + static func == (lhs: FreeAnswerReply, rhs: FreeAnswerReply) -> Bool { + if lhs === rhs { return true } + if type(of: lhs) != type(of: rhs) { return false } + if lhs.text != rhs.text { return false } + return true + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.text) } } diff --git a/Stepic/HTMLProcessor.swift b/Stepic/HTMLProcessor.swift index 08475ac36b..7ec839d408 100644 --- a/Stepic/HTMLProcessor.swift +++ b/Stepic/HTMLProcessor.swift @@ -158,7 +158,7 @@ final class HTMLProcessor { static func addStepikURLIfNeeded(url: String) -> String { if url.first == Character("/") { - return "\(StepicApplicationsInfo.stepicURL)/\(url)" + return "\(StepikApplicationsInfo.stepikURL)/\(url)" } else { return url } diff --git a/Stepic/Images.xcassets/Quiz feedback/quiz-feedback-correct.imageset/quiz-feedback-correct.pdf b/Stepic/Images.xcassets/Quiz feedback/quiz-feedback-correct.imageset/quiz-feedback-correct.pdf index 11ffbb1f53950a68a650abbc906a704ef4ef7f88..dde5636bc51244553212c0022bb6c10686703584 100644 GIT binary patch delta 487 zcmdldcS~+UV7>otzC#W?w||TNYn1zVSG9cR?v0_cKN`9iT_!d(-dB%JaPU&PU3sE1 z;TUuB#J-JgnrazJ74r%Y$+G4P96zD0BB^KLqn{MjBzivNv%}xxZK++=O%tm*Za5y@ z#VV5XA?55!xdmUJt$oDiwJ74e^@*7?<5M|~7O(tW@usq5M%OA?^ELk`sU}Z1I5KP1 zCD~a^%&%=oINYTgH`m@+!=QNLMQJ8e%Z=}*aI+XF7#U1%KGoFN)F{!!D9O^$C?zd1CDqu#z|z1XDb*;&EXlwq l$&Sm0pb~{x1v@*g;*!Lol8U0#G%jOPLt`#gRabvEE&z@Fm!ALt delta 460 zcmca5w@+?DV11t<-(dqD=I?u&8Gje4P3FN1-0heJ1X0J+tQ=7icfZ`91Y*#*-W=-nlM5vD4F)%Ac;_yt;Rba9y9c z@b!HCEr&Y}Utzkve|rd5a=4?w^+lGEoza?d0_TpMIe(phU9K8q_r$l-OvZ+rxfrK# zvzRFunN42AvxURJ%+S=x#Kd@V9B-qRp@D({2q@$!aDf>H21X`k=wil(W=4}a`PAx- z%+aL`jE%4;$EMEG1YNJOp#g?@#zrQ_80w5HA!7VU?ld+wvzWY>&n`a2D8%@m He>W}wwd#gh diff --git a/Stepic/Images.xcassets/solutions-icon.imageset/Contents.json b/Stepic/Images.xcassets/solutions-icon.imageset/Contents.json new file mode 100644 index 0000000000..bc37823a96 --- /dev/null +++ b/Stepic/Images.xcassets/solutions-icon.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "solutions-icon.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "preserves-vector-representation" : true + } +} \ No newline at end of file diff --git a/Stepic/Images.xcassets/solutions-icon.imageset/solutions-icon.pdf b/Stepic/Images.xcassets/solutions-icon.imageset/solutions-icon.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a5c277d06505a9f365022cb57488b8a83d4f25bb GIT binary patch literal 1525 zcmY!laBlN;i_PDYXEPBE9>2M!5?f}Zjg4$b$vRbrm+ z^q2lg+$Da|qi41B?a(dVLdJFStll#V_aAyc;p@fu>wi2>?~AWL?2~=&$HZVgk@&)8 zcC!xMVOUXkW@Xs49m+DcdEMTxK3>voshBFMQW>~jrI8qP zGKBT!&8gdD@2VC!3Hoi-VYEG+c|En$y~$(J=0nSCT>VtbnRnT2sO6Dtn6&9aqy)E@ z@Y4wS$EmqahZD9xG&mq}Nzy&5<)XAvkJ&`2UnY_aZ*^ujCzmWPXJo(Ic5C9dt-@zZ zG#nKzuRch)8F?wNb$apMqoJ#R%9gB{de^(x<(-6la&Zr{W~I~D9@Q1;O*fM*uRnSC z^~0sN8{6-1xT*B|i`lB3*H*riDXJ27o%1|V>v{9Zk2TDvk2yQPeJ1G_nk~Dk>RtOA zk2CGLGx=AtdmP)-uQo^Ic~O1NZU%{I$EUx_Z7^FVC*E>E?{NI)I=^+t)!xl~t5)P5 zYdGOvK~mdNiS|8mU&#~p$S}&CVONn}uwL-skDCKYNS4kFpa=>@;^k9q5+VjpzrLcV5p#Z_lfW-7v1yuz@u(<(=MX7luK&`N>Xp%4ueGEgwI0HIj0 zTIc+{5}@K@1yEjuWIXqx{L%s>CqZlgi^5zP479f>F|W7)WK(h_OxRPQq^L9%#&ZU$ za!D=AOim4QcLE7Qe4-zeTAW{6l$=_u01A&FB!7Wi3@Z;1{wfCP)%VU!DF(V9rx~3-BB4=RvQj(dU=K_it4HqjT10w?iLt_I&QxgkAlPFCj>&%g?gSgi@u_Q4k zKOJlkC{Bu@v4DuXilWpsE&~M%E(16KCJ|F(Q-w4IsF!IbRVvWQpm+$%&o5B` kc^r~hJoD1>6+pobj>qDX#G(?g4-JhhjJZ@*UH#p-09TtFQUCw| literal 0 HcmV?d00001 diff --git a/Stepic/MatchingReply.swift b/Stepic/MatchingReply.swift index fd61337c88..a326b25813 100644 --- a/Stepic/MatchingReply.swift +++ b/Stepic/MatchingReply.swift @@ -1,24 +1,39 @@ -// -// MatchingReply.swift -// Stepic -// -// Created by Alexander Karpov on 16.01.17. -// Copyright © 2017 Alex Karpov. All rights reserved. -// - +import Foundation import SwiftyJSON -import UIKit final class MatchingReply: Reply { var ordering: [Int] + var dictValue: [String: Any] { + [JSONKey.ordering.rawValue: self.ordering] + } + + var description: String { + "MatchingReply(ordering: \(self.ordering))" + } + init(ordering: [Int]) { self.ordering = ordering } required init(json: JSON) { - ordering = json["ordering"].arrayValue.map({ $0.intValue }) + self.ordering = json[JSONKey.ordering.rawValue].arrayValue.map { $0.intValue } } - var dictValue: [String: Any] { ["ordering": ordering] } + enum JSONKey: String { + case ordering + } +} + +extension MatchingReply: Hashable { + static func == (lhs: MatchingReply, rhs: MatchingReply) -> Bool { + if lhs === rhs { return true } + if type(of: lhs) != type(of: rhs) { return false } + if lhs.ordering != rhs.ordering { return false } + return true + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.ordering) + } } diff --git a/Stepic/MathReply.swift b/Stepic/MathReply.swift index cb90290d12..c7fc78e6cb 100644 --- a/Stepic/MathReply.swift +++ b/Stepic/MathReply.swift @@ -1,25 +1,39 @@ -// -// MathReply.swift -// Stepic -// -// Created by Alexander Karpov on 26.01.16. -// Copyright © 2016 Alex Karpov. All rights reserved. -// - +import Foundation import SwiftyJSON -import UIKit -final class MathReply: NSObject, Reply { +final class MathReply: Reply { var formula: String + var dictValue: [String: Any] { + [JSONKey.formula.rawValue: self.formula] + } + + var description: String { + "MathReply(formula: \(self.formula))" + } + init(formula: String) { self.formula = formula } required init(json: JSON) { - formula = json["formula"].stringValue - super.init() + self.formula = json[JSONKey.formula.rawValue].stringValue } - var dictValue: [String: Any] { ["formula": formula] } + enum JSONKey: String { + case formula + } +} + +extension MathReply: Hashable { + static func == (lhs: MathReply, rhs: MathReply) -> Bool { + if lhs === rhs { return true } + if type(of: lhs) != type(of: rhs) { return false } + if lhs.formula != rhs.formula { return false } + return true + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.formula) + } } diff --git a/Stepic/Model.xcdatamodeld/.xccurrentversion b/Stepic/Model.xcdatamodeld/.xccurrentversion index e20b509268..ab2b791b4a 100644 --- a/Stepic/Model.xcdatamodeld/.xccurrentversion +++ b/Stepic/Model.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - Model_is_certificate_issued.xcdatamodel + Model_ discussion_threads.xcdatamodel diff --git a/Stepic/Model.xcdatamodeld/Model_ discussion_threads.xcdatamodel/contents b/Stepic/Model.xcdatamodeld/Model_ discussion_threads.xcdatamodel/contents new file mode 100644 index 0000000000..1cfdaa4007 --- /dev/null +++ b/Stepic/Model.xcdatamodeld/Model_ discussion_threads.xcdatamodel/contents @@ -0,0 +1,316 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Stepic/NotificationDataExtractor.swift b/Stepic/NotificationDataExtractor.swift index 296053f3cd..fcb06b99ae 100644 --- a/Stepic/NotificationDataExtractor.swift +++ b/Stepic/NotificationDataExtractor.swift @@ -52,7 +52,7 @@ final class NotificationDataExtractor { } if let commentsLink = HTMLParsingUtil.getLink(self.text, index: 2) { - let urlString = StepicApplicationsInfo.stepicURL + commentsLink + let urlString = StepikApplicationsInfo.stepikURL + commentsLink return URL(string: urlString) } else { return nil diff --git a/Stepic/NotificationStatusButton.swift b/Stepic/NotificationStatusButton.swift index 70434b173f..7e7acd979a 100644 --- a/Stepic/NotificationStatusButton.swift +++ b/Stepic/NotificationStatusButton.swift @@ -26,7 +26,7 @@ final class NotificationStatusButton: UIButton { return mark }() - private let unreadMarkColor = UIColor.stepicGreen + private let unreadMarkColor = UIColor.stepikGreen private let unreadMarkColorHightlighted = UIColor(red: 91 / 255, green: 183 / 255, blue: 91 / 255, alpha: 1.0) override func awakeFromNib() { diff --git a/Stepic/NotificationSuggestionManager.swift b/Stepic/NotificationSuggestionManager.swift index d2b66e816e..1f30e52d56 100644 --- a/Stepic/NotificationSuggestionManager.swift +++ b/Stepic/NotificationSuggestionManager.swift @@ -93,7 +93,7 @@ final class NotificationSuggestionManager { let commonChecks = AuthInfo.shared.isAuthorized && self.isAlertAvailableNow(context: context) && PreferencesContainer.notifications.allowStreaksNotifications == false - && StepicApplicationsInfo.streaksEnabled + && StepikApplicationsInfo.streaksEnabled switch trigger { case .login: diff --git a/Stepic/NotificationsAPI.swift b/Stepic/NotificationsAPI.swift index 85315077f1..2a77b5cd4c 100644 --- a/Stepic/NotificationsAPI.swift +++ b/Stepic/NotificationsAPI.swift @@ -46,7 +46,7 @@ final class NotificationsAPI: APIEndpoint { Promise { seal in checkToken().done { self.manager.request( - "\(StepicApplicationsInfo.apiURL)/\(self.name)/mark-as-read", + "\(StepikApplicationsInfo.apiURL)/\(self.name)/mark-as-read", method: .post, parameters: nil, encoding: JSONEncoding.default, diff --git a/Stepic/NumberReply.swift b/Stepic/NumberReply.swift index f86580f000..6b4afaebee 100644 --- a/Stepic/NumberReply.swift +++ b/Stepic/NumberReply.swift @@ -1,25 +1,39 @@ -// -// NumberReply.swift -// Stepic -// -// Created by Alexander Karpov on 26.01.16. -// Copyright © 2016 Alex Karpov. All rights reserved. -// - +import Foundation import SwiftyJSON -import UIKit -final class NumberReply: NSObject, Reply { +final class NumberReply: Reply { var number: String + var dictValue: [String: Any] { + [JSONKey.number.rawValue: self.number] + } + + var description: String { + "NumberReply(number: \(self.number))" + } + init(number: String) { self.number = number } required init(json: JSON) { - number = json["number"].stringValue - super.init() + self.number = json[JSONKey.number.rawValue].stringValue } - var dictValue: [String: Any] { ["number": number] } + enum JSONKey: String { + case number + } +} + +extension NumberReply: Hashable { + static func == (lhs: NumberReply, rhs: NumberReply) -> Bool { + if lhs === rhs { return true } + if type(of: lhs) != type(of: rhs) { return false } + if lhs.number != rhs.number { return false } + return true + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.number) + } } diff --git a/Stepic/PersistentUserTokenRecoveryManager.swift b/Stepic/PersistentUserTokenRecoveryManager.swift index 1ef83d6ce6..166bc168e5 100644 --- a/Stepic/PersistentUserTokenRecoveryManager.swift +++ b/Stepic/PersistentUserTokenRecoveryManager.swift @@ -13,14 +13,14 @@ import Foundation */ final class PersistentUserTokenRecoveryManager: PersistentRecoveryManager { override func recoverObjectFromDictionary(_ dictionary: [String: Any]) -> DictionarySerializable? { - StepicToken(dictionary: dictionary) + StepikToken(dictionary: dictionary) } - func recoverStepicToken(userId: Int) -> StepicToken? { - self.recoverObjectWithKey("\(userId)") as? StepicToken + func recoverStepicToken(userId: Int) -> StepikToken? { + self.recoverObjectWithKey("\(userId)") as? StepikToken } - func writeStepicToken(_ token: StepicToken, userId: Int) { + func writeStepicToken(_ token: StepikToken, userId: Int) { self.writeObjectWithKey("\(userId)", object: token) } } diff --git a/Stepic/ProfileViewController.swift b/Stepic/ProfileViewController.swift index 40bf1ae152..f73231799c 100644 --- a/Stepic/ProfileViewController.swift +++ b/Stepic/ProfileViewController.swift @@ -177,7 +177,7 @@ final class ProfileViewController: MenuViewController, ProfileView, ControllerWi } if let shareButton = self.shareButton, let shareID = shareID { - self.sharingURL = StepicApplicationsInfo.stepicURL + "/users/\(shareID)" + self.sharingURL = StepikApplicationsInfo.stepikURL + "/users/\(shareID)" if isSettingsHidden { self.navigationItem.rightBarButtonItem = shareButton } else { diff --git a/Stepic/QuizPresenter.swift b/Stepic/QuizPresenter.swift index 741146cb73..781bb249ba 100644 --- a/Stepic/QuizPresenter.swift +++ b/Stepic/QuizPresenter.swift @@ -28,7 +28,7 @@ final class QuizPresenter { } var stepUrl: String { - "\(StepicApplicationsInfo.stepicURL)/lesson/\(step.lessonID)/step/\(step.position)?from_mobile_app=true" + "\(StepikApplicationsInfo.stepikURL)/lesson/\(step.lessonID)/step/\(step.position)?from_mobile_app=true" } init( diff --git a/Stepic/QuizViewController.swift b/Stepic/QuizViewController.swift index 863e3e9e88..ad37b3ec15 100644 --- a/Stepic/QuizViewController.swift +++ b/Stepic/QuizViewController.swift @@ -300,7 +300,7 @@ class QuizViewController: UIViewController, QuizView, QuizControllerDataSource, break } - self.sendButton.backgroundColor = UIColor.stepicGreen + self.sendButton.backgroundColor = UIColor.stepikGreen self.sendButton.titleLabel?.font = UIFont.systemFont(ofSize: 16) self.sendButton.layer.cornerRadius = 6 self.sendButtonLeadingConstraint.constant = -16 diff --git a/Stepic/RateAppManager.swift b/Stepic/RateAppManager.swift index 15b401431b..780f03004d 100644 --- a/Stepic/RateAppManager.swift +++ b/Stepic/RateAppManager.swift @@ -11,7 +11,7 @@ import Foundation final class RateAppManager { private let defaults = UserDefaults.standard - private let correctSubmissionsThreshold = StepicApplicationsInfo.RateApp.correctSubmissionsThreshold + private let correctSubmissionsThreshold = StepikApplicationsInfo.RateApp.correctSubmissionsThreshold private let showRateLaterPressedDateKey = "showRateLaterPressedDateKey" diff --git a/Stepic/RateAppViewController.swift b/Stepic/RateAppViewController.swift index edd8906ec4..7b911b85a7 100644 --- a/Stepic/RateAppViewController.swift +++ b/Stepic/RateAppViewController.swift @@ -48,7 +48,7 @@ final class RateAppViewController: UIViewController { case .appStore: rightButton.titleLabel?.text = NSLocalizedString("AppStore", comment: "") rightButton.setTitle(NSLocalizedString("AppStore", comment: ""), for: .normal) - rightButton.setTitleColor(UIColor.stepicGreen, for: .normal) + rightButton.setTitleColor(UIColor.stepikGreen, for: .normal) case .email: rightButton.titleLabel?.text = NSLocalizedString("Email", comment: "") rightButton.setTitle(NSLocalizedString("Email", comment: ""), for: .normal) diff --git a/Stepic/RecommendationsAPI.swift b/Stepic/RecommendationsAPI.swift index ec050cc5ef..dfb4d156c9 100644 --- a/Stepic/RecommendationsAPI.swift +++ b/Stepic/RecommendationsAPI.swift @@ -24,7 +24,7 @@ final class RecommendationsAPI: APIEndpoint { ) -> Promise<[Int]> { Promise { seal in self.manager.request( - "\(StepicApplicationsInfo.apiURL)/\(self.name)", + "\(StepikApplicationsInfo.apiURL)/\(self.name)", parameters: [ "course": courseId, "count": count @@ -57,7 +57,7 @@ final class RecommendationsAPI: APIEndpoint { return Promise { seal in manager.request( - "\(StepicApplicationsInfo.apiURL)/\(self.reactionName)", + "\(StepikApplicationsInfo.apiURL)/\(self.reactionName)", method: .post, parameters: params, encoding: JSONEncoding.default, diff --git a/Stepic/RegistrationPresenter.swift b/Stepic/RegistrationPresenter.swift index c454509c86..49dc1084dd 100644 --- a/Stepic/RegistrationPresenter.swift +++ b/Stepic/RegistrationPresenter.swift @@ -43,7 +43,7 @@ final class RegistrationPresenter { checkToken().then { () -> Promise<()> in self.authAPI.signUpWithAccount(firstname: name, lastname: " ", email: email, password: password) - }.then { _ -> Promise<(StepicToken, AuthorizationType)> in + }.then { _ -> Promise<(StepikToken, AuthorizationType)> in self.authAPI.signInWithAccount(email: email, password: password) }.then { token, authorizationType -> Promise in AuthInfo.shared.token = token diff --git a/Stepic/RegistrationViewController.swift b/Stepic/RegistrationViewController.swift index 455383c4a5..a67785046f 100644 --- a/Stepic/RegistrationViewController.swift +++ b/Stepic/RegistrationViewController.swift @@ -189,7 +189,7 @@ final class RegistrationViewController: UIViewController { let all = Style.font(.systemFont(ofSize: tosLabel.font.pointSize, weight: UIFont.Weight.regular)) .foregroundColor(UIColor.mainText) .paragraphStyle(paragraphStyle) - let link = Style("a").font(.systemFont(ofSize: tosLabel.font.pointSize, weight: UIFont.Weight.regular)).foregroundColor(UIColor.stepicGreen) + let link = Style("a").font(.systemFont(ofSize: tosLabel.font.pointSize, weight: UIFont.Weight.regular)).foregroundColor(UIColor.stepikGreen) let activeLink = Style.font(.systemFont(ofSize: tosLabel.font.pointSize, weight: UIFont.Weight.regular)) .foregroundColor(UIColor.mainText) .backgroundColor(UIColor(hex: 0xF6F6F6)) diff --git a/Stepic/RemoteConfig.swift b/Stepic/RemoteConfig.swift index 722d08ad17..9b4bb490b6 100644 --- a/Stepic/RemoteConfig.swift +++ b/Stepic/RemoteConfig.swift @@ -29,8 +29,8 @@ final class RemoteConfig { lazy var appDefaults: [String: NSObject] = [ RemoteConfigKeys.showStreaksNotificationTrigger.rawValue: defaultShowStreaksNotificationTrigger.rawValue as NSObject, - RemoteConfigKeys.adaptiveBackendUrl.rawValue: StepicApplicationsInfo.adaptiveRatingURL as NSObject, - RemoteConfigKeys.supportedInAdaptiveModeCourses.rawValue: StepicApplicationsInfo.adaptiveSupportedCourses as NSObject, + RemoteConfigKeys.adaptiveBackendUrl.rawValue: StepikApplicationsInfo.adaptiveRatingURL as NSObject, + RemoteConfigKeys.supportedInAdaptiveModeCourses.rawValue: StepikApplicationsInfo.adaptiveSupportedCourses as NSObject, RemoteConfigKeys.newLessonAvailable.rawValue: true as NSObject ] @@ -53,7 +53,7 @@ final class RemoteConfig { guard let configValue = FirebaseRemoteConfig.RemoteConfig.remoteConfig().configValue( forKey: RemoteConfigKeys.adaptiveBackendUrl.rawValue ).stringValue else { - return StepicApplicationsInfo.adaptiveRatingURL + return StepikApplicationsInfo.adaptiveRatingURL } return configValue @@ -63,7 +63,7 @@ final class RemoteConfig { guard let configValue = FirebaseRemoteConfig.RemoteConfig.remoteConfig().configValue( forKey: RemoteConfigKeys.supportedInAdaptiveModeCourses.rawValue ).stringValue else { - return StepicApplicationsInfo.adaptiveSupportedCourses + return StepikApplicationsInfo.adaptiveSupportedCourses } let courses = configValue.components(separatedBy: ",") diff --git a/Stepic/RemoteVersionManager.swift b/Stepic/RemoteVersionManager.swift index fa11626397..03e2c34361 100644 --- a/Stepic/RemoteVersionManager.swift +++ b/Stepic/RemoteVersionManager.swift @@ -46,7 +46,7 @@ final class RemoteVersionManager: NSObject { } private func getRemoteVersion(success: @escaping (String, String) -> Void, error errorHandler: @escaping (NSError) -> Void) -> Request { - AlamofireDefaultSessionManager.shared.request(StepicApplicationsInfo.versionInfoURL).responseSwiftyJSON({ + AlamofireDefaultSessionManager.shared.request(StepikApplicationsInfo.versionInfoURL).responseSwiftyJSON({ response in var error = response.result.error diff --git a/Stepic/Reply.swift b/Stepic/Reply.swift index 9045581e06..57507f543a 100644 --- a/Stepic/Reply.swift +++ b/Stepic/Reply.swift @@ -1,11 +1,3 @@ -// -// Reply.swift -// Stepic -// -// Created by Alexander Karpov on 20.01.16. -// Copyright © 2016 Alex Karpov. All rights reserved. -// - import Foundation import SwiftyJSON diff --git a/Stepic/RetrieveRequestMaker.swift b/Stepic/RetrieveRequestMaker.swift index 5983873fd6..27f495737f 100644 --- a/Stepic/RetrieveRequestMaker.swift +++ b/Stepic/RetrieveRequestMaker.swift @@ -22,7 +22,7 @@ final class RetrieveRequestMaker { Promise { seal in checkToken().done { manager.request( - "\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)/\(id)", + "\(StepikApplicationsInfo.apiURL)/\(requestEndpoint)/\(id)", method: .get, encoding: URLEncoding.default ).validate().responseSwiftyJSON { response in @@ -53,7 +53,7 @@ final class RetrieveRequestMaker { Promise { seal in checkToken().done { manager.request( - "\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)", + "\(StepikApplicationsInfo.apiURL)/\(requestEndpoint)", method: .get, parameters: params, encoding: URLEncoding.default @@ -113,7 +113,7 @@ final class RetrieveRequestMaker { Promise { seal in checkToken().done { manager.request( - "\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)", + "\(StepikApplicationsInfo.apiURL)/\(requestEndpoint)", method: .get, parameters: params, encoding: URLEncoding.default @@ -187,7 +187,7 @@ final class RetrieveRequestMaker { return Promise { seal in checkToken().done { manager.request( - "\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)", + "\(StepikApplicationsInfo.apiURL)/\(requestEndpoint)", parameters: params, encoding: URLEncoding.default ).validate().responseSwiftyJSON { response in diff --git a/Stepic/SQLReply.swift b/Stepic/SQLReply.swift index 8c0980359f..b711b489d3 100644 --- a/Stepic/SQLReply.swift +++ b/Stepic/SQLReply.swift @@ -1,24 +1,39 @@ -// -// SQLReply.swift -// Stepic -// -// Created by Vladislav Kiryukhin on 17.11.2017. -// Copyright © 2017 Alex Karpov. All rights reserved. -// - import Foundation import SwiftyJSON final class SQLReply: Reply { var code: String + var dictValue: [String: Any] { + [JSONKey.solveSQL.rawValue: self.code] + } + + var description: String { + "SQLReply(solve_sql: \(self.code))" + } + init(code: String) { self.code = code } required init(json: JSON) { - code = json["solve_sql"].stringValue + self.code = json[JSONKey.solveSQL.rawValue].stringValue } - var dictValue: [String: Any] { ["solve_sql": code] } + enum JSONKey: String { + case solveSQL = "solve_sql" + } +} + +extension SQLReply: Hashable { + static func == (lhs: SQLReply, rhs: SQLReply) -> Bool { + if lhs === rhs { return true } + if type(of: lhs) != type(of: rhs) { return false } + if lhs.code != rhs.code { return false } + return true + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.code) + } } diff --git a/Stepic/SearchResultsAPI.swift b/Stepic/SearchResultsAPI.swift index 99579cb85d..8300a6660b 100644 --- a/Stepic/SearchResultsAPI.swift +++ b/Stepic/SearchResultsAPI.swift @@ -31,7 +31,7 @@ final class SearchResultsAPI: APIEndpoint { params["type"] = t } - return manager.request("\(StepicApplicationsInfo.apiURL)/search-results", method: .get, parameters: params, encoding: URLEncoding.default, headers: headers).responseSwiftyJSON({ + return manager.request("\(StepikApplicationsInfo.apiURL)/search-results", method: .get, parameters: params, encoding: URLEncoding.default, headers: headers).responseSwiftyJSON({ response in var error = response.result.error diff --git a/Stepic/Services/DeepLinks/DeepLinkRoute.swift b/Stepic/Services/DeepLinks/DeepLinkRoute.swift index 2d4f967f17..05f2adb988 100644 --- a/Stepic/Services/DeepLinks/DeepLinkRoute.swift +++ b/Stepic/Services/DeepLinks/DeepLinkRoute.swift @@ -13,6 +13,7 @@ enum DeepLinkRoute { case lesson(lessonID: Int, stepID: Int, unitID: Int?) case notifications(section: NotificationsSection) case discussions(lessonID: Int, stepID: Int, discussionID: Int, unitID: Int?) + case solutions(lessonID: Int, stepID: Int, discussionID: Int, unitID: Int?) case profile(userID: Int) case syllabus(courseID: Int) case catalog @@ -82,6 +83,17 @@ enum DeepLinkRoute { self = .discussions(lessonID: lessonID, stepID: stepID, discussionID: discussionID, unitID: unitID) return } + + if let match = Pattern.solutions.regex.firstMatch(in: path), + let lessonIDString = match.captures[0], let lessonID = Int(lessonIDString), + let stepIDString = match.captures[1], let stepID = Int(stepIDString), + let discussionIDString = match.captures[2], let discussionID = Int(discussionIDString), + match.matchedString == path { + let unitID = match.captures[3].flatMap { Int($0) } + self = .solutions(lessonID: lessonID, stepID: stepID, discussionID: discussionID, unitID: unitID) + return + } + return nil } @@ -94,6 +106,7 @@ enum DeepLinkRoute { case syllabus case lesson case discussions + case solutions var regex: Regex { return try! Regex(string: self.pattern, options: [.ignoreCase]) @@ -122,6 +135,8 @@ enum DeepLinkRoute { return #"\#(stepik)\#(lesson)step\/(\d+)(?:\?unit=(\d+))?\/?"# case .discussions: return #"\#(stepik)\#(lesson)step\/(\d+)(?:\?discussion=(\d+))(?:\&unit=(\d+))?\/?\#(queryComponents)"# + case .solutions: + return #"\#(stepik)\#(lesson)step\/(\d+)(?:\?discussion=(\d+))(?:\&unit=(\d+))?&thread=solutions.*"# } } } diff --git a/Stepic/Services/DeepLinks/DeepLinkRouter.swift b/Stepic/Services/DeepLinks/DeepLinkRouter.swift index 6ed2d1bcd6..b39992f909 100644 --- a/Stepic/Services/DeepLinks/DeepLinkRouter.swift +++ b/Stepic/Services/DeepLinks/DeepLinkRouter.swift @@ -7,6 +7,7 @@ // import Foundation +import PromiseKit final class DeepLinkRouter { static var window: UIWindow? { @@ -174,8 +175,8 @@ final class DeepLinkRouter { } if components.count == 4 - && components[1].lowercased() == "users" - && components[3].lowercased() == "certificates" { + && components[1].lowercased() == "users" + && components[3].lowercased() == "certificates" { guard let userID = getID(components[2], reversed: false) else { completion([]) return @@ -242,6 +243,7 @@ final class DeepLinkRouter { .first(where: { $0.name == "amp;reply" })? .value .flatMap(Int.init) + let threadValue = queryItems.first(where: { $0.name == "amp;thread" })?.value AnalyticsReporter.reportEvent( AnalyticsEvents.DeepLink.discussion, @@ -255,6 +257,7 @@ final class DeepLinkRouter { self.routeToDiscussionWithID( discussionID: discussionID, replyID: replyID, + thread: threadValue, lessonID: lessonId, stepID: stepId, unitID: nil, @@ -323,6 +326,7 @@ final class DeepLinkRouter { static func routeToDiscussionWithID( discussionID: Comment.IdType, replyID: Comment.IdType?, + thread: String?, lessonID: Int, stepID: Int, unitID: Int?, @@ -346,19 +350,53 @@ final class DeepLinkRouter { refreshMode: .update, success: { steps in guard let step = steps.first else { - completion([]) - return + return completion([]) } - if let discussionProxyID = step.discussionProxyID { + let threadTypeOrNil: DiscussionThread.ThreadType? = thread == nil + ? .default + : DiscussionThread.ThreadType(rawValue: thread ?? "") + + guard let threadType = threadTypeOrNil else { + return completion([]) + } + + switch threadType { + case .default: + guard let discussionProxyID = step.discussionProxyID else { + return completion([]) + } + let assembly = DiscussionsAssembly( + discussionThreadType: .default, discussionProxyID: discussionProxyID, stepID: step.id, presentationContext: .scrollTo(discussionID: discussionID, replyID: replyID) ) completion(viewControllers + [assembly.makeModule()]) - } else { - completion([]) + case .solutions: + guard let discussionThreadsIDs = step.discussionThreadsArray else { + return completion([]) + } + + ApiDataDownloader.discussionThreads.retrieve(ids: discussionThreadsIDs).done { + discussionThreads, _ in + guard let discussionThread = discussionThreads.first( + where: { $0.threadType == .solutions } + ) else { + return completion([]) + } + + let assembly = DiscussionsAssembly( + discussionThreadType: .solutions, + discussionProxyID: discussionThread.discussionProxy, + stepID: step.id, + presentationContext: .scrollTo(discussionID: discussionID, replyID: replyID) + ) + completion(viewControllers + [assembly.makeModule()]) + }.catch { _ in + completion([]) + } } }, error: { _ in diff --git a/Stepic/Services/DeepLinks/DeepLinkRoutingService.swift b/Stepic/Services/DeepLinks/DeepLinkRoutingService.swift index f1cf9a9fad..ea35762495 100644 --- a/Stepic/Services/DeepLinks/DeepLinkRoutingService.swift +++ b/Stepic/Services/DeepLinks/DeepLinkRoutingService.swift @@ -74,7 +74,7 @@ final class DeepLinkRoutingService { return TabBarRouter(tab: .catalog) case .notifications(let section): return TabBarRouter(notificationsSection: section) - case .course, .coursePromo, .discussions, .lesson, .profile, .syllabus: + case .course, .coursePromo, .discussions, .solutions, .lesson, .profile, .syllabus: return ModalOrPushStackRouter( source: source, destinationStack: moduleStack, @@ -113,6 +113,19 @@ final class DeepLinkRoutingService { DeepLinkRouter.routeToDiscussionWithID( discussionID: discussionID, replyID: nil, + thread: DiscussionThread.ThreadType.default.rawValue, + lessonID: lessonID, + stepID: stepID, + unitID: unitID, + completion: { moduleStack in + seal.fulfill(moduleStack) + } + ) + case .solutions(let lessonID, let stepID, let discussionID, let unitID): + DeepLinkRouter.routeToDiscussionWithID( + discussionID: discussionID, + replyID: nil, + thread: DiscussionThread.ThreadType.solutions.rawValue, lessonID: lessonID, stepID: stepID, unitID: unitID, diff --git a/Stepic/Services/Notifications/Registration/NotificationsRegistrationService.swift b/Stepic/Services/Notifications/Registration/NotificationsRegistrationService.swift index 92d1b0cb0a..7360d36bf6 100644 --- a/Stepic/Services/Notifications/Registration/NotificationsRegistrationService.swift +++ b/Stepic/Services/Notifications/Registration/NotificationsRegistrationService.swift @@ -102,7 +102,7 @@ final class NotificationsRegistrationService: NotificationsRegistrationServicePr self.fetchFirebaseAppInstanceID() } - guard StepicApplicationsInfo.shouldRegisterNotifications else { + guard StepikApplicationsInfo.shouldRegisterNotifications else { return } diff --git a/Stepic/Session.swift b/Stepic/Session.swift index 240eca8674..3f720bbcf7 100644 --- a/Stepic/Session.swift +++ b/Stepic/Session.swift @@ -23,7 +23,7 @@ final class Session { static func refresh(completion: @escaping (() -> Void), error errorHandler: @escaping ((String) -> Void)) -> Request? { print("refreshing session") - let stepicURLString = StepicApplicationsInfo.stepicURL + let stepicURLString = StepikApplicationsInfo.stepikURL let stepicURL = URL(string: stepicURLString)! delete() @@ -50,10 +50,10 @@ final class Session { Session.cookieDict = cookieDict - if let csrftoken = cookieDict["\(StepicApplicationsInfo.cookiePrefix)csrftoken"], - let sessionId = cookieDict["\(StepicApplicationsInfo.cookiePrefix)sessionid"] { + if let csrftoken = cookieDict["\(StepikApplicationsInfo.cookiePrefix)csrftoken"], + let sessionId = cookieDict["\(StepikApplicationsInfo.cookiePrefix)sessionid"] { Session.cookieHeaders = [ - "Referer": "\(StepicApplicationsInfo.stepicURL)/", + "Referer": "\(StepikApplicationsInfo.stepikURL)/", "X-CSRFToken": csrftoken, "Cookie": "csrftoken=\(csrftoken); sessionid=\(sessionId)" ] diff --git a/Stepic/SocialAuthPresenter.swift b/Stepic/SocialAuthPresenter.swift index 95e928de3f..2aedf51151 100644 --- a/Stepic/SocialAuthPresenter.swift +++ b/Stepic/SocialAuthPresenter.swift @@ -84,7 +84,7 @@ final class SocialAuthPresenter { SDKProvider.delegate = viewDelegate } - SDKProvider.getAccessInfo().then { socialToken, email -> Promise<(StepicToken, AuthorizationType)> in + SDKProvider.getAccessInfo().then { socialToken, email -> Promise<(StepikToken, AuthorizationType)> in self.authAPI.signUpWithToken(socialToken: socialToken, email: email, provider: SDKProvider.name) }.then { token, authorizationType -> Promise in AuthInfo.shared.token = token diff --git a/Stepic/SocialAuthProviders.swift b/Stepic/SocialAuthProviders.swift index 4513572568..ae019b55c0 100644 --- a/Stepic/SocialAuthProviders.swift +++ b/Stepic/SocialAuthProviders.swift @@ -15,24 +15,24 @@ enum SocialProvider: Int { switch self { case .vk: return SocialProviderInfo(name: self.name, amplitudeName: self.amplitudeName, image: #imageLiteral(resourceName: "vk"), - registerURL: URL(string: "https://stepik.org/accounts/vk/login?next=%2Foauth2%2Fauthorize%2F%3Fclient_id%3D\(StepicApplicationsInfo.social!.clientId)%26response_type%3Dcode")!, + registerURL: URL(string: "https://stepik.org/accounts/vk/login?next=%2Foauth2%2Fauthorize%2F%3Fclient_id%3D\(StepikApplicationsInfo.social!.clientId)%26response_type%3Dcode")!, socialSDKProvider: VKSocialSDKProvider.instance) case .google: return SocialProviderInfo(name: self.name, amplitudeName: self.amplitudeName, image: #imageLiteral(resourceName: "google"), - registerURL: URL(string: "https://stepik.org/accounts/google/login?next=%2Foauth2%2Fauthorize%2F%3Fclient_id%3D\(StepicApplicationsInfo.social!.clientId)%26response_type%3Dcode")!) + registerURL: URL(string: "https://stepik.org/accounts/google/login?next=%2Foauth2%2Fauthorize%2F%3Fclient_id%3D\(StepikApplicationsInfo.social!.clientId)%26response_type%3Dcode")!) case .facebook: return SocialProviderInfo(name: self.name, amplitudeName: self.amplitudeName, image: #imageLiteral(resourceName: "fb"), - registerURL: URL(string: "https://stepik.org/accounts/facebook/login?next=%2Foauth2%2Fauthorize%2F%3Fclient_id%3D\(StepicApplicationsInfo.social!.clientId)%26response_type%3Dcode")!, + registerURL: URL(string: "https://stepik.org/accounts/facebook/login?next=%2Foauth2%2Fauthorize%2F%3Fclient_id%3D\(StepikApplicationsInfo.social!.clientId)%26response_type%3Dcode")!, socialSDKProvider: FBSocialSDKProvider.instance) case .twitter: return SocialProviderInfo(name: self.name, amplitudeName: self.amplitudeName, image: #imageLiteral(resourceName: "twitter"), - registerURL: URL(string: "https://stepik.org/accounts/twitter/login?next=%2Foauth2%2Fauthorize%2F%3Fclient_id%3D\(StepicApplicationsInfo.social!.clientId)%26response_type%3Dcode")!) + registerURL: URL(string: "https://stepik.org/accounts/twitter/login?next=%2Foauth2%2Fauthorize%2F%3Fclient_id%3D\(StepikApplicationsInfo.social!.clientId)%26response_type%3Dcode")!) case .gitHub: return SocialProviderInfo(name: self.name, amplitudeName: self.amplitudeName, image: #imageLiteral(resourceName: "github"), - registerURL: URL(string: "https://stepik.org/accounts/github/login?next=%2Foauth2%2Fauthorize%2F%3Fclient_id%3D\(StepicApplicationsInfo.social!.clientId)%26response_type%3Dcode")!) + registerURL: URL(string: "https://stepik.org/accounts/github/login?next=%2Foauth2%2Fauthorize%2F%3Fclient_id%3D\(StepikApplicationsInfo.social!.clientId)%26response_type%3Dcode")!) case .itMailRu: return SocialProviderInfo(name: self.name, amplitudeName: self.amplitudeName, image: #imageLiteral(resourceName: "mail"), - registerURL: URL(string: "https://stepik.org/accounts/itmailru/login?next=%2Foauth2%2Fauthorize%2F%3Fclient_id%3D\(StepicApplicationsInfo.social!.clientId)%26response_type%3Dcode")!) + registerURL: URL(string: "https://stepik.org/accounts/itmailru/login?next=%2Foauth2%2Fauthorize%2F%3Fclient_id%3D\(StepikApplicationsInfo.social!.clientId)%26response_type%3Dcode")!) } } diff --git a/Stepic/SocialAuthViewController.swift b/Stepic/SocialAuthViewController.swift index b3532d981b..3586cbbd66 100644 --- a/Stepic/SocialAuthViewController.swift +++ b/Stepic/SocialAuthViewController.swift @@ -30,7 +30,7 @@ extension SocialAuthViewController: SocialAuthView { case .error: SVProgressHUD.showError(withStatus: NSLocalizedString("FailedToSignIn", comment: "")) case .existingEmail(let email): - if let url = URL(string: "\(StepicApplicationsInfo.social?.redirectUri ?? "")?error=social_signup_with_existing_email&email=\(email)") { + if let url = URL(string: "\(StepikApplicationsInfo.social?.redirectUri ?? "")?error=social_signup_with_existing_email&email=\(email)") { UIApplication.shared.openURL(url) } else { SVProgressHUD.showError(withStatus: NSLocalizedString("FailedToSignIn", comment: "")) diff --git a/Stepic/SortingReply.swift b/Stepic/SortingReply.swift index 5420f83bd0..374e64a2a5 100644 --- a/Stepic/SortingReply.swift +++ b/Stepic/SortingReply.swift @@ -1,25 +1,39 @@ -// -// SortingReply.swift -// Stepic -// -// Created by Alexander Karpov on 27.01.16. -// Copyright © 2016 Alex Karpov. All rights reserved. -// - +import Foundation import SwiftyJSON -import UIKit -final class SortingReply: NSObject, Reply { +final class SortingReply: Reply { var ordering: [Int] + var dictValue: [String: Any] { + [JSONKey.ordering.rawValue: self.ordering] + } + + var description: String { + "SortingReply(ordering: \(self.ordering))" + } + init(ordering: [Int]) { self.ordering = ordering } required init(json: JSON) { - ordering = json["ordering"].arrayValue.map({ $0.intValue }) - super.init() + self.ordering = json[JSONKey.ordering.rawValue].arrayValue.map { $0.intValue } } - var dictValue: [String: Any] { ["ordering": ordering] } + enum JSONKey: String { + case ordering + } +} + +extension SortingReply: Hashable { + static func == (lhs: SortingReply, rhs: SortingReply) -> Bool { + if lhs === rhs { return true } + if type(of: lhs) != type(of: rhs) { return false } + if lhs.ordering != rhs.ordering { return false } + return true + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.ordering) + } } diff --git a/Stepic/Sources/Controllers/StyledTabBarViewController.swift b/Stepic/Sources/Controllers/StyledTabBarViewController.swift index 37e2b38718..032b9fadf1 100644 --- a/Stepic/Sources/Controllers/StyledTabBarViewController.swift +++ b/Stepic/Sources/Controllers/StyledTabBarViewController.swift @@ -1,7 +1,7 @@ import UIKit final class StyledTabBarViewController: UITabBarController { - private let items = StepicApplicationsInfo.Modules.tabs?.compactMap { TabController(rawValue: $0)?.itemInfo } ?? [] + private let items = StepikApplicationsInfo.Modules.tabs?.compactMap { TabController(rawValue: $0)?.itemInfo } ?? [] private var notificationsBadgeNumber: Int { get { diff --git a/Stepic/Sources/Frameworks/ContentProcessor/ContentProcessingRule.swift b/Stepic/Sources/Frameworks/ContentProcessor/ContentProcessingRule.swift index 245a1739e8..e21cd38228 100644 --- a/Stepic/Sources/Frameworks/ContentProcessor/ContentProcessingRule.swift +++ b/Stepic/Sources/Frameworks/ContentProcessor/ContentProcessingRule.swift @@ -50,7 +50,7 @@ final class AddStepikSiteForRelativeURLsRule: BaseHTMLExtractionRule { return link } - return "\(StepicApplicationsInfo.stepicURL)\(link)" + return "\(StepikApplicationsInfo.stepikURL)\(link)" } } diff --git a/Stepic/Sources/Modules/CourseInfo/CourseInfoInteractor.swift b/Stepic/Sources/Modules/CourseInfo/CourseInfoInteractor.swift index 47cc3b24b0..f735ff649f 100644 --- a/Stepic/Sources/Modules/CourseInfo/CourseInfoInteractor.swift +++ b/Stepic/Sources/Modules/CourseInfo/CourseInfoInteractor.swift @@ -39,9 +39,9 @@ final class CourseInfoInteractor: CourseInfoInteractorProtocol { } if let slug = course.slug { - return "\(StepicApplicationsInfo.stepicURL)/course/\(slug)" + return "\(StepikApplicationsInfo.stepikURL)/course/\(slug)" } else { - return "\(StepicApplicationsInfo.stepicURL)/\(course.id)" + return "\(StepikApplicationsInfo.stepikURL)/\(course.id)" } } diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/Views/Cell/CourseInfoTabSyllabusCellView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/Views/Cell/CourseInfoTabSyllabusCellView.swift index bc191f84f2..64dafc0d35 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/Views/Cell/CourseInfoTabSyllabusCellView.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/Views/Cell/CourseInfoTabSyllabusCellView.swift @@ -25,7 +25,7 @@ extension CourseInfoTabSyllabusCellView { let statsViewHeight: CGFloat = 17.0 let progressViewHeight: CGFloat = 3 - let progressViewMainColor = UIColor.stepicGreen + let progressViewMainColor = UIColor.stepikGreen let progressViewSecondaryColor = UIColor.clear let tapProxyViewSize = CGSize(width: 60, height: 60) diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/Views/Section/CourseInfoTabSyllabusSectionView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/Views/Section/CourseInfoTabSyllabusSectionView.swift index adfa5eda27..15e90fd130 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/Views/Section/CourseInfoTabSyllabusSectionView.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/Views/Section/CourseInfoTabSyllabusSectionView.swift @@ -37,7 +37,7 @@ extension CourseInfoTabSyllabusSectionView { let deadlinesInsets = UIEdgeInsets(top: 16, left: 0, bottom: 19, right: 0) let progressViewHeight: CGFloat = 3 - let progressViewMainColor = UIColor.stepicGreen + let progressViewMainColor = UIColor.stepikGreen let progressViewSecondaryColor = UIColor.clear let tapProxyViewSize = CGSize(width: 60, height: 60) diff --git a/Stepic/Sources/Modules/CourseList/Views/CourseListColorMode.swift b/Stepic/Sources/Modules/CourseList/Views/CourseListColorMode.swift index 33b80020ac..7eb2f40738 100644 --- a/Stepic/Sources/Modules/CourseList/Views/CourseListColorMode.swift +++ b/Stepic/Sources/Modules/CourseList/Views/CourseListColorMode.swift @@ -42,15 +42,15 @@ extension CourseListColorMode { return .init( textColor: UIColor.mainText, backgroundColor: UIColor(hex: 0x535366, alpha: 0.06), - callToActionTextColor: UIColor.stepicGreen, - callToActionBackgroundColor: UIColor.stepicGreen.withAlphaComponent(0.1) + callToActionTextColor: UIColor.stepikGreen, + callToActionBackgroundColor: UIColor.stepikGreen.withAlphaComponent(0.1) ) case .dark: return .init( textColor: UIColor.white, backgroundColor: UIColor(hex: 0xffffff, alpha: 0.1), - callToActionTextColor: UIColor.stepicGreen, - callToActionBackgroundColor: UIColor.stepicGreen.withAlphaComponent(0.1) + callToActionTextColor: UIColor.stepikGreen, + callToActionBackgroundColor: UIColor.stepikGreen.withAlphaComponent(0.1) ) } } diff --git a/Stepic/Sources/Modules/CourseList/Views/Widget/CourseWidgetButton.swift b/Stepic/Sources/Modules/CourseList/Views/Widget/CourseWidgetButton.swift index 1d224cea4d..bab1032719 100644 --- a/Stepic/Sources/Modules/CourseList/Views/Widget/CourseWidgetButton.swift +++ b/Stepic/Sources/Modules/CourseList/Views/Widget/CourseWidgetButton.swift @@ -9,8 +9,8 @@ extension CourseWidgetButton { var textColor = UIColor.mainText var backgroundColor = UIColor(hex: 0x535366, alpha: 0.06) - var callToActionTextColor = UIColor.stepicGreen - var callToActionBackgroundColor = UIColor.stepicGreen.withAlphaComponent(0.1) + var callToActionTextColor = UIColor.stepikGreen + var callToActionBackgroundColor = UIColor.stepikGreen.withAlphaComponent(0.1) } } diff --git a/Stepic/Sources/Modules/Discussions/DiscussionsAssembly.swift b/Stepic/Sources/Modules/Discussions/DiscussionsAssembly.swift index 7a0bdf962b..2a0ea292a9 100644 --- a/Stepic/Sources/Modules/Discussions/DiscussionsAssembly.swift +++ b/Stepic/Sources/Modules/Discussions/DiscussionsAssembly.swift @@ -3,15 +3,18 @@ import UIKit final class DiscussionsAssembly: Assembly { var moduleInput: DiscussionsInputProtocol? + private let discussionThreadType: DiscussionThread.ThreadType private let discussionProxyID: DiscussionProxy.IdType private let stepID: Step.IdType private let presentationContext: Discussions.PresentationContext init( + discussionThreadType: DiscussionThread.ThreadType, discussionProxyID: DiscussionProxy.IdType, stepID: Step.IdType, presentationContext: Discussions.PresentationContext = .fromBeginning ) { + self.discussionThreadType = discussionThreadType self.discussionProxyID = discussionProxyID self.stepID = stepID self.presentationContext = presentationContext @@ -24,15 +27,18 @@ final class DiscussionsAssembly: Assembly { ), commentsNetworkService: CommentsNetworkService(commentsAPI: CommentsAPI()), votesNetworkService: VotesNetworkService(votesAPI: VotesAPI()), + stepsNetworkService: StepsNetworkService(stepsAPI: StepsAPI()), stepsPersistenceService: StepsPersistenceService() ) let presenter = DiscussionsPresenter() let interactor = DiscussionsInteractor( + discussionThreadType: self.discussionThreadType, discussionProxyID: self.discussionProxyID, stepID: self.stepID, presentationContext: self.presentationContext, presenter: presenter, - provider: provider + provider: provider, + discussionsSortTypeStorageManager: DiscussionsSortTypeStorageManager() ) let viewController = DiscussionsViewController(interactor: interactor) diff --git a/Stepic/Sources/Modules/Discussions/DiscussionsDataFlow.swift b/Stepic/Sources/Modules/Discussions/DiscussionsDataFlow.swift index 3fd88992f7..0fe11d4a34 100644 --- a/Stepic/Sources/Modules/Discussions/DiscussionsDataFlow.swift +++ b/Stepic/Sources/Modules/Discussions/DiscussionsDataFlow.swift @@ -35,11 +35,26 @@ enum Discussions { } // MARK: - Use cases - + + /// Update navigation title and buttons + enum NavigationItemUpdate { + struct Response { + let threadType: DiscussionThread.ThreadType + } + + struct ViewModel { + let title: String + let shouldShowSortButton: Bool + let shouldShowComposeButton: Bool + let threadType: DiscussionThread.ThreadType + } + } + // MARK: Fetch comments /// Show discussions enum DiscussionsLoad { - struct Request { } + struct Request {} struct Response { let result: Result @@ -188,11 +203,30 @@ enum Discussions { } } + /// Present solution (submission) + enum SolutionPresentation { + struct Request { + let commentID: Comment.IdType + } + + struct Response { + let stepID: Step.IdType + let submission: Submission + let discussionID: DiscussionThread.IdType + } + + struct ViewModel { + let stepID: Step.IdType + let submission: Submission + let discussionID: DiscussionThread.IdType + } + } + // MARK: - Sort type /// Presents action sheet with available and current sort type (after sort type bar button item click) enum SortTypesPresentation { - struct Request { } + struct Request {} struct Response { let currentSortType: SortType diff --git a/Stepic/Sources/Modules/Discussions/DiscussionsInteractor.swift b/Stepic/Sources/Modules/Discussions/DiscussionsInteractor.swift index 7330a137b5..9bad642e2e 100644 --- a/Stepic/Sources/Modules/Discussions/DiscussionsInteractor.swift +++ b/Stepic/Sources/Modules/Discussions/DiscussionsInteractor.swift @@ -8,12 +8,13 @@ protocol DiscussionsInteractorProtocol { func doDiscussionsLoad(request: Discussions.DiscussionsLoad.Request) func doNextDiscussionsLoad(request: Discussions.NextDiscussionsLoad.Request) func doNextRepliesLoad(request: Discussions.NextRepliesLoad.Request) - func doWriteCommentPresentation(request: Discussions.WriteCommentPresentation.Request ) + func doWriteCommentPresentation(request: Discussions.WriteCommentPresentation.Request) func doCommentDelete(request: Discussions.CommentDelete.Request) func doCommentLike(request: Discussions.CommentLike.Request) func doCommentAbuse(request: Discussions.CommentAbuse.Request) func doSortTypesPresentation(request: Discussions.SortTypesPresentation.Request) func doSortTypeUpdate(request: Discussions.SortTypeUpdate.Request) + func doSolutionPresentation(request: Discussions.SolutionPresentation.Request) } final class DiscussionsInteractor: DiscussionsInteractorProtocol { @@ -24,7 +25,9 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { private let presenter: DiscussionsPresenterProtocol private let provider: DiscussionsProviderProtocol + private let discussionsSortTypeStorageManager: DiscussionsSortTypeStorageManagerProtocol + private let discussionThreadType: DiscussionThread.ThreadType private let discussionProxyID: DiscussionProxy.IdType private let stepID: Step.IdType private let presentationContext: Discussions.PresentationContext @@ -49,6 +52,15 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { } } + private var currentSortType: Discussions.SortType { + get { + self.discussionsSortTypeStorageManager.globalDiscussionsSortType + } + set { + self.discussionsSortTypeStorageManager.globalDiscussionsSortType = newValue + } + } + /// A Boolean value that determines whether the fetch of the replies for root discussion is in progress. private var discussionsIDsFetchingReplies: Set = [] @@ -59,21 +71,23 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { ) init( + discussionThreadType: DiscussionThread.ThreadType, discussionProxyID: DiscussionProxy.IdType, stepID: Step.IdType, presentationContext: Discussions.PresentationContext, presenter: DiscussionsPresenterProtocol, - provider: DiscussionsProviderProtocol + provider: DiscussionsProviderProtocol, + discussionsSortTypeStorageManager: DiscussionsSortTypeStorageManagerProtocol ) { + self.discussionThreadType = discussionThreadType self.discussionProxyID = discussionProxyID self.stepID = stepID self.presentationContext = presentationContext self.presenter = presenter self.provider = provider + self.discussionsSortTypeStorageManager = discussionsSortTypeStorageManager - DiscussionsInteractor.logger.info( - "discussions interactor: did init with presentationContext: \(presentationContext)" - ) + Self.logger.info("discussions interactor: did init with presentationContext: \(presentationContext)") self.reportOpenedEventToAnalytics() } @@ -89,10 +103,16 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { } strongSelf.fetchSemaphore.wait() - DiscussionsInteractor.logger.info("discussions interactor: start fetching discussions") + Self.logger.info("discussions interactor: start fetching discussions") + + DispatchQueue.main.async { + strongSelf.presenter.presentNavigationItemUpdate( + response: .init(threadType: strongSelf.discussionThreadType) + ) + } strongSelf.fetchDiscussions(discussionProxyID: strongSelf.discussionProxyID).done { discussionsData in - DiscussionsInteractor.logger.info("discussions interactor: finish fetching discussions") + Self.logger.info("discussions interactor: finish fetching discussions") DispatchQueue.main.async { strongSelf.presenter.presentDiscussions(response: .init(result: .success(discussionsData))) @@ -101,7 +121,7 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { } } }.catch { error in - DiscussionsInteractor.logger.error("discussions interactor: failed fetch discussions, error: \(error)") + Self.logger.error("discussions interactor: failed fetch discussions, error: \(error)") DispatchQueue.main.async { strongSelf.presenter.presentDiscussions(response: .init(result: .failure(Error.fetchFailed))) } @@ -120,12 +140,10 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { strongSelf.fetchSemaphore.wait() let idsToLoad = strongSelf.getNextDiscussionsIDsToLoad(direction: request.direction) - DiscussionsInteractor.logger.info( - "discussions interactor: start fetching next discussions ids: \(idsToLoad)" - ) + Self.logger.info("discussions interactor: start fetching next discussions ids: \(idsToLoad)") - strongSelf.provider.fetchComments(ids: idsToLoad).done { fetchedComments in - DiscussionsInteractor.logger.info("discussions interactor: finish fetching next discussions") + strongSelf.provider.fetchComments(ids: idsToLoad, stepID: strongSelf.stepID).done { fetchedComments in + Self.logger.info("discussions interactor: finish fetching next discussions") strongSelf.updateDataWithNewComments(fetchedComments) DispatchQueue.main.async { strongSelf.presenter.presentNextDiscussions( @@ -136,9 +154,7 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { ) } }.catch { error in - DiscussionsInteractor.logger.error( - "discussions interactor: failed fetch next discussions, error: \(error)" - ) + Self.logger.error("discussions interactor: failed fetch next discussions, error: \(error)") DispatchQueue.main.async { strongSelf.presenter.presentNextDiscussions( response: .init(result: .failure(Error.fetchFailed), direction: request.direction) @@ -167,10 +183,10 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { strongSelf.fetchSemaphore.wait() let idsToLoad = strongSelf.getNextReplyIDsToLoad(discussion: discussion) - DiscussionsInteractor.logger.info("discussions interactor: start fetching next replies ids: \(idsToLoad)") + Self.logger.info("discussions interactor: start fetching next replies ids: \(idsToLoad)") - strongSelf.provider.fetchComments(ids: idsToLoad).done { fetchedComments in - DiscussionsInteractor.logger.info("discussions interactor: finish fetching next replies") + strongSelf.provider.fetchComments(ids: idsToLoad, stepID: strongSelf.stepID).done { fetchedComments in + Self.logger.info("discussions interactor: finish fetching next replies") strongSelf.updateDataWithNewComments(fetchedComments) strongSelf.discussionsIDsFetchingReplies.remove(discussion.id) @@ -179,7 +195,7 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { strongSelf.presenter.presentNextReplies(response: .init(result: strongSelf.makeDiscussionsData())) } }.catch { error in - DiscussionsInteractor.logger.error("discussions interactor: failed fetch next replies, error: \(error)") + Self.logger.error("discussions interactor: failed fetch next replies, error: \(error)") strongSelf.discussionsIDsFetchingReplies.remove(discussion.id) DispatchQueue.main.async { strongSelf.presenter.presentNextReplies(response: .init(result: strongSelf.makeDiscussionsData())) @@ -198,7 +214,7 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { self.presentWriteComment(commentID: request.commentID) case .edit: guard let commentID = request.commentID else { - return DiscussionsInteractor.logger.error("discussions interactor: unable to edit comment, id is nil") + return Self.logger.error("discussions interactor: unable to edit comment, id is nil") } self.presentEditComment(commentID: commentID) @@ -206,13 +222,13 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { } func doCommentDelete(request: Discussions.CommentDelete.Request) { - DiscussionsInteractor.logger.info("discussions interactor: start deleting comment by id: \(request.commentID)") + Self.logger.info("discussions interactor: start deleting comment by id: \(request.commentID)") self.presenter.presentWaitingState(response: .init(shouldDismiss: false)) let commentID = request.commentID self.provider.deleteComment(id: commentID).done { - DiscussionsInteractor.logger.info("discussions interactor: deleted comment with id: \(commentID)") + Self.logger.info("discussions interactor: deleted comment with id: \(commentID)") if let discussionIndex = self.currentDiscussions.firstIndex(where: { $0.id == commentID }) { self.currentDiscussions.remove(at: discussionIndex) @@ -239,9 +255,7 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { self.presenter.presentCommentDelete(response: .init(result: .success(self.makeDiscussionsData()))) }.cauterize() }.catch { error in - DiscussionsInteractor.logger.error( - "discussions interactor: failed delete comment by id: \(commentID), error: \(error)" - ) + Self.logger.error("discussions interactor: failed delete comment by id: \(commentID), error: \(error)") self.presenter.presentCommentDelete(response: .init(result: .failure(error))) } } @@ -250,7 +264,7 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { func doCommentLike(request: Discussions.CommentLike.Request) { guard let comment = self.getAllComments().first(where: { $0.id == request.commentID }) else { - return DiscussionsInteractor.logger.error( + return Self.logger.error( "discussions interactor: unable like comment, can't find comment with id: \(request.commentID)" ) } @@ -259,12 +273,12 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { let voteValueToSet: VoteValue? = voteValue == .epic ? nil : .epic let vote = Vote(id: comment.vote.id, value: voteValueToSet) - DiscussionsInteractor.logger.info( + Self.logger.info( "discussions interactor: start liking vote from \(voteValue) to \(voteValueToSet ??? "null")" ) self.provider.updateVote(vote).done { vote in - DiscussionsInteractor.logger.info("discussions interactor: finish liking vote") + Self.logger.info("discussions interactor: finish liking vote") comment.vote = vote @@ -280,15 +294,15 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { }.ensure { self.presenter.presentCommentLike(response: .init(result: self.makeDiscussionsData())) }.catch { error in - DiscussionsInteractor.logger.error("discussions interactor: failed like vote, error: \(error)") + Self.logger.error("discussions interactor: failed like vote, error: \(error)") } } else { - DiscussionsInteractor.logger.info("discussions interactor: start liking vote to epic value") + Self.logger.info("discussions interactor: start liking vote to epic value") let vote = Vote(id: comment.vote.id, value: .epic) self.provider.updateVote(vote).done { vote in - DiscussionsInteractor.logger.info("discussions interactor: finish liking vote") + Self.logger.info("discussions interactor: finish liking vote") AnalyticsReporter.reportEvent(AnalyticsEvents.Discussion.liked) comment.vote = vote @@ -296,14 +310,14 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { }.ensure { self.presenter.presentCommentLike(response: .init(result: self.makeDiscussionsData())) }.catch { error in - DiscussionsInteractor.logger.error("discussions interactor: failed like vote, error: \(error)") + Self.logger.error("discussions interactor: failed like vote, error: \(error)") } } } func doCommentAbuse(request: Discussions.CommentAbuse.Request) { guard let comment = self.getAllComments().first(where: { $0.id == request.commentID }) else { - return DiscussionsInteractor.logger.error( + return Self.logger.error( "discussions interactor: unable abuse comment, can't find comment with id: \(request.commentID)" ) } @@ -312,12 +326,12 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { let voteValueToSet: VoteValue? = voteValue == .abuse ? nil : .abuse let vote = Vote(id: comment.vote.id, value: voteValueToSet) - DiscussionsInteractor.logger.info( + Self.logger.info( "discussions interactor: start abusing vote from \(voteValue) to \(voteValueToSet ??? "null")" ) self.provider.updateVote(vote).done { vote in - DiscussionsInteractor.logger.info("discussions interactor: finish abusing vote") + Self.logger.info("discussions interactor: finish abusing vote") comment.vote = vote @@ -333,15 +347,15 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { }.ensure { self.presenter.presentCommentAbuse(response: .init(result: self.makeDiscussionsData())) }.catch { error in - DiscussionsInteractor.logger.error("discussions interactor: failed abuse vote, error: \(error)") + Self.logger.error("discussions interactor: failed abuse vote, error: \(error)") } } else { - DiscussionsInteractor.logger.info("discussions interactor: start abusing vote to abuse value") + Self.logger.info("discussions interactor: start abusing vote to abuse value") let vote = Vote(id: comment.vote.id, value: .abuse) self.provider.updateVote(vote).done { vote in - DiscussionsInteractor.logger.info("discussions interactor: finish abusing vote") + Self.logger.info("discussions interactor: finish abusing vote") AnalyticsReporter.reportEvent(AnalyticsEvents.Discussion.abused) comment.vote = vote @@ -349,7 +363,7 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { }.ensure { self.presenter.presentCommentAbuse(response: .init(result: self.makeDiscussionsData())) }.catch { error in - DiscussionsInteractor.logger.error("discussions interactor: failed abuse vote, error: \(error)") + Self.logger.error("discussions interactor: failed abuse vote, error: \(error)") } } } @@ -373,6 +387,26 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { self.presenter.presentSortTypeUpdate(response: .init(result: self.makeDiscussionsData())) } + // MARK: Solution + + func doSolutionPresentation(request: Discussions.SolutionPresentation.Request) { + guard let comment = self.getAllComments().first(where: { $0.id == request.commentID }) else { + return Self.logger.error( + "discussions interactor: unable present solution, can't find comment with id: \(request.commentID)" + ) + } + + guard let submission = comment.submission else { + return Self.logger.error( + "discussions interactor: unable present solution, no submission: \(request.commentID)" + ) + } + + self.presenter.presentSolution( + response: .init(stepID: self.stepID, submission: submission, discussionID: self.discussionProxyID) + ) + } + // MARK: - Private API // MARK: Fetching helpers @@ -396,7 +430,7 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { return .value(self.getNextDiscussionsIDsToLoad(direction: nil)) } }.then { ids -> Promise<[Comment]> in - self.provider.fetchComments(ids: ids) + self.provider.fetchComments(ids: ids, stepID: self.stepID) }.done { fetchedComments in self.updateDataWithNewComments(fetchedComments) seal.fulfill(self.makeDiscussionsData()) @@ -537,7 +571,7 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { let endIndex = discussionsWindow.startIndex let offset = min( self.getDiscussionsLeftToLoadInLeftHalfCount(discussionsWindow: discussionsWindow), - DiscussionsInteractor.discussionsLoadingInterval + Self.discussionsLoadingInterval ) let startIndex = max(endIndex - offset, 0) @@ -549,7 +583,7 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { let startIndex = discussionsWindow.endIndex == 0 ? 0 : discussionsWindow.endIndex + 1 let offset = min( self.getDiscussionsLeftToLoadInRightHalfCount(discussionsWindow: discussionsWindow), - DiscussionsInteractor.discussionsLoadingInterval + Self.discussionsLoadingInterval ) let endIndex = startIndex + offset @@ -561,7 +595,7 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { return [] } - let loadingInterval = DiscussionsInteractor.discussionsLoadingInterval / 2 + let loadingInterval = Self.discussionsLoadingInterval / 2 let leftOffset = min( self.getDiscussionsLeftToLoadInLeftHalfCount(discussionsWindow: discussionsWindow), loadingInterval @@ -591,7 +625,7 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { for replyID in discussion.repliesIDs { if !loadedIDs.contains(replyID) { idsToLoad.append(replyID) - if idsToLoad.count == DiscussionsInteractor.repliesLoadingInterval { + if idsToLoad.count == Self.repliesLoadingInterval { return idsToLoad } } @@ -644,9 +678,7 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { }() guard let unwrappedComment = comment else { - return DiscussionsInteractor.logger.error( - "discussions interactor: unable edit comment, can't find it by id: \(commentID)" - ) + return Self.logger.error("discussions interactor: unable edit comment, can't find it by id: \(commentID)") } self.presenter.presentWriteComment( @@ -688,14 +720,14 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { // MARK: - DiscussionsInteractor: DiscussionsInputProtocol - -extension DiscussionsInteractor: DiscussionsInputProtocol { } +extension DiscussionsInteractor: DiscussionsInputProtocol {} // MARK: - DiscussionsInteractor: WriteCommentOutputProtocol - extension DiscussionsInteractor: WriteCommentOutputProtocol { func handleCommentCreated(_ comment: Comment) { if let parentID = comment.parentID, - let parentIndex = self.currentDiscussions.firstIndex(where: { $0.id == parentID }) { + let parentIndex = self.currentDiscussions.firstIndex(where: { $0.id == parentID }) { self.currentDiscussions[parentIndex].repliesIDs.append(comment.id) self.currentReplies[parentID, default: []].append(comment) @@ -732,22 +764,3 @@ extension DiscussionsInteractor: WriteCommentOutputProtocol { self.presenter.presentCommentUpdate(response: .init(result: self.makeDiscussionsData())) } } - -// MARK: - DiscussionsInteractor (UserDefaults) - - -// TODO: Move to service. -extension DiscussionsInteractor { - private static let discussionsSortTypeKey = "discussionsSortTypeKey" - - private var currentSortType: Discussions.SortType { - get { - if let sortTypeKey = UserDefaults.standard.string(forKey: DiscussionsInteractor.discussionsSortTypeKey) { - return Discussions.SortType(rawValue: sortTypeKey) ?? .last - } - return .last - } - set { - UserDefaults.standard.set(newValue.rawValue, forKey: DiscussionsInteractor.discussionsSortTypeKey) - } - } -} diff --git a/Stepic/Sources/Modules/Discussions/DiscussionsPresenter.swift b/Stepic/Sources/Modules/Discussions/DiscussionsPresenter.swift index 38a8759f03..40d34af56d 100644 --- a/Stepic/Sources/Modules/Discussions/DiscussionsPresenter.swift +++ b/Stepic/Sources/Modules/Discussions/DiscussionsPresenter.swift @@ -2,6 +2,7 @@ import Kanna import UIKit protocol DiscussionsPresenterProtocol { + func presentNavigationItemUpdate(response: Discussions.NavigationItemUpdate.Response) func presentDiscussions(response: Discussions.DiscussionsLoad.Response) func presentNextDiscussions(response: Discussions.NextDiscussionsLoad.Response) func presentNextReplies(response: Discussions.NextRepliesLoad.Response) @@ -14,12 +15,36 @@ protocol DiscussionsPresenterProtocol { func presentCommentAbuse(response: Discussions.CommentAbuse.Response) func presentSortTypes(response: Discussions.SortTypesPresentation.Response) func presentSortTypeUpdate(response: Discussions.SortTypeUpdate.Response) + func presentSolution(response: Discussions.SolutionPresentation.Response) func presentWaitingState(response: Discussions.BlockingWaitingIndicatorUpdate.Response) } final class DiscussionsPresenter: DiscussionsPresenterProtocol { weak var viewController: DiscussionsViewControllerProtocol? + func presentNavigationItemUpdate(response: Discussions.NavigationItemUpdate.Response) { + switch response.threadType { + case .default: + self.viewController?.displayNavigationItemUpdate( + viewModel: .init( + title: NSLocalizedString("DiscussionsTitle", comment: ""), + shouldShowSortButton: true, + shouldShowComposeButton: true, + threadType: .default + ) + ) + case .solutions: + self.viewController?.displayNavigationItemUpdate( + viewModel: .init( + title: NSLocalizedString("DiscussionThreadSolutionsTitle", comment: ""), + shouldShowSortButton: true, + shouldShowComposeButton: false, + threadType: .solutions + ) + ) + } + } + func presentDiscussions(response: Discussions.DiscussionsLoad.Response) { var viewModel: Discussions.DiscussionsLoad.ViewModel @@ -134,6 +159,16 @@ final class DiscussionsPresenter: DiscussionsPresenterProtocol { self.viewController?.displaySortTypeUpdate(viewModel: .init(data: self.makeDiscussionsData(response.result))) } + func presentSolution(response: Discussions.SolutionPresentation.Response) { + self.viewController?.displaySolution( + viewModel: .init( + stepID: response.stepID, + submission: response.submission, + discussionID: response.discussionID + ) + ) + } + func presentWaitingState(response: Discussions.BlockingWaitingIndicatorUpdate.Response) { self.viewController?.displayBlockingLoadingIndicator(viewModel: .init(shouldDismiss: response.shouldDismiss)) } @@ -234,6 +269,21 @@ final class DiscussionsPresenter: DiscussionsPresenterProtocol { }() let strippedAndTrimmedText = strippedText.trimmingCharacters(in: .whitespacesAndNewlines) + let solution: DiscussionsCommentViewModel.Solution? = { + guard let submission = comment.submission else { + return nil + } + + return .init( + id: submission.id, + title: String( + format: NSLocalizedString("DiscussionThreadCommentSolutionTitle", comment: ""), + arguments: ["\(submission.id)"] + ), + isCorrect: submission.isCorrect + ) + }() + return DiscussionsCommentViewModel( id: comment.id, avatarImageURL: avatarImageURL, @@ -252,7 +302,8 @@ final class DiscussionsPresenter: DiscussionsPresenterProtocol { canEdit: comment.actions.contains(.edit), canDelete: comment.actions.contains(.delete), canVote: comment.actions.contains(.vote), - hasReplies: hasReplies + hasReplies: hasReplies, + solution: solution ) } diff --git a/Stepic/Sources/Modules/Discussions/DiscussionsProvider.swift b/Stepic/Sources/Modules/Discussions/DiscussionsProvider.swift index 54adb1b940..a6eab04609 100644 --- a/Stepic/Sources/Modules/Discussions/DiscussionsProvider.swift +++ b/Stepic/Sources/Modules/Discussions/DiscussionsProvider.swift @@ -3,7 +3,7 @@ import PromiseKit protocol DiscussionsProviderProtocol { func fetchDiscussionProxy(id: DiscussionProxy.IdType) -> Promise - func fetchComments(ids: [Comment.IdType]) -> Promise<[Comment]> + func fetchComments(ids: [Comment.IdType], stepID: Step.IdType) -> Promise<[Comment]> func deleteComment(id: Comment.IdType) -> Promise func updateVote(_ vote: Vote) -> Promise func incrementStepDiscussionsCount(stepID: Step.IdType) -> Promise @@ -14,18 +14,20 @@ final class DiscussionsProvider: DiscussionsProviderProtocol { private let discussionProxiesNetworkService: DiscussionProxiesNetworkServiceProtocol private let commentsNetworkService: CommentsNetworkServiceProtocol private let votesNetworkService: VotesNetworkServiceProtocol - + private let stepsNetworkService: StepsNetworkServiceProtocol private let stepsPersistenceService: StepsPersistenceServiceProtocol init( discussionProxiesNetworkService: DiscussionProxiesNetworkServiceProtocol, commentsNetworkService: CommentsNetworkServiceProtocol, votesNetworkService: VotesNetworkServiceProtocol, + stepsNetworkService: StepsNetworkServiceProtocol, stepsPersistenceService: StepsPersistenceServiceProtocol ) { self.discussionProxiesNetworkService = discussionProxiesNetworkService self.commentsNetworkService = commentsNetworkService self.votesNetworkService = votesNetworkService + self.stepsNetworkService = stepsNetworkService self.stepsPersistenceService = stepsPersistenceService } @@ -39,10 +41,24 @@ final class DiscussionsProvider: DiscussionsProviderProtocol { } } - func fetchComments(ids: [Comment.IdType]) -> Promise<[Comment]> { + func fetchComments(ids: [Comment.IdType], stepID: Step.IdType) -> Promise<[Comment]> { Promise { seal in - self.commentsNetworkService.fetch(ids: ids).done { - seal.fulfill($0) + firstly { + self.stepsPersistenceService.fetch(ids: [stepID]) + }.then { cachedSteps -> Promise<[Step]> in + if cachedSteps.first != nil { + return .value(cachedSteps) + } + + return self.stepsNetworkService.fetch(ids: [stepID]) + }.then { steps -> Promise<[Comment]> in + guard let step = steps.first else { + throw Error.fetchFailed + } + + return self.commentsNetworkService.fetch(ids: ids, blockName: step.block.name) + }.done { comments in + seal.fulfill(comments) }.catch { _ in seal.reject(Error.fetchFailed) } diff --git a/Stepic/Sources/Modules/Discussions/DiscussionsViewController.swift b/Stepic/Sources/Modules/Discussions/DiscussionsViewController.swift index c9331d0ff1..7fcaaf36ba 100644 --- a/Stepic/Sources/Modules/Discussions/DiscussionsViewController.swift +++ b/Stepic/Sources/Modules/Discussions/DiscussionsViewController.swift @@ -3,6 +3,7 @@ import SVProgressHUD import UIKit protocol DiscussionsViewControllerProtocol: AnyObject { + func displayNavigationItemUpdate(viewModel: Discussions.NavigationItemUpdate.ViewModel) func displayDiscussions(viewModel: Discussions.DiscussionsLoad.ViewModel) func displayNextDiscussions(viewModel: Discussions.NextDiscussionsLoad.ViewModel) func displayNextReplies(viewModel: Discussions.NextRepliesLoad.ViewModel) @@ -13,6 +14,7 @@ protocol DiscussionsViewControllerProtocol: AnyObject { func displayCommentDelete(viewModel: Discussions.CommentDelete.ViewModel) func displayCommentLike(viewModel: Discussions.CommentLike.ViewModel) func displayCommentAbuse(viewModel: Discussions.CommentAbuse.ViewModel) + func displaySolution(viewModel: Discussions.SolutionPresentation.ViewModel) func displaySortTypesAlert(viewModel: Discussions.SortTypesPresentation.ViewModel) func displaySortTypeUpdate(viewModel: Discussions.SortTypeUpdate.ViewModel) func displayBlockingLoadingIndicator(viewModel: Discussions.BlockingWaitingIndicatorUpdate.ViewModel) @@ -22,6 +24,7 @@ protocol DiscussionsViewControllerProtocol: AnyObject { final class DiscussionsViewController: UIViewController, ControllerWithStepikPlaceholder { lazy var discussionsView = self.view as? DiscussionsView + lazy var styledNavigationController = self.navigationController as? StyledNavigationController var placeholderContainer = StepikPlaceholderControllerContainer() @@ -38,12 +41,16 @@ final class DiscussionsViewController: UIViewController, ControllerWithStepikPla return tableDataSource }() - private lazy var sortTypeBarButtonItem = UIBarButtonItem( - image: UIImage(named: "discussions-sort")?.withRenderingMode(.alwaysTemplate), - style: .plain, - target: self, - action: #selector(self.didClickSortType) - ) + private lazy var sortTypeBarButtonItem: UIBarButtonItem = { + let button = UIBarButtonItem( + image: UIImage(named: "discussions-sort")?.withRenderingMode(.alwaysTemplate), + style: .plain, + target: self, + action: #selector(self.didClickSortType) + ) + button.isEnabled = false + return button + }() private lazy var composeBarButtonItem = UIBarButtonItem( barButtonSystemItem: .compose, @@ -76,31 +83,18 @@ final class DiscussionsViewController: UIViewController, ControllerWithStepikPla override func viewDidLoad() { super.viewDidLoad() - self.configureNavigationBar() - self.registerPlaceholders() - self.updateState(newState: self.state) self.interactor.doDiscussionsLoad(request: .init()) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - - if let styledNavigationController = self.navigationController as? StyledNavigationController { - styledNavigationController.changeShadowViewAlpha(1.0, sender: self) - } + self.styledNavigationController?.changeShadowViewAlpha(1.0, sender: self) } // MARK: - Private API - private func configureNavigationBar() { - self.title = NSLocalizedString("DiscussionsTitle", comment: "") - - self.navigationItem.rightBarButtonItems = [self.composeBarButtonItem, self.sortTypeBarButtonItem] - self.sortTypeBarButtonItem.isEnabled = false - } - - private func registerPlaceholders() { + private func registerPlaceholders(for discussionThreadType: DiscussionThread.ThreadType) { self.registerPlaceholder( placeholder: StepikPlaceholder( .noConnection, @@ -115,15 +109,24 @@ final class DiscussionsViewController: UIViewController, ControllerWithStepikPla ), for: .connectionError ) - self.registerPlaceholder( - placeholder: StepikPlaceholder( - .emptyDiscussions, - action: { [weak self] in - self?.didClickWriteComment() - } - ), - for: .empty - ) + + switch discussionThreadType { + case .default: + self.registerPlaceholder( + placeholder: StepikPlaceholder( + .emptyDiscussions, + action: { [weak self] in + self?.didClickWriteComment() + } + ), + for: .empty + ) + case .solutions: + self.registerPlaceholder( + placeholder: StepikPlaceholder(.emptySolutions, action: nil), + for: .empty + ) + } } private func updateState(newState: Discussions.ViewControllerState) { @@ -204,6 +207,22 @@ final class DiscussionsViewController: UIViewController, ControllerWithStepikPla // MARK: - DiscussionsViewController: DiscussionsViewControllerProtocol - extension DiscussionsViewController: DiscussionsViewControllerProtocol { + func displayNavigationItemUpdate(viewModel: Discussions.NavigationItemUpdate.ViewModel) { + self.title = viewModel.title + + self.registerPlaceholders(for: viewModel.threadType) + + var rightBarButtonItems = [UIBarButtonItem]() + if viewModel.shouldShowComposeButton { + rightBarButtonItems.append(self.composeBarButtonItem) + } + if viewModel.shouldShowSortButton { + rightBarButtonItems.append(self.sortTypeBarButtonItem) + } + + self.navigationItem.rightBarButtonItems = rightBarButtonItems.isEmpty ? nil : rightBarButtonItems + } + func displayDiscussions(viewModel: Discussions.DiscussionsLoad.ViewModel) { self.updateState(newState: viewModel.state) } @@ -302,6 +321,15 @@ extension DiscussionsViewController: DiscussionsViewControllerProtocol { self.updateDiscussionsData(newData: viewModel.data) } + func displaySolution(viewModel: Discussions.SolutionPresentation.ViewModel) { + let assembly = SolutionAssembly( + stepID: viewModel.stepID, + submission: viewModel.submission, + discussionID: viewModel.discussionID + ) + self.push(module: assembly.makeModule()) + } + func displaySortTypesAlert(viewModel: Discussions.SortTypesPresentation.ViewModel) { let alert = UIAlertController(title: viewModel.title, message: nil, preferredStyle: .actionSheet) @@ -392,6 +420,13 @@ extension DiscussionsViewController: DiscussionsTableViewDataSourceDelegate { self.push(module: assembly.makeModule()) } + func discussionsTableViewDataSource( + _ tableViewDataSource: DiscussionsTableViewDataSource, + didSelectSolution comment: DiscussionsCommentViewModel + ) { + self.interactor.doSolutionPresentation(request: .init(commentID: comment.id)) + } + func discussionsTableViewDataSource( _ tableViewDataSource: DiscussionsTableViewDataSource, didRequestOpenURL url: URL @@ -436,6 +471,18 @@ extension DiscussionsViewController: DiscussionsTableViewDataSourceDelegate { ) { let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + if viewModel.solution != nil { + alert.addAction( + UIAlertAction( + title: NSLocalizedString("DiscussionsAlertActionShowSolutionTitle", comment: ""), + style: .default, + handler: { [weak self] _ in + self?.interactor.doSolutionPresentation(request: .init(commentID: viewModel.id)) + } + ) + ) + } + alert.addAction( UIAlertAction( title: NSLocalizedString("Copy", comment: ""), @@ -446,19 +493,21 @@ extension DiscussionsViewController: DiscussionsTableViewDataSourceDelegate { ) ) - alert.addAction( - UIAlertAction( - title: NSLocalizedString("Reply", comment: ""), - style: .default, - handler: { [weak self] _ in - self?.interactor.doWriteCommentPresentation( - request: .init(commentID: viewModel.id, presentationContext: .create) - ) - } + if viewModel.solution == nil { + alert.addAction( + UIAlertAction( + title: NSLocalizedString("Reply", comment: ""), + style: .default, + handler: { [weak self] _ in + self?.interactor.doWriteCommentPresentation( + request: .init(commentID: viewModel.id, presentationContext: .create) + ) + } + ) ) - ) + } - if viewModel.canEdit { + if viewModel.canEdit && viewModel.solution == nil { alert.addAction( UIAlertAction( title: NSLocalizedString("DiscussionsAlertActionEditTitle", comment: ""), diff --git a/Stepic/Sources/Modules/Discussions/DiscussionsViewModel.swift b/Stepic/Sources/Modules/Discussions/DiscussionsViewModel.swift index 44c099d5af..f4d730b988 100644 --- a/Stepic/Sources/Modules/Discussions/DiscussionsViewModel.swift +++ b/Stepic/Sources/Modules/Discussions/DiscussionsViewModel.swift @@ -30,4 +30,11 @@ struct DiscussionsCommentViewModel { let canDelete: Bool let canVote: Bool let hasReplies: Bool + let solution: Solution? + + struct Solution { + let id: Submission.IdType + let title: String + let isCorrect: Bool + } } diff --git a/Stepic/Sources/Modules/Discussions/Views/Cell/DiscussionsCellView.swift b/Stepic/Sources/Modules/Discussions/Views/Cell/DiscussionsCellView.swift index 65c8bfcbe3..4770daf583 100644 --- a/Stepic/Sources/Modules/Discussions/Views/Cell/DiscussionsCellView.swift +++ b/Stepic/Sources/Modules/Discussions/Views/Cell/DiscussionsCellView.swift @@ -1,6 +1,8 @@ import SnapKit import UIKit +// swiftlint:disable file_length + // MARK: Appearance - extension DiscussionsCellView { @@ -14,7 +16,7 @@ extension DiscussionsCellView { let badgeCornerRadius: CGFloat = 10 let badgeUserRoleWidthDelta: CGFloat = 16 - let badgeUserRoleBackgroundColor = UIColor.stepicGreen + let badgeUserRoleBackgroundColor = UIColor.stepikGreen let badgeIsPinnedBackgroundColor = UIColor(hex: 0x6C7BDF) let badgeIsPinnedImageSize = CGSize(width: 10, height: 10) @@ -36,6 +38,9 @@ extension DiscussionsCellView { let textContentTextLabelFont = UIFont.systemFont(ofSize: 14) let textContentTextLabelTextColor = UIColor.mainDark + let solutionControlHeight = DiscussionsSolutionControl.Appearance.height + let solutionControlInsets = LayoutInsets(top: 8) + let bottomControlsSpacing: CGFloat = 16 let bottomControlsSubgroupSpacing: CGFloat = 8 let bottomControlsInsets = LayoutInsets(top: 8, left: 16, bottom: 16, right: 16) @@ -72,7 +77,7 @@ final class DiscussionsCellView: UIView { private lazy var avatarOverlayButton: UIButton = { let button = HighlightFakeButton() button.highlightedBackgroundColor = UIColor.white.withAlphaComponent(0.5) - button.addTarget(self, action: #selector(self.avatarOverlayButtonClicked), for: .touchUpInside) + button.addTarget(self, action: #selector(self.avatarButtonClicked), for: .touchUpInside) return button }() @@ -156,13 +161,31 @@ final class DiscussionsCellView: UIView { }() private lazy var textContentStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [self.textContentWebBasedTextView, self.textContentTextLabel]) + let stackView = UIStackView( + arrangedSubviews: [ + self.textContentWebBasedTextView, + self.textContentTextLabel, + self.solutionContainerView + ] + ) stackView.axis = .vertical stackView.setContentHuggingPriority(.defaultHigh, for: .vertical) stackView.setContentCompressionResistancePriority(.defaultLow, for: .vertical) return stackView }() + private lazy var solutionControl: DiscussionsSolutionControl = { + let control = DiscussionsSolutionControl() + control.addTarget(self, action: #selector(self.solutionControlClicked), for: .touchUpInside) + return control + }() + + private lazy var solutionContainerView: UIView = { + let view = UIView() + view.isHidden = true + return view + }() + private lazy var dateLabel: UILabel = { let label = UILabel() label.font = self.appearance.dateLabelFont @@ -177,7 +200,7 @@ final class DiscussionsCellView: UIView { button.titleLabel?.font = self.appearance.replyButtonFont button.setTitleColor(self.appearance.replyButtonTextColor, for: .normal) button.setTitle(NSLocalizedString("Reply", comment: ""), for: .normal) - button.addTarget(self, action: #selector(self.replyDidClick), for: .touchUpInside) + button.addTarget(self, action: #selector(self.replyButtonClicked), for: .touchUpInside) return button }() @@ -189,7 +212,7 @@ final class DiscussionsCellView: UIView { imageButton.title = "0" imageButton.image = UIImage(named: "discussions-thumb-up")?.withRenderingMode(.alwaysTemplate) imageButton.titleInsets = self.appearance.voteLikeButtonTitleInsets - imageButton.addTarget(self, action: #selector(self.likeDidClick), for: .touchUpInside) + imageButton.addTarget(self, action: #selector(self.likeImageButtonClicked), for: .touchUpInside) return imageButton }() @@ -201,7 +224,7 @@ final class DiscussionsCellView: UIView { imageButton.title = "0" imageButton.image = UIImage(named: "discussions-thumb-down")?.withRenderingMode(.alwaysTemplate) imageButton.titleInsets = self.appearance.voteDislikeButtonTitleInsets - imageButton.addTarget(self, action: #selector(self.dislikeDidClick), for: .touchUpInside) + imageButton.addTarget(self, action: #selector(self.dislikeImageButtonClicked), for: .touchUpInside) return imageButton }() @@ -247,6 +270,7 @@ final class DiscussionsCellView: UIView { var onLinkClick: ((URL) -> Void)? var onImageClick: ((URL) -> Void)? var onTextContentClick: (() -> Void)? + var onSolutionClick: (() -> Void)? // Content height updates callbacks var onContentLoaded: (() -> Void)? var onNewHeightUpdate: (() -> Void)? @@ -287,16 +311,34 @@ final class DiscussionsCellView: UIView { if let url = viewModel.avatarImageURL { self.avatarImageView.set(with: url) } + + if let solution = viewModel.solution { + self.solutionControl.update( + state: solution.isCorrect ? .correct : .wrong, + title: solution.title + ) + self.solutionContainerView.isHidden = false + } else { + self.solutionContainerView.isHidden = true + } + + self.replyButton.isHidden = viewModel.solution != nil } func calculateContentHeight(maxPreferredWidth: CGFloat) -> CGFloat { let userInfoHeight = (self.isBadgesHidden ? 0 : self.appearance.badgesStackViewHeight) + (self.isBadgesHidden ? 0 : self.appearance.nameLabelInsets.top) + self.appearance.nameLabelHeight + + let solutionHeight = self.solutionContainerView.isHidden + ? 0 + : self.appearance.solutionControlInsets.top + self.appearance.solutionControlHeight + return self.appearance.avatarImageViewInsets.top + userInfoHeight + self.appearance.textContentContainerViewInsets.top + self.getTextContentHeight(maxPreferredWidth: maxPreferredWidth) + + solutionHeight + self.appearance.bottomControlsInsets.top + self.appearance.bottomControlsHeight + self.appearance.bottomControlsInsets.bottom @@ -403,25 +445,30 @@ final class DiscussionsCellView: UIView { // MARK: Actions @objc - private func replyDidClick() { + private func replyButtonClicked() { self.onReplyClick?() } @objc - private func likeDidClick() { + private func likeImageButtonClicked() { self.onLikeClick?() } @objc - private func dislikeDidClick() { + private func dislikeImageButtonClicked() { self.onDislikeClick?() } @objc - private func avatarOverlayButtonClicked() { + private func avatarButtonClicked() { self.onAvatarClick?() } + @objc + private func solutionControlClicked() { + self.onSolutionClick?() + } + @objc private func textContentWebBasedTextViewClicked(_ sender: UITapGestureRecognizer) { let workItem = DispatchWorkItem { [weak self] in @@ -438,7 +485,7 @@ final class DiscussionsCellView: UIView { self.pendingTextViewClickWorkItem = workItem DispatchQueue.main.asyncAfter( - deadline: .now() + DiscussionsCellView.processedContentTextViewClickDelay, + deadline: .now() + Self.processedContentTextViewClickDelay, execute: workItem ) } @@ -453,6 +500,7 @@ extension DiscussionsCellView: ProgrammaticallyInitializableViewProtocol { self.addSubview(self.badgesStackView) self.addSubview(self.nameLabel) self.addSubview(self.textContentStackView) + self.solutionContainerView.addSubview(self.solutionControl) self.addSubview(self.bottomControlsStackView) } @@ -497,6 +545,13 @@ extension DiscussionsCellView: ProgrammaticallyInitializableViewProtocol { .offset(-self.appearance.textContentContainerViewInsets.bottom) } + self.solutionControl.translatesAutoresizingMaskIntoConstraints = false + self.solutionControl.snp.makeConstraints { make in + make.top.equalToSuperview().offset(self.appearance.solutionControlInsets.top) + make.leading.bottom.trailing.equalToSuperview() + make.height.equalTo(self.appearance.solutionControlHeight) + } + self.bottomControlsStackView.translatesAutoresizingMaskIntoConstraints = false self.bottomControlsStackView.snp.makeConstraints { make in make.leading.equalTo(self.avatarImageView.snp.trailing).offset(self.appearance.bottomControlsInsets.left) @@ -552,7 +607,7 @@ extension DiscussionsCellView: ProcessedContentTextViewDelegate { } private func asyncResetClickOnLinkOrImage() { - DispatchQueue.main.asyncAfter(deadline: .now() + DiscussionsCellView.resetClickOnLinkOrImageDelay) { + DispatchQueue.main.asyncAfter(deadline: .now() + Self.resetClickOnLinkOrImageDelay) { self.didClickOnLinkOrImage = false } } diff --git a/Stepic/Sources/Modules/Discussions/Views/Cell/DiscussionsTableViewCell.swift b/Stepic/Sources/Modules/Discussions/Views/Cell/DiscussionsTableViewCell.swift index 191a96c23b..2adb466a32 100644 --- a/Stepic/Sources/Modules/Discussions/Views/Cell/DiscussionsTableViewCell.swift +++ b/Stepic/Sources/Modules/Discussions/Views/Cell/DiscussionsTableViewCell.swift @@ -42,6 +42,9 @@ final class DiscussionsTableViewCell: UITableViewCell, Reusable { cellView.onTextContentClick = { [weak self] in self?.onTextContentClick?() } + cellView.onSolutionClick = { [weak self] in + self?.onSolutionClick?() + } cellView.onContentLoaded = { [weak self] in self?.onContentLoaded?() } @@ -77,6 +80,7 @@ final class DiscussionsTableViewCell: UITableViewCell, Reusable { var onLinkClick: ((URL) -> Void)? var onImageClick: ((URL) -> Void)? var onTextContentClick: (() -> Void)? + var onSolutionClick: (() -> Void)? // Content callbacks var onContentLoaded: (() -> Void)? var onNewHeightUpdate: ((CGFloat) -> Void)? diff --git a/Stepic/Sources/Modules/Discussions/Views/DiscussionsSolutionControl.swift b/Stepic/Sources/Modules/Discussions/Views/DiscussionsSolutionControl.swift new file mode 100644 index 0000000000..24764cd56f --- /dev/null +++ b/Stepic/Sources/Modules/Discussions/Views/DiscussionsSolutionControl.swift @@ -0,0 +1,150 @@ +import SnapKit +import UIKit + +extension DiscussionsSolutionControl { + struct Appearance { + static let height: CGFloat = 40 + + let cornerRadius: CGFloat = 6 + let borderWidth: CGFloat = 1 + let borderColor = UIColor(hex: 0xCCCCCC) + + let iconSize = CGSize(width: 24, height: 24) + let iconInsets = LayoutInsets(top: 8, left: 8, bottom: 8, right: 8) + + let titleTextColor = UIColor.mainDark + let titleFont = UIFont.systemFont(ofSize: 14) + let titleInsets = LayoutInsets(top: 8, left: 8, bottom: 8, right: 8) + } +} + +final class DiscussionsSolutionControl: UIControl { + let appearance: Appearance + + private lazy var borderLayer: CAShapeLayer = { + let borderLayer = CAShapeLayer() + borderLayer.lineWidth = self.appearance.borderWidth + borderLayer.strokeColor = self.appearance.borderColor.cgColor + borderLayer.fillColor = UIColor.clear.cgColor + return borderLayer + }() + + private lazy var imageView = UIImageView() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.textColor = self.appearance.titleTextColor + label.font = self.appearance.titleFont + label.numberOfLines = 1 + return label + }() + + override var isHighlighted: Bool { + didSet { + self.imageView.alpha = self.isHighlighted ? 0.5 : 1.0 + self.titleLabel.alpha = self.isHighlighted ? 0.5 : 1.0 + } + } + + 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") + } + + override func layoutSubviews() { + super.layoutSubviews() + + DispatchQueue.main.async { + self.updateCorners() + } + } + + func update(state: SolutionState, title: String?) { + self.imageView.image = state.icon?.withRenderingMode(.alwaysTemplate) + self.imageView.contentMode = .scaleAspectFit + self.imageView.tintColor = state.tintColor + + self.titleLabel.text = title + } + + private func updateCorners() { + let path = UIBezierPath( + roundedRect: self.bounds, + byRoundingCorners: .allCorners, + cornerRadii: CGSize(width: self.appearance.cornerRadius, height: self.appearance.cornerRadius) + ) + let mask = CAShapeLayer() + mask.path = path.cgPath + self.layer.mask = mask + + let borderPath = UIBezierPath( + roundedRect: self.bounds, + byRoundingCorners: .allCorners, + cornerRadii: CGSize(width: self.appearance.cornerRadius, height: self.appearance.cornerRadius) + ) + self.borderLayer.path = borderPath.cgPath + self.borderLayer.frame = self.bounds + } + + // MARK: Types + + enum SolutionState { + case correct + case wrong + + var tintColor: UIColor { + switch self { + case .correct: + return UIColor(hex: 0x66CC66) + case .wrong: + return UIColor(hex: 0xFF7965) + } + } + + var icon: UIImage? { + switch self { + case .correct: + return UIImage(named: "quiz-mark-correct") + case .wrong: + return UIImage(named: "quiz-mark-wrong") + } + } + } +} + +extension DiscussionsSolutionControl: ProgrammaticallyInitializableViewProtocol { + func setupView() { + self.layer.addSublayer(self.borderLayer) + } + + func addSubviews() { + self.addSubview(self.imageView) + self.addSubview(self.titleLabel) + } + + func makeConstraints() { + self.imageView.translatesAutoresizingMaskIntoConstraints = false + self.imageView.snp.makeConstraints { make in + make.size.equalTo(self.appearance.iconSize) + make.top.equalToSuperview().offset(self.appearance.iconInsets.top) + make.leading.equalToSuperview().offset(self.appearance.iconInsets.left) + make.bottom.equalToSuperview().offset(-self.appearance.iconInsets.bottom) + } + + self.titleLabel.translatesAutoresizingMaskIntoConstraints = false + self.titleLabel.snp.makeConstraints { make in + make.centerY.equalTo(self.imageView.snp.centerY) + make.leading.equalTo(self.imageView.snp.trailing).offset(self.appearance.titleInsets.left) + make.trailing.equalToSuperview().offset(-self.appearance.titleInsets.right) + } + } +} diff --git a/Stepic/Sources/Modules/Discussions/Views/DiscussionsTableViewDataSource.swift b/Stepic/Sources/Modules/Discussions/Views/DiscussionsTableViewDataSource.swift index 8a55c1a68c..cf90486333 100644 --- a/Stepic/Sources/Modules/Discussions/Views/DiscussionsTableViewDataSource.swift +++ b/Stepic/Sources/Modules/Discussions/Views/DiscussionsTableViewDataSource.swift @@ -19,6 +19,10 @@ protocol DiscussionsTableViewDataSourceDelegate: AnyObject { _ tableViewDataSource: DiscussionsTableViewDataSource, didSelectAvatar comment: DiscussionsCommentViewModel ) + func discussionsTableViewDataSource( + _ tableViewDataSource: DiscussionsTableViewDataSource, + didSelectSolution comment: DiscussionsCommentViewModel + ) func discussionsTableViewDataSource( _ tableViewDataSource: DiscussionsTableViewDataSource, didSelectLoadMoreRepliesForDiscussion discussion: DiscussionsDiscussionViewModel @@ -105,7 +109,7 @@ extension DiscussionsTableViewDataSource: UITableViewDataSource { private func numberOfRowsInSection(_ section: Int) -> Int { self.viewModels[section].replies.count - + DiscussionsTableViewDataSource.parentDiscussionInset + + Self.parentDiscussionInset + self.loadMoreRepliesInset(section: section) } @@ -137,10 +141,10 @@ extension DiscussionsTableViewDataSource: UITableViewDataSource { let discussionViewModel = self.viewModels[indexPath.section] let commentType: DiscussionsTableViewCell.ViewModel.CommentType = - indexPath.row == DiscussionsTableViewDataSource.parentDiscussionRowIndex ? .discussion : .reply + indexPath.row == Self.parentDiscussionRowIndex ? .discussion : .reply let commentViewModel = commentType == .discussion ? discussionViewModel.comment - : discussionViewModel.replies[indexPath.row - DiscussionsTableViewDataSource.parentDiscussionInset] + : discussionViewModel.replies[indexPath.row - Self.parentDiscussionInset] let commentID = commentViewModel.id cell.onContentLoaded = { [weak self, weak cell, weak tableView] in @@ -190,6 +194,11 @@ extension DiscussionsTableViewDataSource: UITableViewDataSource { strongTableView.delegate?.tableView?(strongTableView, didSelectRowAt: indexPath) } } + cell.onSolutionClick = { [weak self] in + if let strongSelf = self { + strongSelf.delegate?.discussionsTableViewDataSource(strongSelf, didSelectSolution: commentViewModel) + } + } let separatorStyle: DiscussionsTableViewCell.ViewModel.SeparatorStyle = { if indexPath.row == tableView.numberOfRows(inSection: indexPath.section) - 1 { @@ -240,7 +249,7 @@ extension DiscussionsTableViewDataSource: UITableViewDataSource { self.pendingTableViewUpdateWorkItem = workItem DispatchQueue.main.asyncAfter( - deadline: .now() + DiscussionsTableViewDataSource.tableViewUpdatesDelay, + deadline: .now() + Self.tableViewUpdatesDelay, execute: workItem ) } @@ -259,7 +268,7 @@ extension DiscussionsTableViewDataSource: UITableViewDelegate { } guard let comment = self.getCommentViewModel(at: indexPath) else { - return DiscussionsTableViewDataSource.estimatedRowHeight + return Self.estimatedRowHeight } if let cellHeight = self.cellHeightByCommentID[comment.id] { @@ -277,7 +286,7 @@ extension DiscussionsTableViewDataSource: UITableViewDelegate { return cellHeight } - return DiscussionsTableViewDataSource.estimatedRowHeight + return Self.estimatedRowHeight }() return height.rounded(.up) @@ -316,13 +325,12 @@ extension DiscussionsTableViewDataSource: UITableViewDelegate { // MARK: Private helpers private func getCommentViewModel(at indexPath: IndexPath) -> DiscussionsCommentViewModel? { - if indexPath.row == DiscussionsTableViewDataSource.parentDiscussionRowIndex { + if indexPath.row == Self.parentDiscussionRowIndex { return self.viewModels[safe: indexPath.section]?.comment } - return self.viewModels[safe: indexPath.section]?.replies[ - safe: indexPath.row - DiscussionsTableViewDataSource.parentDiscussionInset - ] + return self.viewModels[safe: indexPath.section]? + .replies[safe: indexPath.row - Self.parentDiscussionInset] } private func getDiscussionPrototypeCell(tableView: UITableView) -> DiscussionsTableViewCell { @@ -330,12 +338,12 @@ extension DiscussionsTableViewDataSource: UITableViewDelegate { return discussionPrototypeCell } - let dequeuedReusableCell = tableView.dequeueReusableCell( + let reusableCell = tableView.dequeueReusableCell( withIdentifier: DiscussionsTableViewCell.defaultReuseIdentifier ) as? DiscussionsTableViewCell - dequeuedReusableCell?.updateConstraintsIfNeeded() + reusableCell?.updateConstraintsIfNeeded() - self.discussionPrototypeCell = dequeuedReusableCell + self.discussionPrototypeCell = reusableCell return self.discussionPrototypeCell.require() } diff --git a/Stepic/Sources/Modules/Lesson/LessonPresenter.swift b/Stepic/Sources/Modules/Lesson/LessonPresenter.swift index d4ea50a8cc..ba55308e33 100644 --- a/Stepic/Sources/Modules/Lesson/LessonPresenter.swift +++ b/Stepic/Sources/Modules/Lesson/LessonPresenter.swift @@ -130,7 +130,7 @@ final class LessonPresenter: LessonPresenterProtocol { lessonTitle: lessonTitle, steps: steps, stepLinkMaker: { - "\(StepicApplicationsInfo.stepicURL)/lesson/\(lesson.id)/step/\($0)?from_mobile_app=true" + "\(StepikApplicationsInfo.stepikURL)/lesson/\(lesson.id)/step/\($0)?from_mobile_app=true" }, startStepIndex: startStepIndex ) diff --git a/Stepic/Sources/Modules/Quizzes/BaseQuiz/BaseQuizInteractor.swift b/Stepic/Sources/Modules/Quizzes/BaseQuiz/BaseQuizInteractor.swift index 0391737c83..7b38e6c8ed 100644 --- a/Stepic/Sources/Modules/Quizzes/BaseQuiz/BaseQuizInteractor.swift +++ b/Stepic/Sources/Modules/Quizzes/BaseQuiz/BaseQuizInteractor.swift @@ -135,6 +135,8 @@ final class BaseQuizInteractor: BaseQuizInteractorProtocol { self.presentSubmission(attempt: attempt, submission: submission, cachedReply: reply) + self.moduleOutput?.handleSubmissionEvaluated() + if submission.status == "correct" { self.moduleOutput?.handleCorrectSubmission() diff --git a/Stepic/Sources/Modules/Quizzes/BaseQuiz/BaseQuizPresenter.swift b/Stepic/Sources/Modules/Quizzes/BaseQuiz/BaseQuizPresenter.swift index 990591fefc..7b0daa37cb 100644 --- a/Stepic/Sources/Modules/Quizzes/BaseQuiz/BaseQuizPresenter.swift +++ b/Stepic/Sources/Modules/Quizzes/BaseQuiz/BaseQuizPresenter.swift @@ -213,7 +213,7 @@ final class BaseQuizPresenter: BaseQuizPresenterProtocol { } private func makeURL(for step: Step) -> URL { - let link = "\(StepicApplicationsInfo.stepicURL)/lesson/\(step.lessonID)/step/\(step.position)?from_mobile_app=true" + let link = "\(StepikApplicationsInfo.stepikURL)/lesson/\(step.lessonID)/step/\(step.position)?from_mobile_app=true" guard let url = URL(string: link) else { fatalError("Invalid step link") } diff --git a/Stepic/Sources/Modules/Quizzes/BaseQuiz/BaseQuizView.swift b/Stepic/Sources/Modules/Quizzes/BaseQuiz/BaseQuizView.swift index c58e044f8e..31632ec1c1 100644 --- a/Stepic/Sources/Modules/Quizzes/BaseQuiz/BaseQuizView.swift +++ b/Stepic/Sources/Modules/Quizzes/BaseQuiz/BaseQuizView.swift @@ -11,7 +11,7 @@ protocol BaseQuizViewDelegate: AnyObject { extension BaseQuizView { struct Appearance { - let submitButtonBackgroundColor = UIColor.stepicGreen + let submitButtonBackgroundColor = UIColor.stepikGreen let submitButtonHeight: CGFloat = 44 let submitButtonTextColor = UIColor.white let submitButtonCornerRadius: CGFloat = 6 diff --git a/Stepic/Sources/Modules/Quizzes/BaseQuiz/BaseQuizOutputProtocol.swift b/Stepic/Sources/Modules/Quizzes/BaseQuiz/InputOutput/BaseQuizOutputProtocol.swift similarity index 78% rename from Stepic/Sources/Modules/Quizzes/BaseQuiz/BaseQuizOutputProtocol.swift rename to Stepic/Sources/Modules/Quizzes/BaseQuiz/InputOutput/BaseQuizOutputProtocol.swift index 887604c0c9..b17efa5a0f 100644 --- a/Stepic/Sources/Modules/Quizzes/BaseQuiz/BaseQuizOutputProtocol.swift +++ b/Stepic/Sources/Modules/Quizzes/BaseQuiz/InputOutput/BaseQuizOutputProtocol.swift @@ -2,5 +2,6 @@ import Foundation protocol BaseQuizOutputProtocol: AnyObject { func handleCorrectSubmission() + func handleSubmissionEvaluated() func handleNextStepNavigation() } diff --git a/Stepic/Sources/Modules/Quizzes/NewCodeQuizFullscreen/Tabs/NewCodeQuizFullscreenCodeViewController.swift b/Stepic/Sources/Modules/Quizzes/NewCodeQuizFullscreen/Tabs/NewCodeQuizFullscreenCodeViewController.swift index cd24e54a56..6ba08ff096 100644 --- a/Stepic/Sources/Modules/Quizzes/NewCodeQuizFullscreen/Tabs/NewCodeQuizFullscreenCodeViewController.swift +++ b/Stepic/Sources/Modules/Quizzes/NewCodeQuizFullscreen/Tabs/NewCodeQuizFullscreenCodeViewController.swift @@ -15,7 +15,7 @@ protocol NewCodeQuizFullscreenCodeViewControllerDelegate: AnyObject { extension NewCodeQuizFullscreenCodeViewController { enum Appearance { - static let submitButtonBackgroundColor = UIColor.stepicGreen + static let submitButtonBackgroundColor = UIColor.stepikGreen static let submitButtonHeight: CGFloat = 44 static let submitButtonTextColor = UIColor.white static let submitButtonCornerRadius: CGFloat = 6 diff --git a/Stepic/Sources/Modules/Solution/SolutionAssembly.swift b/Stepic/Sources/Modules/Solution/SolutionAssembly.swift index 2f4594e7f9..b4c3527685 100644 --- a/Stepic/Sources/Modules/Solution/SolutionAssembly.swift +++ b/Stepic/Sources/Modules/Solution/SolutionAssembly.swift @@ -2,31 +2,32 @@ import UIKit final class SolutionAssembly: Assembly { private let stepID: Step.IdType - private let submissionID: Submission.IdType + private let submission: Submission + private let discussionID: DiscussionThread.IdType - init(stepID: Step.IdType, submissionID: Submission.IdType) { + init(stepID: Step.IdType, submission: Submission, discussionID: DiscussionThread.IdType) { self.stepID = stepID - self.submissionID = submissionID + self.submission = submission + self.discussionID = discussionID } func makeModule() -> UIViewController { let provider = SolutionProvider( stepsPersistenceService: StepsPersistenceService(), - stepsNetworkService: StepsNetworkService(stepsAPI: StepsAPI()), - submissionsNetworkService: SubmissionsNetworkService(submissionsAPI: SubmissionsAPI()), - attemptsNetworkService: AttemptsNetworkService(attemptsAPI: AttemptsAPI()) + stepsNetworkService: StepsNetworkService(stepsAPI: StepsAPI()) ) let presenter = SolutionPresenter() let interactor = SolutionInteractor( stepID: self.stepID, - submissionID: self.submissionID, + submission: self.submission, + discussionID: self.discussionID, presenter: presenter, provider: provider ) let viewController = SolutionViewController(interactor: interactor) viewController.title = String( format: NSLocalizedString("SolutionTitle", comment: ""), - arguments: ["\(self.submissionID)"] + arguments: ["\(self.submission.id)"] ) presenter.viewController = viewController diff --git a/Stepic/Sources/Modules/Solution/SolutionDataFlow.swift b/Stepic/Sources/Modules/Solution/SolutionDataFlow.swift index 6aea204045..c5719fb3f7 100644 --- a/Stepic/Sources/Modules/Solution/SolutionDataFlow.swift +++ b/Stepic/Sources/Modules/Solution/SolutionDataFlow.swift @@ -5,7 +5,7 @@ enum Solution { struct Data { let step: Step let submission: Submission - let attempt: Attempt + let discussionID: DiscussionThread.IdType } struct Request {} diff --git a/Stepic/Sources/Modules/Solution/SolutionInteractor.swift b/Stepic/Sources/Modules/Solution/SolutionInteractor.swift index 1f21c88939..dec1c45d66 100644 --- a/Stepic/Sources/Modules/Solution/SolutionInteractor.swift +++ b/Stepic/Sources/Modules/Solution/SolutionInteractor.swift @@ -7,19 +7,22 @@ protocol SolutionInteractorProtocol { final class SolutionInteractor: SolutionInteractorProtocol { private let stepID: Step.IdType - private let submissionID: Submission.IdType + private let submission: Submission + private let discussionID: DiscussionThread.IdType private let presenter: SolutionPresenterProtocol private let provider: SolutionProviderProtocol init( stepID: Step.IdType, - submissionID: Submission.IdType, + submission: Submission, + discussionID: DiscussionThread.IdType, presenter: SolutionPresenterProtocol, provider: SolutionProviderProtocol ) { self.stepID = stepID - self.submissionID = submissionID + self.submission = submission + self.discussionID = discussionID self.presenter = presenter self.provider = provider } @@ -33,21 +36,12 @@ final class SolutionInteractor: SolutionInteractorProtocol { } return .value(step) - }.then { step -> Promise<(Step, Submission?)> in - self.provider.fetchSubmission(id: self.submissionID, step: step).map { (step, $0) } - }.then { step, submission -> Promise<(Step, Submission, Attempt?)> in - guard let submission = submission else { - throw Error.fetchFailed - } - - return self.provider.fetchAttempt(id: submission.attempt, step: step).map { (step, submission, $0) } - }.done { step, submission, attempt in - guard let attempt = attempt else { - throw Error.fetchFailed - } - - let response = Solution.SolutionLoad.Data(step: step, submission: submission, attempt: attempt) - + }.done { step in + let response = Solution.SolutionLoad.Data( + step: step, + submission: self.submission, + discussionID: self.discussionID + ) self.presenter.presentSolution(response: .init(result: .success(response))) }.catch { error in self.presenter.presentSolution(response: .init(result: .failure(error))) diff --git a/Stepic/Sources/Modules/Solution/SolutionPresenter.swift b/Stepic/Sources/Modules/Solution/SolutionPresenter.swift index 365e0e4ef9..2df2ec0183 100644 --- a/Stepic/Sources/Modules/Solution/SolutionPresenter.swift +++ b/Stepic/Sources/Modules/Solution/SolutionPresenter.swift @@ -12,12 +12,20 @@ final class SolutionPresenter: SolutionPresenterProtocol { case .failure: self.viewController?.displaySolution(viewModel: .init(state: .error)) case .success(let data): - let viewModel = self.makeViewModel(step: data.step, submission: data.submission, attempt: data.attempt) + let viewModel = self.makeViewModel( + step: data.step, + submission: data.submission, + discussionID: data.discussionID + ) self.viewController?.displaySolution(viewModel: .init(state: .result(data: viewModel))) } } - private func makeViewModel(step: Step, submission: Submission, attempt: Attempt) -> SolutionViewModel { + private func makeViewModel( + step: Step, + submission: Submission, + discussionID: DiscussionThread.IdType + ) -> SolutionViewModel { let quizStatus: QuizStatus = { switch submission.status { case "wrong": @@ -53,12 +61,12 @@ final class SolutionPresenter: SolutionPresenterProtocol { step: step, quizStatus: quizStatus, reply: submission.reply, - dataset: attempt.dataset, + dataset: submission.attempt?.dataset, feedback: submission.feedback, feedbackTitle: feedbackTitle, hintContent: hintContent, codeDetails: codeDetails, - solutionURL: self.makeURL(for: step) + solutionURL: self.makeURL(for: step, discussionID: discussionID) ) } @@ -103,9 +111,13 @@ final class SolutionPresenter: SolutionPresenterProtocol { return processor.processContent() } - // TODO: Format https://stepik.org/lesson/290850/step/1?discussion=1387392&thread=solutions&unit=272337 - private func makeURL(for step: Step) -> URL? { - let link = "\(StepicApplicationsInfo.stepicURL)/lesson/\(step.lessonID)/step/\(step.position)?from_mobile_app=true" + private func makeURL(for step: Step, discussionID: DiscussionThread.IdType) -> URL? { + let link = "\(StepikApplicationsInfo.stepikURL)" + + "/lesson/\(step.lessonID)" + + "/step/\(step.position)" + + "?from_mobile_app=true" + + "&discussion=\(discussionID)" + + "&thread=solutions" return URL(string: link) } } diff --git a/Stepic/Sources/Modules/Solution/SolutionProvider.swift b/Stepic/Sources/Modules/Solution/SolutionProvider.swift index 8f3af9e715..2e74523931 100644 --- a/Stepic/Sources/Modules/Solution/SolutionProvider.swift +++ b/Stepic/Sources/Modules/Solution/SolutionProvider.swift @@ -3,26 +3,18 @@ import PromiseKit protocol SolutionProviderProtocol { func fetchStep(id: Step.IdType) -> Promise> - func fetchSubmission(id: Submission.IdType, step: Step) -> Promise - func fetchAttempt(id: Attempt.IdType, step: Step) -> Promise } final class SolutionProvider: SolutionProviderProtocol { private let stepsPersistenceService: StepsPersistenceServiceProtocol private let stepsNetworkService: StepsNetworkServiceProtocol - private let submissionsNetworkService: SubmissionsNetworkServiceProtocol - private let attemptsNetworkService: AttemptsNetworkServiceProtocol init( stepsPersistenceService: StepsPersistenceServiceProtocol, - stepsNetworkService: StepsNetworkServiceProtocol, - submissionsNetworkService: SubmissionsNetworkServiceProtocol, - attemptsNetworkService: AttemptsNetworkServiceProtocol + stepsNetworkService: StepsNetworkServiceProtocol ) { self.stepsPersistenceService = stepsPersistenceService self.stepsNetworkService = stepsNetworkService - self.submissionsNetworkService = submissionsNetworkService - self.attemptsNetworkService = attemptsNetworkService } func fetchStep(id: Step.IdType) -> Promise> { @@ -49,20 +41,6 @@ final class SolutionProvider: SolutionProviderProtocol { } } - func fetchSubmission(id: Submission.IdType, step: Step) -> Promise { - self.submissionsNetworkService.fetch(submissionID: id, blockName: step.block.name) - } - - func fetchAttempt(id: Attempt.IdType, step: Step) -> Promise { - Promise { seal in - self.attemptsNetworkService.fetch(ids: [id], blockName: step.block.name).done { attempts, _ in - seal.fulfill(attempts.first) - }.catch { _ in - seal.reject(Error.fetchFailed) - } - } - } - enum Error: Swift.Error { case fetchFailed } diff --git a/Stepic/Sources/Modules/Solution/SolutionView.swift b/Stepic/Sources/Modules/Solution/SolutionView.swift index 8ffbf59840..afce842b62 100644 --- a/Stepic/Sources/Modules/Solution/SolutionView.swift +++ b/Stepic/Sources/Modules/Solution/SolutionView.swift @@ -13,7 +13,7 @@ extension SolutionView { let loadingIndicatorColor = UIColor.mainDark - let actionButtonBackgroundColor = UIColor.stepicGreen + let actionButtonBackgroundColor = UIColor.stepikGreen let actionButtonHeight: CGFloat = 44 let actionButtonTextColor = UIColor.white let actionButtonCornerRadius: CGFloat = 6 diff --git a/Stepic/Sources/Modules/Solution/SolutionViewController.swift b/Stepic/Sources/Modules/Solution/SolutionViewController.swift index d8cbcbe41e..9e02f07366 100644 --- a/Stepic/Sources/Modules/Solution/SolutionViewController.swift +++ b/Stepic/Sources/Modules/Solution/SolutionViewController.swift @@ -11,12 +11,27 @@ final class SolutionViewController: UIViewController, ControllerWithStepikPlaceh lazy var solutionView = self.view as? SolutionView + private lazy var shareBarButtonItem: UIBarButtonItem = { + let item = UIBarButtonItem( + barButtonSystemItem: .action, + target: self, + action: #selector(self.shareButtonClicked) + ) + item.isEnabled = false + return item + }() + private var state: Solution.ViewControllerState { didSet { self.updateState() } } - private var solutionURL: URL? + + private var solutionURL: URL? { + didSet { + self.shareBarButtonItem.isEnabled = self.solutionURL != nil + } + } init( interactor: SolutionInteractorProtocol, @@ -41,6 +56,17 @@ final class SolutionViewController: UIViewController, ControllerWithStepikPlaceh override func viewDidLoad() { super.viewDidLoad() + self.setup() + self.updateState() + + self.interactor.doSolutionLoad(request: .init()) + } + + // MARK: Private API + + private func setup() { + self.navigationItem.rightBarButtonItem = self.shareBarButtonItem + self.registerPlaceholder( placeholder: StepikPlaceholder( .noConnectionQuiz, @@ -51,13 +77,8 @@ final class SolutionViewController: UIViewController, ControllerWithStepikPlaceh ), for: .connectionError ) - self.updateState() - - self.interactor.doSolutionLoad(request: .init()) } - // MARK: Private API - private func updateState() { switch self.state { case .result: @@ -129,6 +150,21 @@ final class SolutionViewController: UIViewController, ControllerWithStepikPlaceh self.solutionView?.endLoading() } + + @objc + private func shareButtonClicked() { + guard let link = self.solutionURL?.absoluteString else { + return + } + + DispatchQueue.global().async { + let sharingViewController = SharingHelper.getSharingController(link) + DispatchQueue.main.async { + sharingViewController.popoverPresentationController?.barButtonItem = self.shareBarButtonItem + self.present(sharingViewController, animated: true, completion: nil) + } + } + } } extension SolutionViewController: SolutionViewControllerProtocol { diff --git a/Stepic/Sources/Modules/Step/StepAssembly.swift b/Stepic/Sources/Modules/Step/StepAssembly.swift index b662cbf5ad..7c2d1c699b 100644 --- a/Stepic/Sources/Modules/Step/StepAssembly.swift +++ b/Stepic/Sources/Modules/Step/StepAssembly.swift @@ -16,7 +16,11 @@ final class StepAssembly: Assembly { stepsPersistenceService: StepsPersistenceService(), stepsNetworkService: StepsNetworkService(stepsAPI: StepsAPI()), stepFontSizeStorageManager: StepFontSizeStorageManager(), - imageStoredFileManager: StoredFileManagerFactory.makeStoredFileManager(type: .image) + imageStoredFileManager: StoredFileManagerFactory.makeStoredFileManager(type: .image), + discussionThreadsNetworkService: DiscussionThreadsNetworkService( + discussionThreadsAPI: DiscussionThreadsAPI() + ), + discussionThreadsPersistenceService: DiscussionThreadsPersistenceService() ) let presenter = StepPresenter() let interactor = StepInteractor(stepID: self.stepID, presenter: presenter, provider: provider) diff --git a/Stepic/Sources/Modules/Step/StepDataFlow.swift b/Stepic/Sources/Modules/Step/StepDataFlow.swift index 541f68100a..621095e90d 100644 --- a/Stepic/Sources/Modules/Step/StepDataFlow.swift +++ b/Stepic/Sources/Modules/Step/StepDataFlow.swift @@ -108,7 +108,21 @@ enum StepDataFlow { } } - /// Present discussions module (list or with write comment on top on empty discussions empty state) + /// Update solutions button (after step loaded and on done) + enum SolutionsButtonUpdate { + struct Request {} + + struct Response { + let result: Result + } + + struct ViewModel { + let title: String? + let isEnabled: Bool + } + } + + /// Present discussions thread (list or with write comment on top on empty discussions empty state) enum DiscussionsPresentation { struct Request {} @@ -119,7 +133,33 @@ enum StepDataFlow { struct ViewModel { let discussionProxyID: DiscussionProxy.IdType let stepID: Step.IdType - let embeddedInWriteComment: Bool + let shouldEmbedInWriteComment: Bool + } + } + + /// Present solutions thread + enum SolutionsPresentation { + struct Request {} + + struct Response { + let step: Step + let discussionThread: DiscussionThread + } + + struct ViewModel { + let stepID: Step.IdType + let discussionProxyID: DiscussionProxy.IdType + } + } + + /// Handle HUD + enum BlockingWaitingIndicatorUpdate { + struct Response { + let shouldDismiss: Bool + } + + struct ViewModel { + let shouldDismiss: Bool } } diff --git a/Stepic/Sources/Modules/Step/StepInteractor.swift b/Stepic/Sources/Modules/Step/StepInteractor.swift index cb9bace527..3307a0e477 100644 --- a/Stepic/Sources/Modules/Step/StepInteractor.swift +++ b/Stepic/Sources/Modules/Step/StepInteractor.swift @@ -9,7 +9,9 @@ protocol StepInteractorProtocol { func doStepViewRequest(request: StepDataFlow.StepViewRequest.Request) func doStepDoneRequest(request: StepDataFlow.StepDoneRequest.Request) func doDiscussionsButtonUpdate(request: StepDataFlow.DiscussionsButtonUpdate.Request) + func doSolutionsButtonUpdate(request: StepDataFlow.SolutionsButtonUpdate.Request) func doDiscussionsPresentation(request: StepDataFlow.DiscussionsPresentation.Request) + func doSolutionsPresentation(request: StepDataFlow.SolutionsPresentation.Request) } final class StepInteractor: StepInteractorProtocol { @@ -52,6 +54,10 @@ final class StepInteractor: StepInteractorProtocol { self.currentStepIndex = step.position - 1 DispatchQueue.main.async { [weak self] in + guard let strongSelf = self else { + return + } + let data = StepDataFlow.StepLoad.Data( step: step, fontSize: fontSize, @@ -62,7 +68,9 @@ final class StepInteractor: StepInteractorProtocol { return nil } ) - self?.presenter.presentStep(response: .init(result: .success(data))) + strongSelf.presenter.presentStep(response: .init(result: .success(data))) + + strongSelf.tryToPresentCachedThenRemoteSolutionsDiscussionThread(step: step) } if !self.didAnalyticsSend { @@ -152,6 +160,17 @@ final class StepInteractor: StepInteractorProtocol { }.cauterize() } + func doSolutionsButtonUpdate(request: StepDataFlow.SolutionsButtonUpdate.Request) { + firstly { + self.provider.fetchDiscussionThreads(stepID: self.stepID) + }.done { fetchResult in + let solutionsDiscussionThread = fetchResult.value.first(where: { $0.threadType == .solutions }) + self.presenter.presentSolutionsButtonUpdate(response: .init(result: .success(solutionsDiscussionThread))) + }.catch { error in + self.presenter.presentSolutionsButtonUpdate(response: .init(result: .failure(error))) + } + } + func doDiscussionsPresentation(request: StepDataFlow.DiscussionsPresentation.Request) { self.provider.fetchCachedStep(id: self.stepID).done { cachedStep in if let cachedStep = cachedStep { @@ -160,7 +179,66 @@ final class StepInteractor: StepInteractorProtocol { }.cauterize() } - // MARK: - Types + func doSolutionsPresentation(request: StepDataFlow.SolutionsPresentation.Request) { + firstly { + self.provider.fetchCachedStep(id: self.stepID) + }.then { cachedStep -> Promise in + if let cachedStep = cachedStep { + return .value(cachedStep) + } else { + self.presenter.presentWaitingState(response: .init(shouldDismiss: false)) + return self.provider.fetchRemoteStep(id: self.stepID) + } + }.then { step -> Promise<(Step, [DiscussionThread]?)> in + guard let step = step else { + throw Error.fetchFailed + } + + if step.discussionThreads?.contains(where: { $0.threadType == .solutions }) ?? false { + return .value((step, step.discussionThreads)) + } + + guard let discussionThreadsIDs = step.discussionThreadsArray else { + return .value((step, nil)) + } + + self.presenter.presentWaitingState(response: .init(shouldDismiss: false)) + + return self.provider.fetchRemoteDiscussionThreads(ids: discussionThreadsIDs).map { (step, $0) } + }.done { step, discussionThreads in + guard let discussionThreads = discussionThreads, + let solutionsDiscussionThread = discussionThreads.first(where: { $0.threadType == .solutions }) else { + return + } + + self.presenter.presentWaitingState(response: .init(shouldDismiss: true)) + self.presenter.presentSolutions(response: .init(step: step, discussionThread: solutionsDiscussionThread)) + }.ensure { + self.presenter.presentWaitingState(response: .init(shouldDismiss: true)) + }.catch { error in + print("new step interactor: error while presenting solutions = \(error)") + } + } + + // MARK: Private API + + private func tryToPresentCachedThenRemoteSolutionsDiscussionThread(step: Step) { + defer { + self.doSolutionsButtonUpdate(request: .init()) + } + + guard let discussionThreads = step.discussionThreads else { + return + } + + guard let solutionsDiscussionThread = discussionThreads.first(where: { $0.threadType == .solutions }) else { + return + } + + self.presenter.presentSolutionsButtonUpdate(response: .init(result: .success(solutionsDiscussionThread))) + } + + // MARK: Types enum Error: Swift.Error { case fetchFailed diff --git a/Stepic/Sources/Modules/Step/StepPresenter.swift b/Stepic/Sources/Modules/Step/StepPresenter.swift index 686615cfb3..a4215f2158 100644 --- a/Stepic/Sources/Modules/Step/StepPresenter.swift +++ b/Stepic/Sources/Modules/Step/StepPresenter.swift @@ -7,7 +7,10 @@ protocol StepPresenterProtocol { func presentPlayStep(response: StepDataFlow.PlayStep.Response) func presentControlsUpdate(response: StepDataFlow.ControlsUpdate.Response) func presentDiscussionsButtonUpdate(response: StepDataFlow.DiscussionsButtonUpdate.Response) + func presentSolutionsButtonUpdate(response: StepDataFlow.SolutionsButtonUpdate.Response) func presentDiscussions(response: StepDataFlow.DiscussionsPresentation.Response) + func presentSolutions(response: StepDataFlow.SolutionsPresentation.Response) + func presentWaitingState(response: StepDataFlow.BlockingWaitingIndicatorUpdate.Response) } final class StepPresenter: StepPresenterProtocol { @@ -62,12 +65,36 @@ final class StepPresenter: StepPresenterProtocol { func presentDiscussionsButtonUpdate(response: StepDataFlow.DiscussionsButtonUpdate.Response) { self.viewController?.displayDiscussionsButtonUpdate( viewModel: .init( - title: self.makeDiscussionsLabelTitle(step: response.step), + title: self.makeDiscussionsButtonTitle(step: response.step), isEnabled: response.step.discussionProxyID != nil ) ) } + func presentSolutionsButtonUpdate(response: StepDataFlow.SolutionsButtonUpdate.Response) { + func displayHideSolutionsButtonUpdate() { + self.viewController?.displaySolutionsButtonUpdate(viewModel: .init(title: nil, isEnabled: false)) + } + + switch response.result { + case .success(let discussionThread): + guard let solutionsDiscussionThread = discussionThread, + solutionsDiscussionThread.threadType == .solutions, + !solutionsDiscussionThread.discussionProxy.isEmpty else { + return displayHideSolutionsButtonUpdate() + } + + self.viewController?.displaySolutionsButtonUpdate( + viewModel: .init( + title: self.makeSolutionsButtonTitle(discussionThread: solutionsDiscussionThread), + isEnabled: solutionsDiscussionThread.discussionsCount > 0 + ) + ) + case .failure: + displayHideSolutionsButtonUpdate() + } + } + func presentDiscussions(response: StepDataFlow.DiscussionsPresentation.Response) { guard let discussionProxyID = response.step.discussionProxyID else { return @@ -77,11 +104,29 @@ final class StepPresenter: StepPresenterProtocol { viewModel: .init( discussionProxyID: discussionProxyID, stepID: response.step.id, - embeddedInWriteComment: (response.step.discussionsCount ?? 0) == 0 + shouldEmbedInWriteComment: (response.step.discussionsCount ?? 0) == 0 + ) + ) + } + + func presentSolutions(response: StepDataFlow.SolutionsPresentation.Response) { + guard response.discussionThread.threadType == .solutions, + !response.discussionThread.discussionProxy.isEmpty else { + return + } + + self.viewController?.displaySolutions( + viewModel: .init( + stepID: response.step.id, + discussionProxyID: response.discussionThread.discussionProxy ) ) } + func presentWaitingState(response: StepDataFlow.BlockingWaitingIndicatorUpdate.Response) { + self.viewController?.displayBlockingLoadingIndicator(viewModel: .init(shouldDismiss: response.shouldDismiss)) + } + // MARK: Private API private func makeViewModel( @@ -129,8 +174,8 @@ final class StepPresenter: StepPresenterProtocol { return true }() - let discussionsLabelTitle = self.makeDiscussionsLabelTitle(step: step) - let urlPath = "\(StepicApplicationsInfo.stepicURL)/lesson/\(step.lessonID)/step/\(step.position)?from_mobile_app=true" + let discussionsLabelTitle = self.makeDiscussionsButtonTitle(step: step) + let urlPath = "\(StepikApplicationsInfo.stepikURL)/lesson/\(step.lessonID)/step/\(step.position)?from_mobile_app=true" let viewModel = StepViewModel( content: contentType, @@ -149,7 +194,7 @@ final class StepPresenter: StepPresenterProtocol { } } - private func makeDiscussionsLabelTitle(step: Step) -> String { + private func makeDiscussionsButtonTitle(step: Step) -> String { if step.discussionProxyID == nil { return NSLocalizedString("DisabledDiscussionsButtonTitle", comment: "") } @@ -164,6 +209,19 @@ final class StepPresenter: StepPresenterProtocol { return NSLocalizedString("NoDiscussionsButtonTitle", comment: "") } + private func makeSolutionsButtonTitle(discussionThread: DiscussionThread) -> String { + if discussionThread.discussionsCount > 0 { + return String( + format: NSLocalizedString("SolutionsButtonTitle", comment: ""), + arguments: [ + FormatterHelper.longNumber(discussionThread.discussionsCount) + ] + ) + } + + return NSLocalizedString("NoSolutionsButtonTitle", comment: "") + } + private func makeProcessedContentHTMLString( _ text: String, fontSize: StepFontSize, diff --git a/Stepic/Sources/Modules/Step/StepProvider.swift b/Stepic/Sources/Modules/Step/StepProvider.swift index aa5d7b1002..14286165ce 100644 --- a/Stepic/Sources/Modules/Step/StepProvider.swift +++ b/Stepic/Sources/Modules/Step/StepProvider.swift @@ -4,8 +4,11 @@ import PromiseKit protocol StepProviderProtocol { func fetchStep(id: Step.IdType) -> Promise> func fetchCachedStep(id: Step.IdType) -> Promise + func fetchRemoteStep(id: Step.IdType) -> Promise func fetchStoredImages(id: Step.IdType) -> Guarantee<[(imageURL: URL, storedFile: StoredFileProtocol)]> func fetchCurrentFontSize() -> Guarantee + func fetchDiscussionThreads(stepID: Step.IdType) -> Promise> + func fetchRemoteDiscussionThreads(ids: [DiscussionThread.IdType]) -> Promise<[DiscussionThread]> } final class StepProvider: StepProviderProtocol { @@ -13,17 +16,23 @@ final class StepProvider: StepProviderProtocol { private let stepsNetworkService: StepsNetworkServiceProtocol private let stepFontSizeStorageManager: StepFontSizeStorageManagerProtocol private let imageStoredFileManager: StoredFileManagerProtocol + private let discussionThreadsNetworkService: DiscussionThreadsNetworkServiceProtocol + private let discussionThreadsPersistenceService: DiscussionThreadsPersistenceServiceProtocol init( stepsPersistenceService: StepsPersistenceServiceProtocol, stepsNetworkService: StepsNetworkServiceProtocol, stepFontSizeStorageManager: StepFontSizeStorageManagerProtocol, - imageStoredFileManager: StoredFileManagerProtocol + imageStoredFileManager: StoredFileManagerProtocol, + discussionThreadsNetworkService: DiscussionThreadsNetworkServiceProtocol, + discussionThreadsPersistenceService: DiscussionThreadsPersistenceServiceProtocol ) { self.stepsPersistenceService = stepsPersistenceService self.stepsNetworkService = stepsNetworkService self.stepFontSizeStorageManager = stepFontSizeStorageManager self.imageStoredFileManager = imageStoredFileManager + self.discussionThreadsNetworkService = discussionThreadsNetworkService + self.discussionThreadsPersistenceService = discussionThreadsPersistenceService } // MARK: Protocol Conforming @@ -62,6 +71,16 @@ final class StepProvider: StepProviderProtocol { } } + func fetchRemoteStep(id: Step.IdType) -> Promise { + Promise { seal in + self.stepsNetworkService.fetch(ids: [id]).done { remoteSteps in + seal.fulfill(remoteSteps.first) + }.catch { _ in + seal.reject(Error.fetchFailed) + } + } + } + func fetchStoredImages(id: Step.IdType) -> Guarantee<[(imageURL: URL, storedFile: StoredFileProtocol)]> { Guarantee { seal in self.fetchCachedStep(id: id).done { step in @@ -82,6 +101,68 @@ final class StepProvider: StepProviderProtocol { } } + func fetchDiscussionThreads(stepID: Step.IdType) -> Promise> { + Promise { seal in + firstly { + self.fetchStep(id: stepID) + }.then { stepFetchResult -> Promise<(Step, [DiscussionThread]?, ([DiscussionThread], Meta)?)> in + guard let step = stepFetchResult.value else { + throw Error.fetchFailed + } + + guard let discussionThreadsIDs = step.discussionThreadsArray, + !discussionThreadsIDs.isEmpty else { + throw Error.emptyDiscussionThreads + } + + let persistenceServicePromise = Guarantee( + self.discussionThreadsPersistenceService.fetch(ids: discussionThreadsIDs), + fallback: nil + ) + let networkServicePromise = Guarantee( + self.discussionThreadsNetworkService.fetch(ids: discussionThreadsIDs), + fallback: nil + ) + + return when( + fulfilled: persistenceServicePromise, + networkServicePromise + ).map { (step, $0, $1) } + }.then { step, cachedDiscussionThreads, remoteFetchResult -> Promise> in + if let remoteDiscussionThreads = remoteFetchResult?.0 { + DispatchQueue.main.async { + step.discussionThreads = remoteDiscussionThreads + CoreDataHelper.shared.save() + } + + let result = FetchResult<[DiscussionThread]>(value: remoteDiscussionThreads, source: .remote) + return .value(result) + } else { + let result = FetchResult<[DiscussionThread]>(value: cachedDiscussionThreads ?? [], source: .cache) + return .value(result) + } + }.done { fetchResult in + seal.fulfill(fetchResult) + }.catch { error in + if case Error.emptyDiscussionThreads = error { + seal.fulfill(.init(value: [], source: .cache)) + } else { + seal.reject(Error.fetchFailed) + } + } + } + } + + func fetchRemoteDiscussionThreads(ids: [DiscussionThread.IdType]) -> Promise<[DiscussionThread]> { + Promise { seal in + self.discussionThreadsNetworkService.fetch(ids: ids).done { remoteDiscussionThreads, _ in + seal.fulfill(remoteDiscussionThreads) + }.catch { _ in + seal.reject(Error.fetchFailed) + } + } + } + // MARK: Private API private func getStepStoredImages(_ step: Step) -> [(imageURL: URL, storedFile: StoredFileProtocol)] { @@ -105,5 +186,6 @@ final class StepProvider: StepProviderProtocol { enum Error: Swift.Error { case fetchFailed + case emptyDiscussionThreads } } diff --git a/Stepic/Sources/Modules/Step/StepView.swift b/Stepic/Sources/Modules/Step/StepView.swift index 8ac382b906..62a1da6972 100644 --- a/Stepic/Sources/Modules/Step/StepView.swift +++ b/Stepic/Sources/Modules/Step/StepView.swift @@ -6,6 +6,7 @@ protocol StepViewDelegate: AnyObject { func stepViewDidRequestPrevious(_ view: StepView) func stepViewDidRequestNext(_ view: StepView) func stepViewDidRequestDiscussions(_ view: StepView) + func stepViewDidRequestSolutions(_ view: StepView) func stepViewDidLoadContent(_ view: StepView) func stepView(_ view: StepView, didRequestFullscreenImage url: URL) @@ -84,6 +85,12 @@ final class StepView: UIView { } strongSelf.delegate?.stepViewDidRequestDiscussions(strongSelf) } + view.onSolutionsButtonClick = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.delegate?.stepViewDidRequestSolutions(strongSelf) + } return view }() @@ -183,6 +190,11 @@ final class StepView: UIView { self.stepControlsView.isDiscussionsButtonEnabled = isEnabled } + func updateSolutionsButton(title: String?, isEnabled: Bool) { + self.stepControlsView.solutionsTitle = title + self.stepControlsView.isSolutionsButtonEnabled = isEnabled + } + // MARK: Private API private func positionVideoPreview() { diff --git a/Stepic/Sources/Modules/Step/StepViewController.swift b/Stepic/Sources/Modules/Step/StepViewController.swift index fa7daf68a5..1a8275d889 100644 --- a/Stepic/Sources/Modules/Step/StepViewController.swift +++ b/Stepic/Sources/Modules/Step/StepViewController.swift @@ -1,4 +1,5 @@ import Agrume +import SVProgressHUD import UIKit protocol StepViewControllerProtocol: AnyObject { @@ -7,7 +8,10 @@ protocol StepViewControllerProtocol: AnyObject { func displayPlayStep(viewModel: StepDataFlow.PlayStep.ViewModel) func displayControlsUpdate(viewModel: StepDataFlow.ControlsUpdate.ViewModel) func displayDiscussionsButtonUpdate(viewModel: StepDataFlow.DiscussionsButtonUpdate.ViewModel) + func displaySolutionsButtonUpdate(viewModel: StepDataFlow.SolutionsButtonUpdate.ViewModel) func displayDiscussions(viewModel: StepDataFlow.DiscussionsPresentation.ViewModel) + func displaySolutions(viewModel: StepDataFlow.SolutionsPresentation.ViewModel) + func displayBlockingLoadingIndicator(viewModel: StepDataFlow.BlockingWaitingIndicatorUpdate.ViewModel) } // MARK: - StepViewController (Animation) - @@ -90,6 +94,7 @@ final class StepViewController: UIViewController, ControllerWithStepikPlaceholde if !self.isFirstAppearance { self.interactor.doDiscussionsButtonUpdate(request: .init()) + self.interactor.doSolutionsButtonUpdate(request: .init()) } } @@ -227,14 +232,19 @@ extension StepViewController: StepViewControllerProtocol { self.stepView?.updateDiscussionButton(title: viewModel.title, isEnabled: viewModel.isEnabled) } + func displaySolutionsButtonUpdate(viewModel: StepDataFlow.SolutionsButtonUpdate.ViewModel) { + self.stepView?.updateSolutionsButton(title: viewModel.title, isEnabled: viewModel.isEnabled) + } + func displayDiscussions(viewModel: StepDataFlow.DiscussionsPresentation.ViewModel) { let discussionsAssembly = DiscussionsAssembly( + discussionThreadType: .default, discussionProxyID: viewModel.discussionProxyID, stepID: viewModel.stepID ) let discussionsViewController = discussionsAssembly.makeModule() - if viewModel.embeddedInWriteComment { + if viewModel.shouldEmbedInWriteComment { let (modalPresentationStyle, navigationBarAppearance) = { () -> (UIModalPresentationStyle, StyledNavigationController.NavigationBarAppearanceState) in if #available(iOS 13.0, *) { @@ -281,6 +291,24 @@ extension StepViewController: StepViewControllerProtocol { self.push(module: discussionsViewController) } } + + func displaySolutions(viewModel: StepDataFlow.SolutionsPresentation.ViewModel) { + let assembly = DiscussionsAssembly( + discussionThreadType: .solutions, + discussionProxyID: viewModel.discussionProxyID, + stepID: viewModel.stepID + ) + + self.push(module: assembly.makeModule()) + } + + func displayBlockingLoadingIndicator(viewModel: StepDataFlow.BlockingWaitingIndicatorUpdate.ViewModel) { + if viewModel.shouldDismiss { + SVProgressHUD.dismiss() + } else { + SVProgressHUD.show() + } + } } // MARK: - StepViewController: StepViewDelegate - @@ -302,6 +330,10 @@ extension StepViewController: StepViewDelegate { self.interactor.doDiscussionsPresentation(request: .init()) } + func stepViewDidRequestSolutions(_ view: StepView) { + self.interactor.doSolutionsPresentation(request: .init()) + } + func stepView(_ view: StepView, didRequestOpenURL url: URL) { guard case .result(let viewModel) = self.state else { return @@ -380,6 +412,10 @@ extension StepViewController: BaseQuizOutputProtocol { self.interactor.doStepDoneRequest(request: .init()) } + func handleSubmissionEvaluated() { + self.interactor.doSolutionsButtonUpdate(request: .init()) + } + func handleNextStepNavigation() { self.interactor.doStepNavigationRequest(request: .init(direction: .next)) } diff --git a/Stepic/Sources/Modules/Step/Views/StepControlsView.swift b/Stepic/Sources/Modules/Step/Views/StepControlsView.swift index 1acb28d615..7cd8fafc83 100644 --- a/Stepic/Sources/Modules/Step/Views/StepControlsView.swift +++ b/Stepic/Sources/Modules/Step/Views/StepControlsView.swift @@ -9,8 +9,11 @@ extension StepControlsView { let navigationButtonsSpacing: CGFloat = 16 let navigationButtonsHeight: CGFloat = 44 - let discussionsButtonHeight: CGFloat = 44 + let discussionThreadButtonHeight: CGFloat = 44 let statisticsViewHeight: CGFloat = 44 + + let separatorColor = UIColor(hex: 0xEAECF0) + let separatorHeight: CGFloat = 1 } } @@ -23,14 +26,51 @@ final class StepControlsView: UIView { return view }() - private lazy var discussionsButton: StepDiscussionsButton = { - let button = StepDiscussionsButton() + private lazy var discussionsButtonTopSeparatorView: UIView = { + let view = UIView() + view.backgroundColor = self.appearance.separatorColor + view.isHidden = true + return view + }() + + private lazy var discussionsButton: StepDiscussionThreadButton = { + let button = StepDiscussionThreadButton(threadItem: .discussions) button.addTarget(self, action: #selector(self.discussionsButtonClicked), for: .touchUpInside) return button }() + private lazy var discussionsButtonBottomSeparatorView: UIView = { + let view = UIView() + view.backgroundColor = self.appearance.separatorColor + view.isHidden = true + return view + }() + + private lazy var solutionsButton: StepDiscussionThreadButton = { + let button = StepDiscussionThreadButton(threadItem: .solutions) + button.addTarget(self, action: #selector(self.solutionsButtonClicked), for: .touchUpInside) + button.isHidden = true + return button + }() + + private lazy var solutionsButtonBottomSeparatorView: UIView = { + let view = UIView() + view.backgroundColor = self.appearance.separatorColor + view.isHidden = true + return view + }() + private lazy var bottomControlsStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [self.statisticsView, self.discussionsButton]) + let stackView = UIStackView( + arrangedSubviews: [ + self.statisticsView, + self.discussionsButtonTopSeparatorView, + self.discussionsButton, + self.discussionsButtonBottomSeparatorView, + self.solutionsButton, + self.solutionsButtonBottomSeparatorView + ] + ) stackView.axis = .vertical stackView.spacing = 0 return stackView @@ -60,7 +100,11 @@ final class StepControlsView: UIView { + stackViewSize.height + (self.navigationState == .none ? 0 : self.appearance.spacing) + self.appearance.statisticsViewHeight - + self.appearance.discussionsButtonHeight + + (self.discussionsButtonTopSeparatorView.isHidden ? 0 : self.appearance.separatorHeight) + + self.appearance.discussionThreadButtonHeight + + (self.discussionsButtonBottomSeparatorView.isHidden ? 0 : self.appearance.separatorHeight) + + (self.solutionsButton.isHidden ? 0 : self.appearance.discussionThreadButtonHeight) + + (self.solutionsButtonBottomSeparatorView.isHidden ? 0 : self.appearance.separatorHeight) ) } @@ -72,7 +116,11 @@ final class StepControlsView: UIView { + self.appearance.navigationButtonsHeight + self.appearance.spacing + self.appearance.statisticsViewHeight - + self.appearance.discussionsButtonHeight + + self.appearance.separatorHeight + + self.appearance.discussionThreadButtonHeight + + self.appearance.separatorHeight + + self.appearance.discussionThreadButtonHeight + + self.appearance.separatorHeight ) } @@ -94,6 +142,19 @@ final class StepControlsView: UIView { } } + var solutionsTitle: String? { + didSet { + self.solutionsButton.title = self.solutionsTitle + self.updateSolutionsButtonVisibility() + } + } + + var isSolutionsButtonEnabled: Bool = true { + didSet { + self.solutionsButton.isEnabled = self.isSolutionsButtonEnabled + } + } + var passedByCount: Int? { didSet { self.statisticsView.passedByCount = self.passedByCount @@ -109,6 +170,7 @@ final class StepControlsView: UIView { } var onDiscussionsButtonClick: (() -> Void)? + var onSolutionsButtonClick: (() -> Void)? var onPreviousButtonClick: (() -> Void)? var onNextButtonClick: (() -> Void)? @@ -148,6 +210,11 @@ final class StepControlsView: UIView { self.onDiscussionsButtonClick?() } + @objc + private func solutionsButtonClicked() { + self.onSolutionsButtonClick?() + } + private func updateNavigationState() { self.navigationStackView.removeAllArrangedSubviews() @@ -184,6 +251,14 @@ final class StepControlsView: UIView { self.statisticsView.isHidden = self.passedByCount == nil && self.correctRatio == nil } + private func updateSolutionsButtonVisibility() { + self.solutionsButton.isHidden = self.solutionsTitle?.isEmpty ?? true + self.solutionsButtonBottomSeparatorView.isHidden = self.solutionsButton.isHidden + + self.discussionsButtonTopSeparatorView.isHidden = self.solutionsButton.isHidden + self.discussionsButtonBottomSeparatorView.isHidden = self.solutionsButton.isHidden + } + // MARK: Enum enum NavigationState { @@ -230,7 +305,27 @@ extension StepControlsView: ProgrammaticallyInitializableViewProtocol { self.discussionsButton.translatesAutoresizingMaskIntoConstraints = false self.discussionsButton.snp.makeConstraints { make in - make.height.equalTo(self.appearance.discussionsButtonHeight) + make.height.equalTo(self.appearance.discussionThreadButtonHeight) + } + + self.solutionsButton.translatesAutoresizingMaskIntoConstraints = false + self.solutionsButton.snp.makeConstraints { make in + make.height.equalTo(self.appearance.discussionThreadButtonHeight) + } + + self.discussionsButtonTopSeparatorView.translatesAutoresizingMaskIntoConstraints = false + self.discussionsButtonTopSeparatorView.snp.makeConstraints { make in + make.height.equalTo(self.appearance.separatorHeight) + } + + self.discussionsButtonBottomSeparatorView.translatesAutoresizingMaskIntoConstraints = false + self.discussionsButtonBottomSeparatorView.snp.makeConstraints { make in + make.height.equalTo(self.appearance.separatorHeight) + } + + self.solutionsButtonBottomSeparatorView.translatesAutoresizingMaskIntoConstraints = false + self.solutionsButtonBottomSeparatorView.snp.makeConstraints { make in + make.height.equalTo(self.appearance.separatorHeight) } } } diff --git a/Stepic/Sources/Modules/Step/Views/StepDiscussionsButton.swift b/Stepic/Sources/Modules/Step/Views/StepDiscussionThreadButton.swift similarity index 71% rename from Stepic/Sources/Modules/Step/Views/StepDiscussionsButton.swift rename to Stepic/Sources/Modules/Step/Views/StepDiscussionThreadButton.swift index aa97134d7e..ca890cd3ca 100644 --- a/Stepic/Sources/Modules/Step/Views/StepDiscussionsButton.swift +++ b/Stepic/Sources/Modules/Step/Views/StepDiscussionThreadButton.swift @@ -1,7 +1,7 @@ import SnapKit import UIKit -extension StepDiscussionsButton { +extension StepDiscussionThreadButton { struct Appearance { let iconSize = CGSize(width: 16, height: 17) let insets = LayoutInsets(left: 16, right: 16) @@ -13,8 +13,9 @@ extension StepDiscussionsButton { } } -final class StepDiscussionsButton: UIControl { +final class StepDiscussionThreadButton: UIControl { let appearance: Appearance + let threadItem: ThreadItem private lazy var contentStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [self.iconImageContainerView, self.textLabel]) @@ -28,8 +29,7 @@ final class StepDiscussionsButton: UIControl { private lazy var iconImageContainerView = UIView() private lazy var iconImageView: UIImageView = { - let image = UIImage(named: "comments-icon") - let view = UIImageView(image: image?.withRenderingMode(.alwaysTemplate)) + let view = UIImageView(image: self.threadItem.icon?.withRenderingMode(.alwaysTemplate)) view.tintColor = self.appearance.mainColor return view }() @@ -41,6 +41,13 @@ final class StepDiscussionsButton: UIControl { return label }() + override var isEnabled: Bool { + didSet { + self.iconImageView.alpha = self.isEnabled ? 1.0 : 0.5 + self.textLabel.alpha = self.isEnabled ? 1.0 : 0.5 + } + } + override var isHighlighted: Bool { didSet { self.iconImageView.alpha = self.isHighlighted ? 0.5 : 1.0 @@ -54,7 +61,12 @@ final class StepDiscussionsButton: UIControl { } } - init(frame: CGRect = .zero, appearance: Appearance = Appearance()) { + init( + frame: CGRect = .zero, + threadItem: ThreadItem = .default, + appearance: Appearance = Appearance() + ) { + self.threadItem = threadItem self.appearance = appearance super.init(frame: frame) @@ -67,9 +79,27 @@ final class StepDiscussionsButton: UIControl { required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + // MARK: Types + + enum ThreadItem { + case discussions + case solutions + + static var `default`: ThreadItem { .discussions } + + fileprivate var icon: UIImage? { + switch self { + case .discussions: + return UIImage(named: "comments-icon") + case .solutions: + return UIImage(named: "solutions-icon") + } + } + } } -extension StepDiscussionsButton: ProgrammaticallyInitializableViewProtocol { +extension StepDiscussionThreadButton: ProgrammaticallyInitializableViewProtocol { func setupView() { self.backgroundColor = self.appearance.backgroundColor } diff --git a/Stepic/Sources/Modules/Step/Views/StepNavigationButton.swift b/Stepic/Sources/Modules/Step/Views/StepNavigationButton.swift index 69b4128f4d..f29e2de363 100644 --- a/Stepic/Sources/Modules/Step/Views/StepNavigationButton.swift +++ b/Stepic/Sources/Modules/Step/Views/StepNavigationButton.swift @@ -10,9 +10,9 @@ extension StepNavigationButton { let cornerRadius: CGFloat = 6 let backgroundColor = UIColor.white - let mainColor = UIColor.stepicGreen + let mainColor = UIColor.stepikGreen - let textColor = UIColor.stepicGreen + let textColor = UIColor.stepikGreen let textFont = UIFont.systemFont(ofSize: 16) } } diff --git a/Stepic/Sources/Services/Managers/DiscussionsSortTypeStorageManager.swift b/Stepic/Sources/Services/Managers/DiscussionsSortTypeStorageManager.swift new file mode 100644 index 0000000000..b01ab4834f --- /dev/null +++ b/Stepic/Sources/Services/Managers/DiscussionsSortTypeStorageManager.swift @@ -0,0 +1,25 @@ +import Foundation + +protocol DiscussionsSortTypeStorageManagerProtocol: AnyObject { + var globalDiscussionsSortType: Discussions.SortType { get set } +} + +final class DiscussionsSortTypeStorageManager: DiscussionsSortTypeStorageManagerProtocol { + var globalDiscussionsSortType: Discussions.SortType { + get { + if let stringValue = UserDefaults.standard.string(forKey: Key.discussionsSortType.rawValue), + let sortType = Discussions.SortType(rawValue: stringValue) { + return sortType + } else { + return .last + } + } + set { + UserDefaults.standard.set(newValue.rawValue, forKey: Key.discussionsSortType.rawValue) + } + } + + private enum Key: String { + case discussionsSortType = "discussionsSortTypeKey" + } +} diff --git a/Stepic/Sources/Services/Models/Network/CommentsNetworkService.swift b/Stepic/Sources/Services/Models/Network/CommentsNetworkService.swift index 580b797a49..400d9ac531 100644 --- a/Stepic/Sources/Services/Models/Network/CommentsNetworkService.swift +++ b/Stepic/Sources/Services/Models/Network/CommentsNetworkService.swift @@ -2,7 +2,7 @@ import Foundation import PromiseKit protocol CommentsNetworkServiceProtocol: AnyObject { - func fetch(ids: [Comment.IdType]) -> Promise<[Comment]> + func fetch(ids: [Comment.IdType], blockName: String) -> Promise<[Comment]> func create(comment: Comment) -> Promise func update(comment: Comment) -> Promise func delete(id: Comment.IdType) -> Promise @@ -15,13 +15,13 @@ final class CommentsNetworkService: CommentsNetworkServiceProtocol { self.commentsAPI = commentsAPI } - func fetch(ids: [Comment.IdType]) -> Promise<[Comment]> { + func fetch(ids: [Comment.IdType], blockName: String) -> Promise<[Comment]> { if ids.isEmpty { return .value([]) } return Promise { seal in - self.commentsAPI.retrieve(ids: ids).done { comments in + self.commentsAPI.retrieve(ids: ids, blockName: blockName).done { comments in let comments = comments.reordered(order: ids, transform: { $0.id }) seal.fulfill(comments) }.catch { _ in diff --git a/Stepic/Sources/Services/Models/Network/DiscussionThreadsNetworkService.swift b/Stepic/Sources/Services/Models/Network/DiscussionThreadsNetworkService.swift new file mode 100644 index 0000000000..3854847cf2 --- /dev/null +++ b/Stepic/Sources/Services/Models/Network/DiscussionThreadsNetworkService.swift @@ -0,0 +1,39 @@ +import Foundation +import PromiseKit + +protocol DiscussionThreadsNetworkServiceProtocol: AnyObject { + func fetch(ids: [DiscussionThread.IdType], page: Int) -> Promise<([DiscussionThread], Meta)> +} + +extension DiscussionThreadsNetworkServiceProtocol { + func fetch(ids: [DiscussionThread.IdType]) -> Promise<([DiscussionThread], Meta)> { + self.fetch(ids: ids, page: 1) + } +} + +final class DiscussionThreadsNetworkService: DiscussionThreadsNetworkServiceProtocol { + private let discussionThreadsAPI: DiscussionThreadsAPI + + init(discussionThreadsAPI: DiscussionThreadsAPI) { + self.discussionThreadsAPI = discussionThreadsAPI + } + + func fetch(ids: [DiscussionThread.IdType], page: Int) -> Promise<([DiscussionThread], Meta)> { + if ids.isEmpty { + return Promise.value(([], Meta.oneAndOnlyPage)) + } + + return Promise { seal in + self.discussionThreadsAPI.retrieve(ids: ids, page: page).done { discussionThreads, meta in + let discussionThreads = discussionThreads.reordered(order: ids, transform: { $0.id }) + seal.fulfill((discussionThreads, meta)) + }.catch { _ in + seal.reject(Error.fetchFailed) + } + } + } + + enum Error: Swift.Error { + case fetchFailed + } +} diff --git a/Stepic/Sources/Services/Models/Persistence/DiscussionThreadsPersistenceService.swift b/Stepic/Sources/Services/Models/Persistence/DiscussionThreadsPersistenceService.swift new file mode 100644 index 0000000000..7411d108a2 --- /dev/null +++ b/Stepic/Sources/Services/Models/Persistence/DiscussionThreadsPersistenceService.swift @@ -0,0 +1,17 @@ +import Foundation +import PromiseKit + +protocol DiscussionThreadsPersistenceServiceProtocol: AnyObject { + func fetch(ids: [DiscussionThread.IdType]) -> Guarantee<[DiscussionThread]> +} + +final class DiscussionThreadsPersistenceService: DiscussionThreadsPersistenceServiceProtocol { + func fetch(ids: [DiscussionThread.IdType]) -> Guarantee<[DiscussionThread]> { + Guarantee { seal in + DiscussionThread.fetchAsync(ids: ids).done { discussionThreads in + let discussionThreads = Array(Set(discussionThreads)).reordered(order: ids, transform: { $0.id }) + seal(discussionThreads) + } + } + } +} diff --git a/Stepic/Sources/Services/NetworkReachabilityService.swift b/Stepic/Sources/Services/NetworkReachabilityService.swift index 63195311aa..53a0487ff2 100644 --- a/Stepic/Sources/Services/NetworkReachabilityService.swift +++ b/Stepic/Sources/Services/NetworkReachabilityService.swift @@ -9,7 +9,7 @@ protocol NetworkReachabilityServiceProtocol: AnyObject { final class NetworkReachabilityService: NetworkReachabilityServiceProtocol { private lazy var reachabilityManager: Alamofire.NetworkReachabilityManager? = { let reachabilityManager = Alamofire.NetworkReachabilityManager( - host: StepicApplicationsInfo.stepicURL + host: StepikApplicationsInfo.stepikURL ) return reachabilityManager }() diff --git a/Stepic/Sources/Views/DownloadControlView.swift b/Stepic/Sources/Views/DownloadControlView.swift index 27068d8f3c..59c9392bc0 100644 --- a/Stepic/Sources/Views/DownloadControlView.swift +++ b/Stepic/Sources/Views/DownloadControlView.swift @@ -5,7 +5,7 @@ extension DownloadControlView { struct Appearance { let circleWidth: CGFloat = 2.8 - let downloadingCircleColor = UIColor.stepicGreen + let downloadingCircleColor = UIColor.stepikGreen let downloadingBackgroundColor = UIColor.mainDark.withAlphaComponent(0.2) let pendingCircleColor = UIColor.mainDark diff --git a/Stepic/Sources/Views/PlayNextCircleControlView.swift b/Stepic/Sources/Views/PlayNextCircleControlView.swift index 23428f78bc..7abcf68133 100644 --- a/Stepic/Sources/Views/PlayNextCircleControlView.swift +++ b/Stepic/Sources/Views/PlayNextCircleControlView.swift @@ -8,7 +8,7 @@ extension PlayNextCircleControlView { let iconImageTintColor = UIColor.white let circleWidth: CGFloat = 4 - let circleProgressColor = UIColor.stepicGreen + let circleProgressColor = UIColor.stepikGreen let circleTrackColor = UIColor.white } } diff --git a/Stepic/Step+CoreDataProperties.swift b/Stepic/Step+CoreDataProperties.swift index dfe76dfb1a..525111a5e1 100644 --- a/Stepic/Step+CoreDataProperties.swift +++ b/Stepic/Step+CoreDataProperties.swift @@ -28,8 +28,10 @@ extension Step { @NSManaged var managedProgress: Progress? @NSManaged var managedOptions: StepOptions? - @NSManaged var managedDiscussionProxy: String? @NSManaged var managedDiscussionsCount: NSNumber? + @NSManaged var managedDiscussionProxy: String? + @NSManaged var managedDiscussionThreadsArray: NSObject? + @NSManaged var managedDiscussionThreads: NSOrderedSet? static var oldEntity: NSEntityDescription { NSEntityDescription.entity(forEntityName: "Step", in: CoreDataHelper.shared.context)! @@ -129,6 +131,15 @@ extension Step { } } + var discussionsCount: Int? { + get { + self.managedDiscussionsCount?.intValue + } + set { + self.managedDiscussionsCount = newValue as NSNumber? + } + } + var discussionProxyID: String? { get { self.managedDiscussionProxy @@ -138,12 +149,25 @@ extension Step { } } - var discussionsCount: Int? { + var discussionThreadsArray: [String]? { get { - self.managedDiscussionsCount?.intValue + self.managedDiscussionThreadsArray as? [String] } set { - self.managedDiscussionsCount = newValue as NSNumber? + self.managedDiscussionThreadsArray = newValue as NSObject? + } + } + + var discussionThreads: [DiscussionThread]? { + get { + self.managedDiscussionThreads?.array as? [DiscussionThread] + } + set { + if let newDiscussionThreads = newValue { + self.managedDiscussionThreads = NSOrderedSet(array: newDiscussionThreads) + } else { + self.managedDiscussionThreads = nil + } } } diff --git a/Stepic/Step.swift b/Stepic/Step.swift index a863e7d992..0435874bb9 100644 --- a/Stepic/Step.swift +++ b/Stepic/Step.swift @@ -35,9 +35,11 @@ final class Step: NSManagedObject, IDFetchable { self.hasReview = false } - self.maxSubmissionsCount = json[JSONKey.maxSubmissionsCount.rawValue].int self.discussionsCount = json[JSONKey.discussionsCount.rawValue].int self.discussionProxyID = json[JSONKey.discussionProxy.rawValue].string + self.discussionThreadsArray = json[JSONKey.discussionThreads.rawValue].arrayObject as? [String] + + self.maxSubmissionsCount = json[JSONKey.maxSubmissionsCount.rawValue].int self.lessonID = json[JSONKey.lesson.rawValue].intValue self.passedByCount = json[JSONKey.passedBy.rawValue].intValue self.correctRatio = json[JSONKey.correctRatio.rawValue].floatValue @@ -110,11 +112,12 @@ final class Step: NSManagedObject, IDFetchable { case actions case doReview = "do_review" case maxSubmissionsCount = "max_submissions_count" - case discussionsCount = "discussions_count" - case discussionProxy = "discussion_proxy" case lesson case editInstructions = "edit_instructions" case passedBy = "passed_by" case correctRatio = "correct_ratio" + case discussionsCount = "discussions_count" + case discussionProxy = "discussion_proxy" + case discussionThreads = "discussion_threads" } } diff --git a/Stepic/StepicsAPI.swift b/Stepic/StepicsAPI.swift index 43b76bbea3..8841b57d3c 100644 --- a/Stepic/StepicsAPI.swift +++ b/Stepic/StepicsAPI.swift @@ -17,7 +17,7 @@ final class StepicsAPI: APIEndpoint { func retrieveCurrentUser() -> Promise { Promise { seal in self.manager.request( - "\(StepicApplicationsInfo.apiURL)/\(name)/1", + "\(StepikApplicationsInfo.apiURL)/\(name)/1", parameters: nil, encoding: URLEncoding.default, headers: AuthInfo.shared.initialHTTPHeaders diff --git a/Stepic/StepicApplicationsInfo.swift b/Stepic/StepikApplicationsInfo.swift similarity index 50% rename from Stepic/StepicApplicationsInfo.swift rename to Stepic/StepikApplicationsInfo.swift index 140c5204c7..29d7e56806 100644 --- a/Stepic/StepicApplicationsInfo.swift +++ b/Stepic/StepikApplicationsInfo.swift @@ -6,13 +6,12 @@ // Copyright © 2015 Alex Karpov. All rights reserved. // -import Foundation import UIKit -struct StepicApplicationsInfo { +struct StepikApplicationsInfo { // Dictionary with auth (encrypted) private static let stepikAuthDic = ApplicationInfo(plist: "Auth") - // Dictionary with configutation + // Dictionary with configuration private static let stepikConfigDic = ApplicationInfo(plist: "Config") // Structure @@ -22,73 +21,80 @@ struct StepicApplicationsInfo { typealias AuthInfo = (clientId: String, clientSecret: String, redirectUri: String, credentials: String) private static func initAuthInfo(idPath: String, secretPath: String, redirectPath: String) -> AuthInfo { - let id = StepicApplicationsInfo.stepikAuthDic?.get(for: idPath) as? String ?? "" - let secret = StepicApplicationsInfo.stepikAuthDic?.get(for: secretPath) as? String ?? "" - let redirect = StepicApplicationsInfo.stepikAuthDic?.get(for: redirectPath) as? String ?? "" + let id = StepikApplicationsInfo.stepikAuthDic?.get(for: idPath) as? String ?? "" + let secret = StepikApplicationsInfo.stepikAuthDic?.get(for: secretPath) as? String ?? "" + let redirect = StepikApplicationsInfo.stepikAuthDic?.get(for: redirectPath) as? String ?? "" let credentials = "\(id):\(secret)".data(using: String.Encoding.utf8)!.base64EncodedString(options: []) return (clientId: id, clientSecret: secret, redirectUri: redirect, credentials: credentials) } - static let social: AuthInfo? = !(StepicApplicationsInfo.stepikAuthDic?.has(path: Root.AuthType.social) ?? false) ? nil : - StepicApplicationsInfo.initAuthInfo(idPath: Root.AuthType.Social.id, - secretPath: Root.AuthType.Social.secret, - redirectPath: Root.AuthType.Social.redirect) - static let password: AuthInfo? = !(StepicApplicationsInfo.stepikAuthDic?.has(path: Root.AuthType.password) ?? false) ? nil : - StepicApplicationsInfo.initAuthInfo(idPath: Root.AuthType.Password.id, - secretPath: Root.AuthType.Password.secret, - redirectPath: Root.AuthType.Password.redirect) + static let social: AuthInfo? = !(StepikApplicationsInfo.stepikAuthDic?.has(path: Root.AuthType.social) ?? false) + ? nil + : StepikApplicationsInfo.initAuthInfo( + idPath: Root.AuthType.Social.id, + secretPath: Root.AuthType.Social.secret, + redirectPath: Root.AuthType.Social.redirect + ) + + static let password: AuthInfo? = !(StepikApplicationsInfo.stepikAuthDic?.has(path: Root.AuthType.password) ?? false) + ? nil + : StepikApplicationsInfo.initAuthInfo( + idPath: Root.AuthType.Password.id, + secretPath: Root.AuthType.Password.secret, + redirectPath: Root.AuthType.Password.redirect + ) // Section: URL - static let appId = StepicApplicationsInfo.stepikConfigDic?.get(for: Root.URL.appId) as? String ?? "" - static let urlScheme = StepicApplicationsInfo.stepikConfigDic?.get(for: Root.URL.scheme) as? String ?? "" - static let apiURL = StepicApplicationsInfo.stepikConfigDic?.get(for: Root.URL.api) as? String ?? "" - static let oauthURL = StepicApplicationsInfo.stepikConfigDic?.get(for: Root.URL.oauth) as? String ?? "" - static let stepicURL = StepicApplicationsInfo.stepikConfigDic?.get(for: Root.URL.stepik) as? String ?? "" - static let versionInfoURL = StepicApplicationsInfo.stepikConfigDic?.get(for: Root.URL.version) as? String ?? "" - static let adaptiveRatingURL = StepicApplicationsInfo.stepikConfigDic?.get(for: Root.URL.adaptiveRating) as? String ?? "" + static let appId = StepikApplicationsInfo.stepikConfigDic?.get(for: Root.URL.appId) as? String ?? "" + static let urlScheme = StepikApplicationsInfo.stepikConfigDic?.get(for: Root.URL.scheme) as? String ?? "" + static let apiURL = StepikApplicationsInfo.stepikConfigDic?.get(for: Root.URL.api) as? String ?? "" + static let oauthURL = StepikApplicationsInfo.stepikConfigDic?.get(for: Root.URL.oauth) as? String ?? "" + static let stepikURL = StepikApplicationsInfo.stepikConfigDic?.get(for: Root.URL.stepik) as? String ?? "" + static let versionInfoURL = StepikApplicationsInfo.stepikConfigDic?.get(for: Root.URL.version) as? String ?? "" + static let adaptiveRatingURL = StepikApplicationsInfo.stepikConfigDic?.get(for: Root.URL.adaptiveRating) as? String ?? "" // Section: Cookie - static let cookiePrefix = StepicApplicationsInfo.stepikConfigDic?.get(for: Root.Cookie.prefix) as? String ?? "" + static let cookiePrefix = StepikApplicationsInfo.stepikConfigDic?.get(for: Root.Cookie.prefix) as? String ?? "" // Section: Feature - static let doesAllowCourseUnenrollment = StepicApplicationsInfo.stepikConfigDic?.get(for: Root.Feature.courseUnenrollment) as? Bool ?? true - static let inAppUpdatesAvailable = StepicApplicationsInfo.stepikConfigDic?.get(for: Root.Feature.inAppUpdates) as? Bool ?? false - static let streaksEnabled = StepicApplicationsInfo.stepikConfigDic?.get(for: Root.Feature.streaks) as? Bool ?? true - static let shouldRegisterNotifications = StepicApplicationsInfo.stepikConfigDic?.get(for: Root.Feature.notifications) as? Bool ?? true + static let doesAllowCourseUnenrollment = StepikApplicationsInfo.stepikConfigDic?.get(for: Root.Feature.courseUnenrollment) as? Bool ?? true + static let inAppUpdatesAvailable = StepikApplicationsInfo.stepikConfigDic?.get(for: Root.Feature.inAppUpdates) as? Bool ?? false + static let streaksEnabled = StepikApplicationsInfo.stepikConfigDic?.get(for: Root.Feature.streaks) as? Bool ?? true + static let shouldRegisterNotifications = StepikApplicationsInfo.stepikConfigDic?.get(for: Root.Feature.notifications) as? Bool ?? true // Section: Adaptive - static let adaptiveSupportedCourses = StepicApplicationsInfo.stepikConfigDic?.get(for: Root.Adaptive.supportedCourses) as? [Int] ?? [] - static let isAdaptive = StepicApplicationsInfo.stepikConfigDic?.get(for: Root.Adaptive.isAdaptive) as? Bool ?? false - static let adaptiveCoursesInfoURL = StepicApplicationsInfo.stepikConfigDic?.get(for: Root.Adaptive.coursesInfoURL) as? String ?? "" + static let adaptiveSupportedCourses = StepikApplicationsInfo.stepikConfigDic?.get(for: Root.Adaptive.supportedCourses) as? [Int] ?? [] + static let isAdaptive = StepikApplicationsInfo.stepikConfigDic?.get(for: Root.Adaptive.isAdaptive) as? Bool ?? false + static let adaptiveCoursesInfoURL = StepikApplicationsInfo.stepikConfigDic?.get(for: Root.Adaptive.coursesInfoURL) as? String ?? "" // Section: RateApp struct RateApp { - static let correctSubmissionsThreshold = StepicApplicationsInfo.stepikConfigDic?.get(for: Root.RateApp.submissionsThreshold) as? Int ?? 4 - static let appStoreURL = URL(string: StepicApplicationsInfo.stepikConfigDic?.get(for: Root.RateApp.appStoreLink) as? String ?? "") + static let correctSubmissionsThreshold = StepikApplicationsInfo.stepikConfigDic?.get(for: Root.RateApp.submissionsThreshold) as? Int ?? 4 + static let appStoreURL = URL(string: StepikApplicationsInfo.stepikConfigDic?.get(for: Root.RateApp.appStoreLink) as? String ?? "") } // Section: Social struct SocialInfo { struct AppIds { - static let vk = StepicApplicationsInfo.stepikConfigDic?.get(for: Root.SocialProviders.vkId) as? String ?? "" - static let facebook = StepicApplicationsInfo.stepikConfigDic?.get(for: Root.SocialProviders.facebookId) as? String ?? "" + static let vk = StepikApplicationsInfo.stepikConfigDic?.get(for: Root.SocialProviders.vkId) as? String ?? "" + static let facebook = StepikApplicationsInfo.stepikConfigDic?.get(for: Root.SocialProviders.facebookId) as? String ?? "" } } // Section: Colors struct Colors { - static var mainGreen = UIColor(hex: StepicApplicationsInfo.stepikConfigDic?.get(for: Root.Colors.mainGreen) as? Int ?? 0x66cc66) - static var mainText = UIColor(hex: StepicApplicationsInfo.stepikConfigDic?.get(for: Root.Colors.mainText) as? Int ?? 0x000000) - static var mainDark = UIColor(hex: StepicApplicationsInfo.stepikConfigDic?.get(for: Root.Colors.mainDark) as? Int ?? 0x000000) + static var mainGreen = UIColor(hex: StepikApplicationsInfo.stepikConfigDic?.get(for: Root.Colors.mainGreen) as? Int ?? 0x66cc66) + static var mainText = UIColor(hex: StepikApplicationsInfo.stepikConfigDic?.get(for: Root.Colors.mainText) as? Int ?? 0x000000) + static var mainDark = UIColor(hex: StepikApplicationsInfo.stepikConfigDic?.get(for: Root.Colors.mainDark) as? Int ?? 0x000000) } // Section: Modules struct Modules { - static let tabs = StepicApplicationsInfo.stepikConfigDic?.get(for: Root.Modules.tabs) as? [String] + static let tabs = StepikApplicationsInfo.stepikConfigDic?.get(for: Root.Modules.tabs) as? [String] } // Section: Versions struct Versions { - static let stories = StepicApplicationsInfo.stepikConfigDic?.get(for: Root.Versions.stories) as? Int + static let stories = StepikApplicationsInfo.stepikConfigDic?.get(for: Root.Versions.stories) as? Int } } diff --git a/Stepic/StepikButton.swift b/Stepic/StepikButton.swift index 231d4fcfec..ed4b736752 100644 --- a/Stepic/StepikButton.swift +++ b/Stepic/StepikButton.swift @@ -56,8 +56,8 @@ class StepikButton: UIButton { setRoundedCorners(cornerRadius: 8, borderWidth: 0) } else { self.backgroundColor = isLightBackground ? UIColor(hex: 0xf6fcf6, alpha: 1) : UIColor(hex: 0x545a67, alpha: 1) - self.setTitleColor(UIColor.stepicGreen, for: .normal) - setRoundedCorners(cornerRadius: 8, borderWidth: 0, borderColor: UIColor.stepicGreen) + self.setTitleColor(UIColor.stepikGreen, for: .normal) + setRoundedCorners(cornerRadius: 8, borderWidth: 0, borderColor: UIColor.stepikGreen) } } diff --git a/Stepic/StepikPlaceholderStyle+Placeholders.swift b/Stepic/StepikPlaceholderStyle+Placeholders.swift index dfd604eb8b..413d50b08c 100644 --- a/Stepic/StepikPlaceholderStyle+Placeholders.swift +++ b/Stepic/StepikPlaceholderStyle+Placeholders.swift @@ -102,6 +102,12 @@ extension StepikPlaceholder.Style { text: NSLocalizedString("Refreshing", comment: ""), buttonTitle: nil ) + static let emptySolutions = StepikPlaceholderStyle( + id: "emptySolutions", + image: PlaceholderImage(image: #imageLiteral(resourceName: "new-empty-empty"), scale: 0.99), + text: NSLocalizedString("DiscussionsPlaceholderEmptySolutionsTitle", comment: ""), + buttonTitle: nil + ) static let emptyProfileLoading = StepikPlaceholderStyle( id: "emptyProfileLoading", image: PlaceholderImage(image: #imageLiteral(resourceName: "new-empty-empty"), scale: 0.99), diff --git a/Stepic/StepicToken.swift b/Stepic/StepikToken.swift similarity index 57% rename from Stepic/StepicToken.swift rename to Stepic/StepikToken.swift index edd737c5a4..f562c8b11a 100644 --- a/Stepic/StepicToken.swift +++ b/Stepic/StepikToken.swift @@ -9,20 +9,20 @@ import SwiftyJSON import UIKit -final class StepicToken: DictionarySerializable { +final class StepikToken: DictionarySerializable { let accessToken: String! let refreshToken: String! let tokenType: String! let expireDate: Date! - //delta used to have a gap when token expires + /// Delta used to have a gap when token expires. let expireDelta = TimeInterval(1000) init(json: JSON) { - accessToken = json["access_token"].stringValue - refreshToken = json["refresh_token"].stringValue - tokenType = json["token_type"].stringValue - expireDate = Date().addingTimeInterval(json["expires_in"].doubleValue - expireDelta) + self.accessToken = json["access_token"].stringValue + self.refreshToken = json["refresh_token"].stringValue + self.tokenType = json["token_type"].stringValue + self.expireDate = Date().addingTimeInterval(json["expires_in"].doubleValue - expireDelta) } init(accessToken: String, refreshToken: String, tokenType: String, expireDate: Date) { @@ -33,13 +33,18 @@ final class StepicToken: DictionarySerializable { } required convenience init?(dictionary: [String: Any]) { - if let aToken = dictionary["access_token"] as? String, - let rToken = dictionary["refresh_token"] as? String, - let tType = dictionary["token_type"] as? String { - self.init(accessToken: aToken, refreshToken: rToken, tokenType: tType, expireDate: Date(timeIntervalSince1970: dictionary["expire_date"] as? TimeInterval ?? 0.0)) - } else { + guard let accessToken = dictionary["access_token"] as? String, + let refreshToken = dictionary["refresh_token"] as? String, + let tokenType = dictionary["token_type"] as? String else { return nil } + + self.init( + accessToken: accessToken, + refreshToken: refreshToken, + tokenType: tokenType, + expireDate: Date(timeIntervalSince1970: dictionary["expire_date"] as? TimeInterval ?? 0.0) + ) } func serializeToDictionary() -> [String: Any] { self.getDictionary() } diff --git a/Stepic/StoriesPresenter.swift b/Stepic/StoriesPresenter.swift index 9d0f592277..234b52cfcc 100644 --- a/Stepic/StoriesPresenter.swift +++ b/Stepic/StoriesPresenter.swift @@ -83,7 +83,7 @@ final class StoriesPresenter: StoriesPresenterProtocol { self.storyTemplatesAPI.retrieve( isPublished: isPublished, language: ContentLanguageService().globalContentLanguage, - maxVersion: StepicApplicationsInfo.Versions.stories ?? 0 + maxVersion: StepikApplicationsInfo.Versions.stories ?? 0 ).done { [weak self] stories, _ in guard let strongSelf = self else { return diff --git a/Stepic/Submission.swift b/Stepic/Submission.swift index 23ee79dc8c..404416e5b4 100644 --- a/Stepic/Submission.swift +++ b/Stepic/Submission.swift @@ -12,72 +12,107 @@ import UIKit final class Submission: JSONSerializable { typealias IdType = Int - var id: Int = 0 + var id: IdType = 0 var status: String? - var reply: Reply? - var attempt: Int = 0 var hint: String? var feedback: SubmissionFeedback? + var reply: Reply? + var attemptID: Attempt.IdType = 0 + var attempt: Attempt? + + var isCorrect: Bool { self.status == "correct" } + + var json: JSON { + [ + JSONKey.attempt.rawValue: attemptID, + JSONKey.reply.rawValue: reply?.dictValue ?? "" + ] + } init(json: JSON, stepName: String) { - id = json["id"].intValue - status = json["status"].string - attempt = json["attempt"].intValue - hint = json["hint"].string - reply = nil - reply = getReplyFromJSON(json["reply"], stepName: stepName) - feedback = SubmissionFeedback(json: json["feedback"]) + self.update(json: json) + self.reply = nil + self.reply = self.getReplyFromJSON(json[JSONKey.reply.rawValue], stepName: stepName) } init(attempt: Int, reply: Reply) { - self.attempt = attempt + self.attemptID = attempt self.reply = reply } required init(json: JSON) { - update(json: json) + self.update(json: json) } func update(json: JSON) { - id = json["id"].intValue - status = json["status"].string - attempt = json["attempt"].intValue - hint = json["hint"].string - feedback = SubmissionFeedback(json: json["feedback"]) + self.id = json[JSONKey.id.rawValue].intValue + self.status = json[JSONKey.status.rawValue].string + self.hint = json[JSONKey.hint.rawValue].string + self.feedback = SubmissionFeedback(json: json[JSONKey.feedback.rawValue]) + self.attemptID = json[JSONKey.attempt.rawValue].intValue } func initReply(json: JSON, stepName: String) { - reply = getReplyFromJSON(json, stepName: stepName) + self.reply = self.getReplyFromJSON(json, stepName: stepName) } func hasEqualId(json: JSON) -> Bool { - self.id == json["id"].int - } - - var json: JSON { - [ - "attempt": attempt, - "reply": reply?.dictValue ?? "" - ] + self.id == json[JSONKey.id.rawValue].int } private func getReplyFromJSON(_ json: JSON, stepName: String) -> Reply? { switch stepName { - case "choice" : return ChoiceReply(json: json) - case "string" : return TextReply(json: json) - case "number": return NumberReply(json: json) - case "free-answer": return FreeAnswerReply(json: json) - case "math": return MathReply(json: json) - case "sorting": return SortingReply(json: json) - case "matching": return MatchingReply(json: json) - case "code": return CodeReply(json: json) - case "sql": return SQLReply(json: json) - default: return nil + case "choice": + return ChoiceReply(json: json) + case "string": + return TextReply(json: json) + case "number": + return NumberReply(json: json) + case "free-answer": + return FreeAnswerReply(json: json) + case "math": + return MathReply(json: json) + case "sorting": + return SortingReply(json: json) + case "matching": + return MatchingReply(json: json) + case "code": + return CodeReply(json: json) + case "sql": + return SQLReply(json: json) + default: + return nil } } + + // MARK: Types + + enum JSONKey: String { + case id + case status + case hint + case attempt + case reply + case feedback + } +} + +extension Submission: CustomDebugStringConvertible { + var debugDescription: String { + """ + Submission(id: \(id), \ + status: \(status ?? "nil"), \ + hint: \(hint ?? "nil"), \ + feedback: \(feedback ??? "nil"), \ + reply: \(reply ??? "nil"), \ + attemptID: \(attemptID), \ + attempt: \(attempt ??? "nil")) + """ + } } enum SubmissionFeedback { + case text(_ string: String) case options(_ choices: [String]) init?(json: JSON) { @@ -85,6 +120,10 @@ enum SubmissionFeedback { self = .options(options) return } + if let stringValue = json.string { + self = .text(stringValue) + return + } return nil } } diff --git a/Stepic/SubmissionsAPI.swift b/Stepic/SubmissionsAPI.swift index bc783e646e..9e497c7892 100644 --- a/Stepic/SubmissionsAPI.swift +++ b/Stepic/SubmissionsAPI.swift @@ -14,7 +14,18 @@ import SwiftyJSON final class SubmissionsAPI: APIEndpoint { override var name: String { "submissions" } - @discardableResult private func retrieve(stepName: String, objectName: String, objectId: Int, isDescending: Bool? = true, page: Int? = 1, userId: Int? = nil, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, success: @escaping ([Submission], Meta) -> Void, error errorHandler: @escaping (String) -> Void) -> Request? { + @discardableResult + private func retrieve( + stepName: String, + objectName: String, + objectId: Int, + isDescending: Bool? = true, + page: Int? = 1, + userId: Int? = nil, + headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, + success: @escaping ([Submission], Meta) -> Void, + error errorHandler: @escaping (String) -> Void + ) -> Request? { var params: Parameters = [:] params[objectName] = objectId @@ -28,9 +39,13 @@ final class SubmissionsAPI: APIEndpoint { params["user"] = user } - return manager.request("\(StepicApplicationsInfo.apiURL)/submissions", method: .get, parameters: params, encoding: URLEncoding.default, headers: headers).responseSwiftyJSON({ - response in - + return manager.request( + "\(StepikApplicationsInfo.apiURL)/submissions", + method: .get, + parameters: params, + encoding: URLEncoding.default, + headers: headers + ).responseSwiftyJSON { response in var error = response.result.error var json: JSON = [:] if response.result.value == nil { @@ -58,7 +73,7 @@ final class SubmissionsAPI: APIEndpoint { errorHandler("Response status code is wrong(\(String(describing: response?.statusCode)))") return } - }) + } } @discardableResult @@ -150,7 +165,7 @@ final class SubmissionsAPI: APIEndpoint { let params: Parameters = [:] return self.manager.request( - "\(StepicApplicationsInfo.apiURL)/submissions/\(submissionId)", + "\(StepikApplicationsInfo.apiURL)/submissions/\(submissionId)", parameters: params, encoding: URLEncoding.default, headers: headers diff --git a/Stepic/TextReply.swift b/Stepic/TextReply.swift index 3df59c591d..f7fe6c20a1 100644 --- a/Stepic/TextReply.swift +++ b/Stepic/TextReply.swift @@ -1,25 +1,39 @@ -// -// TextReply.swift -// Stepic -// -// Created by Alexander Karpov on 26.01.16. -// Copyright © 2016 Alex Karpov. All rights reserved. -// - +import Foundation import SwiftyJSON -import UIKit -final class TextReply: NSObject, Reply { +final class TextReply: Reply, CustomStringConvertible { var text: String + var dictValue: [String: Any] { + [JSONKey.text.rawValue: self.text] + } + + var description: String { + "TextReply(text: \(self.text))" + } + init(text: String) { self.text = text } required init(json: JSON) { - text = json["text"].stringValue - super.init() + self.text = json[JSONKey.text.rawValue].stringValue } - var dictValue: [String: Any] { ["text": text] } + enum JSONKey: String { + case text + } +} + +extension TextReply: Hashable { + static func == (lhs: TextReply, rhs: TextReply) -> Bool { + if lhs === rhs { return true } + if type(of: lhs) != type(of: rhs) { return false } + if lhs.text != rhs.text { return false } + return true + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.text) + } } diff --git a/Stepic/UpdateRequestMaker.swift b/Stepic/UpdateRequestMaker.swift index b9e0b7f7ef..f7cda3a621 100644 --- a/Stepic/UpdateRequestMaker.swift +++ b/Stepic/UpdateRequestMaker.swift @@ -25,7 +25,7 @@ final class UpdateRequestMaker { checkToken().done { manager.request( - "\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)/\(updatingObject.id)", + "\(StepikApplicationsInfo.apiURL)/\(requestEndpoint)/\(updatingObject.id)", method: .put, parameters: params, encoding: JSONEncoding.default diff --git a/Stepic/VKSocialSDKProvider.swift b/Stepic/VKSocialSDKProvider.swift index 4148ac5fbd..9b6652d089 100644 --- a/Stepic/VKSocialSDKProvider.swift +++ b/Stepic/VKSocialSDKProvider.swift @@ -24,7 +24,7 @@ final class VKSocialSDKProvider: NSObject, SocialSDKProvider { private var sdkInstance: VKSdk override private init() { - sdkInstance = VKSdk.initialize(withAppId: StepicApplicationsInfo.SocialInfo.AppIds.vk) + sdkInstance = VKSdk.initialize(withAppId: StepikApplicationsInfo.SocialInfo.AppIds.vk) super.init() sdkInstance.register(self) sdkInstance.uiDelegate = self diff --git a/Stepic/ViewsAPI.swift b/Stepic/ViewsAPI.swift index 6a15cdc0ed..c2bc128f83 100644 --- a/Stepic/ViewsAPI.swift +++ b/Stepic/ViewsAPI.swift @@ -55,7 +55,7 @@ final class ViewsAPI: APIEndpoint { } return self.manager.request( - "\(StepicApplicationsInfo.apiURL)/views", + "\(StepikApplicationsInfo.apiURL)/views", method: .post, parameters: params, encoding: JSONEncoding.default, diff --git a/Stepic/WebControllerManager.swift b/Stepic/WebControllerManager.swift index 683cb7907a..1f6f538485 100644 --- a/Stepic/WebControllerManager.swift +++ b/Stepic/WebControllerManager.swift @@ -166,10 +166,10 @@ extension WebControllerManager: WKNavigationDelegate { let rurl = navigationAction.request.url if let url = rurl { - if url.scheme == StepicApplicationsInfo.urlScheme { + if url.scheme == StepikApplicationsInfo.urlScheme { UIApplication.shared.openURL(url) } else if url.absoluteString.contains("social_signup_with_existing_email") { - if let url = URL(string: "\(StepicApplicationsInfo.social?.redirectUri ?? "")?\(url.query ?? "")") { + if let url = URL(string: "\(StepikApplicationsInfo.social?.redirectUri ?? "")?\(url.query ?? "")") { self.dismissWebControllerWithKey("social auth", animated: false, completion: { UIApplication.shared.openURL(url) }, error: nil) diff --git a/Stepic/en.lproj/Localizable.strings b/Stepic/en.lproj/Localizable.strings index be44e92443..fa14e160e9 100644 --- a/Stepic/en.lproj/Localizable.strings +++ b/Stepic/en.lproj/Localizable.strings @@ -776,6 +776,8 @@ PreviousLessonNavigation = "Previous lesson"; DiscussionsButtonTitle = "Show comments (%@)"; NoDiscussionsButtonTitle = "Leave a comment"; DisabledDiscussionsButtonTitle = "Comments disabled"; +SolutionsButtonTitle = "Show solutions (%@)"; +NoSolutionsButtonTitle = "No solutions"; LessonTooltipPointsWithScoreTitle = "You got: %@ out of %@ for step"; LessonTooltipPointsTitle = "You will get: %@ for step"; LessonTooltipTimeToCompleteTitle = "%@ for lesson"; @@ -863,12 +865,16 @@ DiscussionsAlertActionLikeTitle = "Like"; DiscussionsAlertActionUnlikeTitle = "Unlike"; DiscussionsAlertActionAbuseTitle = "Abuse"; DiscussionsAlertActionUnabuseTitle = "Don't abuse"; +DiscussionsAlertActionShowSolutionTitle = "Show solution"; DiscussionsSortTypeAlertTitle = "Sort by"; DiscussionsSortTypeLastDiscussions = "Last discussions"; DiscussionsSortTypeMostLikedDiscussions = "Most liked"; DiscussionsSortTypeMostActiveDiscussions = "Most active"; DiscussionsSortTypeRecentActivityDiscussions = "Recent activity"; DiscussionsIsPinnedBadgeTitle = "Pinned"; +DiscussionThreadSolutionsTitle = "Solutions"; +DiscussionThreadCommentSolutionTitle = "Solution %@"; +DiscussionsPlaceholderEmptySolutionsTitle = "No solutions"; /* Write comment */ WriteCommentTitle = "Comment"; diff --git a/Stepic/ru.lproj/Localizable.strings b/Stepic/ru.lproj/Localizable.strings index cf05f272f8..2c1c9281f0 100644 --- a/Stepic/ru.lproj/Localizable.strings +++ b/Stepic/ru.lproj/Localizable.strings @@ -777,6 +777,8 @@ PreviousLessonNavigation = "Предыдущий урок"; DiscussionsButtonTitle = "Показать комментарии (%@)"; NoDiscussionsButtonTitle = "Напишите комментарий"; DisabledDiscussionsButtonTitle = "Комментарии отключены"; +SolutionsButtonTitle = "Форум решений (%@)"; +NoSolutionsButtonTitle = "Решений пока нет"; LessonTooltipPointsWithScoreTitle = "Вы получили: %@ из %@ за шаг"; LessonTooltipPointsTitle = "Вы получите: %@ за шаг"; LessonTooltipTimeToCompleteTitle = "%@ на урок"; @@ -864,12 +866,16 @@ DiscussionsAlertActionLikeTitle = "Нравится"; DiscussionsAlertActionUnlikeTitle = "Больше не нравится"; DiscussionsAlertActionAbuseTitle = "Пожаловаться"; DiscussionsAlertActionUnabuseTitle = "Не жаловаться"; +DiscussionsAlertActionShowSolutionTitle = "Показать решение"; DiscussionsSortTypeAlertTitle = "Сортировать по"; DiscussionsSortTypeLastDiscussions = "Новые обсуждения"; DiscussionsSortTypeMostLikedDiscussions = "Самые популярные"; DiscussionsSortTypeMostActiveDiscussions = "Самые обсуждаемые"; DiscussionsSortTypeRecentActivityDiscussions = "Свежие обновления"; DiscussionsIsPinnedBadgeTitle = "Закреплён"; +DiscussionThreadSolutionsTitle = "Решения"; +DiscussionThreadCommentSolutionTitle = "Решение %@"; +DiscussionsPlaceholderEmptySolutionsTitle = "Решений пока нет"; /* Write comment */ WriteCommentTitle = "Комментарий"; diff --git a/StepicTests/DeepLinkRouteTests.swift b/StepicTests/DeepLinkRouteTests.swift index 3681b2babd..452d396d14 100644 --- a/StepicTests/DeepLinkRouteTests.swift +++ b/StepicTests/DeepLinkRouteTests.swift @@ -202,6 +202,46 @@ class DeepLinkRouteSpec: QuickSpec { } } } + + context("solutions") { + func checkRoute(_ route: DeepLinkRoute, expectedUnitID: Int?) -> ToSucceedResult { + guard case let .solutions(lessonID, stepID, discussionID, unitID) = route else { + return .failed(reason: "wrong enum case") + } + guard lessonID == 172508 else { + return .failed(reason: "wrong lesson id") + } + guard stepID == 1 else { + return .failed(reason: "wrong step id") + } + guard discussionID == 803115 else { + return .failed(reason: "wrong discussion id") + } + guard unitID == expectedUnitID else { + return .failed(reason: "wrong unit id") + } + return .succeeded + } + + it("matches discussions deep link paths with unit id") { + let paths = [ + "https://stepik.org/lesson/172508/step/1?discussion=803115&unit=148015&thread=solutions", + "https://stepik.org/lesson/172508/step/1?discussion=803115&unit=148015&thread=solutions/" + ] + self.checkPaths(paths) { route in + checkRoute(route, expectedUnitID: 148015) + } + } + + it("matches discussions deep link without unit id") { + let paths = [ + "https://stepik.org/lesson/172508/step/1?discussion=803115&thread=solutions" + ] + self.checkPaths(paths) { route in + checkRoute(route, expectedUnitID: nil) + } + } + } } } } diff --git a/StepicTests/Model/SubmissionTests.swift b/StepicTests/Model/SubmissionTests.swift new file mode 100644 index 0000000000..f34bd6b239 --- /dev/null +++ b/StepicTests/Model/SubmissionTests.swift @@ -0,0 +1,336 @@ +import Nimble +import Quick +import SwiftyJSON + +@testable import Stepic + +class SubmissionSpec: QuickSpec { + override func spec() { + describe("Submission") { + describe("JSON pasing") { + it("successfully parses with choices reply") { + let json = JSON(parseJSON: ReplyType.choices.submissionJSONString) + let submission = Submission(json: json, stepName: Block.BlockType.choice.rawValue) + + expect(submission.id) == 164530189 + expect(submission.status) == "correct" + expect(submission.hint) == "" + expect(submission.reply as? ChoiceReply) == ChoiceReply(choices: [false, true, false, false]) + expect(submission.attemptID) == 155142602 + expect(submission.attempt).to(beNil()) + + if case .text(let stringValue) = submission.feedback { + expect(stringValue) == "" + } else { + fail("unexpected SubmissionFeedback type") + } + } + + it("successfully parses with text reply") { + let json = JSON(parseJSON: ReplyType.text.submissionJSONString) + let submission = Submission(json: json, stepName: Block.BlockType.string.rawValue) + + expect(submission.id) == 163700855 + expect(submission.status) == "wrong" + expect(submission.hint) == "" + expect(submission.reply as? TextReply) == TextReply(text: "text") + expect(submission.attemptID) == 145802794 + expect(submission.attempt).to(beNil()) + + if case .text(let stringValue) = submission.feedback { + expect(stringValue) == "" + } else { + fail("unexpected SubmissionFeedback type") + } + } + + it("successfully parses with number reply") { + let json = JSON(parseJSON: ReplyType.number.submissionJSONString) + let submission = Submission(json: json, stepName: Block.BlockType.number.rawValue) + + expect(submission.id) == 155034240 + expect(submission.status) == "correct" + expect(submission.hint) == "Optional feedback on correct submission" + expect(submission.reply as? NumberReply) == NumberReply(number: "25.5") + expect(submission.attemptID) == 145800697 + expect(submission.attempt).to(beNil()) + + if case .text(let stringValue) = submission.feedback { + expect(stringValue) == "Optional feedback on correct submission" + } else { + fail("unexpected SubmissionFeedback type") + } + } + + it("successfully parses with free-answer reply") { + let json = JSON(parseJSON: ReplyType.freeAnswer.submissionJSONString) + let submission = Submission(json: json, stepName: Block.BlockType.freeAnswer.rawValue) + + expect(submission.id) == 155035432 + expect(submission.status) == "correct" + expect(submission.hint) == "" + expect(submission.reply as? FreeAnswerReply) == FreeAnswerReply(text: "test") + expect(submission.attemptID) == 145801887 + expect(submission.attempt).to(beNil()) + + if case .text(let stringValue) = submission.feedback { + expect(stringValue) == "" + } else { + fail("unexpected SubmissionFeedback type") + } + } + + it("successfully parses with math reply") { + let json = JSON(parseJSON: ReplyType.math.submissionJSONString) + let submission = Submission(json: json, stepName: Block.BlockType.math.rawValue) + + expect(submission.id) == 163701768 + expect(submission.status) == "correct" + expect(submission.hint) == "" + expect(submission.reply as? MathReply) == MathReply(formula: "2*x+y/z") + expect(submission.attemptID) == 145803773 + expect(submission.attempt).to(beNil()) + + if case .text(let stringValue) = submission.feedback { + expect(stringValue) == "" + } else { + fail("unexpected SubmissionFeedback type") + } + } + + it("successfully parses with sorting reply") { + let json = JSON(parseJSON: ReplyType.sorting.submissionJSONString) + let submission = Submission(json: json, stepName: Block.BlockType.sorting.rawValue) + + expect(submission.id) == 163701921 + expect(submission.status) == "correct" + expect(submission.hint) == "" + expect(submission.reply as? SortingReply) == SortingReply(ordering: [0, 1, 2]) + expect(submission.attemptID) == 145804003 + expect(submission.attempt).to(beNil()) + + if case .text(let stringValue) = submission.feedback { + expect(stringValue) == "" + } else { + fail("unexpected SubmissionFeedback type") + } + } + + it("successfully parses with matching reply") { + let json = JSON(parseJSON: ReplyType.matching.submissionJSONString) + let submission = Submission(json: json, stepName: Block.BlockType.matching.rawValue) + + expect(submission.id) == 163702173 + expect(submission.status) == "correct" + expect(submission.hint) == "" + expect(submission.reply as? MatchingReply) == MatchingReply(ordering: [2, 1, 0]) + expect(submission.attemptID) == 145805676 + expect(submission.attempt).to(beNil()) + + if case .text(let stringValue) = submission.feedback { + expect(stringValue) == "" + } else { + fail("unexpected SubmissionFeedback type") + } + } + + it("successfully parses with code reply") { + let json = JSON(parseJSON: ReplyType.code.submissionJSONString) + let submission = Submission(json: json, stepName: Block.BlockType.code.rawValue) + + expect(submission.id) == 163968205 + expect(submission.status) == "correct" + expect(submission.hint) == "" + expect(submission.reply as? CodeReply) == CodeReply( + code: "def main():\n \n a, b = map(int, input().split())\n res = a + b\n print(res)\n\n\nif __name__ == \"__main__\":\n main()", + language: .python + ) + expect(submission.attemptID) == 129167799 + expect(submission.attempt).to(beNil()) + + expect(submission.feedback).to(beNil()) + } + + it("successfully parses with SQL reply") { + let json = JSON(parseJSON: ReplyType.sql.submissionJSONString) + let submission = Submission(json: json, stepName: Block.BlockType.sql.rawValue) + + expect(submission.id) == 163702543 + expect(submission.status) == "correct" + expect(submission.hint) == "Affected rows: 1" + expect(submission.reply as? SQLReply) == SQLReply(code: "INSERT INTO users (name) VALUES ('Fluttershy');\n") + expect(submission.attemptID) == 145794719 + expect(submission.attempt).to(beNil()) + + if case .text(let stringValue) = submission.feedback { + expect(stringValue) == "Affected rows: 1" + } else { + fail("unexpected SubmissionFeedback type") + } + } + } + } + } + + enum ReplyType { + case choices + case text + case number + case freeAnswer + case math + case sorting + case matching + case code + case sql + + var submissionJSONString: String { + switch self { + case .choices: + return """ + { + "id": 164530189, + "status": "correct", + "score": 1, + "hint": "", + "feedback": "", + "time": "2020-01-30T10:13:15Z", + "reply": { + "choices": [ + false, + true, + false, + false + ] + }, + "reply_url": null, + "attempt": 155142602, + "session": null, + "eta": 0 + } + """ + case .text: + return """ + { + "id": 163700855, + "status": "wrong", + "score": 0, + "hint": "", + "feedback": "", + "time": "2020-01-27T09:54:55Z", + "reply": { + "text": "text", + "files": [] + }, + "reply_url": null, + "attempt": 145802794, + "session": null, + "eta": 0 + } + """ + case .number: + return """ + { + "id": 155034240, + "status": "correct", + "score": 1, + "hint": "Optional feedback on correct submission", + "feedback": "Optional feedback on correct submission", + "time": "2019-12-20T17:02:33Z", + "reply": { + "number": "25.5" + }, + "reply_url": null, + "attempt": 145800697, + "session": null, + "eta": 0 + } + """ + case .freeAnswer: + return """ + { + "id": 155035432, + "status": "correct", + "score": 1, + "hint": "", + "feedback": "", + "time": "2019-12-20T17:07:35Z", + "reply": { + "text": "test", + "attachments": [] + }, + "reply_url": null, + "attempt": 145801887, + "session": null, + "eta": 0 + } + """ + case .math: + return """ + { + "id": 163701768, + "status": "correct", + "score": 1, + "hint": "", + "feedback": "", + "time": "2020-01-27T09:58:06Z", + "reply": { + "formula": "2*x+y/z" + }, + "reply_url": null, + "attempt": 145803773, + "session": null, + "eta": 0 + } + """ + case .sorting: + return """ + { + "id": 163701921, + "status": "correct", + "score": 1, + "hint": "", + "feedback": "", + "time": "2020-01-27T09:58:38Z", + "reply": { + "ordering": [ + 0, + 1, + 2 + ] + }, + "reply_url": null, + "attempt": 145804003, + "session": null, + "eta": 0 + } + """ + case .matching: + return """ + { + "id": 163702173, + "status": "correct", + "score": 1, + "hint": "", + "feedback": "", + "time": "2020-01-27T09:59:29Z", + "reply": { + "ordering": [ + 2, + 1, + 0 + ] + }, + "reply_url": null, + "attempt": 145805676, + "session": null, + "eta": 0 + } + """ + case .code: + return #"{"id": 163968205, "status": "correct", "score": 1.0, "hint": "", "feedback": {"message": "", "code_style": {"errors": [{"code": "W293", "text": "blank line contains whitespace", "line": " ", "line_number": 1, "column_number": 0}, {"code": "W292", "text": "no newline at end of file", "line": " main()", "line_number": 8, "column_number": 10}]}}, "time": "2020-01-28T08:27:44Z", "reply": {"code": "def main():\n \n a, b = map(int, input().split())\n res = a + b\n print(res)\n\n\nif __name__ == \"__main__\":\n main()", "language": "python3"}, "reply_url": null, "attempt": 129167799, "session": null, "eta": 0}"# + case .sql: + return #"{"id": 163702543, "status": "correct", "score": 1.0, "hint": "Affected rows: 1", "feedback": "Affected rows: 1", "time": "2020-01-27T10:00:50Z", "reply": {"solve_sql": "INSERT INTO users (name) VALUES ('Fluttershy');\n"}, "reply_url": null, "attempt": 145794719, "session": null, "eta": 0}"# + } + } + } +} diff --git a/StepicTests/ModulesTests/NewStepViewControllerTests.swift b/StepicTests/ModulesTests/NewStepViewControllerTests.swift index b9addc807c..728d6f2b82 100644 --- a/StepicTests/ModulesTests/NewStepViewControllerTests.swift +++ b/StepicTests/ModulesTests/NewStepViewControllerTests.swift @@ -14,15 +14,21 @@ private final class NewStepViewControllerMock: StepViewControllerProtocol { } } - func displayStepTextUpdate(viewModel: StepDataFlow.StepTextUpdate.ViewModel) { } + func displayStepTextUpdate(viewModel: StepDataFlow.StepTextUpdate.ViewModel) {} - func displayPlayStep(viewModel: StepDataFlow.PlayStep.ViewModel) { } + func displayPlayStep(viewModel: StepDataFlow.PlayStep.ViewModel) {} - func displayControlsUpdate(viewModel: StepDataFlow.ControlsUpdate.ViewModel) { } + func displayControlsUpdate(viewModel: StepDataFlow.ControlsUpdate.ViewModel) {} - func displayDiscussionsButtonUpdate(viewModel: StepDataFlow.DiscussionsButtonUpdate.ViewModel) { } + func displayDiscussionsButtonUpdate(viewModel: StepDataFlow.DiscussionsButtonUpdate.ViewModel) {} - func displayDiscussions(viewModel: StepDataFlow.DiscussionsPresentation.ViewModel) { } + func displaySolutionsButtonUpdate(viewModel: StepDataFlow.SolutionsButtonUpdate.ViewModel) {} + + func displayDiscussions(viewModel: StepDataFlow.DiscussionsPresentation.ViewModel) {} + + func displaySolutions(viewModel: StepDataFlow.SolutionsPresentation.ViewModel) {} + + func displayBlockingLoadingIndicator(viewModel: StepDataFlow.BlockingWaitingIndicatorUpdate.ViewModel) {} } class NewStepViewControllerSpec: QuickSpec { From 7fecc6e7cbc1b49059389b6740cbbd5fd425d87e Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Mon, 3 Feb 2020 13:13:56 +0300 Subject: [PATCH 07/12] A/B explore search bar style (#640) * Add new explore search bar * Add split test --- Stepic.xcodeproj/project.pbxproj | 8 ++ .../ActiveSplitTestsContainer.swift | 2 +- .../ExploreSearchBarStyleSplitTest.swift | 37 ++++++++ .../Explore/ExploreViewController.swift | 19 +++- .../Explore/View/ExploreSearchBar.swift | 2 +- .../Explore/View/NewExploreSearchBar.swift | 92 +++++++++++++++++++ 6 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 Stepic/Analytics/SplitTests/ActiveTests/ExploreSearchBarStyleSplitTest.swift create mode 100644 Stepic/Sources/Modules/Explore/View/NewExploreSearchBar.swift diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index 58c71b4513..8918d55179 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -655,6 +655,8 @@ 2CC3519A1F68339A004255B6 /* AuthTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC351991F68339A004255B6 /* AuthTextField.swift */; }; 2CC3519C1F6837B4004255B6 /* RegistrationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC3519B1F6837B4004255B6 /* RegistrationViewController.swift */; }; 2CC3519D1F683E7C004255B6 /* AuthNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08D674921D78B46900B60963 /* AuthNavigationViewController.swift */; }; + 2CCA806723E81A4900057562 /* NewExploreSearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CCA806623E81A4900057562 /* NewExploreSearchBar.swift */; }; + 2CCA806923E822A300057562 /* ExploreSearchBarStyleSplitTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CCA806823E822A300057562 /* ExploreSearchBarStyleSplitTest.swift */; }; 2CCC505E21E8EA88004D9FC1 /* PersonalDeadlinesTimeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08194EB920B813AC00B34327 /* PersonalDeadlinesTimeService.swift */; }; 2CD462EA226F4279004E4725 /* FetchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD462E9226F4279004E4725 /* FetchResult.swift */; }; 2CD6E256234E042800F49303 /* EmailAddressesAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD6E255234E042800F49303 /* EmailAddressesAPI.swift */; }; @@ -1820,6 +1822,8 @@ 2CC351961F683140004255B6 /* EmailAuthViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmailAuthViewController.swift; sourceTree = ""; }; 2CC351991F68339A004255B6 /* AuthTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthTextField.swift; sourceTree = ""; }; 2CC3519B1F6837B4004255B6 /* RegistrationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegistrationViewController.swift; sourceTree = ""; }; + 2CCA806623E81A4900057562 /* NewExploreSearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewExploreSearchBar.swift; sourceTree = ""; }; + 2CCA806823E822A300057562 /* ExploreSearchBarStyleSplitTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreSearchBarStyleSplitTest.swift; sourceTree = ""; }; 2CCC7DBD219F09C60000540F /* Model_course_language_code_v27.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_course_language_code_v27.xcdatamodel; sourceTree = ""; }; 2CD462E9226F4279004E4725 /* FetchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchResult.swift; sourceTree = ""; }; 2CD6E254234DFFD500F49303 /* Model_email_addresses.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_email_addresses.xcdatamodel; sourceTree = ""; }; @@ -3781,6 +3785,7 @@ isa = PBXGroup; children = ( 08421BCA21764FC400E8A81B /* ActiveSplitTestsContainer.swift */, + 2CCA806823E822A300057562 /* ExploreSearchBarStyleSplitTest.swift */, ); path = ActiveTests; sourceTree = ""; @@ -5419,6 +5424,7 @@ 62E98D95D858F6D7A5D25EC4 /* ExploreBlockPlaceholderView.swift */, 62E9846ED8A00A1041FFB84E /* ExploreSearchBar.swift */, 62E980A41B871F8FD6D3A412 /* ExploreStoriesContainerView.swift */, + 2CCA806623E81A4900057562 /* NewExploreSearchBar.swift */, ); path = View; sourceTree = ""; @@ -6493,6 +6499,7 @@ 081B7E2A1BAC208200554153 /* StandardsExtensions.swift in Sources */, 0860D9151F10EA690087D61B /* InputAccessoryBuilder.swift in Sources */, 08D2AE4A1C05127500BD8C3D /* AnalyticsHelper.swift in Sources */, + 2CCA806923E822A300057562 /* ExploreSearchBarStyleSplitTest.swift in Sources */, 0860D9121F10C5480087D61B /* CodeSnippetSymbols.swift in Sources */, 083E49DD2072B684004896C0 /* IDFetchable.swift in Sources */, 2C29B63022C664C500730C16 /* ChoiceElementView.swift in Sources */, @@ -6985,6 +6992,7 @@ 62E981A406A91D91498A3B19 /* CourseInfoTabReviewsCellView.swift in Sources */, 62E98462B911999E78E315DB /* CourseInfoTabReviewsTableViewCell.swift in Sources */, 62E98E52C2821A15C7FA70F2 /* CourseInfoTabReviewsAssembly.swift in Sources */, + 2CCA806723E81A4900057562 /* NewExploreSearchBar.swift in Sources */, 2C53DFFB22DDDBFD0084BA2B /* NewCodeQuizViewController.swift in Sources */, 62E98BCFF2B85E440D0B23E8 /* CourseInfoTabReviewsViewController.swift in Sources */, 62E987367E4233CE076E5E04 /* CourseInfoTabReviewsInteractor.swift in Sources */, diff --git a/Stepic/Analytics/SplitTests/ActiveTests/ActiveSplitTestsContainer.swift b/Stepic/Analytics/SplitTests/ActiveTests/ActiveSplitTestsContainer.swift index 51e54c3fa8..ea26e239bf 100644 --- a/Stepic/Analytics/SplitTests/ActiveTests/ActiveSplitTestsContainer.swift +++ b/Stepic/Analytics/SplitTests/ActiveTests/ActiveSplitTestsContainer.swift @@ -15,6 +15,6 @@ final class ActiveSplitTestsContainer { ) static func setActiveTestsGroups() { - // There are no A/B tests now + self.splitTestingService.fetchSplitTest(ExploreSearchBarStyleSplitTest.self).setSplitTestGroup() } } diff --git a/Stepic/Analytics/SplitTests/ActiveTests/ExploreSearchBarStyleSplitTest.swift b/Stepic/Analytics/SplitTests/ActiveTests/ExploreSearchBarStyleSplitTest.swift new file mode 100644 index 0000000000..a854cd4654 --- /dev/null +++ b/Stepic/Analytics/SplitTests/ActiveTests/ExploreSearchBarStyleSplitTest.swift @@ -0,0 +1,37 @@ +import Foundation + +final class ExploreSearchBarStyleSplitTest: SplitTestProtocol { + typealias GroupType = Group + + static let identifier = "explore_search_bar_style" + static let minParticipatingStartVersion = "1.111" + + var currentGroup: Group + var analytics: ABAnalyticsServiceProtocol + + init(currentGroup: Group, analytics: ABAnalyticsServiceProtocol) { + self.currentGroup = currentGroup + self.analytics = analytics + } + + enum Group: String, SplitTestGroupProtocol { + case control = "control" + case test = "test" + + static var groups: [Group] = [.control, .test] + + var searchBarStyle: SearchBarStyle { + switch self { + case .control: + return .legacy + case .test: + return .new + } + } + } + + enum SearchBarStyle { + case new + case legacy + } +} diff --git a/Stepic/Sources/Modules/Explore/ExploreViewController.swift b/Stepic/Sources/Modules/Explore/ExploreViewController.swift index 183026c772..fa11c8b9d6 100644 --- a/Stepic/Sources/Modules/Explore/ExploreViewController.swift +++ b/Stepic/Sources/Modules/Explore/ExploreViewController.swift @@ -25,9 +25,25 @@ final class ExploreViewController: BaseExploreViewController { private var state: Explore.ViewControllerState private lazy var exploreInteractor = self.interactor as? ExploreInteractorProtocol + private let splitTestingService = SplitTestingService( + analyticsService: AnalyticsUserProperties(), + storage: UserDefaults.standard + ) + private var searchResultsModuleInput: SearchResultsModuleInputProtocol? private var searchResultsController: UIViewController? - private lazy var searchBar = ExploreSearchBar() + private lazy var searchBar: ExploreSearchBarProtocol = { + if ExploreSearchBarStyleSplitTest.shouldParticipate { + let splitTest = self.splitTestingService.fetchSplitTest(ExploreSearchBarStyleSplitTest.self) + switch splitTest.currentGroup.searchBarStyle { + case .new: + return NewExploreSearchBar() + case .legacy: + return ExploreSearchBar() + } + } + return ExploreSearchBar() + }() private var isStoriesHidden: Bool = false @@ -71,6 +87,7 @@ final class ExploreViewController: BaseExploreViewController { // Workaround for bug with black space under navigation bar due to different nav bar height // FIXME: see APPS-2093 + // https://stackoverflow.com/a/47976999 DispatchQueue.main.async { [weak self] in self?.navigationController?.view.setNeedsLayout() self?.navigationController?.view.layoutIfNeeded() diff --git a/Stepic/Sources/Modules/Explore/View/ExploreSearchBar.swift b/Stepic/Sources/Modules/Explore/View/ExploreSearchBar.swift index 441e19b357..075ae0b5d4 100644 --- a/Stepic/Sources/Modules/Explore/View/ExploreSearchBar.swift +++ b/Stepic/Sources/Modules/Explore/View/ExploreSearchBar.swift @@ -1,7 +1,7 @@ import SnapKit import UIKit -final class ExploreSearchBar: UISearchBar { +final class ExploreSearchBar: UISearchBar, ExploreSearchBarProtocol { enum Appearance { static let searchFieldPositionAdjustment = UIOffset(horizontal: -6, vertical: 0) static let textColor = UIColor.mainDark.withAlphaComponent(0.3) diff --git a/Stepic/Sources/Modules/Explore/View/NewExploreSearchBar.swift b/Stepic/Sources/Modules/Explore/View/NewExploreSearchBar.swift new file mode 100644 index 0000000000..eb252c92e7 --- /dev/null +++ b/Stepic/Sources/Modules/Explore/View/NewExploreSearchBar.swift @@ -0,0 +1,92 @@ +import SnapKit +import UIKit + +protocol ExploreSearchBarProtocol: UISearchBar { + var searchBarDelegate: UISearchBarDelegate? { get set } +} + +final class NewExploreSearchBar: UISearchBar, ExploreSearchBarProtocol { + enum Appearance { + static let textColor = UIColor.mainDark + + // Height should be fixed and leq than 44pt (due to iOS 11+ strange nav bar) + static let barHeight: CGFloat = 44.0 + + static let placeholderText = NSLocalizedString("SearchCourses", comment: "") + } + + weak var searchBarDelegate: UISearchBarDelegate? + + override var delegate: UISearchBarDelegate? { + willSet { + if newValue !== self { + fatalError("Use property searchBarDelegate to set or get delegate") + } + } + } + + private var searchField: UITextField? { + if #available(iOS 13.0, *) { + return self.searchTextField + } else { + return self.value(forKey: "searchField") as? UITextField + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + self.delegate = self + + self.isTranslucent = false + + self.searchField?.backgroundColor = .clear + self.searchField?.textColor = Appearance.textColor + self.placeholder = Appearance.placeholderText + self.searchField?.rightViewMode = .whileEditing + self.searchBarStyle = .minimal + + self.applySystemFixes() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func applySystemFixes() { + self.translatesAutoresizingMaskIntoConstraints = false + self.snp.makeConstraints { make in + make.height.equalTo(Appearance.barHeight) + } + } +} + +extension NewExploreSearchBar: UISearchBarDelegate { + func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { + searchBar.setShowsCancelButton(true, animated: true) + self.searchBarDelegate?.searchBarTextDidBeginEditing?(searchBar) + } + + func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { + searchBar.setShowsCancelButton(false, animated: true) + self.searchBarDelegate?.searchBarTextDidEndEditing?(searchBar) + } + + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + searchBar.resignFirstResponder() + searchBar.text?.removeAll() + searchBar.endEditing(true) + + searchBar.setShowsCancelButton(false, animated: true) + self.searchBarDelegate?.searchBarCancelButtonClicked?(searchBar) + } + + func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { + self.searchBarDelegate?.searchBar?(searchBar, textDidChange: searchText) + } + + func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { + searchBar.resignFirstResponder() + self.searchBarDelegate?.searchBarSearchButtonClicked?(searchBar) + } +} From 39b3f74739674beb571530690c21739fddd0f1a7 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Mon, 3 Feb 2020 18:04:22 +0300 Subject: [PATCH 08/12] Course progress with points (#641) * Continue course progress with points * Course section & unit progress with points --- Stepic/ContinueLastStepView.swift | 1 + .../CourseInfoTabSyllabusPresenter.swift | 10 ++++++++-- .../Cell/CourseInfoTabSyllabusCellStatsView.swift | 6 +++--- .../Views/Cell/CourseInfoTabSyllabusCellView.swift | 8 ++++---- .../ContinueCourse/ContinueCoursePresenter.swift | 9 ++++++--- .../ContinueCourse/ContinueCourseView.swift | 3 +-- Stepic/en.lproj/Localizable.strings | 5 ++++- Stepic/ru.lproj/Localizable.strings | 5 ++++- 8 files changed, 31 insertions(+), 16 deletions(-) diff --git a/Stepic/ContinueLastStepView.swift b/Stepic/ContinueLastStepView.swift index a365d0330a..fe6f8f265d 100644 --- a/Stepic/ContinueLastStepView.swift +++ b/Stepic/ContinueLastStepView.swift @@ -84,6 +84,7 @@ final class ContinueLastStepView: UIView { let label = UILabel() label.textColor = self.appearance.progressLabelTextColor label.font = self.appearance.progressLabelFont + label.lineBreakMode = .byWordWrapping return label }() diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/CourseInfoTabSyllabusPresenter.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/CourseInfoTabSyllabusPresenter.swift index fc5fb7e095..47c0965685 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/CourseInfoTabSyllabusPresenter.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/CourseInfoTabSyllabusPresenter.swift @@ -227,7 +227,10 @@ final class CourseInfoTabSyllabusPresenter: CourseInfoTabSyllabusPresenterProtoc return nil } - return "\(progress.score)/\(progress.cost)" + return String( + format: NSLocalizedString("CourseInfoTabSyllabusSectionProgressTitle", comment: ""), + arguments: ["\(progress.score)", "\(progress.cost)"] + ) }() let requirementsLabelText = self.makeFormattedSectionRequirementsText( @@ -281,7 +284,10 @@ final class CourseInfoTabSyllabusPresenter: CourseInfoTabSyllabusPresenterProtoc return nil } - return "\(progress.score)/\(progress.cost)" + return String( + format: NSLocalizedString("CourseInfoTabSyllabusUnitProgressTitle", comment: ""), + arguments: ["\(progress.score)", "\(progress.cost)"] + ) }() let timeToCompleteLabelText: String? = { diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/Views/Cell/CourseInfoTabSyllabusCellStatsView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/Views/Cell/CourseInfoTabSyllabusCellStatsView.swift index ea4145b2ec..9498267ba4 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/Views/Cell/CourseInfoTabSyllabusCellStatsView.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/Views/Cell/CourseInfoTabSyllabusCellStatsView.swift @@ -4,7 +4,7 @@ import UIKit extension CourseInfoTabSyllabusCellStatsView { struct Appearance { - let itemsSpacing: CGFloat = 20.0 + let itemsSpacing: CGFloat = 8.0 let itemTextFont = UIFont.systemFont(ofSize: 12, weight: .light) let itemTextColor = UIColor.mainDark @@ -132,10 +132,10 @@ extension CourseInfoTabSyllabusCellStatsView: ProgrammaticallyInitializableViewP func addSubviews() { self.addSubview(self.itemsStackView) - self.itemsStackView.addArrangedSubview(self.learnersView) - self.itemsStackView.addArrangedSubview(self.likesView) self.itemsStackView.addArrangedSubview(self.progressView) self.itemsStackView.addArrangedSubview(self.timeToCompleteView) + self.itemsStackView.addArrangedSubview(self.learnersView) + self.itemsStackView.addArrangedSubview(self.likesView) } func makeConstraints() { diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/Views/Cell/CourseInfoTabSyllabusCellView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/Views/Cell/CourseInfoTabSyllabusCellView.swift index 64dafc0d35..05ce3e44b2 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/Views/Cell/CourseInfoTabSyllabusCellView.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/Views/Cell/CourseInfoTabSyllabusCellView.swift @@ -11,7 +11,7 @@ extension CourseInfoTabSyllabusCellView { let titleTextColor = UIColor.mainDark let titleFont = UIFont.systemFont(ofSize: 14) - let titleLabelInsets = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 8) + let titleLabelInsets = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 16) let downloadButtonInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16) let downloadButtonSize = CGSize(width: 22, height: 22) @@ -21,7 +21,7 @@ extension CourseInfoTabSyllabusCellView { let downloadedSizeLabelTextColor = UIColor.mainDark let downloadedSizeLabelInsets = UIEdgeInsets(top: 2, left: 0, bottom: 0, right: 16) - let statsInsets = UIEdgeInsets(top: 10, left: 0, bottom: 20, right: 0) + let statsInsets = UIEdgeInsets(top: 10, left: 0, bottom: 20, right: 16) let statsViewHeight: CGFloat = 17.0 let progressViewHeight: CGFloat = 3 @@ -258,7 +258,7 @@ extension CourseInfoTabSyllabusCellView: ProgrammaticallyInitializableViewProtoc self.titleLabel.setContentCompressionResistancePriority(.required, for: .vertical) self.titleLabel.snp.makeConstraints { make in make.leading.equalTo(self.coverImageView.snp.trailing).offset(self.appearance.titleLabelInsets.left) - make.trailing.equalTo(self.downloadButton.snp.leading).offset(-self.appearance.titleLabelInsets.left) + make.trailing.equalTo(self.downloadButton.snp.leading).offset(-self.appearance.titleLabelInsets.right) make.top.equalTo(self.coverImageView.snp.top) } @@ -267,7 +267,7 @@ extension CourseInfoTabSyllabusCellView: ProgrammaticallyInitializableViewProtoc self.statsView.snp.makeConstraints { make in make.height.equalTo(self.appearance.statsViewHeight) make.leading.equalTo(self.titleLabel.snp.leading) - make.trailing.lessThanOrEqualTo(self.titleLabel.snp.trailing) + make.trailing.lessThanOrEqualToSuperview().offset(-self.appearance.statsInsets.right) make.top.equalTo(self.titleLabel.snp.bottom).offset(self.appearance.statsInsets.top) make.bottom.equalToSuperview().offset(-self.appearance.statsInsets.bottom) } diff --git a/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCoursePresenter.swift b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCoursePresenter.swift index 198b5d1702..1cb9fa3636 100644 --- a/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCoursePresenter.swift +++ b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCoursePresenter.swift @@ -29,10 +29,13 @@ final class ContinueCoursePresenter: ContinueCoursePresenterProtocol { if let progress = course.progress { var normalizedPercent = progress.percentPassed normalizedPercent.round(.up) - return ( - description: "\(progress.score)/\(progress.cost)", - value: normalizedPercent / 100 + + let progressText = String( + format: NSLocalizedString("ContinueCourseCourseCurrentProgressTitle", comment: ""), + arguments: ["\(progress.score)", "\(progress.cost)"] ) + + return (description: progressText, value: normalizedPercent / 100) } return nil }() diff --git a/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseView.swift b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseView.swift index 5fae80f057..5b43c3fa0b 100644 --- a/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseView.swift +++ b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseView.swift @@ -30,8 +30,7 @@ final class ContinueCourseView: UIView { if let progressDescription = viewModel.progress?.description, let progressValue = viewModel.progress?.value { - self.lastStepView.progressText = "\(NSLocalizedString("YourCurrentProgressIs", comment: "")) " - + "\(progressDescription)" + self.lastStepView.progressText = progressDescription self.lastStepView.progress = progressValue } self.lastStepView.coverImageURL = viewModel.coverImageURL diff --git a/Stepic/en.lproj/Localizable.strings b/Stepic/en.lproj/Localizable.strings index fa14e160e9..1fdce30234 100644 --- a/Stepic/en.lproj/Localizable.strings +++ b/Stepic/en.lproj/Localizable.strings @@ -323,7 +323,6 @@ Enrolled = "Enrolled"; Popular = "Popular"; Home = "Home"; RecommendedCategory = "Recommended courses"; -YourCurrentProgressIs = "Current progress:"; HomePlaceholderAnonymous = "Sign in and start learning right now"; HomePlaceholderEmptyEnrolled = "Enroll for free courses and they will be here"; HomePlaceholderEmptyPopular = "Open courses are on vacation. Please, see later"; @@ -437,10 +436,14 @@ NotificationTabNotificationRequestAlertMessage = "We are trying to make learning CourseSubscriptionNotificationRequestAlertTitle = "Stay tuned"; CourseSubscriptionNotificationRequestAlertMessage = "We are trying to make learning process effective and comfortable. Enable notifications to follow deadlines and promptly receive course updates?"; +ContinueCourseCourseCurrentProgressTitle = "Current progress: %@/%@ points"; + /* Course info */ CourseInfoTitle = "About course"; CourseInfoTabInfo = "Info"; CourseInfoTabSyllabus = "Syllabus"; +CourseInfoTabSyllabusSectionProgressTitle = "%@/%@ points"; +CourseInfoTabSyllabusUnitProgressTitle = "%@/%@ points"; CourseInfoTabSyllabusFailedLoadVideoAlertMessage = "Sorry, but something went wrong, please retry the download."; CourseInfoTabSyllabusDeleteCourseDownloadsConfirmationTitle = "Delete course"; CourseInfoTabSyllabusDeleteCourseDownloadsConfirmationMessage = "Are you sure you want to delete downloaded course?"; diff --git a/Stepic/ru.lproj/Localizable.strings b/Stepic/ru.lproj/Localizable.strings index 2c1c9281f0..e11504fc88 100644 --- a/Stepic/ru.lproj/Localizable.strings +++ b/Stepic/ru.lproj/Localizable.strings @@ -324,7 +324,6 @@ Enrolled = "Мои курсы"; Popular = "Популярные"; Home = "Обучение"; RecommendedCategory = "Подборка"; -YourCurrentProgressIs = "Текущий прогресс:"; HomePlaceholderAnonymous = "Войдите и начните учиться прямо сейчас"; HomePlaceholderEmptyEnrolled = "Запишитесь на курсы и они будут показаны здесь"; HomePlaceholderEmptyPopular = "Открытые курсы сейчас в отпуске. Пожалуйста, зайдите позже"; @@ -439,10 +438,14 @@ NotificationTabNotificationRequestAlertMessage = "Мы пытаемся сдел CourseSubscriptionNotificationRequestAlertTitle = "Следи за обновлениями"; CourseSubscriptionNotificationRequestAlertMessage = "Мы пытаемся сделать процесс обучения максимально удобным и полезным. Разрешить уведомления, чтобы следить за дедлайнами и оперативно получать обновления по курсу?"; +ContinueCourseCourseCurrentProgressTitle = "Текущий прогресс: %@/%@ баллов"; + /* Course info */ CourseInfoTitle = "О курсе"; CourseInfoTabInfo = "Инфо"; CourseInfoTabSyllabus = "Модули"; +CourseInfoTabSyllabusSectionProgressTitle = "%@/%@ баллов"; +CourseInfoTabSyllabusUnitProgressTitle = "%@/%@ баллов"; CourseInfoTabSyllabusFailedLoadVideoAlertMessage = "Не удалось загрузить видео, повторите загрузку позже."; CourseInfoTabSyllabusDeleteCourseDownloadsConfirmationTitle = "Удалить курс"; CourseInfoTabSyllabusDeleteCourseDownloadsConfirmationMessage = "Вы уверены, что хотите удалить загруженный курс?"; From 6116d319a91c5ee6508e82a102e3ac67ea0df317 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Mon, 3 Feb 2020 19:25:47 +0300 Subject: [PATCH 09/12] Fix discussions solution control constraints (#642) * Fix constraints --- .../Views/DiscussionsSolutionControl.swift | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Stepic/Sources/Modules/Discussions/Views/DiscussionsSolutionControl.swift b/Stepic/Sources/Modules/Discussions/Views/DiscussionsSolutionControl.swift index 24764cd56f..8919092292 100644 --- a/Stepic/Sources/Modules/Discussions/Views/DiscussionsSolutionControl.swift +++ b/Stepic/Sources/Modules/Discussions/Views/DiscussionsSolutionControl.swift @@ -9,7 +9,6 @@ extension DiscussionsSolutionControl { let borderWidth: CGFloat = 1 let borderColor = UIColor(hex: 0xCCCCCC) - let iconSize = CGSize(width: 24, height: 24) let iconInsets = LayoutInsets(top: 8, left: 8, bottom: 8, right: 8) let titleTextColor = UIColor.mainDark @@ -132,19 +131,21 @@ extension DiscussionsSolutionControl: ProgrammaticallyInitializableViewProtocol } func makeConstraints() { + self.imageView.setContentCompressionResistancePriority(.required, for: .horizontal) self.imageView.translatesAutoresizingMaskIntoConstraints = false self.imageView.snp.makeConstraints { make in - make.size.equalTo(self.appearance.iconSize) - make.top.equalToSuperview().offset(self.appearance.iconInsets.top) make.leading.equalToSuperview().offset(self.appearance.iconInsets.left) - make.bottom.equalToSuperview().offset(-self.appearance.iconInsets.bottom) + make.trailing.equalTo(self.titleLabel.snp.leading).offset(-self.appearance.iconInsets.right) + make.centerY.equalTo(self.titleLabel.snp.centerY) } + self.titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) self.titleLabel.translatesAutoresizingMaskIntoConstraints = false self.titleLabel.snp.makeConstraints { make in - make.centerY.equalTo(self.imageView.snp.centerY) - make.leading.equalTo(self.imageView.snp.trailing).offset(self.appearance.titleInsets.left) - make.trailing.equalToSuperview().offset(-self.appearance.titleInsets.right) + make.centerY.equalToSuperview() + make.top.greaterThanOrEqualToSuperview().offset(self.appearance.titleInsets.top) + make.bottom.lessThanOrEqualToSuperview().offset(-self.appearance.titleInsets.bottom) + make.trailing.lessThanOrEqualToSuperview().offset(-self.appearance.titleInsets.right) } } } From 24be2722617712e3e208054eae0a9419c16da3b5 Mon Sep 17 00:00:00 2001 From: Stepik Bot Date: Tue, 4 Feb 2020 03:00:42 +0000 Subject: [PATCH 10/12] "Set version to 1.111 & bump build" --- Stepic.xcodeproj/project.pbxproj | 4 ++-- Stepic/Info.plist | 4 ++-- StepicTests/Info.plist | 4 ++-- StickerPackExtension/Info.plist | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index 8918d55179..1531736d29 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -7717,7 +7717,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 177; + CURRENT_PROJECT_VERSION = 178; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = Stepic/Info.plist; @@ -7747,7 +7747,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 177; + CURRENT_PROJECT_VERSION = 178; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = Stepic/Info.plist; diff --git a/Stepic/Info.plist b/Stepic/Info.plist index c632ac037b..30d4e78112 100644 --- a/Stepic/Info.plist +++ b/Stepic/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.110 + 1.111 CFBundleSignature ???? CFBundleURLTypes @@ -54,7 +54,7 @@ CFBundleVersion - 177 + 178 Fabric APIKey diff --git a/StepicTests/Info.plist b/StepicTests/Info.plist index 7c3fe4c247..d1727d7eaf 100644 --- a/StepicTests/Info.plist +++ b/StepicTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.110 + 1.111 CFBundleSignature ???? CFBundleVersion - 177 + 178 diff --git a/StickerPackExtension/Info.plist b/StickerPackExtension/Info.plist index f19f820b74..b41afd7192 100644 --- a/StickerPackExtension/Info.plist +++ b/StickerPackExtension/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.110 + 1.111 CFBundleVersion - 177 + 178 NSExtension NSExtensionPointIdentifier From f471051a32fef85a6911819363a95e54ab938e1c Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Tue, 4 Feb 2020 14:23:02 +0300 Subject: [PATCH 11/12] Fix solution link --- Stepic/Sources/Modules/Discussions/DiscussionsDataFlow.swift | 4 ++-- .../Sources/Modules/Discussions/DiscussionsInteractor.swift | 2 +- Stepic/Sources/Modules/Solution/SolutionAssembly.swift | 4 ++-- Stepic/Sources/Modules/Solution/SolutionDataFlow.swift | 2 +- Stepic/Sources/Modules/Solution/SolutionInteractor.swift | 4 ++-- Stepic/Sources/Modules/Solution/SolutionPresenter.swift | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Stepic/Sources/Modules/Discussions/DiscussionsDataFlow.swift b/Stepic/Sources/Modules/Discussions/DiscussionsDataFlow.swift index 0fe11d4a34..e63188abca 100644 --- a/Stepic/Sources/Modules/Discussions/DiscussionsDataFlow.swift +++ b/Stepic/Sources/Modules/Discussions/DiscussionsDataFlow.swift @@ -212,13 +212,13 @@ enum Discussions { struct Response { let stepID: Step.IdType let submission: Submission - let discussionID: DiscussionThread.IdType + let discussionID: Comment.IdType } struct ViewModel { let stepID: Step.IdType let submission: Submission - let discussionID: DiscussionThread.IdType + let discussionID: Comment.IdType } } diff --git a/Stepic/Sources/Modules/Discussions/DiscussionsInteractor.swift b/Stepic/Sources/Modules/Discussions/DiscussionsInteractor.swift index 9bad642e2e..7ed0af340c 100644 --- a/Stepic/Sources/Modules/Discussions/DiscussionsInteractor.swift +++ b/Stepic/Sources/Modules/Discussions/DiscussionsInteractor.swift @@ -403,7 +403,7 @@ final class DiscussionsInteractor: DiscussionsInteractorProtocol { } self.presenter.presentSolution( - response: .init(stepID: self.stepID, submission: submission, discussionID: self.discussionProxyID) + response: .init(stepID: self.stepID, submission: submission, discussionID: comment.id) ) } diff --git a/Stepic/Sources/Modules/Solution/SolutionAssembly.swift b/Stepic/Sources/Modules/Solution/SolutionAssembly.swift index b4c3527685..e75f80bb91 100644 --- a/Stepic/Sources/Modules/Solution/SolutionAssembly.swift +++ b/Stepic/Sources/Modules/Solution/SolutionAssembly.swift @@ -3,9 +3,9 @@ import UIKit final class SolutionAssembly: Assembly { private let stepID: Step.IdType private let submission: Submission - private let discussionID: DiscussionThread.IdType + private let discussionID: Comment.IdType - init(stepID: Step.IdType, submission: Submission, discussionID: DiscussionThread.IdType) { + init(stepID: Step.IdType, submission: Submission, discussionID: Comment.IdType) { self.stepID = stepID self.submission = submission self.discussionID = discussionID diff --git a/Stepic/Sources/Modules/Solution/SolutionDataFlow.swift b/Stepic/Sources/Modules/Solution/SolutionDataFlow.swift index c5719fb3f7..12c2429e36 100644 --- a/Stepic/Sources/Modules/Solution/SolutionDataFlow.swift +++ b/Stepic/Sources/Modules/Solution/SolutionDataFlow.swift @@ -5,7 +5,7 @@ enum Solution { struct Data { let step: Step let submission: Submission - let discussionID: DiscussionThread.IdType + let discussionID: Comment.IdType } struct Request {} diff --git a/Stepic/Sources/Modules/Solution/SolutionInteractor.swift b/Stepic/Sources/Modules/Solution/SolutionInteractor.swift index dec1c45d66..d39b9b0089 100644 --- a/Stepic/Sources/Modules/Solution/SolutionInteractor.swift +++ b/Stepic/Sources/Modules/Solution/SolutionInteractor.swift @@ -8,7 +8,7 @@ protocol SolutionInteractorProtocol { final class SolutionInteractor: SolutionInteractorProtocol { private let stepID: Step.IdType private let submission: Submission - private let discussionID: DiscussionThread.IdType + private let discussionID: Comment.IdType private let presenter: SolutionPresenterProtocol private let provider: SolutionProviderProtocol @@ -16,7 +16,7 @@ final class SolutionInteractor: SolutionInteractorProtocol { init( stepID: Step.IdType, submission: Submission, - discussionID: DiscussionThread.IdType, + discussionID: Comment.IdType, presenter: SolutionPresenterProtocol, provider: SolutionProviderProtocol ) { diff --git a/Stepic/Sources/Modules/Solution/SolutionPresenter.swift b/Stepic/Sources/Modules/Solution/SolutionPresenter.swift index 2df2ec0183..82956dd855 100644 --- a/Stepic/Sources/Modules/Solution/SolutionPresenter.swift +++ b/Stepic/Sources/Modules/Solution/SolutionPresenter.swift @@ -24,7 +24,7 @@ final class SolutionPresenter: SolutionPresenterProtocol { private func makeViewModel( step: Step, submission: Submission, - discussionID: DiscussionThread.IdType + discussionID: Comment.IdType ) -> SolutionViewModel { let quizStatus: QuizStatus = { switch submission.status { @@ -111,7 +111,7 @@ final class SolutionPresenter: SolutionPresenterProtocol { return processor.processContent() } - private func makeURL(for step: Step, discussionID: DiscussionThread.IdType) -> URL? { + private func makeURL(for step: Step, discussionID: Comment.IdType) -> URL? { let link = "\(StepikApplicationsInfo.stepikURL)" + "/lesson/\(step.lessonID)" + "/step/\(step.position)" From 90d0b412ddf851d1bb3ecb46690ef91edbc57633 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Tue, 4 Feb 2020 14:23:54 +0300 Subject: [PATCH 12/12] Bump build --- Stepic.xcodeproj/project.pbxproj | 4 ++-- Stepic/Info.plist | 2 +- StepicTests/Info.plist | 2 +- StickerPackExtension/Info.plist | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index 1531736d29..84c10e9b48 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -7717,7 +7717,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 178; + CURRENT_PROJECT_VERSION = 179; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = Stepic/Info.plist; @@ -7747,7 +7747,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 178; + CURRENT_PROJECT_VERSION = 179; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = Stepic/Info.plist; diff --git a/Stepic/Info.plist b/Stepic/Info.plist index 30d4e78112..02701dd52a 100644 --- a/Stepic/Info.plist +++ b/Stepic/Info.plist @@ -54,7 +54,7 @@ CFBundleVersion - 178 + 179 Fabric APIKey diff --git a/StepicTests/Info.plist b/StepicTests/Info.plist index d1727d7eaf..0ef156608a 100644 --- a/StepicTests/Info.plist +++ b/StepicTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 178 + 179 diff --git a/StickerPackExtension/Info.plist b/StickerPackExtension/Info.plist index b41afd7192..93c20914d7 100644 --- a/StickerPackExtension/Info.plist +++ b/StickerPackExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.111 CFBundleVersion - 178 + 179 NSExtension NSExtensionPointIdentifier