From e95a857305e44decf39adbde678b059157f6f57e Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Mon, 5 Jul 2021 15:32:50 +0300 Subject: [PATCH 1/8] User courses tab downloaded (#1004) * Add downloaded course list * Add analytics * Update course list on enrollment changes --- .../Events/AmplitudeAnalyticsEvents.swift | 13 +++++ .../CourseListDataBackUpdateService.swift | 4 ++ .../Provider/CourseListNetworkService.swift | 51 +++++++++++++------ .../CourseListPersistenceService.swift | 20 ++++++++ .../CourseList/Provider/CourseListTypes.swift | 14 ++++- .../Modules/Downloads/DownloadsProvider.swift | 15 ++++++ .../UserCourses/UserCoursesAssembly.swift | 3 +- .../UserCourses/UserCoursesDataFlow.swift | 9 ++-- .../UserCoursesViewController.swift | 37 ++++++++++++-- Stepic/en.lproj/Localizable.strings | 1 + Stepic/ru.lproj/Localizable.strings | 1 + 11 files changed, 144 insertions(+), 24 deletions(-) diff --git a/Stepic/Legacy/Analytics/Events/AmplitudeAnalyticsEvents.swift b/Stepic/Legacy/Analytics/Events/AmplitudeAnalyticsEvents.swift index cfb7a0ad73..0830ee1e67 100644 --- a/Stepic/Legacy/Analytics/Events/AmplitudeAnalyticsEvents.swift +++ b/Stepic/Legacy/Analytics/Events/AmplitudeAnalyticsEvents.swift @@ -987,6 +987,19 @@ extension AnalyticsEvent { ) } + // MARK: - UserCourses - + + static let myCoursesScreenOpened = AmplitudeAnalyticsEvent(name: "My courses screen opened") + + static func myCoursesScreenTabOpened(tab: UserCourses.Tab) -> AmplitudeAnalyticsEvent { + AmplitudeAnalyticsEvent( + name: "My courses screen tab opened", + parameters: [ + "tab": tab.rawValue + ] + ) + } + // MARK: - Wishlist - static func wishlistCourseAdded( diff --git a/Stepic/Sources/Modules/CourseList/Interactor/CourseListDataBackUpdateService.swift b/Stepic/Sources/Modules/CourseList/Interactor/CourseListDataBackUpdateService.swift index 1e46094b79..f9f6f5a3c5 100644 --- a/Stepic/Sources/Modules/CourseList/Interactor/CourseListDataBackUpdateService.swift +++ b/Stepic/Sources/Modules/CourseList/Interactor/CourseListDataBackUpdateService.swift @@ -75,6 +75,10 @@ extension CourseListDataBackUpdateService: DataBackUpdateServiceDelegate { if update.contains(.enrollment) { self.handleCourse(course, didUpdateEnrollment: update) + + if self.courseListType is DownloadedCourseListType { + self.delegate?.courseListDataBackUpdateServiceDidUpdateCourseList(self) + } } // If isArchived or isFavorite state was updated then handle specified update and refresh course list diff --git a/Stepic/Sources/Modules/CourseList/Provider/CourseListNetworkService.swift b/Stepic/Sources/Modules/CourseList/Provider/CourseListNetworkService.swift index 439bf5bf24..be654969e4 100644 --- a/Stepic/Sources/Modules/CourseList/Provider/CourseListNetworkService.swift +++ b/Stepic/Sources/Modules/CourseList/Provider/CourseListNetworkService.swift @@ -373,31 +373,52 @@ final class RecommendationsCourseListNetworkService: BaseCourseListNetworkServic } } -final class WishlistCourseListNetworkService: BaseCourseListNetworkService, CourseListNetworkServiceProtocol { - let type: WishlistCourseListType +class BaseCacheCoursesIDsSourceCourseListNetworkService: BaseCourseListNetworkService, + CourseListNetworkServiceProtocol { + func getCoursesIDs() -> Promise<[Course.IdType]> { .value([]) } + + func fetch(page: Int, filterQuery: CourseListFilterQuery?) -> Promise<([Course], Meta)> { + Promise { seal in + self.getCoursesIDs().then { coursesIDs -> Promise<([Course.IdType], [Course])> in + self.coursesAPI.retrieve(ids: coursesIDs).map { (coursesIDs, $0) } + }.done { coursesIDs, courses in + let result = courses.reordered(order: coursesIDs, transform: { $0.id }) + seal.fulfill((result, .oneAndOnlyPage)) + }.catch { _ in + seal.reject(Error.fetchFailed) + } + } + } +} + +final class WishlistCourseListNetworkService: BaseCacheCoursesIDsSourceCourseListNetworkService { private let wishlistStorageManager: WishlistStorageManagerProtocol init( - type: WishlistCourseListType, coursesAPI: CoursesAPI, wishlistStorageManager: WishlistStorageManagerProtocol ) { - self.type = type self.wishlistStorageManager = wishlistStorageManager super.init(coursesAPI: coursesAPI) } - func fetch(page: Int, filterQuery: CourseListFilterQuery?) -> Promise<([Course], Meta)> { - let coursesIDs = self.wishlistStorageManager.coursesIDs - let finalMeta = Meta.oneAndOnlyPage + override func getCoursesIDs() -> Promise<[Course.IdType]> { + .value(self.wishlistStorageManager.coursesIDs) + } +} - return Promise { seal in - self.coursesAPI.retrieve(ids: coursesIDs).done { courses in - let courses = courses.reordered(order: coursesIDs, transform: { $0.id }) - seal.fulfill((courses, finalMeta)) - }.catch { _ in - seal.reject(Error.fetchFailed) - } - } +final class DownloadedCourseListNetworkService: BaseCacheCoursesIDsSourceCourseListNetworkService { + private let downloadedCourseListPersistenceService: DownloadedCourseListPersistenceService + + init( + coursesAPI: CoursesAPI, + downloadedCourseListPersistenceService: DownloadedCourseListPersistenceService + ) { + self.downloadedCourseListPersistenceService = downloadedCourseListPersistenceService + super.init(coursesAPI: coursesAPI) + } + + override func getCoursesIDs() -> Promise<[Course.IdType]> { + self.downloadedCourseListPersistenceService.fetch().mapValues(\.id) } } diff --git a/Stepic/Sources/Modules/CourseList/Provider/CourseListPersistenceService.swift b/Stepic/Sources/Modules/CourseList/Provider/CourseListPersistenceService.swift index e5cde48e5d..3747ec76d5 100644 --- a/Stepic/Sources/Modules/CourseList/Provider/CourseListPersistenceService.swift +++ b/Stepic/Sources/Modules/CourseList/Provider/CourseListPersistenceService.swift @@ -81,3 +81,23 @@ extension VisitedCourseListPersistenceService: VisitedCourseListPersistenceServi self.updateStorageUsingCurrentData() } } + +// MARK: - DownloadedCourseListPersistenceService: CourseListPersistenceService - + +final class DownloadedCourseListPersistenceService: CourseListPersistenceService { + private let downloadsProvider: DownloadsProviderProtocol + + init(downloadsProvider: DownloadsProviderProtocol = DownloadsProvider.default) { + self.downloadsProvider = downloadsProvider + super.init(storage: PassiveCourseListPersistenceStorage(cachedList: [])) + } + + override func fetch() -> Promise<[Course]> { + self.downloadsProvider.fetchCachedCourses().then { courses -> Promise<[Course]> in + let resultCourses = courses.filter(\.enrolled).sorted { $0.id < $1.id } + return .value(resultCourses) + } + } + + override func update(newCachedList: [Course]) {} +} diff --git a/Stepic/Sources/Modules/CourseList/Provider/CourseListTypes.swift b/Stepic/Sources/Modules/CourseList/Provider/CourseListTypes.swift index 74a0ed22dd..dbbfe85b05 100644 --- a/Stepic/Sources/Modules/CourseList/Provider/CourseListTypes.swift +++ b/Stepic/Sources/Modules/CourseList/Provider/CourseListTypes.swift @@ -21,6 +21,10 @@ struct FavoriteCourseListType: CourseListType { var analyticName: String { "favorite_course_list" } } +struct DownloadedCourseListType: CourseListType { + var analyticName: String { "downloaded_course_list" } +} + struct ArchivedCourseListType: CourseListType { var analyticName: String { "archived_course_list" } } @@ -123,6 +127,8 @@ final class CourseListServicesFactory { cacheID: "MyCoursesInfoFavorite" ) ) + } else if self.type is DownloadedCourseListType { + return DownloadedCourseListPersistenceService() } else if self.type is ArchivedCourseListType { return CourseListPersistenceService( storage: DefaultsCourseListPersistenceStorage( @@ -224,12 +230,16 @@ final class CourseListServicesFactory { courseRecommendationsAPI: self.courseRecommendationsAPI ) ) - } else if let type = self.type as? WishlistCourseListType { + } else if type is WishlistCourseListType { return WishlistCourseListNetworkService( - type: type, coursesAPI: self.coursesAPI, wishlistStorageManager: WishlistStorageManager() ) + } else if type is DownloadedCourseListType { + return DownloadedCourseListNetworkService( + coursesAPI: self.coursesAPI, + downloadedCourseListPersistenceService: DownloadedCourseListPersistenceService() + ) } else { fatalError("Unsupported course list type") } diff --git a/Stepic/Sources/Modules/Downloads/DownloadsProvider.swift b/Stepic/Sources/Modules/Downloads/DownloadsProvider.swift index a2fa8a2341..e5915ce6a7 100644 --- a/Stepic/Sources/Modules/Downloads/DownloadsProvider.swift +++ b/Stepic/Sources/Modules/Downloads/DownloadsProvider.swift @@ -102,3 +102,18 @@ final class DownloadsProvider: DownloadsProviderProtocol { return resultSteps } } + +extension DownloadsProvider { + static var `default`: DownloadsProvider { + DownloadsProvider( + coursesPersistenceService: CoursesPersistenceService(), + adaptiveStorageManager: AdaptiveStorageManager.shared, + videoFileManager: VideoStoredFileManager(fileManager: .default), + imageFileManager: ImageStoredFileManager(fileManager: .default), + storageUsageService: StorageUsageService( + videoFileManager: VideoStoredFileManager(fileManager: .default), + imageFileManager: ImageStoredFileManager(fileManager: .default) + ) + ) + } +} diff --git a/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesAssembly.swift b/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesAssembly.swift index 7930dfbd6f..011af1f1ba 100644 --- a/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesAssembly.swift +++ b/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesAssembly.swift @@ -7,7 +7,8 @@ final class UserCoursesAssembly: Assembly { let viewController = UserCoursesViewController( interactor: interactor, availableTabs: UserCourses.Tab.allCases, - initialTab: .allCourses + initialTab: .allCourses, + analytics: StepikAnalytics.shared ) presenter.viewController = viewController diff --git a/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesDataFlow.swift b/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesDataFlow.swift index fba14da691..2760c6ac3f 100644 --- a/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesDataFlow.swift +++ b/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesDataFlow.swift @@ -1,10 +1,11 @@ import Foundation enum UserCourses { - enum Tab: CaseIterable { - case allCourses + enum Tab: String, CaseIterable { + case allCourses = "all" case favorites - case archived + case downloaded + case archived = "archive" var title: String { switch self { @@ -12,6 +13,8 @@ enum UserCourses { return NSLocalizedString("UserCoursesTabAllCoursesTitle", comment: "") case .favorites: return NSLocalizedString("UserCoursesTabFavoritesTitle", comment: "") + case .downloaded: + return NSLocalizedString("UserCoursesTabDownloadedTitle", comment: "") case .archived: return NSLocalizedString("UserCoursesTabArchivedTitle", comment: "") } diff --git a/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesViewController.swift b/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesViewController.swift index 0df6f52346..78e33b2e9a 100644 --- a/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesViewController.swift +++ b/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesViewController.swift @@ -14,6 +14,8 @@ final class UserCoursesViewController: TabmanViewController { static let barTintColor = UIColor.stepikAccent static let barBackgroundColor = UIColor.stepikNavigationBarBackground static let barSeparatorColor = UIColor.stepikOpaqueSeparator + static let barInterButtonSpacing: CGFloat = 16 + static let barContentInset = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) static var navigationBarAppearance: StyledNavigationController.NavigationBarAppearanceState { .init(shadowViewAlpha: 0.0) @@ -27,8 +29,10 @@ final class UserCoursesViewController: TabmanViewController { bar.backgroundView.style = .flat(color: Appearance.barBackgroundColor) bar.indicator.tintColor = Appearance.barTintColor bar.indicator.weight = .light - bar.layout.interButtonSpacing = 0 - bar.layout.contentMode = .fit + bar.layout.interButtonSpacing = Appearance.barInterButtonSpacing + bar.layout.contentInset = Appearance.barContentInset + bar.layout.alignment = .leading + bar.layout.contentMode = .intrinsic let separatorView = UIView() separatorView.backgroundColor = Appearance.barSeparatorColor @@ -48,10 +52,13 @@ final class UserCoursesViewController: TabmanViewController { private let initialTabIndex: Int private var tabViewControllers: [UIViewController?] = [] + private let analytics: Analytics + init( interactor: UserCoursesInteractorProtocol, availableTabs: [UserCourses.Tab], - initialTab: UserCourses.Tab + initialTab: UserCourses.Tab, + analytics: Analytics ) { self.interactor = interactor @@ -64,6 +71,8 @@ final class UserCoursesViewController: TabmanViewController { self.initialTabIndex = 0 } + self.analytics = analytics + super.init(nibName: nil, bundle: nil) } @@ -93,6 +102,26 @@ final class UserCoursesViewController: TabmanViewController { sender: self ) } + + self.analytics.send(.myCoursesScreenOpened) + } + + override func pageboyViewController( + _ pageboyViewController: PageboyViewController, + didScrollToPageAt index: TabmanViewController.PageIndex, + direction: PageboyViewController.NavigationDirection, + animated: Bool + ) { + super.pageboyViewController( + pageboyViewController, + didScrollToPageAt: index, + direction: direction, + animated: animated + ) + + if let selectedTab = self.availableTabs[safe: index] { + self.analytics.send(.myCoursesScreenTabOpened(tab: selectedTab)) + } } // MARK: Private API @@ -135,6 +164,8 @@ private extension UserCourses.Tab { return EnrolledCourseListType() case .favorites: return FavoriteCourseListType() + case .downloaded: + return DownloadedCourseListType() case .archived: return ArchivedCourseListType() } diff --git a/Stepic/en.lproj/Localizable.strings b/Stepic/en.lproj/Localizable.strings index 5a3a63c06d..4a4efa7eed 100644 --- a/Stepic/en.lproj/Localizable.strings +++ b/Stepic/en.lproj/Localizable.strings @@ -539,6 +539,7 @@ CourseBenefitDetailAmountTitle = "Your earnings"; UserCoursesTitle = "My Courses"; UserCoursesTabAllCoursesTitle = "All"; UserCoursesTabFavoritesTitle = "Favorite"; +UserCoursesTabDownloadedTitle = "Downloaded"; UserCoursesTabArchivedTitle = "Archive"; /* User Courses Reviews */ diff --git a/Stepic/ru.lproj/Localizable.strings b/Stepic/ru.lproj/Localizable.strings index 69052e306c..79d71e7c57 100644 --- a/Stepic/ru.lproj/Localizable.strings +++ b/Stepic/ru.lproj/Localizable.strings @@ -541,6 +541,7 @@ CourseBenefitDetailAmountTitle = "Ваш доход"; UserCoursesTitle = "Мои курсы"; UserCoursesTabAllCoursesTitle = "Все"; UserCoursesTabFavoritesTitle = "Избранные"; +UserCoursesTabDownloadedTitle = "Загруженные"; UserCoursesTabArchivedTitle = "Архив"; /* User Courses Reviews */ From 98c5ca7c1996aa09e877a77e1b8a9268f0e382b6 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Tue, 6 Jul 2021 12:06:30 +0300 Subject: [PATCH 2/8] Refactor update course list on enrollment changes APPS-3361 --- .../Interactor/CourseListDataBackUpdateService.swift | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Stepic/Sources/Modules/CourseList/Interactor/CourseListDataBackUpdateService.swift b/Stepic/Sources/Modules/CourseList/Interactor/CourseListDataBackUpdateService.swift index f9f6f5a3c5..5a144caaaa 100644 --- a/Stepic/Sources/Modules/CourseList/Interactor/CourseListDataBackUpdateService.swift +++ b/Stepic/Sources/Modules/CourseList/Interactor/CourseListDataBackUpdateService.swift @@ -46,6 +46,7 @@ final class CourseListDataBackUpdateService: CourseListDataBackUpdateServiceProt self.courseListType is EnrolledCourseListType || self.courseListType is FavoriteCourseListType || self.courseListType is ArchivedCourseListType + || self.courseListType is DownloadedCourseListType } init( @@ -75,10 +76,6 @@ extension CourseListDataBackUpdateService: DataBackUpdateServiceDelegate { if update.contains(.enrollment) { self.handleCourse(course, didUpdateEnrollment: update) - - if self.courseListType is DownloadedCourseListType { - self.delegate?.courseListDataBackUpdateServiceDidUpdateCourseList(self) - } } // If isArchived or isFavorite state was updated then handle specified update and refresh course list @@ -111,7 +108,9 @@ extension CourseListDataBackUpdateService: DataBackUpdateServiceDelegate { private func handleCourse(_ course: Course, didUpdateEnrollment update: DataBackUpdateDescription) { if self.isUserCoursesCourseListType { - if course.enrolled && self.courseListType is EnrolledCourseListType { + if self.courseListType is DownloadedCourseListType { + self.delegate?.courseListDataBackUpdateServiceDidUpdateCourseList(self) + } else if course.enrolled && self.courseListType is EnrolledCourseListType { self.delegate?.courseListDataBackUpdateService(self, didInsertCourse: course) } else { self.delegate?.courseListDataBackUpdateService(self, didDeleteCourse: course) From d296658037e758c098ea81275a00964fc8ad70a2 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Tue, 6 Jul 2021 13:11:36 +0300 Subject: [PATCH 3/8] Update DownloadedCourseList on download state updates APPS-3361 --- .../CourseInfoTabSyllabusAssembly.swift | 1 + .../CourseInfoTabSyllabusInteractor.swift | 29 +++++++++++++++++++ .../CourseListDataBackUpdateService.swift | 4 +++ .../Modules/Settings/SettingsAssembly.swift | 3 +- .../Modules/Settings/SettingsInteractor.swift | 7 ++++- .../Services/DataBackUpdateService.swift | 7 +++++ 6 files changed, 49 insertions(+), 2 deletions(-) diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/CourseInfoTabSyllabusAssembly.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/CourseInfoTabSyllabusAssembly.swift index 7664004286..ab0fba9197 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/CourseInfoTabSyllabusAssembly.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/CourseInfoTabSyllabusAssembly.swift @@ -34,6 +34,7 @@ final class CourseInfoTabSyllabusAssembly: Assembly { networkReachabilityService: NetworkReachabilityService(), tooltipStorageManager: TooltipStorageManager(), useCellularDataForDownloadsStorageManager: UseCellularDataForDownloadsStorageManager(), + dataBackUpdateService: DataBackUpdateService.default, syllabusDownloadsService: SyllabusDownloadsService( videoDownloadingService: VideoDownloadingService.shared, videoFileManager: VideoStoredFileManager(fileManager: .default), diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/CourseInfoTabSyllabusInteractor.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/CourseInfoTabSyllabusInteractor.swift index 0d69d351ee..d9cbb2f0cf 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/CourseInfoTabSyllabusInteractor.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabSyllabus/CourseInfoTabSyllabusInteractor.swift @@ -12,6 +12,7 @@ protocol CourseInfoTabSyllabusInteractorProtocol { } final class CourseInfoTabSyllabusInteractor: CourseInfoTabSyllabusInteractorProtocol { + private static let downloadsDataBackUpdateDebounceInterval: Double = 0.1 private static let maxConcurrentOperations = 3 weak var moduleOutput: CourseInfoTabSyllabusOutputProtocol? @@ -69,6 +70,11 @@ final class CourseInfoTabSyllabusInteractor: CourseInfoTabSyllabusInteractorProt label: "com.AlexKarpov.Stepic.CourseInfoTabSyllabusInteractor.DownloadUpdate" ) + private let dataBackUpdateService: DataBackUpdateServiceProtocol + private let downloadsDataBackUpdateDebouncer = Debouncer( + delay: CourseInfoTabSyllabusInteractor.downloadsDataBackUpdateDebounceInterval + ) + init( presenter: CourseInfoTabSyllabusPresenterProtocol, provider: CourseInfoTabSyllabusProviderProtocol, @@ -77,6 +83,7 @@ final class CourseInfoTabSyllabusInteractor: CourseInfoTabSyllabusInteractorProt networkReachabilityService: NetworkReachabilityServiceProtocol, tooltipStorageManager: TooltipStorageManagerProtocol, useCellularDataForDownloadsStorageManager: UseCellularDataForDownloadsStorageManagerProtocol, + dataBackUpdateService: DataBackUpdateServiceProtocol, syllabusDownloadsService: SyllabusDownloadsServiceProtocol ) { self.presenter = presenter @@ -86,6 +93,7 @@ final class CourseInfoTabSyllabusInteractor: CourseInfoTabSyllabusInteractorProt self.networkReachabilityService = networkReachabilityService self.tooltipStorageManager = tooltipStorageManager self.useCellularDataForDownloadsStorageManager = useCellularDataForDownloadsStorageManager + self.dataBackUpdateService = dataBackUpdateService self.syllabusDownloadsService = syllabusDownloadsService self.syllabusDownloadsService.delegate = self @@ -628,6 +636,16 @@ final class CourseInfoTabSyllabusInteractor: CourseInfoTabSyllabusInteractorProt private func getUniqueIdentifierByUnitID(_ unitID: Unit.IdType) -> UniqueIdentifierType { "\(unitID)" } + private func triggerDownloadsDataBackUpdated() { + self.downloadsDataBackUpdateDebouncer.action = { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.dataBackUpdateService.triggerDownloadsUpdated() + } + } + enum Error: Swift.Error { case fetchFailed } @@ -726,6 +744,8 @@ extension CourseInfoTabSyllabusInteractor: SyllabusDownloadsServiceDelegate { ) { self.reportedToAnalyticsVideoDownloadIDs.remove(videoID) self.analytics.send(isCompleted ? .videoDownloadSucceeded : .videoDownloadFailed) + + self.triggerDownloadsDataBackUpdated() } func syllabusDownloadsService( @@ -733,6 +753,8 @@ extension CourseInfoTabSyllabusInteractor: SyllabusDownloadsServiceDelegate { didReceiveCompletion isCompleted: Bool, forUnitWithID unitID: Unit.IdType ) { + self.triggerDownloadsDataBackUpdated() + guard let unit = self.currentUnits[self.getUniqueIdentifierByUnitID(unitID)] as? Unit else { return } @@ -746,6 +768,8 @@ extension CourseInfoTabSyllabusInteractor: SyllabusDownloadsServiceDelegate { didReceiveCompletion isCompleted: Bool, forSectionWithID sectionID: Section.IdType ) { + self.triggerDownloadsDataBackUpdated() + guard let section = self.currentSections[self.getUniqueIdentifierBySectionID(sectionID)] else { return } @@ -936,6 +960,7 @@ extension CourseInfoTabSyllabusInteractor { }.ensure { self.updateUnitDownloadState(unit, forceSectionUpdate: true) self.updateSyllabusHeader() + self.triggerDownloadsDataBackUpdated() }.catch { error in print("CourseInfoTabSyllabusInteractor :: error while cancelling unit = \(unitID), error = \(error)") } @@ -954,6 +979,7 @@ extension CourseInfoTabSyllabusInteractor { DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { self.updateSectionDownloadState(section) self.updateSyllabusHeader() + self.triggerDownloadsDataBackUpdated() } }.catch { error in print("CourseInfoTabSyllabusInteractor :: error while cancelling section = \(sectionID), error = \(error)") @@ -971,6 +997,7 @@ extension CourseInfoTabSyllabusInteractor { }.ensure { self.updateUnitDownloadState(unit, forceSectionUpdate: true) self.updateSyllabusHeader() + self.triggerDownloadsDataBackUpdated() }.catch { error in print("CourseInfoTabSyllabusInteractor :: error while removing cached unit = \(unitID), error = \(error)") } @@ -987,6 +1014,7 @@ extension CourseInfoTabSyllabusInteractor { }.ensure { self.updateSectionDownloadState(section) self.updateSyllabusHeader() + self.triggerDownloadsDataBackUpdated() }.catch { error in print("CourseInfoTabSyllabusInteractor :: error while removing cached section = \(sectionID), error = \(error)") } @@ -1007,6 +1035,7 @@ extension CourseInfoTabSyllabusInteractor { }.ensure { self.updateSyllabusHeader() course.sections.forEach { self.updateSectionDownloadState($0) } + self.triggerDownloadsDataBackUpdated() }.catch { error in print("CourseInfoTabSyllabusInteractor :: error while removing cached course = \(courseID), error = \(error)") } diff --git a/Stepic/Sources/Modules/CourseList/Interactor/CourseListDataBackUpdateService.swift b/Stepic/Sources/Modules/CourseList/Interactor/CourseListDataBackUpdateService.swift index 5a144caaaa..6aa095f23a 100644 --- a/Stepic/Sources/Modules/CourseList/Interactor/CourseListDataBackUpdateService.swift +++ b/Stepic/Sources/Modules/CourseList/Interactor/CourseListDataBackUpdateService.swift @@ -99,6 +99,10 @@ extension CourseListDataBackUpdateService: DataBackUpdateServiceDelegate { if self.courseListType is WishlistCourseListType { self.delegate?.courseListDataBackUpdateServiceDidUpdateCourseList(self) } + case .downloads: + if self.courseListType is DownloadedCourseListType { + self.delegate?.courseListDataBackUpdateServiceDidUpdateCourseList(self) + } default: break } diff --git a/Stepic/Sources/Modules/Settings/SettingsAssembly.swift b/Stepic/Sources/Modules/Settings/SettingsAssembly.swift index 09d25c7b11..ec39dda0a6 100644 --- a/Stepic/Sources/Modules/Settings/SettingsAssembly.swift +++ b/Stepic/Sources/Modules/Settings/SettingsAssembly.swift @@ -32,7 +32,8 @@ final class SettingsAssembly: Assembly { analytics: StepikAnalytics.shared, userAccountService: UserAccountService(), remoteConfig: .shared, - downloadsDeletionService: DownloadsDeletionService() + downloadsDeletionService: DownloadsDeletionService(), + dataBackUpdateService: DataBackUpdateService.default ) let viewController = SettingsViewController( interactor: interactor, diff --git a/Stepic/Sources/Modules/Settings/SettingsInteractor.swift b/Stepic/Sources/Modules/Settings/SettingsInteractor.swift index c553f5fdbe..05f9a55199 100644 --- a/Stepic/Sources/Modules/Settings/SettingsInteractor.swift +++ b/Stepic/Sources/Modules/Settings/SettingsInteractor.swift @@ -38,6 +38,7 @@ final class SettingsInteractor: SettingsInteractorProtocol { private let remoteConfig: RemoteConfig private let downloadsDeletionService: DownloadsDeletionServiceProtocol + private let dataBackUpdateService: DataBackUpdateServiceProtocol private var settingsData: Settings.SettingsData { .init( @@ -66,7 +67,8 @@ final class SettingsInteractor: SettingsInteractorProtocol { analytics: Analytics, userAccountService: UserAccountServiceProtocol, remoteConfig: RemoteConfig, - downloadsDeletionService: DownloadsDeletionServiceProtocol + downloadsDeletionService: DownloadsDeletionServiceProtocol, + dataBackUpdateService: DataBackUpdateServiceProtocol ) { self.presenter = presenter self.provider = provider @@ -74,6 +76,7 @@ final class SettingsInteractor: SettingsInteractorProtocol { self.userAccountService = userAccountService self.remoteConfig = remoteConfig self.downloadsDeletionService = downloadsDeletionService + self.dataBackUpdateService = dataBackUpdateService } func doSettingsLoad(request: Settings.SettingsLoad.Request) { @@ -181,6 +184,8 @@ final class SettingsInteractor: SettingsInteractorProtocol { self.downloadsDeletionService.deleteAllDownloads() }.done { self.presenter.presentDeleteAllContentResult(response: .init(isSuccessful: true)) + }.ensure { + self.dataBackUpdateService.triggerDownloadsUpdated() }.catch { _ in self.presenter.presentDeleteAllContentResult(response: .init(isSuccessful: false)) } diff --git a/Stepic/Sources/Services/DataBackUpdateService.swift b/Stepic/Sources/Services/DataBackUpdateService.swift index 07debbe16c..7da4bcbc10 100644 --- a/Stepic/Sources/Services/DataBackUpdateService.swift +++ b/Stepic/Sources/Services/DataBackUpdateService.swift @@ -11,6 +11,7 @@ enum DataBackUpdateTarget { case userCourse(UserCourse) case visitedCourse case wishlist([Course.IdType]) + case downloads } /// Affected fields in updated object @@ -65,6 +66,8 @@ protocol DataBackUpdateServiceProtocol: AnyObject { func triggerVisitedCourseListUpdate() /// Report about wishlist update func triggerWishlistUpdate(coursesIDs: [Course.IdType]) + /// Report about downloads update + func triggerDownloadsUpdated() } final class DataBackUpdateService: DataBackUpdateServiceProtocol { @@ -216,6 +219,10 @@ final class DataBackUpdateService: DataBackUpdateServiceProtocol { self.postNotification(target: .wishlist(coursesIDs)) } + func triggerDownloadsUpdated() { + self.postNotification(target: .downloads) + } + // MARK: Private methods private func postNotification(description: DataBackUpdateDescription? = nil, target: DataBackUpdateTarget) { From a79a59846ab6fbbe800c28c97f2eebd370946b8f Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Wed, 7 Jul 2021 00:45:29 +0300 Subject: [PATCH 4/8] Course info review summary (#1005) * Update CourseInfo tabs order * Make view model * Add summary layout draft * Update header layout --- Stepic.xcodeproj/project.pbxproj | 40 ++++++ .../CourseReviewSummary.swift | 4 + Stepic/Sources/Helpers/FormatterHelper.swift | 13 ++ .../CourseInfo/CourseInfoAssembly.swift | 2 +- .../CourseInfo/CourseInfoPresenter.swift | 9 +- .../CourseInfoTabReviewsDataFlow.swift | 3 +- .../CourseInfoTabReviewsPresenter.swift | 20 ++- .../CourseInfoTabReviewsViewController.swift | 1 + .../CourseInfoTabReviewsViewModel.swift | 22 +++ .../Views/CourseInfoTabReviewsView.swift | 32 +++-- .../CourseInfoTabReviewsHeaderView.swift | 132 ++++++++++++------ .../CourseInfoTabReviewsReviewButton.swift | 92 ++++++++++++ ...ewsSummaryDistributionProgressesView.swift | 94 +++++++++++++ ...ourseInfoTabReviewsSummaryRatingView.swift | 95 +++++++++++++ .../CourseInfoTabReviewsSummaryView.swift | 125 +++++++++++++++++ ...iewsSummaryDistributionCountItemView.swift | 98 +++++++++++++ ...ReviewsSummaryDistributionCountsView.swift | 82 +++++++++++ Stepic/Sources/Views/CourseRatingView.swift | 6 + Stepic/en.lproj/Localizable.strings | 5 + Stepic/ru.lproj/Localizable.strings | 5 + 20 files changed, 817 insertions(+), 63 deletions(-) create mode 100644 Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/CourseInfoTabReviewsReviewButton.swift create mode 100644 Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/Summary/CourseInfoTabReviewsSummaryDistributionProgressesView.swift create mode 100644 Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/Summary/CourseInfoTabReviewsSummaryRatingView.swift create mode 100644 Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/Summary/CourseInfoTabReviewsSummaryView.swift create mode 100644 Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/Summary/DistributionCounts/CourseInfoTabReviewsSummaryDistributionCountItemView.swift create mode 100644 Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/Summary/DistributionCounts/CourseInfoTabReviewsSummaryDistributionCountsView.swift diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index 2ec412896e..0ae9359876 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -717,6 +717,10 @@ 2C7D7FAD2669382200C8F690 /* WishlistWidgetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7D7FAC2669382200C8F690 /* WishlistWidgetViewModel.swift */; }; 2C7EFCED24D08CFF003A4E93 /* NewProfileSocialProfilesSkeletonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7EFCEC24D08CFF003A4E93 /* NewProfileSocialProfilesSkeletonView.swift */; }; 2C7EFCEF24D08D80003A4E93 /* NewProfileSocialProfilesSkeletonProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7EFCEE24D08D80003A4E93 /* NewProfileSocialProfilesSkeletonProfileView.swift */; }; + 2C7F4160269484DA00BD5C48 /* CourseInfoTabReviewsSummaryRatingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7F415F269484DA00BD5C48 /* CourseInfoTabReviewsSummaryRatingView.swift */; }; + 2C7F416226948CCE00BD5C48 /* CourseInfoTabReviewsSummaryDistributionProgressesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7F416126948CCE00BD5C48 /* CourseInfoTabReviewsSummaryDistributionProgressesView.swift */; }; + 2C7F4164269492DF00BD5C48 /* CourseInfoTabReviewsSummaryDistributionCountsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7F4163269492DF00BD5C48 /* CourseInfoTabReviewsSummaryDistributionCountsView.swift */; }; + 2C7F41672694938700BD5C48 /* CourseInfoTabReviewsSummaryDistributionCountItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7F41662694938700BD5C48 /* CourseInfoTabReviewsSummaryDistributionCountItemView.swift */; }; 2C7F641C23B0F5B7006C7648 /* PlayNextCircleControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7F641B23B0F5B7006C7648 /* PlayNextCircleControlView.swift */; }; 2C7F641E23B12208006C7648 /* AutoplayStorageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7F641D23B12208006C7648 /* AutoplayStorageManager.swift */; }; 2C7F783A2271D5480089FDD7 /* LayoutInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7F78392271D5480089FDD7 /* LayoutInsets.swift */; }; @@ -824,6 +828,7 @@ 2CA3E8AF24C173C600FA3059 /* CourseInfoTabInfoAboutBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA3E8AE24C173C600FA3059 /* CourseInfoTabInfoAboutBlockView.swift */; }; 2CA47D4A248ABF0E00925335 /* LogoutDataClearService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA47D49248ABF0E00925335 /* LogoutDataClearService.swift */; }; 2CA51BB024AB9B4300A400AE /* NewProfileUserActivityViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA51BAF24AB9B4300A400AE /* NewProfileUserActivityViewModel.swift */; }; + 2CA603592694D73900BABA4D /* CourseInfoTabReviewsReviewButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA603582694D73900BABA4D /* CourseInfoTabReviewsReviewButton.swift */; }; 2CA6A1E6266624F4000AAA90 /* ReviewsAndWishlistContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA6A1E5266624F4000AAA90 /* ReviewsAndWishlistContainerViewController.swift */; }; 2CA6A1E82666551F000AAA90 /* UserCoursesReviewsWidgetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA6A1E72666551F000AAA90 /* UserCoursesReviewsWidgetViewModel.swift */; }; 2CA6A1EA26665AC2000AAA90 /* UserCoursesReviewsBlockSkeletonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA6A1E926665AC2000AAA90 /* UserCoursesReviewsBlockSkeletonView.swift */; }; @@ -1009,6 +1014,7 @@ 2CDB95892412C08100F676A7 /* SpotlightContinueUserActivityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CDB95882412C08100F676A7 /* SpotlightContinueUserActivityService.swift */; }; 2CDBCCCF23EB777E005D2370 /* SubmissionURLProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CDBCCCE23EB777E005D2370 /* SubmissionURLProvider.swift */; }; 2CDC9EFA24E4FD0D00916BAE /* CourseInfoTryForFreeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CDC9EF924E4FD0D00916BAE /* CourseInfoTryForFreeButton.swift */; }; + 2CDCDD19268B5CDE002A4889 /* CourseInfoTabReviewsSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CDCDD18268B5CDE002A4889 /* CourseInfoTabReviewsSummaryView.swift */; }; 2CDE82E2221D5CCB00C41887 /* highlight.js in Resources */ = {isa = PBXBuildFile; fileRef = 2CDE82E1221D5CCB00C41887 /* highlight.js */; }; 2CE02A772176649F009C633C /* UserNotificationsCenterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CE02A762176649F009C633C /* UserNotificationsCenterDelegate.swift */; }; 2CE05969268A1DCB00369E59 /* CourseRevenueTabPurchasesCellSkeletonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CE05968268A1DCB00369E59 /* CourseRevenueTabPurchasesCellSkeletonView.swift */; }; @@ -2581,6 +2587,10 @@ 2C7D7FAC2669382200C8F690 /* WishlistWidgetViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WishlistWidgetViewModel.swift; sourceTree = ""; }; 2C7EFCEC24D08CFF003A4E93 /* NewProfileSocialProfilesSkeletonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewProfileSocialProfilesSkeletonView.swift; sourceTree = ""; }; 2C7EFCEE24D08D80003A4E93 /* NewProfileSocialProfilesSkeletonProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewProfileSocialProfilesSkeletonProfileView.swift; sourceTree = ""; }; + 2C7F415F269484DA00BD5C48 /* CourseInfoTabReviewsSummaryRatingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseInfoTabReviewsSummaryRatingView.swift; sourceTree = ""; }; + 2C7F416126948CCE00BD5C48 /* CourseInfoTabReviewsSummaryDistributionProgressesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseInfoTabReviewsSummaryDistributionProgressesView.swift; sourceTree = ""; }; + 2C7F4163269492DF00BD5C48 /* CourseInfoTabReviewsSummaryDistributionCountsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseInfoTabReviewsSummaryDistributionCountsView.swift; sourceTree = ""; }; + 2C7F41662694938700BD5C48 /* CourseInfoTabReviewsSummaryDistributionCountItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseInfoTabReviewsSummaryDistributionCountItemView.swift; sourceTree = ""; }; 2C7F641B23B0F5B7006C7648 /* PlayNextCircleControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayNextCircleControlView.swift; sourceTree = ""; }; 2C7F641D23B12208006C7648 /* AutoplayStorageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoplayStorageManager.swift; sourceTree = ""; }; 2C7F78392271D5480089FDD7 /* LayoutInsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutInsets.swift; sourceTree = ""; }; @@ -2691,6 +2701,7 @@ 2CA3E8AE24C173C600FA3059 /* CourseInfoTabInfoAboutBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseInfoTabInfoAboutBlockView.swift; sourceTree = ""; }; 2CA47D49248ABF0E00925335 /* LogoutDataClearService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoutDataClearService.swift; sourceTree = ""; }; 2CA51BAF24AB9B4300A400AE /* NewProfileUserActivityViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewProfileUserActivityViewModel.swift; sourceTree = ""; }; + 2CA603582694D73900BABA4D /* CourseInfoTabReviewsReviewButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseInfoTabReviewsReviewButton.swift; sourceTree = ""; }; 2CA6A1E5266624F4000AAA90 /* ReviewsAndWishlistContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewsAndWishlistContainerViewController.swift; sourceTree = ""; }; 2CA6A1E72666551F000AAA90 /* UserCoursesReviewsWidgetViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCoursesReviewsWidgetViewModel.swift; sourceTree = ""; }; 2CA6A1E926665AC2000AAA90 /* UserCoursesReviewsBlockSkeletonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCoursesReviewsBlockSkeletonView.swift; sourceTree = ""; }; @@ -2891,6 +2902,7 @@ 2CDBCCCE23EB777E005D2370 /* SubmissionURLProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmissionURLProvider.swift; sourceTree = ""; }; 2CDC9EF924E4FD0D00916BAE /* CourseInfoTryForFreeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseInfoTryForFreeButton.swift; sourceTree = ""; }; 2CDC9EFB24E516F800916BAE /* Model_course_preview_lesson_id_v63.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_course_preview_lesson_id_v63.xcdatamodel; sourceTree = ""; }; + 2CDCDD18268B5CDE002A4889 /* CourseInfoTabReviewsSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseInfoTabReviewsSummaryView.swift; sourceTree = ""; }; 2CDE82E1221D5CCB00C41887 /* highlight.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = highlight.js; sourceTree = ""; }; 2CE02A762176649F009C633C /* UserNotificationsCenterDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationsCenterDelegate.swift; sourceTree = ""; }; 2CE05968268A1DCB00369E59 /* CourseRevenueTabPurchasesCellSkeletonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseRevenueTabPurchasesCellSkeletonView.swift; sourceTree = ""; }; @@ -4667,6 +4679,26 @@ path = SocialProfiles; sourceTree = ""; }; + 2C7F415E26947F9E00BD5C48 /* Summary */ = { + isa = PBXGroup; + children = ( + 2C7F416126948CCE00BD5C48 /* CourseInfoTabReviewsSummaryDistributionProgressesView.swift */, + 2C7F415F269484DA00BD5C48 /* CourseInfoTabReviewsSummaryRatingView.swift */, + 2CDCDD18268B5CDE002A4889 /* CourseInfoTabReviewsSummaryView.swift */, + 2C7F41652694936700BD5C48 /* DistributionCounts */, + ); + path = Summary; + sourceTree = ""; + }; + 2C7F41652694936700BD5C48 /* DistributionCounts */ = { + isa = PBXGroup; + children = ( + 2C7F41662694938700BD5C48 /* CourseInfoTabReviewsSummaryDistributionCountItemView.swift */, + 2C7F4163269492DF00BD5C48 /* CourseInfoTabReviewsSummaryDistributionCountsView.swift */, + ); + path = DistributionCounts; + sourceTree = ""; + }; 2C83E2102549758300D0C1F3 /* Checkbox */ = { isa = PBXGroup; children = ( @@ -8244,6 +8276,8 @@ isa = PBXGroup; children = ( 62E980B94835CC8DCAC9EEAC /* CourseInfoTabReviewsHeaderView.swift */, + 2CA603582694D73900BABA4D /* CourseInfoTabReviewsReviewButton.swift */, + 2C7F415E26947F9E00BD5C48 /* Summary */, ); path = Header; sourceTree = ""; @@ -10552,6 +10586,7 @@ 080CE1581E9566220089A27F /* ViewsAPI.swift in Sources */, 2CC3519C1F6837B4004255B6 /* RegistrationViewController.swift in Sources */, 08C5E4FD1C315272004AA626 /* AudioManager.swift in Sources */, + 2CA603592694D73900BABA4D /* CourseInfoTabReviewsReviewButton.swift in Sources */, 2C7FEE7F1FDFCEF200B2B4F1 /* OnboardingAnimatedView.swift in Sources */, 2CB9529C229F29F000A6117A /* ViewsNetworkService.swift in Sources */, 088E73F42061BDAA00D458E3 /* StepikModelView.swift in Sources */, @@ -10606,6 +10641,7 @@ 2C1AC2E7255B249800E6ECA9 /* CatalogBlocksAPI.swift in Sources */, 2CB62BDB2019ECB800B5E336 /* OnboardingCardStepViewController.swift in Sources */, 08E7CA1020DAF6E0004F8563 /* AnalyticsUserProperties.swift in Sources */, + 2C7F41672694938700BD5C48 /* CourseInfoTabReviewsSummaryDistributionCountItemView.swift in Sources */, 2C1AC31E255B476A00E6ECA9 /* CatalogBlocksRepository.swift in Sources */, 2C2972052147F5FD001502BD /* CourseTag.swift in Sources */, 08EF9A081C91D0F800433E4A /* StepikVideoPlayerViewController.swift in Sources */, @@ -10682,6 +10718,7 @@ 2CE9BF46248D08A8004F6659 /* CodeSamplesPersistenceService.swift in Sources */, 2C92669720B5C7CF00525AFC /* PlaceholderTableViewCell.swift in Sources */, 2CC0754720177A2E004A6005 /* AdaptiveStatsViewController.swift in Sources */, + 2C7F4164269492DF00BD5C48 /* CourseInfoTabReviewsSummaryDistributionCountsView.swift in Sources */, 2C80C829201A504700ABB312 /* StepCardView.swift in Sources */, 084156961BCBFFBD006B8C73 /* Step.swift in Sources */, 08D2F72D2122E4B5009BA052 /* StoryNavigationDelegate.swift in Sources */, @@ -10893,6 +10930,7 @@ 62E980CB3EB9E38E716EB433 /* TabSegmentedControlView.swift in Sources */, 62E98ABA0A56F069278EF47D /* CourseReviewSummariesNetworkService.swift in Sources */, 62E98F5E955545F7A75F77BC /* ProgressesNetworkService.swift in Sources */, + 2CDCDD19268B5CDE002A4889 /* CourseInfoTabReviewsSummaryView.swift in Sources */, 62E983B1F742209528DC222A /* ContentLanguageService.swift in Sources */, 62E9826EBCAB31DA927E56FE /* Reusable.swift in Sources */, 62E98E916518CC0446F2DCDF /* ProgrammaticallyInitializableViewProtocol.swift in Sources */, @@ -11146,6 +11184,7 @@ 62E9869C4E9F0A5B30173F4C /* DiscussionsTableViewDataSource.swift in Sources */, 2CFFC6262681F1CE005D3082 /* ExpandContentControl.swift in Sources */, 62E982C1E3391BF739E8A215 /* DiscussionsView.swift in Sources */, + 2C7F4160269484DA00BD5C48 /* CourseInfoTabReviewsSummaryRatingView.swift in Sources */, 62E981E3990C9BF7DFFD3A93 /* DiscussionsCellView.swift in Sources */, 62E981260E846E172FAAA065 /* DiscussionsLoadMoreTableViewCell.swift in Sources */, 2CF4661525402077002415AF /* TableQuizSelectColumnsViewController.swift in Sources */, @@ -11718,6 +11757,7 @@ 0AB9C1D11180119E2B231364 /* CourseRevenueTabPurchasesAssembly.swift in Sources */, C235570B5328ABC518EB64C7 /* CourseRevenueTabPurchasesDataFlow.swift in Sources */, 7E680169115F6645EFE51847 /* CourseRevenueTabPurchasesInteractor.swift in Sources */, + 2C7F416226948CCE00BD5C48 /* CourseInfoTabReviewsSummaryDistributionProgressesView.swift in Sources */, F318CE674BFD2383135D5F95 /* CourseRevenueTabPurchasesPresenter.swift in Sources */, E9EA0D1ED18C79E73AAA88EA /* CourseRevenueTabPurchasesProvider.swift in Sources */, F76BB6781CF813DDCD8F6C5C /* CourseRevenueTabPurchasesView.swift in Sources */, diff --git a/Stepic/Legacy/Model/Entities/CourseReviewSummary/CourseReviewSummary.swift b/Stepic/Legacy/Model/Entities/CourseReviewSummary/CourseReviewSummary.swift index 4fee9f116f..8c9b7e809b 100644 --- a/Stepic/Legacy/Model/Entities/CourseReviewSummary/CourseReviewSummary.swift +++ b/Stepic/Legacy/Model/Entities/CourseReviewSummary/CourseReviewSummary.swift @@ -13,6 +13,10 @@ import SwiftyJSON final class CourseReviewSummary: NSManagedObject, JSONSerializable, IDFetchable { typealias IdType = Int + var rating: Int { + self.count > 0 ? Int(round(self.average)) : 0 + } + required convenience init(json: JSON) { self.init() self.initialize(json) diff --git a/Stepic/Sources/Helpers/FormatterHelper.swift b/Stepic/Sources/Helpers/FormatterHelper.swift index 77644d4032..81f58e6c53 100644 --- a/Stepic/Sources/Helpers/FormatterHelper.swift +++ b/Stepic/Sources/Helpers/FormatterHelper.swift @@ -183,6 +183,19 @@ enum FormatterHelper { return "\(count) \(pluralizedCountString)" } + /// Format reviews count with localized and pluralized suffix; 1 -> "1 review", 5 -> "5 reviews" + static func reviewSummariesCount(_ count: Int) -> String { + let pluralizedCountString = StringHelper.pluralize( + number: count, + forms: [ + NSLocalizedString("reviewSummaries1", comment: ""), + NSLocalizedString("reviewSummaries234", comment: ""), + NSLocalizedString("reviewSummaries567890", comment: "") + ] + ) + return "\(count) \(pluralizedCountString)" + } + // MARK: Date /// Format days count with localized and pluralized suffix; 1 -> "1 day", 5 -> "5 days" diff --git a/Stepic/Sources/Modules/CourseInfo/CourseInfoAssembly.swift b/Stepic/Sources/Modules/CourseInfo/CourseInfoAssembly.swift index be1dd95aa0..74948c4ac9 100644 --- a/Stepic/Sources/Modules/CourseInfo/CourseInfoAssembly.swift +++ b/Stepic/Sources/Modules/CourseInfo/CourseInfoAssembly.swift @@ -97,6 +97,6 @@ final class CourseInfoAssembly: Assembly { let adaptiveManager = AdaptiveStorageManager() return adaptiveManager.canOpenInAdaptiveMode(courseId: self.courseID) ? [.info, .reviews] - : [.info, .syllabus, .reviews] + : [.info, .reviews, .syllabus] } } diff --git a/Stepic/Sources/Modules/CourseInfo/CourseInfoPresenter.swift b/Stepic/Sources/Modules/CourseInfo/CourseInfoPresenter.swift index 800ee3ebca..7d3ce818b4 100644 --- a/Stepic/Sources/Modules/CourseInfo/CourseInfoPresenter.swift +++ b/Stepic/Sources/Modules/CourseInfo/CourseInfoPresenter.swift @@ -253,14 +253,7 @@ final class CourseInfoPresenter: CourseInfoPresenterProtocol { isCourseRevenueAvailable: Bool, promoCode: PromoCode? ) -> CourseInfoHeaderViewModel { - let rating: Int = { - if let reviewsCount = course.reviewSummary?.count, - let averageRating = course.reviewSummary?.average, - reviewsCount > 0 { - return Int(round(averageRating)) - } - return 0 - }() + let rating = course.reviewSummary?.rating ?? 0 let progress: CourseInfoProgressViewModel? = { if let progress = course.progress { diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/CourseInfoTabReviewsDataFlow.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/CourseInfoTabReviewsDataFlow.swift index 229d6ac88a..b26ff13b01 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/CourseInfoTabReviewsDataFlow.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/CourseInfoTabReviewsDataFlow.swift @@ -6,6 +6,7 @@ enum CourseInfoTabReviews { struct ReviewsResult { let reviews: [CourseInfoTabReviewsViewModel] let hasNextPage: Bool + let summary: CourseInfoTabReviewsSummaryViewModel? let writeCourseReviewState: WriteCourseReviewState } @@ -123,7 +124,7 @@ enum CourseInfoTabReviews { enum WriteCourseReviewState { case write case edit - case hide + case summary case banner(String) } } diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/CourseInfoTabReviewsPresenter.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/CourseInfoTabReviewsPresenter.swift index 3929acc00f..fdd0c734a0 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/CourseInfoTabReviewsPresenter.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/CourseInfoTabReviewsPresenter.swift @@ -25,6 +25,7 @@ final class CourseInfoTabReviewsPresenter: CourseInfoTabReviewsPresenterProtocol ) }, hasNextPage: data.hasNextPage, + summary: self.makeSummaryViewModel(course: data.course), writeCourseReviewState: self.getWriteCourseReviewState( course: data.course, currentUserReview: data.currentUserReview @@ -49,6 +50,7 @@ final class CourseInfoTabReviewsPresenter: CourseInfoTabReviewsPresenterProtocol ) }, hasNextPage: response.hasNextPage, + summary: self.makeSummaryViewModel(course: response.course), writeCourseReviewState: self.getWriteCourseReviewState( course: response.course, currentUserReview: response.currentUserReview @@ -133,12 +135,28 @@ final class CourseInfoTabReviewsPresenter: CourseInfoTabReviewsPresenterProtocol ) } + private func makeSummaryViewModel(course: Course) -> CourseInfoTabReviewsSummaryViewModel? { + guard let reviewSummary = course.reviewSummary else { + return nil + } + + return CourseInfoTabReviewsSummaryViewModel( + rating: reviewSummary.rating, + averageRating: reviewSummary.average, + reviewsCount: reviewSummary.count, + reviewsDistribution: reviewSummary.distribution, + formattedRating: String(format: "%.1f", reviewSummary.average), + formattedReviewsCount: FormatterHelper.reviewSummariesCount(reviewSummary.count), + formattedReviewsDistribution: reviewSummary.distribution.map(FormatterHelper.longNumber(_:)) + ) + } + private func getWriteCourseReviewState( course: Course, currentUserReview: CourseReview? ) -> CourseInfoTabReviews.WriteCourseReviewState { if course.progressId == nil { - return .hide + return .summary } if currentUserReview != nil { diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/CourseInfoTabReviewsViewController.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/CourseInfoTabReviewsViewController.swift index b5335e5e8a..dd1b584863 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/CourseInfoTabReviewsViewController.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/CourseInfoTabReviewsViewController.swift @@ -87,6 +87,7 @@ final class CourseInfoTabReviewsViewController: UIViewController { self.tableDataSource.viewModels = data.reviews self.courseInfoTabReviewsView?.updateTableViewData(dataSource: self.tableDataSource) + self.courseInfoTabReviewsView?.summaryViewModel = data.summary self.courseInfoTabReviewsView?.writeCourseReviewState = data.writeCourseReviewState self.updatePagination(hasNextPage: data.hasNextPage, hasError: false) diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/CourseInfoTabReviewsViewModel.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/CourseInfoTabReviewsViewModel.swift index 3778f97924..80bfd856e8 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/CourseInfoTabReviewsViewModel.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/CourseInfoTabReviewsViewModel.swift @@ -12,3 +12,25 @@ struct CourseInfoTabReviewsViewModel { let score: Int let isCurrentUserReview: Bool } + +struct CourseInfoTabReviewsSummaryViewModel { + let rating: Int + let averageRating: Float + let reviewsCount: Int + let reviewsDistribution: [Int] + let formattedRating: String + let formattedReviewsCount: String + let formattedReviewsDistribution: [String] + + static var empty: CourseInfoTabReviewsSummaryViewModel { + .init( + rating: 0, + averageRating: 0, + reviewsCount: 0, + reviewsDistribution: [], + formattedRating: "", + formattedReviewsCount: "", + formattedReviewsDistribution: [] + ) + } +} diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/CourseInfoTabReviewsView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/CourseInfoTabReviewsView.swift index 01c58ee5cc..748d96e56a 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/CourseInfoTabReviewsView.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/CourseInfoTabReviewsView.swift @@ -31,13 +31,10 @@ final class CourseInfoTabReviewsView: UIView { private lazy var headerView: CourseInfoTabReviewsHeaderView = { let headerView = CourseInfoTabReviewsHeaderView() - - // Disable masks to prevent constraints breaking headerView.translatesAutoresizingMaskIntoConstraints = false headerView.snp.makeConstraints { make in - make.height.equalTo(self.appearance.headerViewHeight) + self.headerViewHeightConstraint = make.height.equalTo(self.appearance.headerViewHeight).constraint } - headerView.onWriteReviewButtonClick = { [weak self] in guard let strongSelf = self else { return @@ -45,7 +42,6 @@ final class CourseInfoTabReviewsView: UIView { strongSelf.delegate?.courseInfoTabReviewsViewDidRequestWriteReview(strongSelf) } - headerView.onEditReviewButtonClick = { [weak self] in guard let strongSelf = self else { return @@ -53,7 +49,6 @@ final class CourseInfoTabReviewsView: UIView { strongSelf.delegate?.courseInfoTabReviewsViewDidRequestEditReview(strongSelf) } - return headerView }() @@ -94,6 +89,9 @@ final class CourseInfoTabReviewsView: UIView { return view }() + private var headerViewHeightConstraint: Constraint? + private var currentHeaderHeight: CGFloat? + private var errorPlaceholderViewTopConstraint: Constraint? // Proxify delegates @@ -102,7 +100,7 @@ final class CourseInfoTabReviewsView: UIView { private var shouldShowPaginationView = false var paginationView: UIView? - var writeCourseReviewState: CourseInfoTabReviews.WriteCourseReviewState = .hide { + var writeCourseReviewState: CourseInfoTabReviews.WriteCourseReviewState = .summary { didSet { switch self.writeCourseReviewState { case .write: @@ -111,8 +109,11 @@ final class CourseInfoTabReviewsView: UIView { case .edit: self.tableView.tableHeaderView = self.headerView self.headerView.shouldShowEditReviewButton = true - case .hide: - self.tableView.tableHeaderView = nil + case .summary: + self.tableView.tableHeaderView = self.headerView + self.headerView.shouldShowWriteReviewButton = false + self.headerView.shouldShowEditReviewButton = false + self.headerView.shouldShowWriteReviewBanner = false case .banner(let text): self.tableView.tableHeaderView = self.headerView self.headerView.writeReviewBannerText = text @@ -121,6 +122,12 @@ final class CourseInfoTabReviewsView: UIView { } } + var summaryViewModel: CourseInfoTabReviewsSummaryViewModel? { + didSet { + self.headerView.summaryViewModel = self.summaryViewModel + } + } + init(frame: CGRect = .zero, appearance: Appearance = Appearance()) { self.appearance = appearance super.init(frame: frame) @@ -137,6 +144,13 @@ final class CourseInfoTabReviewsView: UIView { override func layoutSubviews() { super.layoutSubviews() + + let newHeaderHeight = self.headerView.intrinsicContentSize.height + if self.currentHeaderHeight != newHeaderHeight { + self.currentHeaderHeight = newHeaderHeight + self.headerViewHeightConstraint?.update(offset: newHeaderHeight) + } + self.tableView.layoutTableHeaderView() } diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/CourseInfoTabReviewsHeaderView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/CourseInfoTabReviewsHeaderView.swift index bb29656b15..7442de5170 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/CourseInfoTabReviewsHeaderView.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/CourseInfoTabReviewsHeaderView.swift @@ -3,18 +3,17 @@ import UIKit extension CourseInfoTabReviewsHeaderView { struct Appearance { - let buttonSpacing: CGFloat = 16.0 + let stackViewSpacing: CGFloat = 16 + let stackViewInsets = LayoutInsets(top: 16) - let buttonTintColor = UIColor.stepikMaterialPrimaryText - let buttonFont = Typography.subheadlineFont - let buttonImageInsets = UIEdgeInsets(top: 1.5, left: 0, bottom: 0, right: 0) - let buttonTitleInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 0) - let buttonImageSize = CGSize(width: 15, height: 15) + let summaryViewInsets = LayoutInsets(horizontal: 16) - let labelFont = Typography.subheadlineFont - let labelTextColor = UIColor.stepikMaterialSecondaryText + let reviewButtonHeight: CGFloat = 44 + let reviewButtonInsets = LayoutInsets(horizontal: 16) - let insets = UIEdgeInsets(top: 0, left: 18, bottom: 0, right: 18) + let reviewDescriptionLabelFont = Typography.subheadlineFont + let reviewDescriptionLabelTextColor = UIColor.stepikMaterialSecondaryText + let reviewDescriptionLabelInsets = LayoutInsets(horizontal: 16) let separatorHeight: CGFloat = 0.5 } @@ -25,35 +24,50 @@ final class CourseInfoTabReviewsHeaderView: UIView { private lazy var stackView: UIStackView = { let stackView = UIStackView() - stackView.axis = .horizontal - stackView.spacing = self.appearance.buttonSpacing + stackView.axis = .vertical + stackView.spacing = self.appearance.stackViewSpacing return stackView }() - private lazy var reviewButton: ImageButton = { - let button = ImageButton() - button.image = UIImage(named: "course-info-reviews-write")?.withRenderingMode(.alwaysTemplate) - button.tintColor = self.appearance.buttonTintColor + private lazy var summaryView = CourseInfoTabReviewsSummaryView() + + private lazy var summaryContainerView: UIView = { + let view = UIView() + view.isHidden = true + return view + }() + + private lazy var summarySeparatorView: SeparatorView = { + let view = SeparatorView() + view.isHidden = true + return view + }() + + private lazy var reviewButton: CourseInfoTabReviewsReviewButton = { + let button = CourseInfoTabReviewsReviewButton() button.title = NSLocalizedString("WriteCourseReviewActionCreate", comment: "") - button.font = self.appearance.buttonFont - button.imageInsets = self.appearance.buttonImageInsets - button.titleInsets = self.appearance.buttonTitleInsets - button.imageSize = self.appearance.buttonImageSize button.addTarget(self, action: #selector(self.onReviewButtonClicked), for: .touchUpInside) - button.isHidden = true return button }() + private lazy var reviewButtonContainerView: UIView = { + let view = UIView() + view.isHidden = true + return view + }() + private lazy var reviewDescriptionLabel: UILabel = { let label = UILabel() label.text = NSLocalizedString("WriteCourseReviewActionNotAllowedDescription", comment: "") - label.font = self.appearance.labelFont - label.textColor = self.appearance.labelTextColor + label.font = self.appearance.reviewDescriptionLabelFont + label.textColor = self.appearance.reviewDescriptionLabelTextColor label.numberOfLines = 0 return label }() - private lazy var separatorView = SeparatorView() + private lazy var reviewDescriptionContainerView = UIView() + + private lazy var reviewSeparatorView = SeparatorView() private var currentReviewAction = ReviewAction.write @@ -73,19 +87,39 @@ final class CourseInfoTabReviewsHeaderView: UIView { var shouldShowWriteReviewBanner = true { didSet { - self.reviewDescriptionLabel.isHidden = !self.shouldShowWriteReviewBanner + self.reviewDescriptionContainerView.isHidden = !self.shouldShowWriteReviewBanner + self.invalidateIntrinsicContentSize() } } var writeReviewBannerText: String? { didSet { self.reviewDescriptionLabel.text = self.writeReviewBannerText + self.invalidateIntrinsicContentSize() + } + } + + var summaryViewModel: CourseInfoTabReviewsSummaryViewModel? { + didSet { + self.summaryView.configure(viewModel: self.summaryViewModel ?? .empty) + self.summaryContainerView.isHidden = self.summaryViewModel == nil + self.summarySeparatorView.isHidden = self.summaryViewModel == nil + + self.invalidateIntrinsicContentSize() } } var onWriteReviewButtonClick: (() -> Void)? var onEditReviewButtonClick: (() -> Void)? + override var intrinsicContentSize: CGSize { + let stackViewIntrinsicContentSize = self.stackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + return CGSize( + width: UIView.noIntrinsicMetric, + height: self.appearance.stackViewInsets.top + stackViewIntrinsicContentSize.height + ) + } + init(frame: CGRect = .zero, appearance: Appearance = Appearance()) { self.appearance = appearance super.init(frame: frame) @@ -104,12 +138,15 @@ final class CourseInfoTabReviewsHeaderView: UIView { private func updateReviewButton() { let isVisible = self.shouldShowWriteReviewButton || self.shouldShowEditReviewButton + if isVisible { self.shouldShowWriteReviewBanner = false } - self.reviewButton.isHidden = !isVisible + self.reviewButtonContainerView.isHidden = !isVisible self.reviewButton.title = self.currentReviewAction.title + + self.invalidateIntrinsicContentSize() } @objc @@ -144,35 +181,44 @@ final class CourseInfoTabReviewsHeaderView: UIView { extension CourseInfoTabReviewsHeaderView: ProgrammaticallyInitializableViewProtocol { func addSubviews() { self.addSubview(self.stackView) - self.addSubview(self.reviewDescriptionLabel) - self.addSubview(self.separatorView) - self.stackView.addArrangedSubview(self.reviewButton) + self.summaryContainerView.addSubview(self.summaryView) + self.reviewButtonContainerView.addSubview(self.reviewButton) + self.reviewDescriptionContainerView.addSubview(self.reviewDescriptionLabel) + + self.stackView.addArrangedSubview(self.summaryContainerView) + self.stackView.addArrangedSubview(self.summarySeparatorView) + self.stackView.addArrangedSubview(self.reviewButtonContainerView) + self.stackView.addArrangedSubview(self.reviewDescriptionContainerView) + self.stackView.addArrangedSubview(self.reviewSeparatorView) } func makeConstraints() { self.stackView.translatesAutoresizingMaskIntoConstraints = false self.stackView.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(self.appearance.insets.left) - make.top.equalToSuperview().offset(self.appearance.insets.top) - make.trailing.lessThanOrEqualToSuperview().offset(-self.appearance.insets.right).priority(999) + make.edges.equalToSuperview().inset(self.appearance.stackViewInsets.edgeInsets) + } + + self.summaryView.translatesAutoresizingMaskIntoConstraints = false + self.summaryView.snp.makeConstraints { make in + make.edges.equalToSuperview().inset(self.appearance.summaryViewInsets.edgeInsets) + } + + self.summarySeparatorView.translatesAutoresizingMaskIntoConstraints = false + self.summarySeparatorView.snp.makeConstraints { $0.height.equalTo(self.appearance.separatorHeight) } + + self.reviewButton.translatesAutoresizingMaskIntoConstraints = false + self.reviewButton.snp.makeConstraints { make in + make.edges.equalToSuperview().inset(self.appearance.reviewButtonInsets.edgeInsets) + make.height.equalTo(self.appearance.reviewButtonHeight) } self.reviewDescriptionLabel.translatesAutoresizingMaskIntoConstraints = false self.reviewDescriptionLabel.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(self.appearance.insets.left) - make.top.equalToSuperview() - make.trailing.equalToSuperview().offset(-self.appearance.insets.right) - make.bottom.equalTo(self.separatorView.snp.top) + make.edges.equalToSuperview().inset(self.appearance.reviewDescriptionLabelInsets.edgeInsets) } - self.separatorView.translatesAutoresizingMaskIntoConstraints = false - self.separatorView.snp.makeConstraints { make in - make.top - .equalTo(self.stackView.snp.bottom) - .offset(self.appearance.insets.bottom) - make.leading.trailing.bottom.equalToSuperview() - make.height.equalTo(self.appearance.separatorHeight) - } + self.reviewSeparatorView.translatesAutoresizingMaskIntoConstraints = false + self.reviewSeparatorView.snp.makeConstraints { $0.height.equalTo(self.appearance.separatorHeight) } } } diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/CourseInfoTabReviewsReviewButton.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/CourseInfoTabReviewsReviewButton.swift new file mode 100644 index 0000000000..964e41d054 --- /dev/null +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/CourseInfoTabReviewsReviewButton.swift @@ -0,0 +1,92 @@ +import SnapKit +import UIKit + +extension CourseInfoTabReviewsReviewButton { + struct Appearance { + let iconImageViewSize = CGSize(width: 18, height: 18) + let iconImageViewTintColor = UIColor.white + let iconImageViewInsets = LayoutInsets.default + + let titleLabelFont = Typography.bodyFont + let titleLabelTextColor = UIColor.white + + let cornerRadius: CGFloat = 8 + let backgroundColor = UIColor.stepikVioletFixed + } +} + +final class CourseInfoTabReviewsReviewButton: UIControl { + let appearance: Appearance + + private lazy var iconImageView: UIImageView = { + let view = UIImageView() + view.image = UIImage(named: "course-info-reviews-write")?.withRenderingMode(.alwaysTemplate) + view.tintColor = self.appearance.iconImageViewTintColor + view.contentMode = .scaleAspectFit + return view + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = self.appearance.titleLabelFont + label.textColor = self.appearance.titleLabelTextColor + label.numberOfLines = 1 + label.textAlignment = .center + return label + }() + + override var isHighlighted: Bool { + didSet { + self.alpha = self.isHighlighted ? 0.5 : 1.0 + } + } + + var title: String? { + didSet { + self.titleLabel.text = self.title + } + } + + init( + frame: CGRect = .zero, + appearance: Appearance = Appearance() + ) { + self.appearance = appearance + super.init(frame: frame) + + self.setupView() + self.addSubviews() + self.makeConstraints() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension CourseInfoTabReviewsReviewButton: ProgrammaticallyInitializableViewProtocol { + func setupView() { + self.backgroundColor = self.appearance.backgroundColor + self.setRoundedCorners(cornerRadius: self.appearance.cornerRadius) + } + + func addSubviews() { + self.addSubview(self.iconImageView) + self.addSubview(self.titleLabel) + } + + func makeConstraints() { + self.iconImageView.translatesAutoresizingMaskIntoConstraints = false + self.iconImageView.snp.makeConstraints { make in + make.leading.equalToSuperview().offset(self.appearance.iconImageViewInsets.left) + make.size.equalTo(self.appearance.iconImageViewSize) + make.centerY.equalTo(self.titleLabel.snp.centerY) + } + + self.titleLabel.translatesAutoresizingMaskIntoConstraints = false + self.titleLabel.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } +} diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/Summary/CourseInfoTabReviewsSummaryDistributionProgressesView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/Summary/CourseInfoTabReviewsSummaryDistributionProgressesView.swift new file mode 100644 index 0000000000..dc3213f697 --- /dev/null +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/Summary/CourseInfoTabReviewsSummaryDistributionProgressesView.swift @@ -0,0 +1,94 @@ +import SnapKit +import UIKit + +extension CourseInfoTabReviewsSummaryDistributionProgressesView { + struct Appearance { + let progressViewSecondaryColor = UIColor.stepikGreenFixed.withAlphaComponent(0.12) + let progressViewMainColor = UIColor.stepikGreenFixed + let progressViewHeight: CGFloat = 4 + + let stackViewSpacing: CGFloat = 8 + } +} + +final class CourseInfoTabReviewsSummaryDistributionProgressesView: UIView { + private static let maxProgressesCount = 5 + + let appearance: Appearance + + private lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = self.appearance.stackViewSpacing + stackView.distribution = .equalSpacing + return stackView + }() + + var progresses: [Float] = Array( + repeating: 0, + count: CourseInfoTabReviewsSummaryDistributionProgressesView.maxProgressesCount + ) { + didSet { + self.updateProgresses() + } + } + + override var intrinsicContentSize: CGSize { + self.stackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + } + + 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") + } + + private func updateProgresses() { + self.stackView.removeAllArrangedSubviews() + + for idx in 0.. UIView { diff --git a/Stepic/en.lproj/Localizable.strings b/Stepic/en.lproj/Localizable.strings index 4a4efa7eed..99237aadac 100644 --- a/Stepic/en.lproj/Localizable.strings +++ b/Stepic/en.lproj/Localizable.strings @@ -338,6 +338,9 @@ authors567890 = "authors"; reviews1 = "review"; reviews234 = "reviews"; reviews567890 = "reviews"; +reviewSummaries1 = "review"; +reviewSummaries234 = "reviews"; +reviewSummaries567890 = "reviews"; SearchPlaceholderEmpty = "Sorry, we didn't find any courses matching your request"; SearchPlaceholderError = "Error while searching courses. Press to try again"; TagPlaceholderError = "Error while loading courses. Press to try again"; @@ -841,6 +844,8 @@ WriteCourseReviewPlaceholder = "Review"; WriteCourseReviewRatingHint = "Tap a Star to Rate"; WriteCourseReviewActionNotAllowedDescription = "To write a review, complete more than 80% steps"; +CourseInfoTabReviewsOutOfRatingTitle = "out of 5"; + /* ExamEGERussian */ "ErrorMessage" = "Sorry, something went wrong: please try again later."; diff --git a/Stepic/ru.lproj/Localizable.strings b/Stepic/ru.lproj/Localizable.strings index 79d71e7c57..7960da7e46 100644 --- a/Stepic/ru.lproj/Localizable.strings +++ b/Stepic/ru.lproj/Localizable.strings @@ -339,6 +339,9 @@ authors567890 = "авторов"; reviews1 = "отзыв"; reviews234 = "отзыва"; reviews567890 = "отзывов"; +reviewSummaries1 = "оценка"; +reviewSummaries234 = "оценки"; +reviewSummaries567890 = "оценок"; SearchPlaceholderEmpty = "Упс, мы не нашли ни одного курса по запросу"; SearchPlaceholderError = "Ошибка при поиске курсов. Нажмите, чтобы попробовать еще раз"; TagPlaceholderError = "Ошибка при загрузке курсов. Нажмите, чтобы попробовать еще раз"; @@ -842,6 +845,8 @@ WriteCourseReviewPlaceholder = "Отзыв"; WriteCourseReviewRatingHint = "Коснитесь для оценки"; WriteCourseReviewActionNotAllowedDescription = "Чтобы оставить отзыв, пройдите больше 80% материалов"; +CourseInfoTabReviewsOutOfRatingTitle = "из 5"; + /* ExamEGERussian */ "ErrorMessage" = "Извините, что-то пошло не так: повторите попытку позже."; From 3d819b79801dcbda21e877189dbdfa944e7e3e64 Mon Sep 17 00:00:00 2001 From: Stepik Bot Date: Wed, 7 Jul 2021 03:01:35 +0000 Subject: [PATCH 5/8] "Set version to 1.180 & bump build" --- Stepic.xcodeproj/project.pbxproj | 24 +++++++++++----------- Stepic/Info-Develop.plist | 4 ++-- Stepic/Info-Production.plist | 4 ++-- Stepic/Info-Release.plist | 4 ++-- StepicTests/Info-Develop.plist | 4 ++-- StepicTests/Info-Production.plist | 4 ++-- StepicTests/Info-Release.plist | 4 ++-- StickerPackExtension/Info-Develop.plist | 4 ++-- StickerPackExtension/Info-Production.plist | 4 ++-- StickerPackExtension/Info-Release.plist | 4 ++-- 10 files changed, 30 insertions(+), 30 deletions(-) diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index 0ae9359876..646d75a2ad 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -12030,7 +12030,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 348; + CURRENT_PROJECT_VERSION = 349; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Production.plist"; @@ -12055,7 +12055,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 348; + CURRENT_PROJECT_VERSION = 349; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Production.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -12197,7 +12197,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 348; + CURRENT_PROJECT_VERSION = 349; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Production.plist"; @@ -12227,7 +12227,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 348; + CURRENT_PROJECT_VERSION = 349; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; @@ -12318,7 +12318,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 348; + CURRENT_PROJECT_VERSION = 349; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Develop.plist"; @@ -12370,7 +12370,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 348; + CURRENT_PROJECT_VERSION = 349; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Develop.plist"; @@ -12451,7 +12451,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 348; + CURRENT_PROJECT_VERSION = 349; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Develop.plist"; @@ -12499,7 +12499,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 348; + CURRENT_PROJECT_VERSION = 349; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Develop.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -13020,7 +13020,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 348; + CURRENT_PROJECT_VERSION = 349; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Release.plist"; @@ -13074,7 +13074,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 348; + CURRENT_PROJECT_VERSION = 349; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Release.plist"; @@ -13156,7 +13156,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 348; + CURRENT_PROJECT_VERSION = 349; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Release.plist"; @@ -13204,7 +13204,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 348; + CURRENT_PROJECT_VERSION = 349; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Release.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; diff --git a/Stepic/Info-Develop.plist b/Stepic/Info-Develop.plist index b2a9f81c24..93cd486cfc 100644 --- a/Stepic/Info-Develop.plist +++ b/Stepic/Info-Develop.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.179-develop + 1.180-develop CFBundleSignature ???? CFBundleURLTypes @@ -62,7 +62,7 @@ CFBundleVersion - 348 + 349 FacebookAppID 171127739724012 FacebookDisplayName diff --git a/Stepic/Info-Production.plist b/Stepic/Info-Production.plist index fbe89ae1b4..53bfd27310 100644 --- a/Stepic/Info-Production.plist +++ b/Stepic/Info-Production.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.179 + 1.180 CFBundleSignature ???? CFBundleURLTypes @@ -62,7 +62,7 @@ CFBundleVersion - 348 + 349 FacebookAppID 171127739724012 FacebookDisplayName diff --git a/Stepic/Info-Release.plist b/Stepic/Info-Release.plist index 1aaba98d90..3ad6a92ee0 100644 --- a/Stepic/Info-Release.plist +++ b/Stepic/Info-Release.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.179-release + 1.180-release CFBundleSignature ???? CFBundleURLTypes @@ -62,7 +62,7 @@ CFBundleVersion - 348 + 349 FacebookAppID 171127739724012 FacebookDisplayName diff --git a/StepicTests/Info-Develop.plist b/StepicTests/Info-Develop.plist index 5fc684d973..500280ca54 100644 --- a/StepicTests/Info-Develop.plist +++ b/StepicTests/Info-Develop.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.179-develop + 1.180-develop CFBundleSignature ???? CFBundleVersion - 348 + 349 diff --git a/StepicTests/Info-Production.plist b/StepicTests/Info-Production.plist index a75be0a3b9..386c77f70e 100644 --- a/StepicTests/Info-Production.plist +++ b/StepicTests/Info-Production.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.179 + 1.180 CFBundleSignature ???? CFBundleVersion - 348 + 349 diff --git a/StepicTests/Info-Release.plist b/StepicTests/Info-Release.plist index e0777d3dfe..c2239ca62e 100644 --- a/StepicTests/Info-Release.plist +++ b/StepicTests/Info-Release.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.179-release + 1.180-release CFBundleSignature ???? CFBundleVersion - 348 + 349 diff --git a/StickerPackExtension/Info-Develop.plist b/StickerPackExtension/Info-Develop.plist index b71a709c10..710d9094de 100644 --- a/StickerPackExtension/Info-Develop.plist +++ b/StickerPackExtension/Info-Develop.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.179-develop + 1.180-develop CFBundleVersion - 348 + 349 NSExtension NSExtensionPointIdentifier diff --git a/StickerPackExtension/Info-Production.plist b/StickerPackExtension/Info-Production.plist index 0b60e7f5bf..7342f89d90 100644 --- a/StickerPackExtension/Info-Production.plist +++ b/StickerPackExtension/Info-Production.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.179 + 1.180 CFBundleVersion - 348 + 349 NSExtension NSExtensionPointIdentifier diff --git a/StickerPackExtension/Info-Release.plist b/StickerPackExtension/Info-Release.plist index 08b11acba7..e20da8e924 100644 --- a/StickerPackExtension/Info-Release.plist +++ b/StickerPackExtension/Info-Release.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.179-release + 1.180-release CFBundleVersion - 348 + 349 NSExtension NSExtensionPointIdentifier From 5655731b4b3640b114f9bc44a8e60876a75ad34f Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Wed, 7 Jul 2021 09:50:53 +0300 Subject: [PATCH 6/8] Update release notes for beta testers --- fastlane/release-notes.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fastlane/release-notes.txt b/fastlane/release-notes.txt index 96f01ea28b..9e08618c74 100644 --- a/fastlane/release-notes.txt +++ b/fastlane/release-notes.txt @@ -1,2 +1,3 @@ Что тестировать: -- Экран доходов по курсу APPS-3337, APPS-3369 \ No newline at end of file +- Добавить секцию загруженных курсов в мои курсы APPS-3361 +- Отображать summary отзывов по курсу APPS-3364 \ No newline at end of file From f93b80e66e4a1bfc2bb8b1e30d479579b722aed2 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Wed, 7 Jul 2021 09:53:42 +0300 Subject: [PATCH 7/8] Set version to 1.180 & bump build --- Stepic.xcodeproj/project.pbxproj | 24 +++++++++++----------- Stepic/Info-Develop.plist | 2 +- Stepic/Info-Production.plist | 2 +- Stepic/Info-Release.plist | 2 +- StepicTests/Info-Develop.plist | 2 +- StepicTests/Info-Production.plist | 2 +- StepicTests/Info-Release.plist | 2 +- StepicUITests/Info-Develop.plist | 4 ++-- StepicUITests/Info-Production.plist | 4 ++-- StepicUITests/Info-Release.plist | 4 ++-- StepicWidget/Info-Develop.plist | 4 ++-- StepicWidget/Info-Production.plist | 4 ++-- StepicWidget/Info-Release.plist | 4 ++-- StickerPackExtension/Info-Develop.plist | 2 +- StickerPackExtension/Info-Production.plist | 2 +- StickerPackExtension/Info-Release.plist | 2 +- 16 files changed, 33 insertions(+), 33 deletions(-) diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index 646d75a2ad..71ca232b16 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -12030,7 +12030,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 349; + CURRENT_PROJECT_VERSION = 350; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Production.plist"; @@ -12055,7 +12055,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 349; + CURRENT_PROJECT_VERSION = 350; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Production.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -12197,7 +12197,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 349; + CURRENT_PROJECT_VERSION = 350; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Production.plist"; @@ -12227,7 +12227,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 349; + CURRENT_PROJECT_VERSION = 350; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; @@ -12318,7 +12318,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 349; + CURRENT_PROJECT_VERSION = 350; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Develop.plist"; @@ -12370,7 +12370,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 349; + CURRENT_PROJECT_VERSION = 350; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Develop.plist"; @@ -12451,7 +12451,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 349; + CURRENT_PROJECT_VERSION = 350; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Develop.plist"; @@ -12499,7 +12499,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 349; + CURRENT_PROJECT_VERSION = 350; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Develop.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -13020,7 +13020,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 349; + CURRENT_PROJECT_VERSION = 350; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Release.plist"; @@ -13074,7 +13074,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 349; + CURRENT_PROJECT_VERSION = 350; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Release.plist"; @@ -13156,7 +13156,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 349; + CURRENT_PROJECT_VERSION = 350; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Release.plist"; @@ -13204,7 +13204,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 349; + CURRENT_PROJECT_VERSION = 350; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Release.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; diff --git a/Stepic/Info-Develop.plist b/Stepic/Info-Develop.plist index 93cd486cfc..d744e77460 100644 --- a/Stepic/Info-Develop.plist +++ b/Stepic/Info-Develop.plist @@ -62,7 +62,7 @@ CFBundleVersion - 349 + 350 FacebookAppID 171127739724012 FacebookDisplayName diff --git a/Stepic/Info-Production.plist b/Stepic/Info-Production.plist index 53bfd27310..26ea19da87 100644 --- a/Stepic/Info-Production.plist +++ b/Stepic/Info-Production.plist @@ -62,7 +62,7 @@ CFBundleVersion - 349 + 350 FacebookAppID 171127739724012 FacebookDisplayName diff --git a/Stepic/Info-Release.plist b/Stepic/Info-Release.plist index 3ad6a92ee0..8c581276f1 100644 --- a/Stepic/Info-Release.plist +++ b/Stepic/Info-Release.plist @@ -62,7 +62,7 @@ CFBundleVersion - 349 + 350 FacebookAppID 171127739724012 FacebookDisplayName diff --git a/StepicTests/Info-Develop.plist b/StepicTests/Info-Develop.plist index 500280ca54..17ae02c646 100644 --- a/StepicTests/Info-Develop.plist +++ b/StepicTests/Info-Develop.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 349 + 350 diff --git a/StepicTests/Info-Production.plist b/StepicTests/Info-Production.plist index 386c77f70e..2319a7db3f 100644 --- a/StepicTests/Info-Production.plist +++ b/StepicTests/Info-Production.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 349 + 350 diff --git a/StepicTests/Info-Release.plist b/StepicTests/Info-Release.plist index c2239ca62e..a64a283252 100644 --- a/StepicTests/Info-Release.plist +++ b/StepicTests/Info-Release.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 349 + 350 diff --git a/StepicUITests/Info-Develop.plist b/StepicUITests/Info-Develop.plist index ece2614645..8670161a80 100644 --- a/StepicUITests/Info-Develop.plist +++ b/StepicUITests/Info-Develop.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.179-develop + 1.180-develop CFBundleVersion - 348 + 350 diff --git a/StepicUITests/Info-Production.plist b/StepicUITests/Info-Production.plist index dea1821e91..4b0ecf411e 100644 --- a/StepicUITests/Info-Production.plist +++ b/StepicUITests/Info-Production.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.179 + 1.180 CFBundleVersion - 348 + 350 diff --git a/StepicUITests/Info-Release.plist b/StepicUITests/Info-Release.plist index 393a8e6e8e..7532db6550 100644 --- a/StepicUITests/Info-Release.plist +++ b/StepicUITests/Info-Release.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.179-release + 1.180-release CFBundleVersion - 348 + 350 diff --git a/StepicWidget/Info-Develop.plist b/StepicWidget/Info-Develop.plist index 2831200400..fa1e43ddd2 100644 --- a/StepicWidget/Info-Develop.plist +++ b/StepicWidget/Info-Develop.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.179-develop + 1.180-develop CFBundleVersion - 348 + 350 NSExtension NSExtensionPointIdentifier diff --git a/StepicWidget/Info-Production.plist b/StepicWidget/Info-Production.plist index 01eaaa5066..f6213c3f67 100644 --- a/StepicWidget/Info-Production.plist +++ b/StepicWidget/Info-Production.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.179 + 1.180 CFBundleVersion - 348 + 350 NSExtension NSExtensionPointIdentifier diff --git a/StepicWidget/Info-Release.plist b/StepicWidget/Info-Release.plist index eeaa3982a5..65d6aedad2 100644 --- a/StepicWidget/Info-Release.plist +++ b/StepicWidget/Info-Release.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.179-release + 1.180-release CFBundleVersion - 348 + 350 NSExtension NSExtensionPointIdentifier diff --git a/StickerPackExtension/Info-Develop.plist b/StickerPackExtension/Info-Develop.plist index 710d9094de..9b46865441 100644 --- a/StickerPackExtension/Info-Develop.plist +++ b/StickerPackExtension/Info-Develop.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.180-develop CFBundleVersion - 349 + 350 NSExtension NSExtensionPointIdentifier diff --git a/StickerPackExtension/Info-Production.plist b/StickerPackExtension/Info-Production.plist index 7342f89d90..8057e7a84a 100644 --- a/StickerPackExtension/Info-Production.plist +++ b/StickerPackExtension/Info-Production.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.180 CFBundleVersion - 349 + 350 NSExtension NSExtensionPointIdentifier diff --git a/StickerPackExtension/Info-Release.plist b/StickerPackExtension/Info-Release.plist index e20da8e924..7758efb769 100644 --- a/StickerPackExtension/Info-Release.plist +++ b/StickerPackExtension/Info-Release.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.180-release CFBundleVersion - 349 + 350 NSExtension NSExtensionPointIdentifier From 3079b72253648c7db30c62be0b5e377ad7062a90 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Thu, 8 Jul 2021 11:52:47 +0300 Subject: [PATCH 8/8] Add fixes APPS-3364 --- .../Cell/CourseInfoTabReviewsCellView.swift | 2 +- .../CourseInfoTabReviewsHeaderView.swift | 10 ++++++++ ...ewsSummaryDistributionProgressesView.swift | 3 --- ...ourseInfoTabReviewsSummaryRatingView.swift | 2 +- .../CourseInfoTabReviewsSummaryView.swift | 24 ++++++++++++++++--- ...iewsSummaryDistributionCountItemView.swift | 3 ++- 6 files changed, 35 insertions(+), 9 deletions(-) diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Cell/CourseInfoTabReviewsCellView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Cell/CourseInfoTabReviewsCellView.swift index 28014a2763..1837b329df 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Cell/CourseInfoTabReviewsCellView.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Cell/CourseInfoTabReviewsCellView.swift @@ -21,7 +21,7 @@ extension CourseInfoTabReviewsCellView { let textLabelFont = Typography.subheadlineFont let textLabelInsets = UIEdgeInsets(top: 8, left: 0, bottom: 20, right: 20) - let clearStarsColor = UIColor.stepikMaterialPrimaryText + let clearStarsColor = UIColor.stepikMaterialDisabledText } } diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/CourseInfoTabReviewsHeaderView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/CourseInfoTabReviewsHeaderView.swift index 7442de5170..461a59cc90 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/CourseInfoTabReviewsHeaderView.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/CourseInfoTabReviewsHeaderView.swift @@ -75,6 +75,8 @@ final class CourseInfoTabReviewsHeaderView: UIView { didSet { self.currentReviewAction = .write self.updateReviewButton() + self.updateReviewSeparatorVisibility() + self.invalidateIntrinsicContentSize() } } @@ -82,12 +84,15 @@ final class CourseInfoTabReviewsHeaderView: UIView { didSet { self.currentReviewAction = .edit self.updateReviewButton() + self.updateReviewSeparatorVisibility() + self.invalidateIntrinsicContentSize() } } var shouldShowWriteReviewBanner = true { didSet { self.reviewDescriptionContainerView.isHidden = !self.shouldShowWriteReviewBanner + self.updateReviewSeparatorVisibility() self.invalidateIntrinsicContentSize() } } @@ -149,6 +154,11 @@ final class CourseInfoTabReviewsHeaderView: UIView { self.invalidateIntrinsicContentSize() } + private func updateReviewSeparatorVisibility() { + self.reviewSeparatorView.isHidden = self.reviewButtonContainerView.isHidden + && self.reviewDescriptionContainerView.isHidden + } + @objc private func onReviewButtonClicked() { switch self.currentReviewAction { diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/Summary/CourseInfoTabReviewsSummaryDistributionProgressesView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/Summary/CourseInfoTabReviewsSummaryDistributionProgressesView.swift index dc3213f697..a43e6ecd42 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/Summary/CourseInfoTabReviewsSummaryDistributionProgressesView.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/Summary/CourseInfoTabReviewsSummaryDistributionProgressesView.swift @@ -6,8 +6,6 @@ extension CourseInfoTabReviewsSummaryDistributionProgressesView { let progressViewSecondaryColor = UIColor.stepikGreenFixed.withAlphaComponent(0.12) let progressViewMainColor = UIColor.stepikGreenFixed let progressViewHeight: CGFloat = 4 - - let stackViewSpacing: CGFloat = 8 } } @@ -19,7 +17,6 @@ final class CourseInfoTabReviewsSummaryDistributionProgressesView: UIView { private lazy var stackView: UIStackView = { let stackView = UIStackView() stackView.axis = .vertical - stackView.spacing = self.appearance.stackViewSpacing stackView.distribution = .equalSpacing return stackView }() diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/Summary/CourseInfoTabReviewsSummaryRatingView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/Summary/CourseInfoTabReviewsSummaryRatingView.swift index cea64e5df8..158705cbbf 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/Summary/CourseInfoTabReviewsSummaryRatingView.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/Summary/CourseInfoTabReviewsSummaryRatingView.swift @@ -7,7 +7,7 @@ extension CourseInfoTabReviewsSummaryRatingView { let titleLabelTextColor = UIColor.stepikMaterialPrimaryText let titleLabelInsets = LayoutInsets(bottom: 4) - let clearStarsColor = UIColor.stepikMaterialPrimaryText + let clearStarsColor = UIColor.stepikMaterialDisabledText let starsSize = CGSize(width: 11, height: 11) } } diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/Summary/CourseInfoTabReviewsSummaryView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/Summary/CourseInfoTabReviewsSummaryView.swift index eeb4e17820..8a45d76ec8 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/Summary/CourseInfoTabReviewsSummaryView.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/Summary/CourseInfoTabReviewsSummaryView.swift @@ -5,6 +5,11 @@ extension CourseInfoTabReviewsSummaryView { struct Appearance { let stackViewSpacing: CGFloat = 16 + let progressesViewWidthRatio: CGFloat = 0.33 + let progressesViewInsets = LayoutInsets(top: 3.5) + + let distributionCountsViewInsets = LayoutInsets(bottom: -3.5) + let subtitleLabelFont = Typography.caption1Font let subtitleLabelTextColor = UIColor.stepikMaterialSecondaryText let subtitleLabelInsets = LayoutInsets.default @@ -27,6 +32,8 @@ final class CourseInfoTabReviewsSummaryView: UIView { private lazy var progressesView = CourseInfoTabReviewsSummaryDistributionProgressesView() + private lazy var progressesContainerView = UIView() + private lazy var progressesSubtitleLabel: UILabel = { let label = UILabel() label.font = self.appearance.subtitleLabelFont @@ -37,6 +44,8 @@ final class CourseInfoTabReviewsSummaryView: UIView { private lazy var distributionCountsView = CourseInfoTabReviewsSummaryDistributionCountsView() + private lazy var distributionCountsContainerView = UIView() + private lazy var stackView: UIStackView = { let stackView = UIStackView() stackView.axis = .horizontal @@ -92,9 +101,12 @@ extension CourseInfoTabReviewsSummaryView: ProgrammaticallyInitializableViewProt self.addSubview(self.ratingSubtitleLabel) self.addSubview(self.progressesSubtitleLabel) + self.progressesContainerView.addSubview(self.progressesView) + self.distributionCountsContainerView.addSubview(self.distributionCountsView) + self.stackView.addArrangedSubview(self.ratingView) - self.stackView.addArrangedSubview(self.progressesView) - self.stackView.addArrangedSubview(self.distributionCountsView) + self.stackView.addArrangedSubview(self.progressesContainerView) + self.stackView.addArrangedSubview(self.distributionCountsContainerView) } func makeConstraints() { @@ -105,7 +117,13 @@ extension CourseInfoTabReviewsSummaryView: ProgrammaticallyInitializableViewProt self.progressesView.translatesAutoresizingMaskIntoConstraints = false self.progressesView.snp.makeConstraints { make in - make.width.greaterThanOrEqualToSuperview().multipliedBy(0.33) + make.edges.equalToSuperview().inset(self.appearance.progressesViewInsets.edgeInsets) + make.width.greaterThanOrEqualToSuperview().multipliedBy(self.appearance.progressesViewWidthRatio) + } + + self.distributionCountsView.translatesAutoresizingMaskIntoConstraints = false + self.distributionCountsView.snp.makeConstraints { make in + make.edges.equalToSuperview().inset(self.appearance.distributionCountsViewInsets.edgeInsets) } self.ratingSubtitleLabel.translatesAutoresizingMaskIntoConstraints = false diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/Summary/DistributionCounts/CourseInfoTabReviewsSummaryDistributionCountItemView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/Summary/DistributionCounts/CourseInfoTabReviewsSummaryDistributionCountItemView.swift index 21266bdcdc..8c42b27084 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/Summary/DistributionCounts/CourseInfoTabReviewsSummaryDistributionCountItemView.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabReviews/Views/Header/Summary/DistributionCounts/CourseInfoTabReviewsSummaryDistributionCountItemView.swift @@ -3,7 +3,7 @@ import UIKit extension CourseInfoTabReviewsSummaryDistributionCountItemView { struct Appearance { - let titleLabelFont = Typography.caption2Font + let titleLabelFont = UIFont.systemFont(ofSize: 11) let titleLabelTextColor = UIColor.stepikMaterialSecondaryText let starsSpacing: CGFloat = 3 @@ -86,6 +86,7 @@ extension CourseInfoTabReviewsSummaryDistributionCountItemView: Programmatically self.titleLabel.translatesAutoresizingMaskIntoConstraints = false self.titleLabel.snp.makeConstraints { make in make.top.leading.bottom.equalToSuperview() + make.height.equalTo(self.appearance.titleLabelFont.pointSize) } self.starsView.translatesAutoresizingMaskIntoConstraints = false