From a7eb8e100ba2702887bdf0ddfa465fb12ab01474 Mon Sep 17 00:00:00 2001 From: Saeed Bashir Date: Mon, 11 Dec 2023 14:21:10 +0500 Subject: [PATCH] feat: navigate to course component from course dates tab (#189) * feat: navigate to course component from course dates tab * chore: renamed methods and used already fetched course data for course hyperlinks * refactor: address review feedback --- Course/Course/Data/CourseRepository.swift | 12 ++++---- Course/Course/Domain/CourseInteractor.swift | 12 ++++---- .../Container/CourseContainerViewModel.swift | 2 +- Course/Course/Presentation/CourseRouter.swift | 10 +++++++ .../Presentation/Dates/CourseDatesView.swift | 8 ++++-- .../Dates/CourseDatesViewModel.swift | 14 ++++++++++ .../Details/CourseDetailsViewModel.swift | 2 +- Course/Course/SwiftGen/Strings.swift | 2 ++ Course/Course/en.lproj/Localizable.strings | 1 + Course/Course/uk.lproj/Localizable.strings | 1 + Course/CourseTests/CourseMock.generated.swift | 20 ++++++------- OpenEdX/Router.swift | 28 +++++++++++++++++++ 12 files changed, 86 insertions(+), 26 deletions(-) diff --git a/Course/Course/Data/CourseRepository.swift b/Course/Course/Data/CourseRepository.swift index 72abf3394..39903dc87 100644 --- a/Course/Course/Data/CourseRepository.swift +++ b/Course/Course/Data/CourseRepository.swift @@ -11,8 +11,8 @@ import Core public protocol CourseRepositoryProtocol { func getCourseDetails(courseID: String) async throws -> CourseDetails func getCourseBlocks(courseID: String) async throws -> CourseStructure - func getCourseDetailsOffline(courseID: String) async throws -> CourseDetails - func getCourseBlocksOffline(courseID: String) throws -> CourseStructure + func getLoadedCourseDetails(courseID: String) async throws -> CourseDetails + func getLoadedCourseBlocks(courseID: String) throws -> CourseStructure func enrollToCourse(courseID: String) async throws -> Bool func blockCompletionRequest(courseID: String, blockID: String) async throws func getHandouts(courseID: String) async throws -> String? @@ -48,7 +48,7 @@ public class CourseRepository: CourseRepositoryProtocol { return response } - public func getCourseDetailsOffline(courseID: String) async throws -> CourseDetails { + public func getLoadedCourseDetails(courseID: String) async throws -> CourseDetails { return try persistence.loadCourseDetails(courseID: courseID) } @@ -61,7 +61,7 @@ public class CourseRepository: CourseRepositoryProtocol { return parsedStructure } - public func getCourseBlocksOffline(courseID: String) throws -> CourseStructure { + public func getLoadedCourseBlocks(courseID: String) throws -> CourseStructure { let localData = try persistence.loadCourseStructure(courseID: courseID) return parseCourseStructure(course: localData) } @@ -259,7 +259,7 @@ class CourseRepositoryMock: CourseRepositoryProtocol { } } - func getCourseDetailsOffline(courseID: String) async throws -> CourseDetails { + func getLoadedCourseDetails(courseID: String) async throws -> CourseDetails { return CourseDetails( courseID: "courseID", org: "Organization", @@ -276,7 +276,7 @@ class CourseRepositoryMock: CourseRepositoryProtocol { ) } - func getCourseBlocksOffline(courseID: String) throws -> CourseStructure { + func getLoadedCourseBlocks(courseID: String) throws -> CourseStructure { let decoder = JSONDecoder() let jsonData = Data(courseStructureJson.utf8) let courseBlocks = try decoder.decode(DataLayer.CourseStructure.self, from: jsonData) diff --git a/Course/Course/Domain/CourseInteractor.swift b/Course/Course/Domain/CourseInteractor.swift index 872b22bde..3e3b18f71 100644 --- a/Course/Course/Domain/CourseInteractor.swift +++ b/Course/Course/Domain/CourseInteractor.swift @@ -13,8 +13,8 @@ public protocol CourseInteractorProtocol { func getCourseDetails(courseID: String) async throws -> CourseDetails func getCourseBlocks(courseID: String) async throws -> CourseStructure func getCourseVideoBlocks(fullStructure: CourseStructure) -> CourseStructure - func getCourseDetailsOffline(courseID: String) async throws -> CourseDetails - func getCourseBlocksOffline(courseID: String) async throws -> CourseStructure + func getLoadedCourseDetails(courseID: String) async throws -> CourseDetails + func getLoadedCourseBlocks(courseID: String) async throws -> CourseStructure func enrollToCourse(courseID: String) async throws -> Bool func blockCompletionRequest(courseID: String, blockID: String) async throws func getHandouts(courseID: String) async throws -> String? @@ -62,12 +62,12 @@ public class CourseInteractor: CourseInteractorProtocol { ) } - public func getCourseDetailsOffline(courseID: String) async throws -> CourseDetails { - return try await repository.getCourseDetailsOffline(courseID: courseID) + public func getLoadedCourseDetails(courseID: String) async throws -> CourseDetails { + return try await repository.getLoadedCourseDetails(courseID: courseID) } - public func getCourseBlocksOffline(courseID: String) async throws -> CourseStructure { - return try repository.getCourseBlocksOffline(courseID: courseID) + public func getLoadedCourseBlocks(courseID: String) async throws -> CourseStructure { + return try repository.getLoadedCourseBlocks(courseID: courseID) } public func enrollToCourse(courseID: String) async throws -> Bool { diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index fd86eb4c3..861a87d93 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -98,7 +98,7 @@ public class CourseContainerViewModel: BaseCourseViewModel { } } } else { - courseStructure = try await interactor.getCourseBlocksOffline(courseID: courseID) + courseStructure = try await interactor.getLoadedCourseBlocks(courseID: courseID) } courseVideosStructure = interactor.getCourseVideoBlocks(fullStructure: courseStructure!) setDownloadsStates() diff --git a/Course/Course/Presentation/CourseRouter.swift b/Course/Course/Presentation/CourseRouter.swift index 3b4e6de7d..065d5395f 100644 --- a/Course/Course/Presentation/CourseRouter.swift +++ b/Course/Course/Presentation/CourseRouter.swift @@ -60,6 +60,11 @@ public protocol CourseRouter: BaseRouter { router: Course.CourseRouter, cssInjector: CSSInjector ) + + func showCourseComponent( + componentID: String, + courseStructure: CourseStructure + ) } // Mark - For testing and SwiftUI preview @@ -119,5 +124,10 @@ public class CourseRouterMock: BaseRouterMock, CourseRouter { cssInjector: CSSInjector ) {} + public func showCourseComponent( + componentID: String, + courseStructure: CourseStructure + ) {} + } #endif diff --git a/Course/Course/Presentation/Dates/CourseDatesView.swift b/Course/Course/Presentation/Dates/CourseDatesView.swift index 433b0e5ec..36bb79b9f 100644 --- a/Course/Course/Presentation/Dates/CourseDatesView.swift +++ b/Course/Course/Presentation/Dates/CourseDatesView.swift @@ -143,7 +143,8 @@ struct CourseDateListView: View { lastDate: viewModel.sortedDates.last, allHaveSameStatus: allHaveSameStatus) - BlockStatusView(block: block, + BlockStatusView(viewModel: viewModel, + block: block, allHaveSameStatus: allHaveSameStatus, blocks: blocks) @@ -159,6 +160,7 @@ struct CourseDateListView: View { } struct BlockStatusView: View { + let viewModel: CourseDatesViewModel let block: CourseDateBlock let allHaveSameStatus: Bool let blocks: [CourseDateBlock] @@ -227,7 +229,9 @@ struct BlockStatusView: View { } }()) .onTapGesture { - + Task { + await viewModel.showCourseDetails(componentID: block.firstComponentBlockID) + } } } diff --git a/Course/Course/Presentation/Dates/CourseDatesViewModel.swift b/Course/Course/Presentation/Dates/CourseDatesViewModel.swift index e60d413d9..75851d85e 100644 --- a/Course/Course/Presentation/Dates/CourseDatesViewModel.swift +++ b/Course/Course/Presentation/Dates/CourseDatesViewModel.swift @@ -27,6 +27,7 @@ public class CourseDatesViewModel: ObservableObject { let cssInjector: CSSInjector let router: CourseRouter let connectivity: ConnectivityProtocol + let courseID: String public init( interactor: CourseInteractorProtocol, @@ -39,6 +40,7 @@ public class CourseDatesViewModel: ObservableObject { self.router = router self.cssInjector = cssInjector self.connectivity = connectivity + self.courseID = courseID } var sortedDates: [Date] { @@ -69,4 +71,16 @@ public class CourseDatesViewModel: ObservableObject { } } } + + func showCourseDetails(componentID: String) async { + do { + let courseStructure = try await interactor.getLoadedCourseBlocks(courseID: courseID) + router.showCourseComponent( + componentID: componentID, + courseStructure: courseStructure + ) + } catch _ { + errorMessage = CourseLocalization.Error.componentNotFount + } + } } diff --git a/Course/Course/Presentation/Details/CourseDetailsViewModel.swift b/Course/Course/Presentation/Details/CourseDetailsViewModel.swift index ac2283d17..d0facca5a 100644 --- a/Course/Course/Presentation/Details/CourseDetailsViewModel.swift +++ b/Course/Course/Presentation/Details/CourseDetailsViewModel.swift @@ -64,7 +64,7 @@ public class CourseDetailsViewModel: ObservableObject { isShowProgress = false } else { - courseDetails = try await interactor.getCourseDetailsOffline(courseID: courseID) + courseDetails = try await interactor.getLoadedCourseDetails(courseID: courseID) if let isEnrolled = courseDetails?.isEnrolled { self.courseDetails?.isEnrolled = isEnrolled } diff --git a/Course/Course/SwiftGen/Strings.swift b/Course/Course/SwiftGen/Strings.swift index 2eaafb3bf..4815bb970 100644 --- a/Course/Course/SwiftGen/Strings.swift +++ b/Course/Course/SwiftGen/Strings.swift @@ -74,6 +74,8 @@ public enum CourseLocalization { public static let viewCourse = CourseLocalization.tr("Localizable", "DETAILS.VIEW_COURSE", fallback: "View course") } public enum Error { + /// Course component not found, please reload + public static let componentNotFount = CourseLocalization.tr("Localizable", "ERROR.COMPONENT_NOT_FOUNT", fallback: "Course component not found, please reload") /// You are not connected to the Internet. Please check your Internet connection. public static let noInternet = CourseLocalization.tr("Localizable", "ERROR.NO_INTERNET", fallback: "You are not connected to the Internet. Please check your Internet connection.") /// Reload diff --git a/Course/Course/en.lproj/Localizable.strings b/Course/Course/en.lproj/Localizable.strings index 3152f86c7..65d83b0b6 100644 --- a/Course/Course/en.lproj/Localizable.strings +++ b/Course/Course/en.lproj/Localizable.strings @@ -32,6 +32,7 @@ "ERROR.NO_INTERNET" = "You are not connected to the Internet. Please check your Internet connection."; "ERROR.RELOAD" = "Reload"; +"ERROR.COMPONENT_NOT_FOUNT" = "Course component not found, please reload"; "ALERT.ROTATE_DEVICE" = "Rotate your device to view this video in full screen."; diff --git a/Course/Course/uk.lproj/Localizable.strings b/Course/Course/uk.lproj/Localizable.strings index 302297084..d479f9381 100644 --- a/Course/Course/uk.lproj/Localizable.strings +++ b/Course/Course/uk.lproj/Localizable.strings @@ -31,6 +31,7 @@ "ERROR.NO_INTERNET" = "Ви не підключені до Інтернету. Перевірте підключення до Інтернету і спробуйте ще."; "ERROR.RELOAD" = "Перезавантажити"; +"ERROR.COMPONENT_NOT_FOUNT" = "Course component not found, please reload"; "ALERT.ROTATE_DEVICE" = "Поверніть пристрій, щоб переглянути це відео на весь екран."; diff --git a/Course/CourseTests/CourseMock.generated.swift b/Course/CourseTests/CourseMock.generated.swift index ef2212269..bd324ab2d 100644 --- a/Course/CourseTests/CourseMock.generated.swift +++ b/Course/CourseTests/CourseMock.generated.swift @@ -1649,13 +1649,13 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { return __value } - open func getCourseDetailsOffline(courseID: String) throws -> CourseDetails { - addInvocation(.m_getCourseDetailsOffline__courseID_courseID(Parameter.value(`courseID`))) - let perform = methodPerformValue(.m_getCourseDetailsOffline__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void - perform?(`courseID`) + open func getLoadedCourseDetails(courseID: String) throws -> CourseDetails { + addInvocation(.m_getCourseDetailsOffline__courseID_courseID(Parameter.value(courseID))) + let perform = methodPerformValue(.m_getCourseDetailsOffline__courseID_courseID(Parameter.value(courseID))) as? (String) -> Void + perform?(courseID) var __value: CourseDetails do { - __value = try methodReturnValue(.m_getCourseDetailsOffline__courseID_courseID(Parameter.value(`courseID`))).casted() + __value = try methodReturnValue(.m_getCourseDetailsOffline__courseID_courseID(Parameter.value(courseID))).casted() } catch MockError.notStubed { onFatalFailure("Stub return value not specified for getCourseDetailsOffline(courseID: String). Use given") Failure("Stub return value not specified for getCourseDetailsOffline(courseID: String). Use given") @@ -1665,13 +1665,13 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { return __value } - open func getCourseBlocksOffline(courseID: String) throws -> CourseStructure { - addInvocation(.m_getCourseBlocksOffline__courseID_courseID(Parameter.value(`courseID`))) - let perform = methodPerformValue(.m_getCourseBlocksOffline__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void - perform?(`courseID`) + open func getLoadedCourseBlocks(courseID: String) throws -> CourseStructure { + addInvocation(.m_getCourseBlocksOffline__courseID_courseID(Parameter.value(courseID))) + let perform = methodPerformValue(.m_getCourseBlocksOffline__courseID_courseID(Parameter.value(courseID))) as? (String) -> Void + perform?(courseID) var __value: CourseStructure do { - __value = try methodReturnValue(.m_getCourseBlocksOffline__courseID_courseID(Parameter.value(`courseID`))).casted() + __value = try methodReturnValue(.m_getCourseBlocksOffline__courseID_courseID(Parameter.value(courseID))).casted() } catch MockError.notStubed { onFatalFailure("Stub return value not specified for getCourseBlocksOffline(courseID: String). Use given") Failure("Stub return value not specified for getCourseBlocksOffline(courseID: String). Use given") diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index 264508884..aac5a0fa8 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -311,6 +311,34 @@ public class Router: AuthorizationRouter, navigationController.pushViewController(controller, animated: true) } + public func showCourseComponent( + componentID: String, + courseStructure: CourseStructure) { + courseStructure.childs.enumerated().forEach { chapterIndex, chapter in + chapter.childs.enumerated().forEach { sequentialIndex, sequential in + sequential.childs.enumerated().forEach { verticalIndex, vertical in + vertical.childs.forEach { block in + if block.id == componentID { + DispatchQueue.main.async { [weak self] in + guard let self else { return } + self.showCourseUnit( + courseName: courseStructure.displayName, + blockId: block.blockId, + courseID: courseStructure.id, + sectionName: sequential.displayName, + verticalIndex: verticalIndex, + chapters: courseStructure.childs, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex) + } + return + } + } + } + } + } + } + public func replaceCourseUnit( courseName: String, blockId: String,