diff --git a/Core/Core/Data/CoreStorage.swift b/Core/Core/Data/CoreStorage.swift index 60837da41..ef09cd079 100644 --- a/Core/Core/Data/CoreStorage.swift +++ b/Core/Core/Data/CoreStorage.swift @@ -13,12 +13,13 @@ public protocol CoreStorage { var pushToken: String? {get set} var appleSignFullName: String? {get set} var appleSignEmail: String? {get set} - var cookiesDate: String? {get set} + var cookiesDate: Date? {get set} var reviewLastShownVersion: String? {get set} var lastReviewDate: Date? {get set} var user: DataLayer.User? {get set} var userSettings: UserSettings? {get set} var resetAppSupportDirectoryUserData: Bool? {get set} + var useRelativeDates: Bool {get set} func clear() } @@ -29,12 +30,13 @@ public class CoreStorageMock: CoreStorage { public var pushToken: String? public var appleSignFullName: String? public var appleSignEmail: String? - public var cookiesDate: String? + public var cookiesDate: Date? public var reviewLastShownVersion: String? public var lastReviewDate: Date? public var user: DataLayer.User? public var userSettings: UserSettings? public var resetAppSupportDirectoryUserData: Bool? + public var useRelativeDates: Bool = true public func clear() {} public init() {} diff --git a/Core/Core/Data/Model/Data_CourseDates.swift b/Core/Core/Data/Model/Data_CourseDates.swift index 35616b020..e17f767ce 100644 --- a/Core/Core/Data/Model/Data_CourseDates.swift +++ b/Core/Core/Data/Model/Data_CourseDates.swift @@ -166,7 +166,7 @@ public extension DataLayer { } public extension DataLayer.CourseDates { - var domain: CourseDates { + func domain(useRelativeDates: Bool) -> CourseDates { return CourseDates( datesBannerInfo: DatesBannerInfo( missedDeadlines: datesBannerInfo?.missedDeadlines ?? false, @@ -186,7 +186,8 @@ public extension DataLayer.CourseDates { linkText: block.linkText ?? nil, title: block.title, extraInfo: block.extraInfo, - firstComponentBlockID: block.firstComponentBlockID) + firstComponentBlockID: block.firstComponentBlockID, + useRelativeDates: useRelativeDates) }, hasEnded: hasEnded, learnerIsFullAccess: learnerIsFullAccess, diff --git a/Core/Core/Data/Repository/AuthRepository.swift b/Core/Core/Data/Repository/AuthRepository.swift index e4adf93ea..d00441ee6 100644 --- a/Core/Core/Data/Repository/AuthRepository.swift +++ b/Core/Core/Data/Repository/AuthRepository.swift @@ -88,15 +88,14 @@ public class AuthRepository: AuthRepositoryProtocol { public func getCookies(force: Bool) async throws { if let cookiesCreatedDate = appStorage.cookiesDate, !force { - let cookiesCreated = Date(iso8601: cookiesCreatedDate) - let cookieLifetimeLimit = cookiesCreated.addingTimeInterval(60 * 60) + let cookieLifetimeLimit = cookiesCreatedDate.addingTimeInterval(60 * 60) if Date() > cookieLifetimeLimit { _ = try await api.requestData(AuthEndpoint.getAuthCookies) - appStorage.cookiesDate = Date().dateToString(style: .iso8601) + appStorage.cookiesDate = Date() } } else { _ = try await api.requestData(AuthEndpoint.getAuthCookies) - appStorage.cookiesDate = Date().dateToString(style: .iso8601) + appStorage.cookiesDate = Date() } } diff --git a/Core/Core/Domain/Model/CourseDates.swift b/Core/Core/Domain/Model/CourseDates.swift index f3fbcdd9a..11c0e943d 100644 --- a/Core/Core/Domain/Model/CourseDates.swift +++ b/Core/Core/Domain/Model/CourseDates.swift @@ -156,9 +156,10 @@ public struct CourseDateBlock: Identifiable { public let title: String public let extraInfo: String? public let firstComponentBlockID: String + public let useRelativeDates: Bool public var formattedDate: String { - return date.dateToString(style: .shortWeekdayMonthDayYear) + return date.dateToString(style: .shortWeekdayMonthDayYear, useRelativeDates: useRelativeDates) } public var isInPast: Bool { diff --git a/Core/Core/Extensions/DateExtension.swift b/Core/Core/Extensions/DateExtension.swift index 7be0c84ec..cb07e81f6 100644 --- a/Core/Core/Extensions/DateExtension.swift +++ b/Core/Core/Extensions/DateExtension.swift @@ -14,7 +14,7 @@ public extension Date { var date: Date var dateFormatter: DateFormatter? dateFormatter = DateFormatter() - dateFormatter?.locale = Locale(identifier: "en_US_POSIX") + dateFormatter?.locale = .current date = formats.compactMap { format in dateFormatter?.dateFormat = format @@ -33,16 +33,75 @@ public extension Date { self.init(timeInterval: 0, since: date) } - func timeAgoDisplay() -> String { - let formatter = RelativeDateTimeFormatter() - formatter.locale = .current - formatter.unitsStyle = .full - formatter.locale = Locale(identifier: "en_US_POSIX") - if description == Date().description { - return CoreLocalization.Date.justNow - } else { - return formatter.localizedString(for: self, relativeTo: Date()) + func timeAgoDisplay(dueIn: Bool = false) -> String { + let currentDate = Date() + let calendar = Calendar.current + + let dueString = dueIn ? CoreLocalization.Date.due : "" + let dueInString = dueIn ? CoreLocalization.Date.dueIn : "" + + let startOfCurrentDate = calendar.startOfDay(for: currentDate) + let startOfSelfDate = calendar.startOfDay(for: self) + + let daysRemaining = Calendar.current.dateComponents( + [.day], + from: startOfCurrentDate, + to: self + ).day ?? 0 + + // Calculate date ranges + guard let sevenDaysAgo = calendar.date(byAdding: .day, value: -7, to: startOfCurrentDate), + let sevenDaysAhead = calendar.date(byAdding: .day, value: 7, to: startOfCurrentDate) else { + return dueInString + self.dateToString(style: .mmddyy, useRelativeDates: false) + } + + let isCurrentYear = calendar.component(.year, from: self) == calendar.component(.year, from: startOfCurrentDate) + + if calendar.isDateInToday(startOfSelfDate) { + return dueString + CoreLocalization.Date.today + } + + if calendar.isDateInYesterday(startOfSelfDate) { + return dueString + CoreLocalization.yesterday + } + + if calendar.isDateInTomorrow(startOfSelfDate) { + return dueString + CoreLocalization.tomorrow } + + if startOfSelfDate > startOfCurrentDate && startOfSelfDate <= sevenDaysAhead { + let weekdayFormatter = DateFormatter() + weekdayFormatter.dateFormat = "EEEE" + if startOfSelfDate == calendar.date(byAdding: .day, value: 1, to: startOfCurrentDate) { + return dueInString + CoreLocalization.tomorrow + } else if startOfSelfDate == calendar.date(byAdding: .day, value: 7, to: startOfCurrentDate) { + return CoreLocalization.Date.next(weekdayFormatter.string(from: startOfSelfDate)) + } else { + return dueIn ? ( + CoreLocalization.Date.dueInDays(daysRemaining) + ) : weekdayFormatter.string(from: startOfSelfDate) + } + } + + if startOfSelfDate < startOfCurrentDate && startOfSelfDate >= sevenDaysAgo { + guard let daysAgo = calendar.dateComponents([.day], from: startOfSelfDate, to: startOfCurrentDate).day else { + return self.dateToString(style: .mmddyy, useRelativeDates: false) + } + return CoreLocalization.Date.daysAgo(daysAgo) + } + + let specificFormatter = DateFormatter() + specificFormatter.dateFormat = isCurrentYear ? "MMMM d" : "MMMM d, yyyy" + return dueInString + specificFormatter.string(from: self) + } + + func isDateInNextWeek(date: Date, currentDate: Date) -> Bool { + let calendar = Calendar.current + guard let nextWeek = calendar.date(byAdding: .weekOfYear, value: 1, to: currentDate) else { return false } + let startOfNextWeek = calendar.startOfDay(for: nextWeek) + guard let endOfNextWeek = calendar.date(byAdding: .day, value: 6, to: startOfNextWeek) else { return false } + let startOfSelfDate = calendar.startOfDay(for: date) + return startOfSelfDate >= startOfNextWeek && startOfSelfDate <= endOfNextWeek } init(subtitleTime: String) { @@ -100,29 +159,34 @@ public extension Date { return totalSeconds } - func dateToString(style: DateStringStyle) -> String { + func dateToString(style: DateStringStyle, useRelativeDates: Bool, dueIn: Bool = false) -> String { let dateFormatter = DateFormatter() - dateFormatter.locale = Locale(identifier: "en_US_POSIX") - switch style { - case .courseStartsMonthDDYear: - dateFormatter.dateFormat = CoreLocalization.DateFormat.mmmDdYyyy - case .courseEndsMonthDDYear: - dateFormatter.dateFormat = CoreLocalization.DateFormat.mmmDdYyyy - case .endedMonthDay: - dateFormatter.dateFormat = CoreLocalization.DateFormat.mmmmDd - case .mmddyy: - dateFormatter.dateFormat = "dd.MM.yy" - case .monthYear: - dateFormatter.dateFormat = "MMMM yyyy" - case .startDDMonthYear: - dateFormatter.dateFormat = "dd MMM yyyy" - case .lastPost: - dateFormatter.dateFormat = CoreLocalization.DateFormat.mmmDdYyyy - case .iso8601: - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" - case .shortWeekdayMonthDayYear: - applyShortWeekdayMonthDayYear(dateFormatter: dateFormatter) + dateFormatter.locale = .current + + if useRelativeDates { + return timeAgoDisplay(dueIn: dueIn) + } else { + switch style { + case .courseStartsMonthDDYear: + dateFormatter.dateStyle = .medium + case .courseEndsMonthDDYear: + dateFormatter.dateStyle = .medium + case .endedMonthDay: + dateFormatter.dateFormat = CoreLocalization.DateFormat.mmmmDd + case .mmddyy: + dateFormatter.dateFormat = "dd.MM.yy" + case .monthYear: + dateFormatter.dateFormat = "MMMM yyyy" + case .startDDMonthYear: + dateFormatter.dateFormat = "dd MMM yyyy" + case .lastPost: + dateFormatter.dateFormat = CoreLocalization.DateFormat.mmmDdYyyy + case .iso8601: + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" + case .shortWeekdayMonthDayYear: + applyShortWeekdayMonthDayYear(dateFormatter: dateFormatter) + } } let date = dateFormatter.string(from: self) @@ -160,52 +224,19 @@ public extension Date { case .iso8601: return date case .shortWeekdayMonthDayYear: - return getShortWeekdayMonthDayYear(dateFormatterString: date) + return ( + dueIn ? CoreLocalization.Date.dueIn : "" + ) + getShortWeekdayMonthDayYear(dateFormatterString: date) } } private func applyShortWeekdayMonthDayYear(dateFormatter: DateFormatter) { - if isCurrentYear() { - let days = Calendar.current.dateComponents([.day], from: self, to: Date()) - if let day = days.day, (-6 ... -2).contains(day) { - dateFormatter.dateFormat = "EEEE" - } else { - dateFormatter.dateFormat = "MMMM d" - } - } else { dateFormatter.dateFormat = "MMMM d, yyyy" - } } private func getShortWeekdayMonthDayYear(dateFormatterString: String) -> String { let days = Calendar.current.dateComponents([.day], from: self, to: Date()) - - if let day = days.day { - guard isCurrentYear() else { - // It's past year or future year - return dateFormatterString - } - - switch day { - case -6...(-2): - return dateFormatterString - case 2...6: - return timeAgoDisplay() - case -1: - return CoreLocalization.tomorrow - case 1: - return CoreLocalization.yesterday - default: - if day > 6 || day < -6 { - return dateFormatterString - } else { - // It means, date is in hours past due or upcoming - return timeAgoDisplay() - } - } - } else { - return dateFormatterString - } + return dateFormatterString } func isCurrentYear() -> Bool { diff --git a/Core/Core/SwiftGen/Strings.swift b/Core/Core/SwiftGen/Strings.swift index ab42f4176..ed9aa825d 100644 --- a/Core/Core/SwiftGen/Strings.swift +++ b/Core/Core/SwiftGen/Strings.swift @@ -137,14 +137,32 @@ public enum CoreLocalization { public static let courseEnds = CoreLocalization.tr("Localizable", "DATE.COURSE_ENDS", fallback: "Course Ends") /// Course Starts public static let courseStarts = CoreLocalization.tr("Localizable", "DATE.COURSE_STARTS", fallback: "Course Starts") + /// %@ Days Ago + public static func daysAgo(_ p1: Any) -> String { + return CoreLocalization.tr("Localizable", "DATE.DAYS_AGO", String(describing: p1), fallback: "%@ Days Ago") + } + /// Due + public static let due = CoreLocalization.tr("Localizable", "DATE.DUE", fallback: "Due ") + /// Due in + public static let dueIn = CoreLocalization.tr("Localizable", "DATE.DUE_IN", fallback: "Due in ") + /// Due in %@ Days + public static func dueInDays(_ p1: Any) -> String { + return CoreLocalization.tr("Localizable", "DATE.DUE_IN_DAYS", String(describing: p1), fallback: "Due in %@ Days") + } /// Ended public static let ended = CoreLocalization.tr("Localizable", "DATE.ENDED", fallback: "Ended") /// Just now public static let justNow = CoreLocalization.tr("Localizable", "DATE.JUST_NOW", fallback: "Just now") + /// Next %@ + public static func next(_ p1: Any) -> String { + return CoreLocalization.tr("Localizable", "DATE.NEXT", String(describing: p1), fallback: "Next %@") + } /// Start public static let start = CoreLocalization.tr("Localizable", "DATE.START", fallback: "Start") /// Started public static let started = CoreLocalization.tr("Localizable", "DATE.STARTED", fallback: "Started") + /// Today + public static let today = CoreLocalization.tr("Localizable", "DATE.TODAY", fallback: "Today") } public enum DateFormat { /// MMM dd, yyyy diff --git a/Core/Core/View/Base/CourseCellView.swift b/Core/Core/View/Base/CourseCellView.swift index a996c3383..35f0c5d61 100644 --- a/Core/Core/View/Base/CourseCellView.swift +++ b/Core/Core/View/Base/CourseCellView.swift @@ -27,12 +27,12 @@ public struct CourseCellView: View { private var cellsCount: Int private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } - public init(model: CourseItem, type: CellType, index: Int, cellsCount: Int) { + public init(model: CourseItem, type: CellType, index: Int, cellsCount: Int, useRelativeDates: Bool) { self.type = type self.courseImage = model.imageURL self.courseName = model.name - self.courseStart = model.courseStart?.dateToString(style: .startDDMonthYear) ?? "" - self.courseEnd = model.courseEnd?.dateToString(style: .endedMonthDay) ?? "" + self.courseStart = model.courseStart?.dateToString(style: .startDDMonthYear, useRelativeDates: useRelativeDates) ?? "" + self.courseEnd = model.courseEnd?.dateToString(style: .endedMonthDay, useRelativeDates: useRelativeDates) ?? "" self.courseOrg = model.org self.index = Double(index) + 1 self.cellsCount = cellsCount @@ -148,10 +148,10 @@ struct CourseCellView_Previews: PreviewProvider { .ignoresSafeArea() VStack(spacing: 0) { // Divider() - CourseCellView(model: course, type: .discovery, index: 1, cellsCount: 3) + CourseCellView(model: course, type: .discovery, index: 1, cellsCount: 3, useRelativeDates: true) .previewLayout(.fixed(width: 180, height: 260)) // Divider() - CourseCellView(model: course, type: .discovery, index: 2, cellsCount: 3) + CourseCellView(model: course, type: .discovery, index: 2, cellsCount: 3, useRelativeDates: false) .previewLayout(.fixed(width: 180, height: 260)) // Divider() } diff --git a/Core/Core/en.lproj/Localizable.strings b/Core/Core/en.lproj/Localizable.strings index 1f8389f1f..b7bc68aad 100644 --- a/Core/Core/en.lproj/Localizable.strings +++ b/Core/Core/en.lproj/Localizable.strings @@ -53,6 +53,12 @@ "DATE.START" = "Start"; "DATE.STARTED" = "Started"; "DATE.JUST_NOW" = "Just now"; +"DATE.TODAY" = "Today"; +"DATE.NEXT" = "Next %@"; +"DATE.DAYS_AGO" = "%@ Days Ago"; +"DATE.DUE" = "Due "; +"DATE.DUE_IN" = "Due in "; +"DATE.DUE_IN_DAYS" = "Due in %@ Days"; "ALERT.ACCEPT" = "ACCEPT"; "ALERT.CANCEL" = "CANCEL"; diff --git a/Course/Course/Data/CourseRepository.swift b/Course/Course/Data/CourseRepository.swift index e5d4106b5..dc90d9bd4 100644 --- a/Course/Course/Data/CourseRepository.swift +++ b/Course/Course/Data/CourseRepository.swift @@ -101,7 +101,7 @@ public class CourseRepository: CourseRepositoryProtocol { public func getCourseDates(courseID: String) async throws -> CourseDates { let courseDates = try await api.requestData( CourseEndpoint.getCourseDates(courseID: courseID) - ).mapResponse(DataLayer.CourseDates.self).domain + ).mapResponse(DataLayer.CourseDates.self).domain(useRelativeDates: coreStorage.useRelativeDates) persistence.saveCourseDates(courseID: courseID, courseDates: courseDates) return courseDates } @@ -276,7 +276,7 @@ class CourseRepositoryMock: CourseRepositoryProtocol { do { let courseDates = try CourseRepository.courseDatesJSON.data(using: .utf8)!.mapResponse(DataLayer.CourseDates.self) - return courseDates.domain + return courseDates.domain(useRelativeDates: true) } catch { throw error } diff --git a/Course/CourseTests/Presentation/Unit/CourseDateViewModelTests.swift b/Course/CourseTests/Presentation/Unit/CourseDateViewModelTests.swift index c88637989..841c56bbc 100644 --- a/Course/CourseTests/Presentation/Unit/CourseDateViewModelTests.swift +++ b/Course/CourseTests/Presentation/Unit/CourseDateViewModelTests.swift @@ -147,7 +147,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockID1" + firstComponentBlockID: "blockID1", + useRelativeDates: true ) let block2 = CourseDateBlock( @@ -161,7 +162,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockID1" + firstComponentBlockID: "blockID1", + useRelativeDates: true ) let courseDates = CourseDates( @@ -195,7 +197,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockID1" + firstComponentBlockID: "blockID1", + useRelativeDates: true ) let block2 = CourseDateBlock( @@ -209,7 +212,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockID1" + firstComponentBlockID: "blockID1", + useRelativeDates: true ) let courseDates = CourseDates( @@ -242,7 +246,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestAssignment", extraInfo: nil, - firstComponentBlockID: "blockID3" + firstComponentBlockID: "blockID3", + useRelativeDates: true ) XCTAssertEqual(block.blockStatus, .dueNext) @@ -260,7 +265,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: CourseLocalization.CourseDates.today, extraInfo: nil, - firstComponentBlockID: "blockIDTest" + firstComponentBlockID: "blockIDTest", + useRelativeDates: true ) XCTAssertEqual(block.title, "Today", "Block title for 'today' should be 'Today'") @@ -278,7 +284,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockIDTest" + firstComponentBlockID: "blockIDTest", + useRelativeDates: true ) XCTAssertEqual(block.blockStatus, .completed, "Block status for a completed assignment should be 'completed'") } @@ -295,7 +302,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockIDTest" + firstComponentBlockID: "blockIDTest", + useRelativeDates: true ) XCTAssertEqual(block.blockStatus, .verifiedOnly, "Block status for a block without learner access should be 'verifiedOnly'") @@ -313,7 +321,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockIDTest" + firstComponentBlockID: "blockIDTest", + useRelativeDates: true ) XCTAssertEqual(block.blockStatus, .pastDue, "Block status for a past due assignment should be 'pastDue'") @@ -331,7 +340,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockIDTest" + firstComponentBlockID: "blockIDTest", + useRelativeDates: true ) XCTAssertTrue(availableAssignment.canShowLink, "Available assignments should be hyperlinked.") } @@ -348,7 +358,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockIDTest" + firstComponentBlockID: "blockIDTest", + useRelativeDates: true ) XCTAssertTrue(block.isAssignment) @@ -366,7 +377,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockIDTest" + firstComponentBlockID: "blockIDTest", + useRelativeDates: true ) XCTAssertEqual(block.blockStatus, BlockStatus.courseStartDate) @@ -384,7 +396,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockIDTest" + firstComponentBlockID: "blockIDTest", + useRelativeDates: true ) XCTAssertEqual(block.blockStatus, BlockStatus.courseEndDate) @@ -402,7 +415,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockIDTest" + firstComponentBlockID: "blockIDTest", + useRelativeDates: true ) XCTAssertTrue(block.isVerifiedOnly, "Block should be identified as 'verified only' when the learner has no access.") @@ -420,7 +434,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockIDTest" + firstComponentBlockID: "blockIDTest", + useRelativeDates: true ) XCTAssertTrue(block.isComplete, "Block should be marked as completed.") @@ -438,7 +453,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockIDTest" + firstComponentBlockID: "blockIDTest", + useRelativeDates: true ) XCTAssertEqual(block.blockStatus, .unreleased, "Block status should be set to 'unreleased' for unreleased assignments.") @@ -456,7 +472,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockIDTest" + firstComponentBlockID: "blockIDTest", + useRelativeDates: true ) XCTAssertEqual(block.blockStatus, .verifiedOnly) @@ -475,7 +492,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockIDTest" + firstComponentBlockID: "blockIDTest", + useRelativeDates: true ) XCTAssertEqual(block.blockStatus, .unreleased) diff --git a/Dashboard/Dashboard/Presentation/AllCoursesView.swift b/Dashboard/Dashboard/Presentation/AllCoursesView.swift index 332ae13c4..4960ae42a 100644 --- a/Dashboard/Dashboard/Presentation/AllCoursesView.swift +++ b/Dashboard/Dashboard/Presentation/AllCoursesView.swift @@ -58,6 +58,7 @@ public struct AllCoursesView: View { .disabled(viewModel.fetchInProgress) .frameLimit(width: proxy.size.width) if let myEnrollments = viewModel.myEnrollments { + let useRelativeDates = viewModel.storage.useRelativeDates LazyVGrid(columns: columns(), spacing: 15) { ForEach( Array(myEnrollments.courses.enumerated()), @@ -88,7 +89,8 @@ public struct AllCoursesView: View { courseStartDate: course.courseStart, courseEndDate: course.courseEnd, hasAccess: course.hasAccess, - showProgress: true + showProgress: true, + useRelativeDates: useRelativeDates ).padding(8) }) .accessibilityIdentifier("course_item") @@ -196,7 +198,8 @@ struct AllCoursesView_Previews: PreviewProvider { let vm = AllCoursesViewModel( interactor: DashboardInteractor.mock, connectivity: Connectivity(), - analytics: DashboardAnalyticsMock() + analytics: DashboardAnalyticsMock(), + storage: CoreStorageMock() ) AllCoursesView(viewModel: vm, router: DashboardRouterMock()) diff --git a/Dashboard/Dashboard/Presentation/AllCoursesViewModel.swift b/Dashboard/Dashboard/Presentation/AllCoursesViewModel.swift index 750c0936b..439f329f7 100644 --- a/Dashboard/Dashboard/Presentation/AllCoursesViewModel.swift +++ b/Dashboard/Dashboard/Presentation/AllCoursesViewModel.swift @@ -29,6 +29,7 @@ public class AllCoursesViewModel: ObservableObject { } let connectivity: ConnectivityProtocol + let storage: CoreStorage private let interactor: DashboardInteractorProtocol private let analytics: DashboardAnalytics private var onCourseEnrolledCancellable: AnyCancellable? @@ -36,11 +37,13 @@ public class AllCoursesViewModel: ObservableObject { public init( interactor: DashboardInteractorProtocol, connectivity: ConnectivityProtocol, - analytics: DashboardAnalytics + analytics: DashboardAnalytics, + storage: CoreStorage ) { self.interactor = interactor self.connectivity = connectivity self.analytics = analytics + self.storage = storage onCourseEnrolledCancellable = NotificationCenter.default .publisher(for: .onCourseEnrolled) diff --git a/Dashboard/Dashboard/Presentation/Elements/CourseCardView.swift b/Dashboard/Dashboard/Presentation/Elements/CourseCardView.swift index 2c93c7d33..e493c00d5 100644 --- a/Dashboard/Dashboard/Presentation/Elements/CourseCardView.swift +++ b/Dashboard/Dashboard/Presentation/Elements/CourseCardView.swift @@ -20,6 +20,7 @@ struct CourseCardView: View { private let courseEndDate: Date? private let hasAccess: Bool private let showProgress: Bool + private let useRelativeDates: Bool init( courseName: String, @@ -29,7 +30,8 @@ struct CourseCardView: View { courseStartDate: Date?, courseEndDate: Date?, hasAccess: Bool, - showProgress: Bool + showProgress: Bool, + useRelativeDates: Bool ) { self.courseName = courseName self.courseImage = courseImage @@ -39,6 +41,7 @@ struct CourseCardView: View { self.courseEndDate = courseEndDate self.hasAccess = hasAccess self.showProgress = showProgress + self.useRelativeDates = useRelativeDates } var body: some View { @@ -85,12 +88,12 @@ struct CourseCardView: View { private var courseTitle: some View { VStack(alignment: .leading, spacing: 3) { if let courseEndDate { - Text(courseEndDate.dateToString(style: .courseEndsMonthDDYear)) + Text(courseEndDate.dateToString(style: .courseEndsMonthDDYear, useRelativeDates: useRelativeDates)) .font(Theme.Fonts.labelSmall) .foregroundStyle(Theme.Colors.textSecondaryLight) .multilineTextAlignment(.leading) } else if let courseStartDate { - Text(courseStartDate.dateToString(style: .courseStartsMonthDDYear)) + Text(courseStartDate.dateToString(style: .courseStartsMonthDDYear, useRelativeDates: useRelativeDates)) .font(Theme.Fonts.labelSmall) .foregroundStyle(Theme.Colors.textSecondaryLight) .multilineTextAlignment(.leading) @@ -119,7 +122,8 @@ struct CourseCardView: View { courseStartDate: nil, courseEndDate: Date(), hasAccess: true, - showProgress: true + showProgress: true, + useRelativeDates: true ).frame(width: 170) } #endif diff --git a/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift b/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift index fc18526a9..83680dc7c 100644 --- a/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift +++ b/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift @@ -23,6 +23,7 @@ public struct PrimaryCardView: View { private let progressPossible: Int private let canResume: Bool private let resumeTitle: String? + private let useRelativeDates: Bool private var assignmentAction: (String?) -> Void private var openCourseAction: () -> Void private var resumeAction: () -> Void @@ -39,6 +40,7 @@ public struct PrimaryCardView: View { progressPossible: Int, canResume: Bool, resumeTitle: String?, + useRelativeDates: Bool, assignmentAction: @escaping (String?) -> Void, openCourseAction: @escaping () -> Void, resumeAction: @escaping () -> Void @@ -54,6 +56,7 @@ public struct PrimaryCardView: View { self.progressPossible = progressPossible self.canResume = canResume self.resumeTitle = resumeTitle + self.useRelativeDates = useRelativeDates self.assignmentAction = assignmentAction self.openCourseAction = openCourseAction self.resumeAction = resumeAction @@ -110,9 +113,10 @@ public struct PrimaryCardView: View { ).day ?? 0 courseButton( title: futureAssignment.title, - description: DashboardLocalization.Learn.PrimaryCard.dueDays( - futureAssignment.type, - daysRemaining + description: futureAssignment.date.dateToString( + style: .shortWeekdayMonthDayYear, + useRelativeDates: useRelativeDates, + dueIn: true ), icon: CoreAssets.chapter.swiftUIImage, selected: false, @@ -125,7 +129,7 @@ public struct PrimaryCardView: View { courseButton( title: DashboardLocalization.Learn.PrimaryCard.futureAssignments( futureAssignments.count, - firtsData.date.dateToString(style: .lastPost) + firtsData.date.dateToString(style: .lastPost, useRelativeDates: useRelativeDates) ), description: nil, icon: CoreAssets.chapter.swiftUIImage, @@ -235,11 +239,11 @@ public struct PrimaryCardView: View { .foregroundStyle(Theme.Colors.textPrimary) .lineLimit(3) if let courseEndDate { - Text(courseEndDate.dateToString(style: .courseEndsMonthDDYear)) + Text(courseEndDate.dateToString(style: .courseEndsMonthDDYear, useRelativeDates: useRelativeDates)) .font(Theme.Fonts.labelMedium) .foregroundStyle(Theme.Colors.textSecondaryLight) } else if let courseStartDate { - Text(courseStartDate.dateToString(style: .courseStartsMonthDDYear)) + Text(courseStartDate.dateToString(style: .courseStartsMonthDDYear, useRelativeDates: useRelativeDates)) .font(Theme.Fonts.labelMedium) .foregroundStyle(Theme.Colors.textSecondaryLight) } @@ -261,13 +265,23 @@ struct PrimaryCardView_Previews: PreviewProvider { courseImage: "https://thumbs.dreamstime.com/b/logo-edx-samsung-tablet-edx-massive-open-online-course-mooc-provider-hosts-online-university-level-courses-wide-117763805.jpg", courseStartDate: nil, courseEndDate: Date(), - futureAssignments: [], + futureAssignments: [ + Assignment( + type: "Lesson", + title: "HomeWork", + description: "Some description", + date: Date().addingTimeInterval(64000 * 3), + complete: false, + firstComponentBlockId: "123" + ) + ], pastAssignments: [], progressEarned: 10, progressPossible: 45, canResume: true, resumeTitle: "Course Chapter 1", - assignmentAction: {_ in }, + useRelativeDates: false, + assignmentAction: { _ in }, openCourseAction: {}, resumeAction: {} ) diff --git a/Dashboard/Dashboard/Presentation/ListDashboardView.swift b/Dashboard/Dashboard/Presentation/ListDashboardView.swift index 90a3e5eba..a2678b3ff 100644 --- a/Dashboard/Dashboard/Presentation/ListDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/ListDashboardView.swift @@ -54,14 +54,15 @@ public struct ListDashboardView: View { if viewModel.courses.isEmpty && !viewModel.fetchInProgress { EmptyPageIcon() } else { + let useRelativeDates = viewModel.storage.useRelativeDates ForEach(Array(viewModel.courses.enumerated()), id: \.offset) { index, course in - CourseCellView( model: course, type: .dashboard, index: index, - cellsCount: viewModel.courses.count + cellsCount: viewModel.courses.count, + useRelativeDates: useRelativeDates ) .padding(.horizontal, 20) .listRowBackground(Color.clear) @@ -157,7 +158,8 @@ struct ListDashboardView_Previews: PreviewProvider { let vm = ListDashboardViewModel( interactor: DashboardInteractor.mock, connectivity: Connectivity(), - analytics: DashboardAnalyticsMock() + analytics: DashboardAnalyticsMock(), + storage: CoreStorageMock() ) let router = DashboardRouterMock() diff --git a/Dashboard/Dashboard/Presentation/ListDashboardViewModel.swift b/Dashboard/Dashboard/Presentation/ListDashboardViewModel.swift index f962824e9..112865e86 100644 --- a/Dashboard/Dashboard/Presentation/ListDashboardViewModel.swift +++ b/Dashboard/Dashboard/Presentation/ListDashboardViewModel.swift @@ -29,15 +29,18 @@ public class ListDashboardViewModel: ObservableObject { let connectivity: ConnectivityProtocol private let interactor: DashboardInteractorProtocol private let analytics: DashboardAnalytics + let storage: CoreStorage private var onCourseEnrolledCancellable: AnyCancellable? private var refreshEnrollmentsCancellable: AnyCancellable? public init(interactor: DashboardInteractorProtocol, connectivity: ConnectivityProtocol, - analytics: DashboardAnalytics) { + analytics: DashboardAnalytics, + storage: CoreStorage) { self.interactor = interactor self.connectivity = connectivity self.analytics = analytics + self.storage = storage onCourseEnrolledCancellable = NotificationCenter.default .publisher(for: .onCourseEnrolled) diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift index 7ac041110..d182a6a44 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift @@ -75,6 +75,7 @@ public struct PrimaryCourseDashboardView: View { progressPossible: primary.progressPossible, canResume: primary.lastVisitedBlockID != nil, resumeTitle: primary.resumeTitle, + useRelativeDates: viewModel.storage.useRelativeDates, assignmentAction: { lastVisitedBlockID in router.showCourseScreens( courseID: primary.courseID, @@ -199,6 +200,7 @@ public struct PrimaryCourseDashboardView: View { @ViewBuilder private func courses(_ enrollments: PrimaryEnrollment) -> some View { + let useRelativeDates = viewModel.storage.useRelativeDates ForEach( Array(enrollments.courses.enumerated()), id: \.offset @@ -228,7 +230,8 @@ public struct PrimaryCourseDashboardView: View { courseStartDate: nil, courseEndDate: nil, hasAccess: course.hasAccess, - showProgress: false + showProgress: false, + useRelativeDates: useRelativeDates ).frame(width: idiom == .pad ? nil : 120) } ) @@ -330,7 +333,8 @@ struct PrimaryCourseDashboardView_Previews: PreviewProvider { interactor: DashboardInteractor.mock, connectivity: Connectivity(), analytics: DashboardAnalyticsMock(), - config: ConfigMock() + config: ConfigMock(), + storage: CoreStorageMock() ) PrimaryCourseDashboardView( diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift index 60ffe9c0b..f1a74f773 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift @@ -30,6 +30,7 @@ public class PrimaryCourseDashboardViewModel: ObservableObject { private let interactor: DashboardInteractorProtocol private let analytics: DashboardAnalytics let config: ConfigProtocol + let storage: CoreStorage private var cancellables = Set() private let ipadPageSize = 7 @@ -39,12 +40,14 @@ public class PrimaryCourseDashboardViewModel: ObservableObject { interactor: DashboardInteractorProtocol, connectivity: ConnectivityProtocol, analytics: DashboardAnalytics, - config: ConfigProtocol + config: ConfigProtocol, + storage: CoreStorage ) { self.interactor = interactor self.connectivity = connectivity self.analytics = analytics self.config = config + self.storage = storage let enrollmentPublisher = NotificationCenter.default.publisher(for: .onCourseEnrolled) let completionPublisher = NotificationCenter.default.publisher(for: .onblockCompletionRequested) diff --git a/Dashboard/DashboardTests/Presentation/DashboardViewModelTests.swift b/Dashboard/DashboardTests/Presentation/DashboardViewModelTests.swift index 1d3ab3db9..a5fb4e9b7 100644 --- a/Dashboard/DashboardTests/Presentation/DashboardViewModelTests.swift +++ b/Dashboard/DashboardTests/Presentation/DashboardViewModelTests.swift @@ -18,7 +18,12 @@ final class ListDashboardViewModelTests: XCTestCase { let interactor = DashboardInteractorProtocolMock() let connectivity = ConnectivityProtocolMock() let analytics = DashboardAnalyticsMock() - let viewModel = ListDashboardViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) + let viewModel = ListDashboardViewModel( + interactor: interactor, + connectivity: connectivity, + analytics: analytics, + storage: CoreStorageMock() + ) let items = [ CourseItem(name: "Test", @@ -67,7 +72,12 @@ final class ListDashboardViewModelTests: XCTestCase { let interactor = DashboardInteractorProtocolMock() let connectivity = ConnectivityProtocolMock() let analytics = DashboardAnalyticsMock() - let viewModel = ListDashboardViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) + let viewModel = ListDashboardViewModel( + interactor: interactor, + connectivity: connectivity, + analytics: analytics, + storage: CoreStorageMock() + ) let items = [ CourseItem(name: "Test", @@ -116,7 +126,12 @@ final class ListDashboardViewModelTests: XCTestCase { let interactor = DashboardInteractorProtocolMock() let connectivity = ConnectivityProtocolMock() let analytics = DashboardAnalyticsMock() - let viewModel = ListDashboardViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) + let viewModel = ListDashboardViewModel( + interactor: interactor, + connectivity: connectivity, + analytics: analytics, + storage: CoreStorageMock() + ) Given(connectivity, .isInternetAvaliable(getter: true)) Given(interactor, .getEnrollments(page: .any, willThrow: NoCachedDataError()) ) @@ -134,7 +149,12 @@ final class ListDashboardViewModelTests: XCTestCase { let interactor = DashboardInteractorProtocolMock() let connectivity = ConnectivityProtocolMock() let analytics = DashboardAnalyticsMock() - let viewModel = ListDashboardViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) + let viewModel = ListDashboardViewModel( + interactor: interactor, + connectivity: connectivity, + analytics: analytics, + storage: CoreStorageMock() + ) Given(connectivity, .isInternetAvaliable(getter: true)) Given(interactor, .getEnrollments(page: .any, willThrow: NSError()) ) diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift index b8d9aa860..5816da080 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift @@ -106,12 +106,14 @@ public struct DiscoveryView: View { .padding(.bottom, 20) Spacer() }.padding(.leading, 10) + let useRelativeDates = viewModel.storage.useRelativeDates ForEach(Array(viewModel.courses.enumerated()), id: \.offset) { index, course in CourseCellView( model: course, type: .discovery, index: index, - cellsCount: viewModel.courses.count + cellsCount: viewModel.courses.count, + useRelativeDates: useRelativeDates ).padding(.horizontal, 24) .onAppear { Task { diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryViewModel.swift b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryViewModel.swift index 8f64f458e..9c091f847 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryViewModel.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryViewModel.swift @@ -37,7 +37,7 @@ public class DiscoveryViewModel: ObservableObject { let connectivity: ConnectivityProtocol private let interactor: DiscoveryInteractorProtocol private let analytics: DiscoveryAnalytics - private let storage: CoreStorage + let storage: CoreStorage public init( router: DiscoveryRouter, diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift index 99d7ecea9..531a7db3e 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift @@ -110,12 +110,16 @@ public struct SearchView: View { LazyVStack { let searchResults = viewModel.searchResults.enumerated() + let useRelativeDates = viewModel.storage.useRelativeDates ForEach( Array(searchResults), id: \.offset) { index, course in - CourseCellView(model: course, - type: .discovery, - index: index, - cellsCount: viewModel.searchResults.count) + CourseCellView( + model: course, + type: .discovery, + index: index, + cellsCount: viewModel.searchResults.count, + useRelativeDates: useRelativeDates + ) .padding(.horizontal, 24) .onAppear { Task { @@ -219,7 +223,8 @@ struct SearchView_Previews: PreviewProvider { interactor: DiscoveryInteractor.mock, connectivity: Connectivity(), router: router, - analytics: DiscoveryAnalyticsMock(), + analytics: DiscoveryAnalyticsMock(), + storage: CoreStorageMock(), debounce: .searchDebounce ) diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/SearchViewModel.swift b/Discovery/Discovery/Presentation/NativeDiscovery/SearchViewModel.swift index 8f0c6ff1c..76f3ea137 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/SearchViewModel.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/SearchViewModel.swift @@ -32,6 +32,7 @@ public class SearchViewModel: ObservableObject { let router: DiscoveryRouter let analytics: DiscoveryAnalytics + let storage: CoreStorage private let interactor: DiscoveryInteractorProtocol let connectivity: ConnectivityProtocol @@ -39,12 +40,14 @@ public class SearchViewModel: ObservableObject { connectivity: ConnectivityProtocol, router: DiscoveryRouter, analytics: DiscoveryAnalytics, + storage: CoreStorage, debounce: Debounce ) { self.interactor = interactor self.connectivity = connectivity self.router = router self.analytics = analytics + self.storage = storage self.debounce = debounce $searchText diff --git a/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift b/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift index e1596add9..3aa8e9394 100644 --- a/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift +++ b/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift @@ -31,7 +31,8 @@ final class SearchViewModelTests: XCTestCase { interactor: interactor, connectivity: connectivity, router: router, - analytics: analytics, + analytics: analytics, + storage: CoreStorageMock(), debounce: .test ) @@ -94,6 +95,7 @@ final class SearchViewModelTests: XCTestCase { connectivity: connectivity, router: router, analytics: analytics, + storage: CoreStorageMock(), debounce: .test ) @@ -122,6 +124,7 @@ final class SearchViewModelTests: XCTestCase { connectivity: connectivity, router: router, analytics: analytics, + storage: CoreStorageMock(), debounce: .test ) @@ -155,6 +158,7 @@ final class SearchViewModelTests: XCTestCase { connectivity: connectivity, router: router, analytics: analytics, + storage: CoreStorageMock(), debounce: .test ) diff --git a/Discussion/Discussion/Domain/Model/UserThread.swift b/Discussion/Discussion/Domain/Model/UserThread.swift index 4b33833a5..f28e2a532 100644 --- a/Discussion/Discussion/Domain/Model/UserThread.swift +++ b/Discussion/Discussion/Domain/Model/UserThread.swift @@ -87,19 +87,24 @@ public struct UserThread { } public extension UserThread { - func discussionPost(action: @escaping () -> Void) -> DiscussionPost { - return DiscussionPost(id: id, - title: title, - replies: commentCount, - lastPostDate: updatedAt, - lastPostDateFormatted: updatedAt.dateToString(style: .lastPost), - isFavorite: following, - type: type, - unreadCommentCount: unreadCommentCount, - action: action, - hasEndorsed: hasEndorsed, - voteCount: voteCount, - numPages: numPages) + func discussionPost(useRelativeDates: Bool, action: @escaping () -> Void) -> DiscussionPost { + return DiscussionPost( + id: id, + title: title, + replies: commentCount, + lastPostDate: updatedAt, + lastPostDateFormatted: updatedAt.dateToString( + style: .lastPost, + useRelativeDates: useRelativeDates + ), + isFavorite: following, + type: type, + unreadCommentCount: unreadCommentCount, + action: action, + hasEndorsed: hasEndorsed, + voteCount: voteCount, + numPages: numPages + ) } } diff --git a/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift b/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift index 4a955aa2e..eacf00109 100644 --- a/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift +++ b/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift @@ -44,16 +44,19 @@ public class BaseResponsesViewModel { internal let interactor: DiscussionInteractorProtocol internal let router: DiscussionRouter internal let config: ConfigProtocol + internal let storage: CoreStorage internal let addPostSubject = CurrentValueSubject(nil) init( interactor: DiscussionInteractorProtocol, router: DiscussionRouter, - config: ConfigProtocol + config: ConfigProtocol, + storage: CoreStorage ) { self.interactor = interactor self.router = router self.config = config + self.storage = storage } @MainActor diff --git a/Discussion/Discussion/Presentation/Comments/Base/CommentCell.swift b/Discussion/Discussion/Presentation/Comments/Base/CommentCell.swift index 7d8c8b155..f4c56c102 100644 --- a/Discussion/Discussion/Presentation/Comments/Base/CommentCell.swift +++ b/Discussion/Discussion/Presentation/Comments/Base/CommentCell.swift @@ -14,6 +14,7 @@ public struct CommentCell: View { private let comment: Post private let addCommentAvailable: Bool + private let useRelativeDates: Bool private var onAvatarTap: ((String) -> Void) private var onLikeTap: (() -> Void) private var onReportTap: (() -> Void) @@ -26,6 +27,7 @@ public struct CommentCell: View { public init( comment: Post, addCommentAvailable: Bool, + useRelativeDates: Bool, leftLineEnabled: Bool = false, onAvatarTap: @escaping (String) -> Void, onLikeTap: @escaping () -> Void, @@ -35,6 +37,7 @@ public struct CommentCell: View { ) { self.comment = comment self.addCommentAvailable = addCommentAvailable + self.useRelativeDates = useRelativeDates self.leftLineEnabled = leftLineEnabled self.onAvatarTap = onAvatarTap self.onLikeTap = onLikeTap @@ -59,7 +62,7 @@ public struct CommentCell: View { VStack(alignment: .leading) { Text(comment.authorName) .font(Theme.Fonts.titleSmall) - Text(comment.postDate.dateToString(style: .lastPost)) + Text(comment.postDate.dateToString(style: .lastPost, useRelativeDates: useRelativeDates)) .font(Theme.Fonts.labelSmall) .foregroundColor(Theme.Colors.textSecondary) } @@ -179,15 +182,19 @@ struct CommentView_Previews: PreviewProvider { CommentCell( comment: comment, addCommentAvailable: true, + useRelativeDates: true, leftLineEnabled: false, - onAvatarTap: {_ in}, + onAvatarTap: { + _ in + }, onLikeTap: {}, onReportTap: {}, onCommentsTap: {}, onFetchMore: {}) CommentCell( comment: comment, - addCommentAvailable: true, + addCommentAvailable: true, + useRelativeDates: true, leftLineEnabled: false, onAvatarTap: {_ in}, onLikeTap: {}, @@ -202,7 +209,8 @@ struct CommentView_Previews: PreviewProvider { VStack(spacing: 0) { CommentCell( comment: comment, - addCommentAvailable: true, + addCommentAvailable: true, + useRelativeDates: true, leftLineEnabled: false, onAvatarTap: {_ in}, onLikeTap: {}, @@ -211,7 +219,8 @@ struct CommentView_Previews: PreviewProvider { onFetchMore: {}) CommentCell( comment: comment, - addCommentAvailable: true, + addCommentAvailable: true, + useRelativeDates: true, leftLineEnabled: false, onAvatarTap: {_ in}, onLikeTap: {}, diff --git a/Discussion/Discussion/Presentation/Comments/Base/ParentCommentView.swift b/Discussion/Discussion/Presentation/Comments/Base/ParentCommentView.swift index 3594f26c5..3fce895d6 100644 --- a/Discussion/Discussion/Presentation/Comments/Base/ParentCommentView.swift +++ b/Discussion/Discussion/Presentation/Comments/Base/ParentCommentView.swift @@ -14,6 +14,7 @@ public struct ParentCommentView: View { private let comments: Post private var isThread: Bool + private let useRelativeDates: Bool private var onAvatarTap: ((String) -> Void) private var onLikeTap: (() -> Void) private var onReportTap: (() -> Void) @@ -24,6 +25,7 @@ public struct ParentCommentView: View { public init( comments: Post, isThread: Bool, + useRelativeDates: Bool, onAvatarTap: @escaping (String) -> Void, onLikeTap: @escaping () -> Void, onReportTap: @escaping () -> Void, @@ -31,6 +33,7 @@ public struct ParentCommentView: View { ) { self.comments = comments self.isThread = isThread + self.useRelativeDates = useRelativeDates self.onAvatarTap = onAvatarTap self.onLikeTap = onLikeTap self.onReportTap = onReportTap @@ -55,7 +58,7 @@ public struct ParentCommentView: View { .font(Theme.Fonts.titleMedium) .foregroundColor(Theme.Colors.textPrimary) Text(comments.postDate - .dateToString(style: .lastPost)) + .dateToString(style: .lastPost, useRelativeDates: useRelativeDates)) .font(Theme.Fonts.labelSmall) .foregroundColor(Theme.Colors.textSecondaryLight) } @@ -169,7 +172,8 @@ struct ParentCommentView_Previews: PreviewProvider { return VStack { ParentCommentView( comments: comment, - isThread: true, + isThread: true, + useRelativeDates: true, onAvatarTap: {_ in}, onLikeTap: {}, onReportTap: {}, diff --git a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift index d4dab6464..91a5d9dda 100644 --- a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift +++ b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift @@ -59,7 +59,9 @@ public struct ResponsesView: View { if let comments = viewModel.postComments { ParentCommentView( comments: comments, - isThread: false, onAvatarTap: { username in + isThread: false, + useRelativeDates: viewModel.storage.useRelativeDates, + onAvatarTap: { username in viewModel.router.showUserDetails(username: username) }, onLikeTap: { @@ -99,12 +101,15 @@ public struct ResponsesView: View { .padding(.leading, 24) .font(Theme.Fonts.titleMedium) .foregroundColor(Theme.Colors.textPrimary) + let useRelativeDates = viewModel.storage.useRelativeDates ForEach( Array(comments.comments.enumerated()), id: \.offset ) { index, comment in CommentCell( comment: comment, - addCommentAvailable: false, leftLineEnabled: true, + addCommentAvailable: false, + useRelativeDates: useRelativeDates, + leftLineEnabled: true, onAvatarTap: { username in viewModel.router.showUserDetails(username: username) }, @@ -238,6 +243,7 @@ struct ResponsesView_Previews: PreviewProvider { interactor: DiscussionInteractor(repository: DiscussionRepositoryMock()), router: DiscussionRouterMock(), config: ConfigMock(), + storage: CoreStorageMock(), threadStateSubject: .init(nil) ) let post = Post( diff --git a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift index c8992fd8a..b73697264 100644 --- a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift +++ b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift @@ -20,10 +20,11 @@ public class ResponsesViewModel: BaseResponsesViewModel, ObservableObject { interactor: DiscussionInteractorProtocol, router: DiscussionRouter, config: ConfigProtocol, + storage: CoreStorage, threadStateSubject: CurrentValueSubject ) { self.threadStateSubject = threadStateSubject - super.init(interactor: interactor, router: router, config: config) + super.init(interactor: interactor, router: router, config: config, storage: storage) } func generateCommentsResponses(comments: [UserComment], parentComment: Post) -> Post? { diff --git a/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift b/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift index b764ed0d6..1d39962f2 100644 --- a/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift +++ b/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift @@ -44,6 +44,7 @@ public struct ThreadView: View { ParentCommentView( comments: comments, isThread: true, + useRelativeDates: viewModel.storage.useRelativeDates, onAvatarTap: { username in viewModel.router.showUserDetails(username: username) }, @@ -92,11 +93,13 @@ public struct ThreadView: View { .padding(.leading, 24) .font(Theme.Fonts.titleMedium) .foregroundColor(Theme.Colors.textPrimary) + let useRelativeDates = viewModel.storage.useRelativeDates ForEach(Array(comments.comments.enumerated()), id: \.offset) { index, comment in CommentCell( comment: comment, addCommentAvailable: true, + useRelativeDates: useRelativeDates, onAvatarTap: { username in viewModel.router.showUserDetails(username: username) }, @@ -281,10 +284,13 @@ struct CommentsView_Previews: PreviewProvider { abuseFlagged: true, hasEndorsed: true, numPages: 3) - let vm = ThreadViewModel(interactor: DiscussionInteractor.mock, - router: DiscussionRouterMock(), - config: ConfigMock(), - postStateSubject: .init(nil)) + let vm = ThreadViewModel( + interactor: DiscussionInteractor.mock, + router: DiscussionRouterMock(), + config: ConfigMock(), + storage: CoreStorageMock(), + postStateSubject: .init(nil) + ) ThreadView(thread: userThread, viewModel: vm) .preferredColorScheme(.light) diff --git a/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift b/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift index 2fb75b60f..452ea98f2 100644 --- a/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift +++ b/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift @@ -16,17 +16,17 @@ public class ThreadViewModel: BaseResponsesViewModel, ObservableObject { internal let threadStateSubject = CurrentValueSubject(nil) private var cancellable: AnyCancellable? private let postStateSubject: CurrentValueSubject - public var isBlackedOut: Bool = false public init( interactor: DiscussionInteractorProtocol, router: DiscussionRouter, config: ConfigProtocol, + storage: CoreStorage, postStateSubject: CurrentValueSubject ) { self.postStateSubject = postStateSubject - super.init(interactor: interactor, router: router, config: config) + super.init(interactor: interactor, router: router, config: config, storage: storage) cancellable = threadStateSubject .receive(on: RunLoop.main) diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift index c9fd88dd7..440666e85 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift @@ -195,7 +195,8 @@ struct DiscussionSearchTopicsView_Previews: PreviewProvider { static var previews: some View { let vm = DiscussionSearchTopicsViewModel( courseID: "123", - interactor: DiscussionInteractor.mock, + interactor: DiscussionInteractor.mock, + storage: CoreStorageMock(), router: DiscussionRouterMock(), debounce: .searchDebounce ) diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModel.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModel.swift index 95afa250b..83b7b2741 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModel.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModel.swift @@ -39,16 +39,19 @@ public class DiscussionSearchTopicsViewModel: ObservableObject { let router: DiscussionRouter private let interactor: DiscussionInteractorProtocol + private let storage: CoreStorage private let debounce: Debounce public init( courseID: String, interactor: DiscussionInteractorProtocol, + storage: CoreStorage, router: DiscussionRouter, debounce: Debounce ) { self.courseID = courseID self.interactor = interactor + self.storage = storage self.router = router self.debounce = debounce @@ -157,7 +160,7 @@ public class DiscussionSearchTopicsViewModel: ObservableObject { private func generatePosts(threads: [UserThread]) -> [DiscussionPost] { var result: [DiscussionPost] = [] for thread in threads { - result.append(thread.discussionPost(action: { [weak self] in + result.append(thread.discussionPost(useRelativeDates: storage.useRelativeDates, action: { [weak self] in guard let self else { return } self.router.showThread( thread: thread, diff --git a/Discussion/Discussion/Presentation/Posts/PostsView.swift b/Discussion/Discussion/Presentation/Posts/PostsView.swift index 9664d1f19..1ab2fc0fb 100644 --- a/Discussion/Discussion/Presentation/Posts/PostsView.swift +++ b/Discussion/Discussion/Presentation/Posts/PostsView.swift @@ -326,7 +326,8 @@ struct PostsView_Previews: PreviewProvider { let vm = PostsViewModel( interactor: DiscussionInteractor.mock, router: router, - config: ConfigMock() + config: ConfigMock(), + storage: CoreStorageMock() ) PostsView(courseID: "course_id", diff --git a/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift b/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift index b1676c70b..8115ea0f2 100644 --- a/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift +++ b/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift @@ -80,17 +80,20 @@ public class PostsViewModel: ObservableObject { private let interactor: DiscussionInteractorProtocol private let router: DiscussionRouter private let config: ConfigProtocol + private let storage: CoreStorage internal let postStateSubject = CurrentValueSubject(nil) private var cancellable: AnyCancellable? public init( interactor: DiscussionInteractorProtocol, router: DiscussionRouter, - config: ConfigProtocol + config: ConfigProtocol, + storage: CoreStorage ) { self.interactor = interactor self.router = router self.config = config + self.storage = storage cancellable = postStateSubject .receive(on: RunLoop.main) @@ -130,17 +133,24 @@ public class PostsViewModel: ObservableObject { var result: [DiscussionPost] = [] if let threads = threads?.threads { for thread in threads { - result.append(thread.discussionPost(action: { [weak self] in - guard let self, let actualThread = self.threads.threads - .first(where: {$0.id == thread.id }) else { return } - - self.router.showThread( - thread: actualThread, - postStateSubject: self.postStateSubject, - isBlackedOut: self.isBlackedOut ?? false, - animated: true + result.append( + thread.discussionPost( + useRelativeDates: storage.useRelativeDates, + action: { + [weak self] in + guard let self, + let actualThread = self.threads.threads + .first(where: {$0.id == thread.id }) else { return } + + self.router.showThread( + thread: actualThread, + postStateSubject: self.postStateSubject, + isBlackedOut: self.isBlackedOut ?? false, + animated: true + ) + } ) - })) + ) } } diff --git a/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift b/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift index b6940c034..d862278fa 100644 --- a/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift @@ -50,11 +50,27 @@ final class BaseResponsesViewModelTests: XCTestCase { abuseFlagged: false, closed: false) + var interactor: DiscussionInteractorProtocolMock! + var router: DiscussionRouterMock! + var config: ConfigMock! + var viewModel: BaseResponsesViewModel! + + override func setUp() async throws { + try await super.setUp() + + interactor = DiscussionInteractorProtocolMock() + router = DiscussionRouterMock() + config = ConfigMock() + viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock() + ) + } + func testVoteThreadSuccess() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + var result = false viewModel.postComments = post @@ -73,10 +89,7 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testVoteResponseSuccess() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + var result = false @@ -97,10 +110,7 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testVoteParentThreadSuccess() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + var result = false @@ -120,10 +130,7 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testVoteParentResponseSuccess() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + var result = false @@ -145,10 +152,7 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testVoteNoInternetError() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + var result = false @@ -167,10 +171,7 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testVoteUnknownError() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + var result = false @@ -187,10 +188,6 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testFlagThreadSuccess() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) var result = false @@ -210,10 +207,6 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testFlagCommentSuccess() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) var result = false @@ -233,10 +226,6 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testFlagNoInternetError() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) var result = false @@ -255,10 +244,6 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testFlagUnknownError() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) var result = false @@ -275,10 +260,6 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testFollowThreadSuccess() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) var result = false @@ -298,10 +279,6 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testFollowThreadNoInternetError() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) var result = false @@ -320,10 +297,6 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testFollowThreadUnknownError() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) var result = false @@ -340,10 +313,6 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testAddNewPost() { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) viewModel.postComments = post diff --git a/Discussion/DiscussionTests/Presentation/Comment/ThreadViewModelTests.swift b/Discussion/DiscussionTests/Presentation/Comment/ThreadViewModelTests.swift index 7da4cdf55..97ab5f718 100644 --- a/Discussion/DiscussionTests/Presentation/Comment/ThreadViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/Comment/ThreadViewModelTests.swift @@ -212,6 +212,7 @@ final class ThreadViewModelTests: XCTestCase { let viewModel = ThreadViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), postStateSubject: .init(.readed(id: "1"))) Given(interactor, .readBody(threadID: .any, willProduce: {_ in})) @@ -241,6 +242,7 @@ final class ThreadViewModelTests: XCTestCase { let viewModel = ThreadViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), postStateSubject: .init(.readed(id: "1"))) Given(interactor, .readBody(threadID: .any, willProduce: {_ in})) @@ -270,6 +272,7 @@ final class ThreadViewModelTests: XCTestCase { let viewModel = ThreadViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), postStateSubject: .init(.readed(id: "1"))) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -301,6 +304,7 @@ final class ThreadViewModelTests: XCTestCase { let viewModel = ThreadViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), postStateSubject: .init(.readed(id: "1"))) Given(interactor, .readBody(threadID: .any, willThrow: NSError())) @@ -328,6 +332,7 @@ final class ThreadViewModelTests: XCTestCase { let viewModel = ThreadViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), postStateSubject: .init(.readed(id: "1"))) let post = Post(authorName: "", @@ -368,6 +373,7 @@ final class ThreadViewModelTests: XCTestCase { let viewModel = ThreadViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), postStateSubject: .init(.readed(id: "1"))) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -392,6 +398,7 @@ final class ThreadViewModelTests: XCTestCase { let viewModel = ThreadViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), postStateSubject: .init(.readed(id: "1"))) Given(interactor, .addCommentTo(threadID: .any, rawBody: .any, parentID: .any, willThrow: NSError()) ) @@ -415,6 +422,7 @@ final class ThreadViewModelTests: XCTestCase { let viewModel = ThreadViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), postStateSubject: .init(.readed(id: "1"))) viewModel.totalPages = 2 diff --git a/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModelTests.swift b/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModelTests.swift index f94484fc2..33a98349c 100644 --- a/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModelTests.swift @@ -18,7 +18,8 @@ final class DiscussionSearchTopicsViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let viewModel = DiscussionSearchTopicsViewModel(courseID: "123", - interactor: interactor, + interactor: interactor, + storage: CoreStorageMock(), router: router, debounce: .test) @@ -71,6 +72,7 @@ final class DiscussionSearchTopicsViewModelTests: XCTestCase { let router = DiscussionRouterMock() let viewModel = DiscussionSearchTopicsViewModel(courseID: "123", interactor: interactor, + storage: CoreStorageMock(), router: router, debounce: .test) @@ -100,6 +102,7 @@ final class DiscussionSearchTopicsViewModelTests: XCTestCase { let router = DiscussionRouterMock() let viewModel = DiscussionSearchTopicsViewModel(courseID: "123", interactor: interactor, + storage: CoreStorageMock(), router: router, debounce: .test) @@ -127,6 +130,7 @@ final class DiscussionSearchTopicsViewModelTests: XCTestCase { let router = DiscussionRouterMock() let viewModel = DiscussionSearchTopicsViewModel(courseID: "123", interactor: interactor, + storage: CoreStorageMock(), router: router, debounce: .test) diff --git a/Discussion/DiscussionTests/Presentation/Posts/PostViewModelTests.swift b/Discussion/DiscussionTests/Presentation/Posts/PostViewModelTests.swift index 052930f5b..9b9275d55 100644 --- a/Discussion/DiscussionTests/Presentation/Posts/PostViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/Posts/PostViewModelTests.swift @@ -102,14 +102,29 @@ final class PostViewModelTests: XCTestCase { ]) let discussionInfo = DiscussionInfo(discussionID: "1", blackouts: []) + + var interactor: DiscussionInteractorProtocolMock! + var router: DiscussionRouterMock! + var config: ConfigMock! + var viewModel: PostsViewModel! + + override func setUp() async throws { + try await super.setUp() + + interactor = DiscussionInteractorProtocolMock() + router = DiscussionRouterMock() + config = ConfigMock() + viewModel = PostsViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock() + ) + } func testGetThreadListSuccess() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() var result = false - let viewModel = PostsViewModel(interactor: interactor, router: router, config: config) viewModel.courseID = "1" viewModel.type = .allPosts @@ -145,11 +160,8 @@ final class PostViewModelTests: XCTestCase { } func testGetThreadListNoInternetError() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() var result = false - let viewModel = PostsViewModel(interactor: interactor, router: router, config: config) + viewModel.isBlackedOut = false let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -170,11 +182,8 @@ final class PostViewModelTests: XCTestCase { } func testGetThreadListUnknownError() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() var result = false - let viewModel = PostsViewModel(interactor: interactor, router: router, config: config) + viewModel.isBlackedOut = false Given(interactor, .getThreadsList(courseID: .any, type: .any, sort: .any, filter: .any, page: .any, willThrow: NSError())) @@ -193,11 +202,7 @@ final class PostViewModelTests: XCTestCase { } func testSortingAndFilters() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = PostsViewModel(interactor: interactor, router: router, config: config) - + Given(interactor, .getThreadsList(courseID: .any, type: .any, sort: .any, filter: .any, page: .any, willReturn: threads)) Given(interactor, .getCourseDiscussionInfo(courseID: "1", willReturn: discussionInfo)) diff --git a/Discussion/DiscussionTests/Presentation/Responses/ResponsesViewModelTests.swift b/Discussion/DiscussionTests/Presentation/Responses/ResponsesViewModelTests.swift index a7c59806a..2b010617d 100644 --- a/Discussion/DiscussionTests/Presentation/Responses/ResponsesViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/Responses/ResponsesViewModelTests.swift @@ -108,6 +108,7 @@ final class ResponsesViewModelTests: XCTestCase { let viewModel = ResponsesViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), threadStateSubject: .init(.postAdded(id: "1"))) Given(interactor, .getCommentResponses(commentID: .any, page: .any, @@ -135,6 +136,7 @@ final class ResponsesViewModelTests: XCTestCase { let viewModel = ResponsesViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), threadStateSubject: .init(.postAdded(id: "1"))) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -161,6 +163,7 @@ final class ResponsesViewModelTests: XCTestCase { let viewModel = ResponsesViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), threadStateSubject: .init(.postAdded(id: "1"))) Given(interactor, .getCommentResponses(commentID: .any, page: .any, willThrow: NSError())) @@ -184,6 +187,7 @@ final class ResponsesViewModelTests: XCTestCase { let viewModel = ResponsesViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), threadStateSubject: .init(.postAdded(id: "1"))) Given(interactor, .addCommentTo(threadID: .any, rawBody: .any, parentID: .any, willReturn: post)) @@ -205,6 +209,7 @@ final class ResponsesViewModelTests: XCTestCase { let viewModel = ResponsesViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), threadStateSubject: .init(.postAdded(id: "1"))) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -228,6 +233,7 @@ final class ResponsesViewModelTests: XCTestCase { let viewModel = ResponsesViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), threadStateSubject: .init(.postAdded(id: "1"))) Given(interactor, .addCommentTo(threadID: .any, rawBody: .any, parentID: .any, willThrow: NSError())) @@ -249,6 +255,7 @@ final class ResponsesViewModelTests: XCTestCase { let viewModel = ResponsesViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), threadStateSubject: .init(.postAdded(id: "1"))) viewModel.totalPages = 2 diff --git a/OpenEdX/DI/ScreenAssembly.swift b/OpenEdX/DI/ScreenAssembly.swift index f76ef7808..578e94df6 100644 --- a/OpenEdX/DI/ScreenAssembly.swift +++ b/OpenEdX/DI/ScreenAssembly.swift @@ -142,6 +142,7 @@ class ScreenAssembly: Assembly { connectivity: r.resolve(ConnectivityProtocol.self)!, router: r.resolve(DiscoveryRouter.self)!, analytics: r.resolve(DiscoveryAnalytics.self)!, + storage: r.resolve(CoreStorage.self)!, debounce: .searchDebounce ) } @@ -168,7 +169,8 @@ class ScreenAssembly: Assembly { ListDashboardViewModel( interactor: r.resolve(DashboardInteractorProtocol.self)!, connectivity: r.resolve(ConnectivityProtocol.self)!, - analytics: r.resolve(DashboardAnalytics.self)! + analytics: r.resolve(DashboardAnalytics.self)!, + storage: r.resolve(CoreStorage.self)! ) } @@ -177,7 +179,8 @@ class ScreenAssembly: Assembly { interactor: r.resolve(DashboardInteractorProtocol.self)!, connectivity: r.resolve(ConnectivityProtocol.self)!, analytics: r.resolve(DashboardAnalytics.self)!, - config: r.resolve(ConfigProtocol.self)! + config: r.resolve(ConfigProtocol.self)!, + storage: r.resolve(CoreStorage.self)! ) } @@ -185,7 +188,8 @@ class ScreenAssembly: Assembly { AllCoursesViewModel( interactor: r.resolve(DashboardInteractorProtocol.self)!, connectivity: r.resolve(ConnectivityProtocol.self)!, - analytics: r.resolve(DashboardAnalytics.self)! + analytics: r.resolve(DashboardAnalytics.self)!, + storage: r.resolve(CoreStorage.self)! ) } @@ -520,6 +524,7 @@ class ScreenAssembly: Assembly { DiscussionSearchTopicsViewModel( courseID: courseID, interactor: r.resolve(DiscussionInteractorProtocol.self)!, + storage: r.resolve(CoreStorage.self)!, router: r.resolve(DiscussionRouter.self)!, debounce: .searchDebounce ) @@ -529,7 +534,8 @@ class ScreenAssembly: Assembly { PostsViewModel( interactor: r.resolve(DiscussionInteractorProtocol.self)!, router: r.resolve(DiscussionRouter.self)!, - config: r.resolve(ConfigProtocol.self)! + config: r.resolve(ConfigProtocol.self)!, + storage: r.resolve(CoreStorage.self)! ) } @@ -538,6 +544,7 @@ class ScreenAssembly: Assembly { interactor: r.resolve(DiscussionInteractorProtocol.self)!, router: r.resolve(DiscussionRouter.self)!, config: r.resolve(ConfigProtocol.self)!, + storage: r.resolve(CoreStorage.self)!, postStateSubject: subject ) } @@ -547,6 +554,7 @@ class ScreenAssembly: Assembly { interactor: r.resolve(DiscussionInteractorProtocol.self)!, router: r.resolve(DiscussionRouter.self)!, config: r.resolve(ConfigProtocol.self)!, + storage: r.resolve(CoreStorage.self)!, threadStateSubject: subject ) } diff --git a/OpenEdX/Data/AppStorage.swift b/OpenEdX/Data/AppStorage.swift index 5f8b8f924..d064da7aa 100644 --- a/OpenEdX/Data/AppStorage.swift +++ b/OpenEdX/Data/AppStorage.swift @@ -88,9 +88,9 @@ public class AppStorage: CoreStorage, ProfileStorage, WhatsNewStorage, CourseSto } } - public var cookiesDate: String? { + public var cookiesDate: Date? { get { - return userDefaults.string(forKey: KEY_COOKIES_DATE) + return userDefaults.object(forKey: KEY_COOKIES_DATE) as? Date } set(newValue) { if let newValue { @@ -123,7 +123,13 @@ public class AppStorage: CoreStorage, ProfileStorage, WhatsNewStorage, CourseSto } set(newValue) { if let newValue { - userDefaults.set(newValue.dateToString(style: .iso8601), forKey: KEY_REVIEW_LAST_REVIEW_DATE) + userDefaults.set( + newValue.dateToString( + style: .iso8601, + useRelativeDates: false + ), + forKey: KEY_REVIEW_LAST_REVIEW_DATE + ) } else { userDefaults.removeObject(forKey: KEY_REVIEW_LAST_REVIEW_DATE) } @@ -285,7 +291,13 @@ public class AppStorage: CoreStorage, ProfileStorage, WhatsNewStorage, CourseSto } set(newValue) { if let newValue { - userDefaults.set(newValue.dateToString(style: .iso8601), forKey: KEY_LAST_CALENDAR_UPDATE_DATE) + userDefaults.set( + newValue.dateToString( + style: .iso8601, + useRelativeDates: useRelativeDates + ), + forKey: KEY_LAST_CALENDAR_UPDATE_DATE + ) } else { userDefaults.removeObject(forKey: KEY_LAST_CALENDAR_UPDATE_DATE) } @@ -318,6 +330,16 @@ public class AppStorage: CoreStorage, ProfileStorage, WhatsNewStorage, CourseSto } } + public var useRelativeDates: Bool { + get { + // We use userDefaults.object to return the default value as true + return userDefaults.object(forKey: KEY_USE_RELATIVE_DATES) as? Bool ?? true + } + set { + userDefaults.set(newValue, forKey: KEY_USE_RELATIVE_DATES) + } + } + public func clear() { accessToken = nil refreshToken = nil @@ -346,4 +368,5 @@ public class AppStorage: CoreStorage, ProfileStorage, WhatsNewStorage, CourseSto private let KEY_HIDE_INACTIVE_COURSES = "hideInactiveCourses" private let KEY_FIRST_CALENDAR_UPDATE = "firstCalendarUpdate" private let KEY_RESET_APP_SUPPORT_DIRECTORY_USER_DATA = "resetAppSupportDirectoryUserData" + private let KEY_USE_RELATIVE_DATES = "useRelativeDates" } diff --git a/Profile/Profile.xcodeproj/project.pbxproj b/Profile/Profile.xcodeproj/project.pbxproj index 0005825de..2401233cd 100644 --- a/Profile/Profile.xcodeproj/project.pbxproj +++ b/Profile/Profile.xcodeproj/project.pbxproj @@ -40,6 +40,7 @@ 0288C4EA2BC6AE2D009158B9 /* ManageAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0288C4E92BC6AE2D009158B9 /* ManageAccountView.swift */; }; 0288C4EC2BC6AE82009158B9 /* ManageAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0288C4EB2BC6AE82009158B9 /* ManageAccountViewModel.swift */; }; 029301DA2938948500E99AB8 /* ProfileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029301D92938948500E99AB8 /* ProfileType.swift */; }; + 0294987A2C4E4332008FD0E7 /* RelativeDatesToggleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029498792C4E4332008FD0E7 /* RelativeDatesToggleView.swift */; }; 02A4833329B7710A00D33F33 /* DeleteAccountViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A4833229B7710A00D33F33 /* DeleteAccountViewModelTests.swift */; }; 02A9A91D2978194A00B55797 /* ProfileViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A9A91C2978194A00B55797 /* ProfileViewModelTests.swift */; }; 02A9A91E2978194A00B55797 /* Profile.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 020F834A28DB4CCD0062FA70 /* Profile.framework */; platformFilter = ios; }; @@ -105,6 +106,7 @@ 0288C4E92BC6AE2D009158B9 /* ManageAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageAccountView.swift; sourceTree = ""; }; 0288C4EB2BC6AE82009158B9 /* ManageAccountViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageAccountViewModel.swift; sourceTree = ""; }; 029301D92938948500E99AB8 /* ProfileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileType.swift; sourceTree = ""; }; + 029498792C4E4332008FD0E7 /* RelativeDatesToggleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelativeDatesToggleView.swift; sourceTree = ""; }; 02A4833229B7710A00D33F33 /* DeleteAccountViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DeleteAccountViewModelTests.swift; path = ProfileTests/Presentation/DeleteAccount/DeleteAccountViewModelTests.swift; sourceTree = SOURCE_ROOT; }; 02A9A91A2978194A00B55797 /* ProfileTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ProfileTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 02A9A91C2978194A00B55797 /* ProfileViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModelTests.swift; sourceTree = ""; }; @@ -340,6 +342,7 @@ 022301E72BF4C4E70028A287 /* ToggleWithDescriptionView.swift */, 02F81DDE2BF4D83E002D3604 /* CalendarDialogView.swift */, 02F81DE22BF502B9002D3604 /* SyncSelector.swift */, + 029498792C4E4332008FD0E7 /* RelativeDatesToggleView.swift */, ); path = Elements; sourceTree = ""; @@ -675,6 +678,7 @@ 021C90D72BC98734004876AF /* DatesAndCalendarViewModel.swift in Sources */, 021D925528DC92F800ACC565 /* ProfileInteractor.swift in Sources */, 029301DA2938948500E99AB8 /* ProfileType.swift in Sources */, + 0294987A2C4E4332008FD0E7 /* RelativeDatesToggleView.swift in Sources */, 0259104429C39C9E004B5A55 /* SettingsView.swift in Sources */, 02B089432A9F832200754BD4 /* ProfileStorage.swift in Sources */, 022213D72C0E18A300B917E6 /* CourseCalendarState.swift in Sources */, diff --git a/Profile/Profile/Data/ProfileRepository.swift b/Profile/Profile/Data/ProfileRepository.swift index 2bd35cdfe..a593f7380 100644 --- a/Profile/Profile/Data/ProfileRepository.swift +++ b/Profile/Profile/Data/ProfileRepository.swift @@ -162,7 +162,7 @@ public class ProfileRepository: ProfileRepositoryProtocol { public func getCourseDates(courseID: String) async throws -> CourseDates { let courseDates = try await api.requestData( ProfileEndpoint.getCourseDates(courseID: courseID) - ).mapResponse(DataLayer.CourseDates.self).domain + ).mapResponse(DataLayer.CourseDates.self).domain(useRelativeDates: storage.useRelativeDates) return courseDates } } diff --git a/Profile/Profile/Data/ProfileStorage.swift b/Profile/Profile/Data/ProfileStorage.swift index 8110ada04..88e8fe48b 100644 --- a/Profile/Profile/Data/ProfileStorage.swift +++ b/Profile/Profile/Data/ProfileStorage.swift @@ -11,6 +11,7 @@ import UIKit public protocol ProfileStorage { var userProfile: DataLayer.UserProfile? {get set} + var useRelativeDates: Bool {get set} var calendarSettings: CalendarSettings? {get set} var hideInactiveCourses: Bool? {get set} var lastLoginUsername: String? {get set} @@ -23,6 +24,7 @@ public protocol ProfileStorage { public class ProfileStorageMock: ProfileStorage { public var userProfile: DataLayer.UserProfile? + public var useRelativeDates: Bool = true public var calendarSettings: CalendarSettings? public var hideInactiveCourses: Bool? public var lastLoginUsername: String? diff --git a/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift b/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift index 886909605..35ea4a7f7 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift @@ -44,7 +44,7 @@ public struct DatesAndCalendarView: View { ScrollView { Group { calendarSyncCard -// relativeDatesToggle + RelativeDatesToggleView(useRelativeDates: $viewModel.profileStorage.useRelativeDates) } .padding(.horizontal, isHorizontal ? 48 : 0) } @@ -177,31 +177,6 @@ public struct DatesAndCalendarView: View { .cardStyle(bgColor: Theme.Colors.textInputUnfocusedBackground, strokeColor: .clear) } } - - // MARK: - Options Toggle - private var relativeDatesToggle: some View { - VStack(alignment: .leading) { - Text(ProfileLocalization.Options.title) - .font(Theme.Fonts.labelLarge) - .foregroundColor(Theme.Colors.textPrimary) - HStack(spacing: 16) { - Toggle("", isOn: $viewModel.useRelativeDates) - .frame(width: 50) - .tint(Theme.Colors.accentColor) - Text(ProfileLocalization.Options.useRelativeDates) - .font(Theme.Fonts.bodyLarge) - .foregroundColor(Theme.Colors.textPrimary) - } - Text(ProfileLocalization.Options.showRelativeDates) - .font(Theme.Fonts.labelMedium) - .foregroundColor(Theme.Colors.textPrimary) - } - .padding(.horizontal, 24) - .frame(minWidth: 0, - maxWidth: .infinity, - alignment: .top) - .accessibilityIdentifier("relative_dates_toggle") - } } #if DEBUG diff --git a/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarViewModel.swift b/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarViewModel.swift index 560d18978..667546c24 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarViewModel.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarViewModel.swift @@ -16,7 +16,6 @@ import Core // MARK: - DatesAndCalendarViewModel public class DatesAndCalendarViewModel: ObservableObject { - @Published var useRelativeDates: Bool = false @Published var showCalendaAccessDenied: Bool = false @Published var showDisableCalendarSync: Bool = false @Published var showError: Bool = false @@ -73,7 +72,7 @@ public class DatesAndCalendarViewModel: ObservableObject { var router: ProfileRouter private var interactor: ProfileInteractorProtocol - private var profileStorage: ProfileStorage + @Published var profileStorage: ProfileStorage private var persistence: ProfilePersistenceProtocol private var calendarManager: CalendarManagerProtocol private var connectivity: ConnectivityProtocol @@ -187,8 +186,7 @@ public class DatesAndCalendarViewModel: ObservableObject { colorSelection: colorString, calendarName: calendarName, accountSelection: accountSelection, - courseCalendarSync: self.courseCalendarSync, - useRelativeDates: self.useRelativeDates + courseCalendarSync: self.courseCalendarSync ) profileStorage.lastCalendarName = calendarName } diff --git a/Profile/Profile/Presentation/DatesAndCalendar/Elements/RelativeDatesToggleView.swift b/Profile/Profile/Presentation/DatesAndCalendar/Elements/RelativeDatesToggleView.swift new file mode 100644 index 000000000..967e52245 --- /dev/null +++ b/Profile/Profile/Presentation/DatesAndCalendar/Elements/RelativeDatesToggleView.swift @@ -0,0 +1,42 @@ +// +// RelativeDatesToggleView.swift +// Profile +// +// Created by  Stepanok Ivan on 22.07.2024. +// + +import SwiftUI +import Theme + +struct RelativeDatesToggleView: View { + @Binding var useRelativeDates: Bool + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Text(ProfileLocalization.Options.title) + .font(Theme.Fonts.labelLarge) + .foregroundColor(Theme.Colors.textPrimary) + HStack(spacing: 16) { + Toggle("", isOn: $useRelativeDates) + .frame(width: 50) + .tint(Theme.Colors.accentColor) + Text(ProfileLocalization.Options.useRelativeDates) + .font(Theme.Fonts.bodyLarge) + .foregroundColor(Theme.Colors.textPrimary) + } + Text( + useRelativeDates + ? ProfileLocalization.Options.showRelativeDates + : ProfileLocalization.Options.showFullDates + ) + .font(Theme.Fonts.labelMedium) + .foregroundColor(Theme.Colors.textPrimary) + } + .padding(.top, 14) + .padding(.horizontal, 24) + .frame(minWidth: 0, + maxWidth: .infinity, + alignment: .leading) + .accessibilityIdentifier("relative_dates_toggle") + } +} diff --git a/Profile/Profile/Presentation/DatesAndCalendar/Models/CalendarSettings.swift b/Profile/Profile/Presentation/DatesAndCalendar/Models/CalendarSettings.swift index 7c1970d0c..b4b63bb4e 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/Models/CalendarSettings.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/Models/CalendarSettings.swift @@ -12,20 +12,17 @@ public struct CalendarSettings: Codable { public var calendarName: String? public var accountSelection: String public var courseCalendarSync: Bool - public var useRelativeDates: Bool public init( colorSelection: String, calendarName: String?, accountSelection: String, - courseCalendarSync: Bool, - useRelativeDates: Bool + courseCalendarSync: Bool ) { self.colorSelection = colorSelection self.calendarName = calendarName self.accountSelection = accountSelection self.courseCalendarSync = courseCalendarSync - self.useRelativeDates = useRelativeDates } enum CodingKeys: String, CodingKey { @@ -33,7 +30,6 @@ public struct CalendarSettings: Codable { case calendarName case accountSelection case courseCalendarSync - case useRelativeDates } public init(from decoder: Decoder) throws { @@ -42,7 +38,6 @@ public struct CalendarSettings: Codable { self.calendarName = try container.decode(String.self, forKey: .calendarName) self.accountSelection = try container.decode(String.self, forKey: .accountSelection) self.courseCalendarSync = try container.decode(Bool.self, forKey: .courseCalendarSync) - self.useRelativeDates = try container.decode(Bool.self, forKey: .useRelativeDates) } public func encode(to encoder: Encoder) throws { @@ -51,6 +46,5 @@ public struct CalendarSettings: Codable { try container.encode(calendarName, forKey: .calendarName) try container.encode(accountSelection, forKey: .accountSelection) try container.encode(courseCalendarSync, forKey: .courseCalendarSync) - try container.encode(useRelativeDates, forKey: .useRelativeDates) } } diff --git a/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift b/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift index 82414e263..154c869e3 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift @@ -95,7 +95,7 @@ public struct SyncCalendarOptionsView: View { coursesToSync .padding(.bottom, 24) } -// relativeDatesToggle + RelativeDatesToggleView(useRelativeDates: $viewModel.profileStorage.useRelativeDates) } .padding(.horizontal, isHorizontal ? 48 : 0) .frameLimit(width: proxy.size.width) @@ -258,21 +258,6 @@ public struct SyncCalendarOptionsView: View { strokeColor: .clear ) } - - @ViewBuilder - private var relativeDatesToggle: some View { - Divider() - .padding(.horizontal, 24) - - optionTitle(ProfileLocalization.Options.title) - .padding(.vertical, 16) - ToggleWithDescriptionView( - text: ProfileLocalization.Options.useRelativeDates, - description: ProfileLocalization.Options.showRelativeDates, - toggle: $viewModel.reconnectRequired - ) - .padding(.horizontal, 24) - } } #if DEBUG diff --git a/Profile/Profile/SwiftGen/Strings.swift b/Profile/Profile/SwiftGen/Strings.swift index 0bcf37eec..0963ce3b5 100644 --- a/Profile/Profile/SwiftGen/Strings.swift +++ b/Profile/Profile/SwiftGen/Strings.swift @@ -240,6 +240,8 @@ public enum ProfileLocalization { public static let title = ProfileLocalization.tr("Localizable", "LOGOUT_ALERT.TITLE", fallback: "Confirm log out") } public enum Options { + /// Show full dates like “January 1, 2021” + public static let showFullDates = ProfileLocalization.tr("Localizable", "OPTIONS.SHOW_FULL_DATES", fallback: "Show full dates like “January 1, 2021”") /// Show relative dates like “Tomorrow” and “Yesterday” public static let showRelativeDates = ProfileLocalization.tr("Localizable", "OPTIONS.SHOW_RELATIVE_DATES", fallback: "Show relative dates like “Tomorrow” and “Yesterday”") /// Options diff --git a/Profile/Profile/en.lproj/Localizable.strings b/Profile/Profile/en.lproj/Localizable.strings index 2a680ddb2..42a2871fb 100644 --- a/Profile/Profile/en.lproj/Localizable.strings +++ b/Profile/Profile/en.lproj/Localizable.strings @@ -112,6 +112,7 @@ "OPTIONS.TITLE" = "Options"; "OPTIONS.USE_RELATIVE_DATES" = "Use relative dates"; "OPTIONS.SHOW_RELATIVE_DATES" = "Show relative dates like “Tomorrow” and “Yesterday”"; +"OPTIONS.SHOW_FULL_DATES" = "Show full dates like “January 1, 2021”"; "DATES_AND_CALENDAR.TITLE" = "Dates & Calendar"; "COURSE_CALENDAR_SYNC.TITLE" = "Course Calendar Sync"; diff --git a/Profile/ProfileTests/ProfileMock.generated.swift b/Profile/ProfileTests/ProfileMock.generated.swift index 534a1dde5..843268a30 100644 --- a/Profile/ProfileTests/ProfileMock.generated.swift +++ b/Profile/ProfileTests/ProfileMock.generated.swift @@ -2230,9 +2230,9 @@ open class ProfileAnalyticsMock: ProfileAnalytics, Mock { } open func profileTrackEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { - addInvocation(.m_profileEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(biValue))) - let perform = methodPerformValue(.m_profileEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(biValue))) as? (AnalyticsEvent, EventBIValue) -> Void - perform?(`event`, biValue) + addInvocation(.m_profileTrackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_profileTrackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + perform?(`event`, `biValue`) } open func profileScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { @@ -2258,7 +2258,7 @@ open class ProfileAnalyticsMock: ProfileAnalytics, Mock { case m_profileWifiToggle__action_action(Parameter) case m_profileUserDeleteAccountClicked case m_profileDeleteAccountSuccess__success_success(Parameter) - case m_profileEvent__eventbiValue_biValue(Parameter, Parameter) + case m_profileTrackEvent__eventbiValue_biValue(Parameter, Parameter) case m_profileScreenEvent__eventbiValue_biValue(Parameter, Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -2305,7 +2305,7 @@ open class ProfileAnalyticsMock: ProfileAnalytics, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSuccess, rhs: rhsSuccess, with: matcher), lhsSuccess, rhsSuccess, "success")) return Matcher.ComparisonResult(results) - case (.m_profileEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_profileEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): + case (.m_profileTrackEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_profileTrackEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) @@ -2337,7 +2337,7 @@ open class ProfileAnalyticsMock: ProfileAnalytics, Mock { case let .m_profileWifiToggle__action_action(p0): return p0.intValue case .m_profileUserDeleteAccountClicked: return 0 case let .m_profileDeleteAccountSuccess__success_success(p0): return p0.intValue - case let .m_profileEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + case let .m_profileTrackEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue case let .m_profileScreenEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue } } @@ -2358,7 +2358,7 @@ open class ProfileAnalyticsMock: ProfileAnalytics, Mock { case .m_profileWifiToggle__action_action: return ".profileWifiToggle(action:)" case .m_profileUserDeleteAccountClicked: return ".profileUserDeleteAccountClicked()" case .m_profileDeleteAccountSuccess__success_success: return ".profileDeleteAccountSuccess(success:)" - case .m_profileEvent__eventbiValue_biValue: return ".profileEvent(_:biValue:)" + case .m_profileTrackEvent__eventbiValue_biValue: return ".profileTrackEvent(_:biValue:)" case .m_profileScreenEvent__eventbiValue_biValue: return ".profileScreenEvent(_:biValue:)" } } @@ -2393,7 +2393,7 @@ open class ProfileAnalyticsMock: ProfileAnalytics, Mock { public static func profileWifiToggle(action: Parameter) -> Verify { return Verify(method: .m_profileWifiToggle__action_action(`action`))} public static func profileUserDeleteAccountClicked() -> Verify { return Verify(method: .m_profileUserDeleteAccountClicked)} public static func profileDeleteAccountSuccess(success: Parameter) -> Verify { return Verify(method: .m_profileDeleteAccountSuccess__success_success(`success`))} - public static func profileEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_profileEvent__eventbiValue_biValue(`event`, `biValue`))} + public static func profileTrackEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_profileTrackEvent__eventbiValue_biValue(`event`, `biValue`))} public static func profileScreenEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_profileScreenEvent__eventbiValue_biValue(`event`, `biValue`))} } @@ -2446,8 +2446,8 @@ open class ProfileAnalyticsMock: ProfileAnalytics, Mock { public static func profileDeleteAccountSuccess(success: Parameter, perform: @escaping (Bool) -> Void) -> Perform { return Perform(method: .m_profileDeleteAccountSuccess__success_success(`success`), performs: perform) } - public static func profileEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { - return Perform(method: .m_profileEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + public static func profileTrackEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_profileTrackEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) } public static func profileScreenEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { return Perform(method: .m_profileScreenEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform)