From 390466e56d9db451458d15ef2c53e0507373fcc7 Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Thu, 18 Jul 2024 18:26:12 +0300 Subject: [PATCH 1/8] feat: initial commit --- Core/Core.xcodeproj/project.pbxproj | 4 + Core/Core/Data/Model/CourseDates.swift | 348 +++++++++++++++++++ Course/Course.xcodeproj/project.pbxproj | 6 +- Course/Course/Domain/Model/CourseDates.swift | 348 +++++++++++++++++++ 4 files changed, 704 insertions(+), 2 deletions(-) create mode 100644 Core/Core/Data/Model/CourseDates.swift create mode 100644 Course/Course/Domain/Model/CourseDates.swift diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index 0b440083..6a43baa4 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 021D925028DC89D100ACC565 /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021D924F28DC89D100ACC565 /* UserProfile.swift */; }; 021D925728DCF12900ACC565 /* AlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021D925628DCF12900ACC565 /* AlertView.swift */; }; 022020462C11BB2200D15795 /* Data_CourseDates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022020452C11BB2200D15795 /* Data_CourseDates.swift */; }; + 0225440E2C4961A300EEC33F /* CourseDates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0225440D2C4961A300EEC33F /* CourseDates.swift */; }; 02280F5B294B4E6F0032823A /* Connectivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02280F5A294B4E6F0032823A /* Connectivity.swift */; }; 02284C182A3B1AE00007117F /* UIApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02284C172A3B1AE00007117F /* UIApplicationExtension.swift */; }; 02286D162C106393005EEC8D /* CourseDates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02286D152C106393005EEC8D /* CourseDates.swift */; }; @@ -212,6 +213,7 @@ 021D924F28DC89D100ACC565 /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = ""; }; 021D925628DCF12900ACC565 /* AlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertView.swift; sourceTree = ""; }; 022020452C11BB2200D15795 /* Data_CourseDates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_CourseDates.swift; sourceTree = ""; }; + 0225440D2C4961A300EEC33F /* CourseDates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDates.swift; sourceTree = ""; }; 02280F5A294B4E6F0032823A /* Connectivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connectivity.swift; sourceTree = ""; }; 02284C172A3B1AE00007117F /* UIApplicationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationExtension.swift; sourceTree = ""; }; 02286D152C106393005EEC8D /* CourseDates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDates.swift; sourceTree = ""; }; @@ -625,6 +627,7 @@ 07E0939E2B308D2800F1E4B2 /* Data_Certificate.swift */, 02A8C5802C05DBB4004B91FF /* Data_EnrollmentsStatus.swift */, 022020452C11BB2200D15795 /* Data_CourseDates.swift */, + 0225440D2C4961A300EEC33F /* CourseDates.swift */, ); path = Model; sourceTree = ""; @@ -1228,6 +1231,7 @@ E0D5861C2B2FF85B009B4BA7 /* RawStringExtactable.swift in Sources */, 02EBC7592C19DE1100BE182C /* CalendarManagerProtocol.swift in Sources */, 027BD3BD2909478B00392132 /* UIView+EnclosingScrollView.swift in Sources */, + 0225440E2C4961A300EEC33F /* CourseDates.swift in Sources */, BA8FA6682AD59A5700EA029A /* SocialAuthButton.swift in Sources */, 02D400612B0678190029D168 /* SKStoreReviewControllerExtension.swift in Sources */, 02A4833C29B8C57800D33F33 /* DownloadView.swift in Sources */, diff --git a/Core/Core/Data/Model/CourseDates.swift b/Core/Core/Data/Model/CourseDates.swift new file mode 100644 index 00000000..5f7eb504 --- /dev/null +++ b/Core/Core/Data/Model/CourseDates.swift @@ -0,0 +1,348 @@ +// +// CourseDates.swift +// Core +// +// Created by  Stepanok Ivan on 05.06.2024. лже файл +// + +import Foundation +import CryptoKit + +public struct CourseDates { + public let datesBannerInfo: DatesBannerInfo + public let courseDateBlocks: [CourseDateBlock] + public let hasEnded, learnerIsFullAccess: Bool + public let userTimezone: String? + + public var statusDatesBlocks: [CompletionStatus: [Date: [CourseDateBlock]]] { + var statusDatesBlocks: [CompletionStatus: [Date: [CourseDateBlock]]] = [:] + var statusBlocks: [CompletionStatus: [CourseDateBlock]] = [:] + + for block in courseDateBlocks { + let date = block.date + switch true { + case block.complete ?? false || block.blockStatus == .courseStartDate: + statusBlocks[.completed, default: []].append(block) + case date.isInPast: + statusBlocks[.pastDue, default: []].append(block) + case date.isToday: + if date < Date() { + statusBlocks[.pastDue, default: []].append(block) + } else { + statusBlocks[.today, default: []].append(block) + } + case date.isThisWeek: + statusBlocks[.thisWeek, default: []].append(block) + case date.isNextWeek: + statusBlocks[.nextWeek, default: []].append(block) + case date.isUpcoming: + statusBlocks[.upcoming, default: []].append(block) + default: + statusBlocks[.upcoming, default: []].append(block) + } + } + + for status in statusBlocks.keys { + let courseDateBlocks = statusBlocks[status] + var dateToCourseDateBlockDict: [Date: [CourseDateBlock]] = [:] + + for block in courseDateBlocks ?? [] { + let date = block.date + dateToCourseDateBlockDict[date, default: []].append(block) + } + statusDatesBlocks[status] = dateToCourseDateBlockDict + } + + return statusDatesBlocks + } + + public var dateBlocks: [Date: [CourseDateBlock]] { + return courseDateBlocks.reduce(into: [:]) { result, block in + let date = block.date + result[date, default: []].append(block) + } + } + + public init( + datesBannerInfo: DatesBannerInfo, + courseDateBlocks: [CourseDateBlock], + hasEnded: Bool, + learnerIsFullAccess: Bool, + userTimezone: String? + ) { + self.datesBannerInfo = datesBannerInfo + self.courseDateBlocks = courseDateBlocks + self.hasEnded = hasEnded + self.learnerIsFullAccess = learnerIsFullAccess + self.userTimezone = userTimezone + } + + public var checksum: String { + var combinedString = "" + for block in self.courseDateBlocks { + let assignmentType = block.assignmentType ?? "" + combinedString += assignmentType + block.firstComponentBlockID + block.date.description + } + + let checksumData = SHA256.hash(data: Data(combinedString.utf8)) + let checksumString = checksumData.map { String(format: "%02hhx", $0) }.joined() + return checksumString + } +} + +public extension Date { + static var today: Date { + return Calendar.current.startOfDay(for: Date()) + } + + static func compare(_ fromDate: Date, to toDate: Date) -> ComparisonResult { + if fromDate > toDate { + return .orderedDescending + } else if fromDate < toDate { + return .orderedAscending + } + return .orderedSame + } + + var isInPast: Bool { + return Date.compare(self, to: .today) == .orderedAscending + } + + var isToday: Bool { + let calendar = Calendar.current + let selfComponents = calendar.dateComponents([.year, .month, .day], from: self) + let todayComponents = calendar.dateComponents([.year, .month, .day], from: .today) + return selfComponents == todayComponents + } + + var isInFuture: Bool { + return Date.compare(self, to: .today) == .orderedDescending + } + + var isThisWeek: Bool { + // Items due within the next 7 days (7*24 hours from now) + let calendar = Calendar.current + let nextDay = calendar.date(byAdding: .day, value: 1, to: .today) ?? .distantPast + let nextSeventhDay = calendar.date(byAdding: .day, value: 8, to: .today) ?? .distantPast + return (nextDay...nextSeventhDay).contains(self) + } + + var isNextWeek: Bool { + // Items due within the next 14 days (14*24 hours from now) + let calendar = Calendar.current + let nextEighthDay = calendar.date(byAdding: .day, value: 8, to: .today) ?? .distantPast + let nextFourteenthDay = calendar.date(byAdding: .day, value: 15, to: .today) ?? .distantPast + return (nextEighthDay...nextFourteenthDay).contains(self) + } + + var isUpcoming: Bool { + // Items due after the next 14 days (14*24 hours from now) + let calendar = Calendar.current + let nextFourteenthDay = calendar.date(byAdding: .day, value: 14, to: .today) ?? .distantPast + return Date.compare(self, to: nextFourteenthDay) == .orderedDescending + } +} + +public struct CourseDateBlock: Identifiable { + public let id: UUID = UUID() + + public let assignmentType: String? + public let complete: Bool? + public let date: Date + public let dateType, description: String + public let learnerHasAccess: Bool + public let link: String + public let linkText: String? + public let title: String + public let extraInfo: String? + public let firstComponentBlockID: String + + public var formattedDate: String { + return date.dateToString(style: .shortWeekdayMonthDayYear) + } + + public var isInPast: Bool { + return date.isInPast + } + + public var isToday: Bool { + if dateType.isEmpty { + return true + } else { + return date.isToday + } + } + + public var isInFuture: Bool { + return date.isInFuture + } + + public var isThisWeek: Bool { + return date.isThisWeek + } + + public var isNextWeek: Bool { + return date.isNextWeek + } + + public var isUpcoming: Bool { + return date.isUpcoming + } + + public var isAssignment: Bool { + return BlockStatus.status(of: dateType) == .assignment + } + + public var isVerifiedOnly: Bool { + return !learnerHasAccess + } + + public var isComplete: Bool { + return complete ?? false + } + + public var isLearnerAssignment: Bool { + return learnerHasAccess && isAssignment + } + + public var isPastDue: Bool { + return !isComplete && (date < .today) + } + + public var isUnreleased: Bool { + return link.isEmpty + } + + public var canShowLink: Bool { + return !isUnreleased && isLearnerAssignment + } + + public var isAvailable: Bool { + return learnerHasAccess && (!isUnreleased || !isLearnerAssignment) + } + + public var blockStatus: BlockStatus { + if isComplete { + return .completed + } + + if !learnerHasAccess { + return .verifiedOnly + } + + if isAssignment { + if isInPast { + return isUnreleased ? .unreleased : .pastDue + } else if isToday || isInFuture { + return isUnreleased ? .unreleased : .dueNext + } + } + + return BlockStatus.status(of: dateType) + } + + public var blockImage: ImageAsset? { + if !learnerHasAccess { + return CoreAssets.lockIcon + } + + if isAssignment { + return CoreAssets.assignmentIcon + } + + switch blockStatus { + case .courseStartDate, .courseEndDate: + return CoreAssets.schoolCapIcon + case .verifiedUpgradeDeadline, .verificationDeadlineDate: + return CoreAssets.calendarIcon + case .courseExpiredDate: + return CoreAssets.lockWithWatchIcon + case .certificateAvailbleDate: + return CoreAssets.certificateIcon + default: + return CoreAssets.calendarIcon + } + } +} + +public struct DatesBannerInfo { + public let missedDeadlines, contentTypeGatingEnabled, missedGatedContent: Bool + public let verifiedUpgradeLink: String? + public let status: DataLayer.BannerInfoStatus? + + public init( + missedDeadlines: Bool, + contentTypeGatingEnabled: Bool, + missedGatedContent: Bool, + verifiedUpgradeLink: String?, + status: DataLayer.BannerInfoStatus? + ) { + self.missedDeadlines = missedDeadlines + self.contentTypeGatingEnabled = contentTypeGatingEnabled + self.missedGatedContent = missedGatedContent + self.verifiedUpgradeLink = verifiedUpgradeLink + self.status = status + } +} + +public struct CourseDateBanner { + public let datesBannerInfo: DatesBannerInfo + public let hasEnded: Bool + + public init(datesBannerInfo: DatesBannerInfo, hasEnded: Bool) { + self.datesBannerInfo = datesBannerInfo + self.hasEnded = hasEnded + } +} + +public enum BlockStatus { + case completed + case pastDue + case dueNext + case unreleased + case verifiedOnly + case assignment + case verifiedUpgradeDeadline + case courseExpiredDate + case verificationDeadlineDate + case certificateAvailbleDate + case courseStartDate + case courseEndDate + case event + + static func status(of type: String) -> BlockStatus { + switch type { + case "assignment-due-date": return .assignment + case "verified-upgrade-deadline": return .verifiedUpgradeDeadline + case "course-expired-date": return .courseExpiredDate + case "verification-deadline-date": return .verificationDeadlineDate + case "certificate-available-date": return .certificateAvailbleDate + case "course-start-date": return .courseStartDate + case "course-end-date": return .courseEndDate + default: return .event + } + } +} + +public enum CompletionStatus: String { + case completed = "Completed" + case pastDue = "Past Due" + case today = "Today" + case thisWeek = "This Week" + case nextWeek = "Next Week" + case upcoming = "Upcoming" +} + +public extension Array { + mutating func modifyForEach(_ body: (_ element: inout Element) -> Void) { + for index in indices { + modifyElement(atIndex: index) { body(&$0) } + } + } + + mutating func modifyElement(atIndex index: Index, _ modifyElement: (_ element: inout Element) -> Void) { + var element = self[index] + modifyElement(&element) + self[index] = element + } +} diff --git a/Course/Course.xcodeproj/project.pbxproj b/Course/Course.xcodeproj/project.pbxproj index 33c43eea..53d0e243 100644 --- a/Course/Course.xcodeproj/project.pbxproj +++ b/Course/Course.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 02197DC52C1AFC0600CC8FF2 /* CalendarSyncStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EBC7542C19CFCF00BE182C /* CalendarSyncStatusView.swift */; }; + 022544102C4961E400EEC33F /* CourseDates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0225440F2C4961E400EEC33F /* CourseDates.swift */; }; 02280F5E294B4FDA0032823A /* CourseCoreModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 02280F5C294B4FDA0032823A /* CourseCoreModel.xcdatamodeld */; }; 02280F60294B50030032823A /* CoursePersistenceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02280F5F294B50030032823A /* CoursePersistenceProtocol.swift */; }; 022C64D829ACEC48000F532B /* HandoutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022C64D729ACEC48000F532B /* HandoutsView.swift */; }; @@ -108,6 +109,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 0225440F2C4961E400EEC33F /* CourseDates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDates.swift; sourceTree = ""; }; 02280F5D294B4FDA0032823A /* CourseCoreModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = CourseCoreModel.xcdatamodel; sourceTree = ""; }; 02280F5F294B50030032823A /* CoursePersistenceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoursePersistenceProtocol.swift; sourceTree = ""; }; 022C64D729ACEC48000F532B /* HandoutsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandoutsView.swift; sourceTree = ""; }; @@ -309,8 +311,6 @@ 02B6B3B828E1D12900232911 /* Data */, 02B6B3B528E1D10700232911 /* Domain */, 02EAE2CA28E1F0A700529644 /* Presentation */, - 97CA95212B875EA200A9EDEA /* Views */, - 97EA4D822B84EFA900663F58 /* Managers */, 02B6B3B428E1C49400232911 /* Localizable.strings */, 02C355372C08DCD700501342 /* Localizable.stringsdict */, ); @@ -430,6 +430,7 @@ 02B6B3C228E1DCD100232911 /* CourseDetails.swift */, 0276D75C29DDA3F80004CDF8 /* ResumeBlock.swift */, 022C64E129ADEB83000F532B /* CourseUpdate.swift */, + 0225440F2C4961E400EEC33F /* CourseDates.swift */, ); path = Model; sourceTree = ""; @@ -887,6 +888,7 @@ 067B7B4F2BED339200D1768F /* PlayerDelegateProtocol.swift in Sources */, 0231124D28EDA804002588FB /* CourseUnitView.swift in Sources */, 027020FC28E7362100F54332 /* Data_CourseOutlineResponse.swift in Sources */, + 022544102C4961E400EEC33F /* CourseDates.swift in Sources */, 067B7B532BED339200D1768F /* PlayerServiceProtocol.swift in Sources */, BAD9CA2D2B2736BB00DE790A /* LessonLineProgressView.swift in Sources */, 02197DC52C1AFC0600CC8FF2 /* CalendarSyncStatusView.swift in Sources */, diff --git a/Course/Course/Domain/Model/CourseDates.swift b/Course/Course/Domain/Model/CourseDates.swift new file mode 100644 index 00000000..0889af7b --- /dev/null +++ b/Course/Course/Domain/Model/CourseDates.swift @@ -0,0 +1,348 @@ +// +// CourseDates.swift +// Core +// +// Created by  Stepanok Ivan on 05.06.2024. лже файл 2 +// + +import Foundation +import CryptoKit + +public struct CourseDates { + public let datesBannerInfo: DatesBannerInfo + public let courseDateBlocks: [CourseDateBlock] + public let hasEnded, learnerIsFullAccess: Bool + public let userTimezone: String? + + public var statusDatesBlocks: [CompletionStatus: [Date: [CourseDateBlock]]] { + var statusDatesBlocks: [CompletionStatus: [Date: [CourseDateBlock]]] = [:] + var statusBlocks: [CompletionStatus: [CourseDateBlock]] = [:] + + for block in courseDateBlocks { + let date = block.date + switch true { + case block.complete ?? false || block.blockStatus == .courseStartDate: + statusBlocks[.completed, default: []].append(block) + case date.isInPast: + statusBlocks[.pastDue, default: []].append(block) + case date.isToday: + if date < Date() { + statusBlocks[.pastDue, default: []].append(block) + } else { + statusBlocks[.today, default: []].append(block) + } + case date.isThisWeek: + statusBlocks[.thisWeek, default: []].append(block) + case date.isNextWeek: + statusBlocks[.nextWeek, default: []].append(block) + case date.isUpcoming: + statusBlocks[.upcoming, default: []].append(block) + default: + statusBlocks[.upcoming, default: []].append(block) + } + } + + for status in statusBlocks.keys { + let courseDateBlocks = statusBlocks[status] + var dateToCourseDateBlockDict: [Date: [CourseDateBlock]] = [:] + + for block in courseDateBlocks ?? [] { + let date = block.date + dateToCourseDateBlockDict[date, default: []].append(block) + } + statusDatesBlocks[status] = dateToCourseDateBlockDict + } + + return statusDatesBlocks + } + + public var dateBlocks: [Date: [CourseDateBlock]] { + return courseDateBlocks.reduce(into: [:]) { result, block in + let date = block.date + result[date, default: []].append(block) + } + } + + public init( + datesBannerInfo: DatesBannerInfo, + courseDateBlocks: [CourseDateBlock], + hasEnded: Bool, + learnerIsFullAccess: Bool, + userTimezone: String? + ) { + self.datesBannerInfo = datesBannerInfo + self.courseDateBlocks = courseDateBlocks + self.hasEnded = hasEnded + self.learnerIsFullAccess = learnerIsFullAccess + self.userTimezone = userTimezone + } + + public var checksum: String { + var combinedString = "" + for block in self.courseDateBlocks { + let assignmentType = block.assignmentType ?? "" + combinedString += assignmentType + block.firstComponentBlockID + block.date.description + } + + let checksumData = SHA256.hash(data: Data(combinedString.utf8)) + let checksumString = checksumData.map { String(format: "%02hhx", $0) }.joined() + return checksumString + } +} + +public extension Date { + static var today: Date { + return Calendar.current.startOfDay(for: Date()) + } + + static func compare(_ fromDate: Date, to toDate: Date) -> ComparisonResult { + if fromDate > toDate { + return .orderedDescending + } else if fromDate < toDate { + return .orderedAscending + } + return .orderedSame + } + + var isInPast: Bool { + return Date.compare(self, to: .today) == .orderedAscending + } + + var isToday: Bool { + let calendar = Calendar.current + let selfComponents = calendar.dateComponents([.year, .month, .day], from: self) + let todayComponents = calendar.dateComponents([.year, .month, .day], from: .today) + return selfComponents == todayComponents + } + + var isInFuture: Bool { + return Date.compare(self, to: .today) == .orderedDescending + } + + var isThisWeek: Bool { + // Items due within the next 7 days (7*24 hours from now) + let calendar = Calendar.current + let nextDay = calendar.date(byAdding: .day, value: 1, to: .today) ?? .distantPast + let nextSeventhDay = calendar.date(byAdding: .day, value: 8, to: .today) ?? .distantPast + return (nextDay...nextSeventhDay).contains(self) + } + + var isNextWeek: Bool { + // Items due within the next 14 days (14*24 hours from now) + let calendar = Calendar.current + let nextEighthDay = calendar.date(byAdding: .day, value: 8, to: .today) ?? .distantPast + let nextFourteenthDay = calendar.date(byAdding: .day, value: 15, to: .today) ?? .distantPast + return (nextEighthDay...nextFourteenthDay).contains(self) + } + + var isUpcoming: Bool { + // Items due after the next 14 days (14*24 hours from now) + let calendar = Calendar.current + let nextFourteenthDay = calendar.date(byAdding: .day, value: 14, to: .today) ?? .distantPast + return Date.compare(self, to: nextFourteenthDay) == .orderedDescending + } +} + +public struct CourseDateBlock: Identifiable { + public let id: UUID = UUID() + + public let assignmentType: String? + public let complete: Bool? + public let date: Date + public let dateType, description: String + public let learnerHasAccess: Bool + public let link: String + public let linkText: String? + public let title: String + public let extraInfo: String? + public let firstComponentBlockID: String + + public var formattedDate: String { + return date.dateToString(style: .shortWeekdayMonthDayYear) + } + + public var isInPast: Bool { + return date.isInPast + } + + public var isToday: Bool { + if dateType.isEmpty { + return true + } else { + return date.isToday + } + } + + public var isInFuture: Bool { + return date.isInFuture + } + + public var isThisWeek: Bool { + return date.isThisWeek + } + + public var isNextWeek: Bool { + return date.isNextWeek + } + + public var isUpcoming: Bool { + return date.isUpcoming + } + + public var isAssignment: Bool { + return BlockStatus.status(of: dateType) == .assignment + } + + public var isVerifiedOnly: Bool { + return !learnerHasAccess + } + + public var isComplete: Bool { + return complete ?? false + } + + public var isLearnerAssignment: Bool { + return learnerHasAccess && isAssignment + } + + public var isPastDue: Bool { + return !isComplete && (date < .today) + } + + public var isUnreleased: Bool { + return link.isEmpty + } + + public var canShowLink: Bool { + return !isUnreleased && isLearnerAssignment + } + + public var isAvailable: Bool { + return learnerHasAccess && (!isUnreleased || !isLearnerAssignment) + } + + public var blockStatus: BlockStatus { + if isComplete { + return .completed + } + + if !learnerHasAccess { + return .verifiedOnly + } + + if isAssignment { + if isInPast { + return isUnreleased ? .unreleased : .pastDue + } else if isToday || isInFuture { + return isUnreleased ? .unreleased : .dueNext + } + } + + return BlockStatus.status(of: dateType) + } + + public var blockImage: ImageAsset? { + if !learnerHasAccess { + return CoreAssets.lockIcon + } + + if isAssignment { + return CoreAssets.assignmentIcon + } + + switch blockStatus { + case .courseStartDate, .courseEndDate: + return CoreAssets.schoolCapIcon + case .verifiedUpgradeDeadline, .verificationDeadlineDate: + return CoreAssets.calendarIcon + case .courseExpiredDate: + return CoreAssets.lockWithWatchIcon + case .certificateAvailbleDate: + return CoreAssets.certificateIcon + default: + return CoreAssets.calendarIcon + } + } +} + +public struct DatesBannerInfo { + public let missedDeadlines, contentTypeGatingEnabled, missedGatedContent: Bool + public let verifiedUpgradeLink: String? + public let status: DataLayer.BannerInfoStatus? + + public init( + missedDeadlines: Bool, + contentTypeGatingEnabled: Bool, + missedGatedContent: Bool, + verifiedUpgradeLink: String?, + status: DataLayer.BannerInfoStatus? + ) { + self.missedDeadlines = missedDeadlines + self.contentTypeGatingEnabled = contentTypeGatingEnabled + self.missedGatedContent = missedGatedContent + self.verifiedUpgradeLink = verifiedUpgradeLink + self.status = status + } +} + +public struct CourseDateBanner { + public let datesBannerInfo: DatesBannerInfo + public let hasEnded: Bool + + public init(datesBannerInfo: DatesBannerInfo, hasEnded: Bool) { + self.datesBannerInfo = datesBannerInfo + self.hasEnded = hasEnded + } +} + +public enum BlockStatus { + case completed + case pastDue + case dueNext + case unreleased + case verifiedOnly + case assignment + case verifiedUpgradeDeadline + case courseExpiredDate + case verificationDeadlineDate + case certificateAvailbleDate + case courseStartDate + case courseEndDate + case event + + static func status(of type: String) -> BlockStatus { + switch type { + case "assignment-due-date": return .assignment + case "verified-upgrade-deadline": return .verifiedUpgradeDeadline + case "course-expired-date": return .courseExpiredDate + case "verification-deadline-date": return .verificationDeadlineDate + case "certificate-available-date": return .certificateAvailbleDate + case "course-start-date": return .courseStartDate + case "course-end-date": return .courseEndDate + default: return .event + } + } +} + +public enum CompletionStatus: String { + case completed = "Completed" + case pastDue = "Past Due" + case today = "Today" + case thisWeek = "This Week" + case nextWeek = "Next Week" + case upcoming = "Upcoming" +} + +public extension Array { + mutating func modifyForEach(_ body: (_ element: inout Element) -> Void) { + for index in indices { + modifyElement(atIndex: index) { body(&$0) } + } + } + + mutating func modifyElement(atIndex index: Index, _ modifyElement: (_ element: inout Element) -> Void) { + var element = self[index] + modifyElement(&element) + self[index] = element + } +} From e8008bcd893fb9e3ec2cff83202919857562976e Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Thu, 18 Jul 2024 19:54:05 +0300 Subject: [PATCH 2/8] feat: relative dates --- Core/Core.xcodeproj/project.pbxproj | 8 +- Core/Core/Data/CoreStorage.swift | 6 +- Core/Core/Data/Model/CourseDates.swift | 348 ----------------- ...CourseDates\321\210\320\272\321\217.swift" | 348 +++++++++++++++++ Core/Core/Data/Model/Data_CourseDates.swift | 5 +- .../Core/Data/Repository/AuthRepository.swift | 7 +- Core/Core/Domain/Model/CourseDates.swift | 3 +- Core/Core/Extensions/DateExtension.swift | 77 ++-- Core/Core/View/Base/CourseCellView.swift | 10 +- Course/Course.xcodeproj/project.pbxproj | 8 +- Course/Course/Data/CourseRepository.swift | 4 +- Course/Course/Domain/Model/CourseDates.swift | 348 ----------------- ...ourseDates\321\210\320\272\321\2172.swift" | 349 ++++++++++++++++++ .../Presentation/AllCoursesView.swift | 6 +- .../Presentation/AllCoursesViewModel.swift | 5 +- .../Elements/CourseCardView.swift | 12 +- .../Elements/PrimaryCardView.swift | 14 +- .../Presentation/ListDashboardView.swift | 6 +- .../Presentation/ListDashboardViewModel.swift | 5 +- .../PrimaryCourseDashboardView.swift | 9 +- .../PrimaryCourseDashboardViewModel.swift | 5 +- .../NativeDiscovery/DiscoveryView.swift | 3 +- .../NativeDiscovery/DiscoveryViewModel.swift | 2 +- .../NativeDiscovery/SearchView.swift | 14 +- .../NativeDiscovery/SearchViewModel.swift | 3 + .../Presentation/SearchViewModelTests.swift | 6 +- .../Discussion/Domain/Model/UserThread.swift | 31 +- .../Base/BaseResponsesViewModel.swift | 5 +- .../Comments/Base/CommentCell.swift | 19 +- .../Comments/Base/ParentCommentView.swift | 8 +- .../Comments/Responses/ResponsesView.swift | 11 +- .../Responses/ResponsesViewModel.swift | 3 +- .../Comments/Thread/ThreadView.swift | 17 +- .../Comments/Thread/ThreadViewModel.swift | 4 +- .../DiscussionSearchTopicsView.swift | 3 +- .../DiscussionSearchTopicsViewModel.swift | 5 +- .../Presentation/Posts/PostsView.swift | 3 +- .../Presentation/Posts/PostsViewModel.swift | 32 +- OpenEdX/DI/ScreenAssembly.swift | 16 +- OpenEdX/Data/AppStorage.swift | 30 +- Profile/Profile/Data/ProfileRepository.swift | 2 +- Profile/Profile/Data/ProfileStorage.swift | 2 + .../DatesAndCalendarView.swift | 27 +- .../DatesAndCalendarViewModel.swift | 37 +- .../Models/CalendarSettings.swift | 8 +- .../SyncCalendarOptionsView.swift | 17 +- 46 files changed, 1011 insertions(+), 880 deletions(-) delete mode 100644 Core/Core/Data/Model/CourseDates.swift create mode 100644 "Core/Core/Data/Model/CourseDates\321\210\320\272\321\217.swift" delete mode 100644 Course/Course/Domain/Model/CourseDates.swift create mode 100644 "Course/Course/Domain/Model/CourseDates\321\210\320\272\321\2172.swift" diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index 6a43baa4..bfcd1d58 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -15,7 +15,7 @@ 021D925028DC89D100ACC565 /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021D924F28DC89D100ACC565 /* UserProfile.swift */; }; 021D925728DCF12900ACC565 /* AlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021D925628DCF12900ACC565 /* AlertView.swift */; }; 022020462C11BB2200D15795 /* Data_CourseDates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022020452C11BB2200D15795 /* Data_CourseDates.swift */; }; - 0225440E2C4961A300EEC33F /* CourseDates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0225440D2C4961A300EEC33F /* CourseDates.swift */; }; + 0225440E2C4961A300EEC33F /* CourseDatesшкя.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0225440D2C4961A300EEC33F /* CourseDatesшкя.swift */; }; 02280F5B294B4E6F0032823A /* Connectivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02280F5A294B4E6F0032823A /* Connectivity.swift */; }; 02284C182A3B1AE00007117F /* UIApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02284C172A3B1AE00007117F /* UIApplicationExtension.swift */; }; 02286D162C106393005EEC8D /* CourseDates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02286D152C106393005EEC8D /* CourseDates.swift */; }; @@ -213,7 +213,7 @@ 021D924F28DC89D100ACC565 /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = ""; }; 021D925628DCF12900ACC565 /* AlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertView.swift; sourceTree = ""; }; 022020452C11BB2200D15795 /* Data_CourseDates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_CourseDates.swift; sourceTree = ""; }; - 0225440D2C4961A300EEC33F /* CourseDates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDates.swift; sourceTree = ""; }; + 0225440D2C4961A300EEC33F /* CourseDatesшкя.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CourseDatesшкя.swift"; sourceTree = ""; }; 02280F5A294B4E6F0032823A /* Connectivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connectivity.swift; sourceTree = ""; }; 02284C172A3B1AE00007117F /* UIApplicationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationExtension.swift; sourceTree = ""; }; 02286D152C106393005EEC8D /* CourseDates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDates.swift; sourceTree = ""; }; @@ -627,7 +627,7 @@ 07E0939E2B308D2800F1E4B2 /* Data_Certificate.swift */, 02A8C5802C05DBB4004B91FF /* Data_EnrollmentsStatus.swift */, 022020452C11BB2200D15795 /* Data_CourseDates.swift */, - 0225440D2C4961A300EEC33F /* CourseDates.swift */, + 0225440D2C4961A300EEC33F /* CourseDatesшкя.swift */, ); path = Model; sourceTree = ""; @@ -1231,7 +1231,7 @@ E0D5861C2B2FF85B009B4BA7 /* RawStringExtactable.swift in Sources */, 02EBC7592C19DE1100BE182C /* CalendarManagerProtocol.swift in Sources */, 027BD3BD2909478B00392132 /* UIView+EnclosingScrollView.swift in Sources */, - 0225440E2C4961A300EEC33F /* CourseDates.swift in Sources */, + 0225440E2C4961A300EEC33F /* CourseDatesшкя.swift in Sources */, BA8FA6682AD59A5700EA029A /* SocialAuthButton.swift in Sources */, 02D400612B0678190029D168 /* SKStoreReviewControllerExtension.swift in Sources */, 02A4833C29B8C57800D33F33 /* DownloadView.swift in Sources */, diff --git a/Core/Core/Data/CoreStorage.swift b/Core/Core/Data/CoreStorage.swift index 60837da4..ef09cd07 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/CourseDates.swift b/Core/Core/Data/Model/CourseDates.swift deleted file mode 100644 index 5f7eb504..00000000 --- a/Core/Core/Data/Model/CourseDates.swift +++ /dev/null @@ -1,348 +0,0 @@ -// -// CourseDates.swift -// Core -// -// Created by  Stepanok Ivan on 05.06.2024. лже файл -// - -import Foundation -import CryptoKit - -public struct CourseDates { - public let datesBannerInfo: DatesBannerInfo - public let courseDateBlocks: [CourseDateBlock] - public let hasEnded, learnerIsFullAccess: Bool - public let userTimezone: String? - - public var statusDatesBlocks: [CompletionStatus: [Date: [CourseDateBlock]]] { - var statusDatesBlocks: [CompletionStatus: [Date: [CourseDateBlock]]] = [:] - var statusBlocks: [CompletionStatus: [CourseDateBlock]] = [:] - - for block in courseDateBlocks { - let date = block.date - switch true { - case block.complete ?? false || block.blockStatus == .courseStartDate: - statusBlocks[.completed, default: []].append(block) - case date.isInPast: - statusBlocks[.pastDue, default: []].append(block) - case date.isToday: - if date < Date() { - statusBlocks[.pastDue, default: []].append(block) - } else { - statusBlocks[.today, default: []].append(block) - } - case date.isThisWeek: - statusBlocks[.thisWeek, default: []].append(block) - case date.isNextWeek: - statusBlocks[.nextWeek, default: []].append(block) - case date.isUpcoming: - statusBlocks[.upcoming, default: []].append(block) - default: - statusBlocks[.upcoming, default: []].append(block) - } - } - - for status in statusBlocks.keys { - let courseDateBlocks = statusBlocks[status] - var dateToCourseDateBlockDict: [Date: [CourseDateBlock]] = [:] - - for block in courseDateBlocks ?? [] { - let date = block.date - dateToCourseDateBlockDict[date, default: []].append(block) - } - statusDatesBlocks[status] = dateToCourseDateBlockDict - } - - return statusDatesBlocks - } - - public var dateBlocks: [Date: [CourseDateBlock]] { - return courseDateBlocks.reduce(into: [:]) { result, block in - let date = block.date - result[date, default: []].append(block) - } - } - - public init( - datesBannerInfo: DatesBannerInfo, - courseDateBlocks: [CourseDateBlock], - hasEnded: Bool, - learnerIsFullAccess: Bool, - userTimezone: String? - ) { - self.datesBannerInfo = datesBannerInfo - self.courseDateBlocks = courseDateBlocks - self.hasEnded = hasEnded - self.learnerIsFullAccess = learnerIsFullAccess - self.userTimezone = userTimezone - } - - public var checksum: String { - var combinedString = "" - for block in self.courseDateBlocks { - let assignmentType = block.assignmentType ?? "" - combinedString += assignmentType + block.firstComponentBlockID + block.date.description - } - - let checksumData = SHA256.hash(data: Data(combinedString.utf8)) - let checksumString = checksumData.map { String(format: "%02hhx", $0) }.joined() - return checksumString - } -} - -public extension Date { - static var today: Date { - return Calendar.current.startOfDay(for: Date()) - } - - static func compare(_ fromDate: Date, to toDate: Date) -> ComparisonResult { - if fromDate > toDate { - return .orderedDescending - } else if fromDate < toDate { - return .orderedAscending - } - return .orderedSame - } - - var isInPast: Bool { - return Date.compare(self, to: .today) == .orderedAscending - } - - var isToday: Bool { - let calendar = Calendar.current - let selfComponents = calendar.dateComponents([.year, .month, .day], from: self) - let todayComponents = calendar.dateComponents([.year, .month, .day], from: .today) - return selfComponents == todayComponents - } - - var isInFuture: Bool { - return Date.compare(self, to: .today) == .orderedDescending - } - - var isThisWeek: Bool { - // Items due within the next 7 days (7*24 hours from now) - let calendar = Calendar.current - let nextDay = calendar.date(byAdding: .day, value: 1, to: .today) ?? .distantPast - let nextSeventhDay = calendar.date(byAdding: .day, value: 8, to: .today) ?? .distantPast - return (nextDay...nextSeventhDay).contains(self) - } - - var isNextWeek: Bool { - // Items due within the next 14 days (14*24 hours from now) - let calendar = Calendar.current - let nextEighthDay = calendar.date(byAdding: .day, value: 8, to: .today) ?? .distantPast - let nextFourteenthDay = calendar.date(byAdding: .day, value: 15, to: .today) ?? .distantPast - return (nextEighthDay...nextFourteenthDay).contains(self) - } - - var isUpcoming: Bool { - // Items due after the next 14 days (14*24 hours from now) - let calendar = Calendar.current - let nextFourteenthDay = calendar.date(byAdding: .day, value: 14, to: .today) ?? .distantPast - return Date.compare(self, to: nextFourteenthDay) == .orderedDescending - } -} - -public struct CourseDateBlock: Identifiable { - public let id: UUID = UUID() - - public let assignmentType: String? - public let complete: Bool? - public let date: Date - public let dateType, description: String - public let learnerHasAccess: Bool - public let link: String - public let linkText: String? - public let title: String - public let extraInfo: String? - public let firstComponentBlockID: String - - public var formattedDate: String { - return date.dateToString(style: .shortWeekdayMonthDayYear) - } - - public var isInPast: Bool { - return date.isInPast - } - - public var isToday: Bool { - if dateType.isEmpty { - return true - } else { - return date.isToday - } - } - - public var isInFuture: Bool { - return date.isInFuture - } - - public var isThisWeek: Bool { - return date.isThisWeek - } - - public var isNextWeek: Bool { - return date.isNextWeek - } - - public var isUpcoming: Bool { - return date.isUpcoming - } - - public var isAssignment: Bool { - return BlockStatus.status(of: dateType) == .assignment - } - - public var isVerifiedOnly: Bool { - return !learnerHasAccess - } - - public var isComplete: Bool { - return complete ?? false - } - - public var isLearnerAssignment: Bool { - return learnerHasAccess && isAssignment - } - - public var isPastDue: Bool { - return !isComplete && (date < .today) - } - - public var isUnreleased: Bool { - return link.isEmpty - } - - public var canShowLink: Bool { - return !isUnreleased && isLearnerAssignment - } - - public var isAvailable: Bool { - return learnerHasAccess && (!isUnreleased || !isLearnerAssignment) - } - - public var blockStatus: BlockStatus { - if isComplete { - return .completed - } - - if !learnerHasAccess { - return .verifiedOnly - } - - if isAssignment { - if isInPast { - return isUnreleased ? .unreleased : .pastDue - } else if isToday || isInFuture { - return isUnreleased ? .unreleased : .dueNext - } - } - - return BlockStatus.status(of: dateType) - } - - public var blockImage: ImageAsset? { - if !learnerHasAccess { - return CoreAssets.lockIcon - } - - if isAssignment { - return CoreAssets.assignmentIcon - } - - switch blockStatus { - case .courseStartDate, .courseEndDate: - return CoreAssets.schoolCapIcon - case .verifiedUpgradeDeadline, .verificationDeadlineDate: - return CoreAssets.calendarIcon - case .courseExpiredDate: - return CoreAssets.lockWithWatchIcon - case .certificateAvailbleDate: - return CoreAssets.certificateIcon - default: - return CoreAssets.calendarIcon - } - } -} - -public struct DatesBannerInfo { - public let missedDeadlines, contentTypeGatingEnabled, missedGatedContent: Bool - public let verifiedUpgradeLink: String? - public let status: DataLayer.BannerInfoStatus? - - public init( - missedDeadlines: Bool, - contentTypeGatingEnabled: Bool, - missedGatedContent: Bool, - verifiedUpgradeLink: String?, - status: DataLayer.BannerInfoStatus? - ) { - self.missedDeadlines = missedDeadlines - self.contentTypeGatingEnabled = contentTypeGatingEnabled - self.missedGatedContent = missedGatedContent - self.verifiedUpgradeLink = verifiedUpgradeLink - self.status = status - } -} - -public struct CourseDateBanner { - public let datesBannerInfo: DatesBannerInfo - public let hasEnded: Bool - - public init(datesBannerInfo: DatesBannerInfo, hasEnded: Bool) { - self.datesBannerInfo = datesBannerInfo - self.hasEnded = hasEnded - } -} - -public enum BlockStatus { - case completed - case pastDue - case dueNext - case unreleased - case verifiedOnly - case assignment - case verifiedUpgradeDeadline - case courseExpiredDate - case verificationDeadlineDate - case certificateAvailbleDate - case courseStartDate - case courseEndDate - case event - - static func status(of type: String) -> BlockStatus { - switch type { - case "assignment-due-date": return .assignment - case "verified-upgrade-deadline": return .verifiedUpgradeDeadline - case "course-expired-date": return .courseExpiredDate - case "verification-deadline-date": return .verificationDeadlineDate - case "certificate-available-date": return .certificateAvailbleDate - case "course-start-date": return .courseStartDate - case "course-end-date": return .courseEndDate - default: return .event - } - } -} - -public enum CompletionStatus: String { - case completed = "Completed" - case pastDue = "Past Due" - case today = "Today" - case thisWeek = "This Week" - case nextWeek = "Next Week" - case upcoming = "Upcoming" -} - -public extension Array { - mutating func modifyForEach(_ body: (_ element: inout Element) -> Void) { - for index in indices { - modifyElement(atIndex: index) { body(&$0) } - } - } - - mutating func modifyElement(atIndex index: Index, _ modifyElement: (_ element: inout Element) -> Void) { - var element = self[index] - modifyElement(&element) - self[index] = element - } -} diff --git "a/Core/Core/Data/Model/CourseDates\321\210\320\272\321\217.swift" "b/Core/Core/Data/Model/CourseDates\321\210\320\272\321\217.swift" new file mode 100644 index 00000000..10f40aac --- /dev/null +++ "b/Core/Core/Data/Model/CourseDates\321\210\320\272\321\217.swift" @@ -0,0 +1,348 @@ +//// +//// CourseDates.swift +//// Core +//// +//// Created by  Stepanok Ivan on 05.06.2024. лже файл +//// +// +//import Foundation +//import CryptoKit +// +//public struct CourseDates { +// public let datesBannerInfo: DatesBannerInfo +// public let courseDateBlocks: [CourseDateBlock] +// public let hasEnded, learnerIsFullAccess: Bool +// public let userTimezone: String? +// +// public var statusDatesBlocks: [CompletionStatus: [Date: [CourseDateBlock]]] { +// var statusDatesBlocks: [CompletionStatus: [Date: [CourseDateBlock]]] = [:] +// var statusBlocks: [CompletionStatus: [CourseDateBlock]] = [:] +// +// for block in courseDateBlocks { +// let date = block.date +// switch true { +// case block.complete ?? false || block.blockStatus == .courseStartDate: +// statusBlocks[.completed, default: []].append(block) +// case date.isInPast: +// statusBlocks[.pastDue, default: []].append(block) +// case date.isToday: +// if date < Date() { +// statusBlocks[.pastDue, default: []].append(block) +// } else { +// statusBlocks[.today, default: []].append(block) +// } +// case date.isThisWeek: +// statusBlocks[.thisWeek, default: []].append(block) +// case date.isNextWeek: +// statusBlocks[.nextWeek, default: []].append(block) +// case date.isUpcoming: +// statusBlocks[.upcoming, default: []].append(block) +// default: +// statusBlocks[.upcoming, default: []].append(block) +// } +// } +// +// for status in statusBlocks.keys { +// let courseDateBlocks = statusBlocks[status] +// var dateToCourseDateBlockDict: [Date: [CourseDateBlock]] = [:] +// +// for block in courseDateBlocks ?? [] { +// let date = block.date +// dateToCourseDateBlockDict[date, default: []].append(block) +// } +// statusDatesBlocks[status] = dateToCourseDateBlockDict +// } +// +// return statusDatesBlocks +// } +// +// public var dateBlocks: [Date: [CourseDateBlock]] { +// return courseDateBlocks.reduce(into: [:]) { result, block in +// let date = block.date +// result[date, default: []].append(block) +// } +// } +// +// public init( +// datesBannerInfo: DatesBannerInfo, +// courseDateBlocks: [CourseDateBlock], +// hasEnded: Bool, +// learnerIsFullAccess: Bool, +// userTimezone: String? +// ) { +// self.datesBannerInfo = datesBannerInfo +// self.courseDateBlocks = courseDateBlocks +// self.hasEnded = hasEnded +// self.learnerIsFullAccess = learnerIsFullAccess +// self.userTimezone = userTimezone +// } +// +// public var checksum: String { +// var combinedString = "" +// for block in self.courseDateBlocks { +// let assignmentType = block.assignmentType ?? "" +// combinedString += assignmentType + block.firstComponentBlockID + block.date.description +// } +// +// let checksumData = SHA256.hash(data: Data(combinedString.utf8)) +// let checksumString = checksumData.map { String(format: "%02hhx", $0) }.joined() +// return checksumString +// } +//} +// +//public extension Date { +// static var today: Date { +// return Calendar.current.startOfDay(for: Date()) +// } +// +// static func compare(_ fromDate: Date, to toDate: Date) -> ComparisonResult { +// if fromDate > toDate { +// return .orderedDescending +// } else if fromDate < toDate { +// return .orderedAscending +// } +// return .orderedSame +// } +// +// var isInPast: Bool { +// return Date.compare(self, to: .today) == .orderedAscending +// } +// +// var isToday: Bool { +// let calendar = Calendar.current +// let selfComponents = calendar.dateComponents([.year, .month, .day], from: self) +// let todayComponents = calendar.dateComponents([.year, .month, .day], from: .today) +// return selfComponents == todayComponents +// } +// +// var isInFuture: Bool { +// return Date.compare(self, to: .today) == .orderedDescending +// } +// +// var isThisWeek: Bool { +// // Items due within the next 7 days (7*24 hours from now) +// let calendar = Calendar.current +// let nextDay = calendar.date(byAdding: .day, value: 1, to: .today) ?? .distantPast +// let nextSeventhDay = calendar.date(byAdding: .day, value: 8, to: .today) ?? .distantPast +// return (nextDay...nextSeventhDay).contains(self) +// } +// +// var isNextWeek: Bool { +// // Items due within the next 14 days (14*24 hours from now) +// let calendar = Calendar.current +// let nextEighthDay = calendar.date(byAdding: .day, value: 8, to: .today) ?? .distantPast +// let nextFourteenthDay = calendar.date(byAdding: .day, value: 15, to: .today) ?? .distantPast +// return (nextEighthDay...nextFourteenthDay).contains(self) +// } +// +// var isUpcoming: Bool { +// // Items due after the next 14 days (14*24 hours from now) +// let calendar = Calendar.current +// let nextFourteenthDay = calendar.date(byAdding: .day, value: 14, to: .today) ?? .distantPast +// return Date.compare(self, to: nextFourteenthDay) == .orderedDescending +// } +//} +// +//public struct CourseDateBlock: Identifiable { +// public let id: UUID = UUID() +// +// public let assignmentType: String? +// public let complete: Bool? +// public let date: Date +// public let dateType, description: String +// public let learnerHasAccess: Bool +// public let link: String +// public let linkText: String? +// public let title: String +// public let extraInfo: String? +// public let firstComponentBlockID: String +// +// public var formattedDate: String { +// return date.dateToString(style: .shortWeekdayMonthDayYear) +// } +// +// public var isInPast: Bool { +// return date.isInPast +// } +// +// public var isToday: Bool { +// if dateType.isEmpty { +// return true +// } else { +// return date.isToday +// } +// } +// +// public var isInFuture: Bool { +// return date.isInFuture +// } +// +// public var isThisWeek: Bool { +// return date.isThisWeek +// } +// +// public var isNextWeek: Bool { +// return date.isNextWeek +// } +// +// public var isUpcoming: Bool { +// return date.isUpcoming +// } +// +// public var isAssignment: Bool { +// return BlockStatus.status(of: dateType) == .assignment +// } +// +// public var isVerifiedOnly: Bool { +// return !learnerHasAccess +// } +// +// public var isComplete: Bool { +// return complete ?? false +// } +// +// public var isLearnerAssignment: Bool { +// return learnerHasAccess && isAssignment +// } +// +// public var isPastDue: Bool { +// return !isComplete && (date < .today) +// } +// +// public var isUnreleased: Bool { +// return link.isEmpty +// } +// +// public var canShowLink: Bool { +// return !isUnreleased && isLearnerAssignment +// } +// +// public var isAvailable: Bool { +// return learnerHasAccess && (!isUnreleased || !isLearnerAssignment) +// } +// +// public var blockStatus: BlockStatus { +// if isComplete { +// return .completed +// } +// +// if !learnerHasAccess { +// return .verifiedOnly +// } +// +// if isAssignment { +// if isInPast { +// return isUnreleased ? .unreleased : .pastDue +// } else if isToday || isInFuture { +// return isUnreleased ? .unreleased : .dueNext +// } +// } +// +// return BlockStatus.status(of: dateType) +// } +// +// public var blockImage: ImageAsset? { +// if !learnerHasAccess { +// return CoreAssets.lockIcon +// } +// +// if isAssignment { +// return CoreAssets.assignmentIcon +// } +// +// switch blockStatus { +// case .courseStartDate, .courseEndDate: +// return CoreAssets.schoolCapIcon +// case .verifiedUpgradeDeadline, .verificationDeadlineDate: +// return CoreAssets.calendarIcon +// case .courseExpiredDate: +// return CoreAssets.lockWithWatchIcon +// case .certificateAvailbleDate: +// return CoreAssets.certificateIcon +// default: +// return CoreAssets.calendarIcon +// } +// } +//} +// +//public struct DatesBannerInfo { +// public let missedDeadlines, contentTypeGatingEnabled, missedGatedContent: Bool +// public let verifiedUpgradeLink: String? +// public let status: DataLayer.BannerInfoStatus? +// +// public init( +// missedDeadlines: Bool, +// contentTypeGatingEnabled: Bool, +// missedGatedContent: Bool, +// verifiedUpgradeLink: String?, +// status: DataLayer.BannerInfoStatus? +// ) { +// self.missedDeadlines = missedDeadlines +// self.contentTypeGatingEnabled = contentTypeGatingEnabled +// self.missedGatedContent = missedGatedContent +// self.verifiedUpgradeLink = verifiedUpgradeLink +// self.status = status +// } +//} +// +//public struct CourseDateBanner { +// public let datesBannerInfo: DatesBannerInfo +// public let hasEnded: Bool +// +// public init(datesBannerInfo: DatesBannerInfo, hasEnded: Bool) { +// self.datesBannerInfo = datesBannerInfo +// self.hasEnded = hasEnded +// } +//} +// +//public enum BlockStatus { +// case completed +// case pastDue +// case dueNext +// case unreleased +// case verifiedOnly +// case assignment +// case verifiedUpgradeDeadline +// case courseExpiredDate +// case verificationDeadlineDate +// case certificateAvailbleDate +// case courseStartDate +// case courseEndDate +// case event +// +// static func status(of type: String) -> BlockStatus { +// switch type { +// case "assignment-due-date": return .assignment +// case "verified-upgrade-deadline": return .verifiedUpgradeDeadline +// case "course-expired-date": return .courseExpiredDate +// case "verification-deadline-date": return .verificationDeadlineDate +// case "certificate-available-date": return .certificateAvailbleDate +// case "course-start-date": return .courseStartDate +// case "course-end-date": return .courseEndDate +// default: return .event +// } +// } +//} +// +//public enum CompletionStatus: String { +// case completed = "Completed" +// case pastDue = "Past Due" +// case today = "Today" +// case thisWeek = "This Week" +// case nextWeek = "Next Week" +// case upcoming = "Upcoming" +//} +// +//public extension Array { +// mutating func modifyForEach(_ body: (_ element: inout Element) -> Void) { +// for index in indices { +// modifyElement(atIndex: index) { body(&$0) } +// } +// } +// +// mutating func modifyElement(atIndex index: Index, _ modifyElement: (_ element: inout Element) -> Void) { +// var element = self[index] +// modifyElement(&element) +// self[index] = element +// } +//} diff --git a/Core/Core/Data/Model/Data_CourseDates.swift b/Core/Core/Data/Model/Data_CourseDates.swift index 35616b02..e17f767c 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 e4adf93e..d00441ee 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 5b1c6436..0543748f 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 7be0c84e..5a3f14ea 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 @@ -37,11 +37,31 @@ public extension Date { let formatter = RelativeDateTimeFormatter() formatter.locale = .current formatter.unitsStyle = .full - formatter.locale = Locale(identifier: "en_US_POSIX") - if description == Date().description { - return CoreLocalization.Date.justNow + + let currentDate = Date() + let sevenDaysAgo = Calendar.current.date(byAdding: .day, value: -7, to: currentDate)! + let sevenDaysAhead = Calendar.current.date(byAdding: .day, value: 7, to: currentDate)! + + if self >= sevenDaysAgo && self <= sevenDaysAhead { + if self.description == currentDate.description { + return CoreLocalization.Date.justNow + } else { + return formatter.localizedString(for: self, relativeTo: currentDate) + } } else { - return formatter.localizedString(for: self, relativeTo: Date()) + let specificFormatter = DateFormatter() + specificFormatter.dateFormat = "MMM d" + + let yearFormatter = DateFormatter() + yearFormatter.dateFormat = "yyyy" + let currentYear = yearFormatter.string(from: currentDate) + let dateYear = yearFormatter.string(from: self) + + if currentYear != dateYear { + specificFormatter.dateFormat = "MMM d, yyyy" + } + + return specificFormatter.string(from: self) } } @@ -100,29 +120,34 @@ public extension Date { return totalSeconds } - func dateToString(style: DateStringStyle) -> String { + func dateToString(style: DateStringStyle, useRelativeDates: Bool) -> 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() + } 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) diff --git a/Core/Core/View/Base/CourseCellView.swift b/Core/Core/View/Base/CourseCellView.swift index a996c338..35f0c5d6 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/Course/Course.xcodeproj/project.pbxproj b/Course/Course.xcodeproj/project.pbxproj index 53d0e243..f7303c0e 100644 --- a/Course/Course.xcodeproj/project.pbxproj +++ b/Course/Course.xcodeproj/project.pbxproj @@ -8,7 +8,7 @@ /* Begin PBXBuildFile section */ 02197DC52C1AFC0600CC8FF2 /* CalendarSyncStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EBC7542C19CFCF00BE182C /* CalendarSyncStatusView.swift */; }; - 022544102C4961E400EEC33F /* CourseDates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0225440F2C4961E400EEC33F /* CourseDates.swift */; }; + 022544102C4961E400EEC33F /* CourseDatesшкя2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0225440F2C4961E400EEC33F /* CourseDatesшкя2.swift */; }; 02280F5E294B4FDA0032823A /* CourseCoreModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 02280F5C294B4FDA0032823A /* CourseCoreModel.xcdatamodeld */; }; 02280F60294B50030032823A /* CoursePersistenceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02280F5F294B50030032823A /* CoursePersistenceProtocol.swift */; }; 022C64D829ACEC48000F532B /* HandoutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022C64D729ACEC48000F532B /* HandoutsView.swift */; }; @@ -109,7 +109,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 0225440F2C4961E400EEC33F /* CourseDates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDates.swift; sourceTree = ""; }; + 0225440F2C4961E400EEC33F /* CourseDatesшкя2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CourseDatesшкя2.swift"; sourceTree = ""; }; 02280F5D294B4FDA0032823A /* CourseCoreModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = CourseCoreModel.xcdatamodel; sourceTree = ""; }; 02280F5F294B50030032823A /* CoursePersistenceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoursePersistenceProtocol.swift; sourceTree = ""; }; 022C64D729ACEC48000F532B /* HandoutsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandoutsView.swift; sourceTree = ""; }; @@ -430,7 +430,7 @@ 02B6B3C228E1DCD100232911 /* CourseDetails.swift */, 0276D75C29DDA3F80004CDF8 /* ResumeBlock.swift */, 022C64E129ADEB83000F532B /* CourseUpdate.swift */, - 0225440F2C4961E400EEC33F /* CourseDates.swift */, + 0225440F2C4961E400EEC33F /* CourseDatesшкя2.swift */, ); path = Model; sourceTree = ""; @@ -888,7 +888,7 @@ 067B7B4F2BED339200D1768F /* PlayerDelegateProtocol.swift in Sources */, 0231124D28EDA804002588FB /* CourseUnitView.swift in Sources */, 027020FC28E7362100F54332 /* Data_CourseOutlineResponse.swift in Sources */, - 022544102C4961E400EEC33F /* CourseDates.swift in Sources */, + 022544102C4961E400EEC33F /* CourseDatesшкя2.swift in Sources */, 067B7B532BED339200D1768F /* PlayerServiceProtocol.swift in Sources */, BAD9CA2D2B2736BB00DE790A /* LessonLineProgressView.swift in Sources */, 02197DC52C1AFC0600CC8FF2 /* CalendarSyncStatusView.swift in Sources */, diff --git a/Course/Course/Data/CourseRepository.swift b/Course/Course/Data/CourseRepository.swift index e5d4106b..dc90d9bd 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/Course/Domain/Model/CourseDates.swift b/Course/Course/Domain/Model/CourseDates.swift deleted file mode 100644 index 0889af7b..00000000 --- a/Course/Course/Domain/Model/CourseDates.swift +++ /dev/null @@ -1,348 +0,0 @@ -// -// CourseDates.swift -// Core -// -// Created by  Stepanok Ivan on 05.06.2024. лже файл 2 -// - -import Foundation -import CryptoKit - -public struct CourseDates { - public let datesBannerInfo: DatesBannerInfo - public let courseDateBlocks: [CourseDateBlock] - public let hasEnded, learnerIsFullAccess: Bool - public let userTimezone: String? - - public var statusDatesBlocks: [CompletionStatus: [Date: [CourseDateBlock]]] { - var statusDatesBlocks: [CompletionStatus: [Date: [CourseDateBlock]]] = [:] - var statusBlocks: [CompletionStatus: [CourseDateBlock]] = [:] - - for block in courseDateBlocks { - let date = block.date - switch true { - case block.complete ?? false || block.blockStatus == .courseStartDate: - statusBlocks[.completed, default: []].append(block) - case date.isInPast: - statusBlocks[.pastDue, default: []].append(block) - case date.isToday: - if date < Date() { - statusBlocks[.pastDue, default: []].append(block) - } else { - statusBlocks[.today, default: []].append(block) - } - case date.isThisWeek: - statusBlocks[.thisWeek, default: []].append(block) - case date.isNextWeek: - statusBlocks[.nextWeek, default: []].append(block) - case date.isUpcoming: - statusBlocks[.upcoming, default: []].append(block) - default: - statusBlocks[.upcoming, default: []].append(block) - } - } - - for status in statusBlocks.keys { - let courseDateBlocks = statusBlocks[status] - var dateToCourseDateBlockDict: [Date: [CourseDateBlock]] = [:] - - for block in courseDateBlocks ?? [] { - let date = block.date - dateToCourseDateBlockDict[date, default: []].append(block) - } - statusDatesBlocks[status] = dateToCourseDateBlockDict - } - - return statusDatesBlocks - } - - public var dateBlocks: [Date: [CourseDateBlock]] { - return courseDateBlocks.reduce(into: [:]) { result, block in - let date = block.date - result[date, default: []].append(block) - } - } - - public init( - datesBannerInfo: DatesBannerInfo, - courseDateBlocks: [CourseDateBlock], - hasEnded: Bool, - learnerIsFullAccess: Bool, - userTimezone: String? - ) { - self.datesBannerInfo = datesBannerInfo - self.courseDateBlocks = courseDateBlocks - self.hasEnded = hasEnded - self.learnerIsFullAccess = learnerIsFullAccess - self.userTimezone = userTimezone - } - - public var checksum: String { - var combinedString = "" - for block in self.courseDateBlocks { - let assignmentType = block.assignmentType ?? "" - combinedString += assignmentType + block.firstComponentBlockID + block.date.description - } - - let checksumData = SHA256.hash(data: Data(combinedString.utf8)) - let checksumString = checksumData.map { String(format: "%02hhx", $0) }.joined() - return checksumString - } -} - -public extension Date { - static var today: Date { - return Calendar.current.startOfDay(for: Date()) - } - - static func compare(_ fromDate: Date, to toDate: Date) -> ComparisonResult { - if fromDate > toDate { - return .orderedDescending - } else if fromDate < toDate { - return .orderedAscending - } - return .orderedSame - } - - var isInPast: Bool { - return Date.compare(self, to: .today) == .orderedAscending - } - - var isToday: Bool { - let calendar = Calendar.current - let selfComponents = calendar.dateComponents([.year, .month, .day], from: self) - let todayComponents = calendar.dateComponents([.year, .month, .day], from: .today) - return selfComponents == todayComponents - } - - var isInFuture: Bool { - return Date.compare(self, to: .today) == .orderedDescending - } - - var isThisWeek: Bool { - // Items due within the next 7 days (7*24 hours from now) - let calendar = Calendar.current - let nextDay = calendar.date(byAdding: .day, value: 1, to: .today) ?? .distantPast - let nextSeventhDay = calendar.date(byAdding: .day, value: 8, to: .today) ?? .distantPast - return (nextDay...nextSeventhDay).contains(self) - } - - var isNextWeek: Bool { - // Items due within the next 14 days (14*24 hours from now) - let calendar = Calendar.current - let nextEighthDay = calendar.date(byAdding: .day, value: 8, to: .today) ?? .distantPast - let nextFourteenthDay = calendar.date(byAdding: .day, value: 15, to: .today) ?? .distantPast - return (nextEighthDay...nextFourteenthDay).contains(self) - } - - var isUpcoming: Bool { - // Items due after the next 14 days (14*24 hours from now) - let calendar = Calendar.current - let nextFourteenthDay = calendar.date(byAdding: .day, value: 14, to: .today) ?? .distantPast - return Date.compare(self, to: nextFourteenthDay) == .orderedDescending - } -} - -public struct CourseDateBlock: Identifiable { - public let id: UUID = UUID() - - public let assignmentType: String? - public let complete: Bool? - public let date: Date - public let dateType, description: String - public let learnerHasAccess: Bool - public let link: String - public let linkText: String? - public let title: String - public let extraInfo: String? - public let firstComponentBlockID: String - - public var formattedDate: String { - return date.dateToString(style: .shortWeekdayMonthDayYear) - } - - public var isInPast: Bool { - return date.isInPast - } - - public var isToday: Bool { - if dateType.isEmpty { - return true - } else { - return date.isToday - } - } - - public var isInFuture: Bool { - return date.isInFuture - } - - public var isThisWeek: Bool { - return date.isThisWeek - } - - public var isNextWeek: Bool { - return date.isNextWeek - } - - public var isUpcoming: Bool { - return date.isUpcoming - } - - public var isAssignment: Bool { - return BlockStatus.status(of: dateType) == .assignment - } - - public var isVerifiedOnly: Bool { - return !learnerHasAccess - } - - public var isComplete: Bool { - return complete ?? false - } - - public var isLearnerAssignment: Bool { - return learnerHasAccess && isAssignment - } - - public var isPastDue: Bool { - return !isComplete && (date < .today) - } - - public var isUnreleased: Bool { - return link.isEmpty - } - - public var canShowLink: Bool { - return !isUnreleased && isLearnerAssignment - } - - public var isAvailable: Bool { - return learnerHasAccess && (!isUnreleased || !isLearnerAssignment) - } - - public var blockStatus: BlockStatus { - if isComplete { - return .completed - } - - if !learnerHasAccess { - return .verifiedOnly - } - - if isAssignment { - if isInPast { - return isUnreleased ? .unreleased : .pastDue - } else if isToday || isInFuture { - return isUnreleased ? .unreleased : .dueNext - } - } - - return BlockStatus.status(of: dateType) - } - - public var blockImage: ImageAsset? { - if !learnerHasAccess { - return CoreAssets.lockIcon - } - - if isAssignment { - return CoreAssets.assignmentIcon - } - - switch blockStatus { - case .courseStartDate, .courseEndDate: - return CoreAssets.schoolCapIcon - case .verifiedUpgradeDeadline, .verificationDeadlineDate: - return CoreAssets.calendarIcon - case .courseExpiredDate: - return CoreAssets.lockWithWatchIcon - case .certificateAvailbleDate: - return CoreAssets.certificateIcon - default: - return CoreAssets.calendarIcon - } - } -} - -public struct DatesBannerInfo { - public let missedDeadlines, contentTypeGatingEnabled, missedGatedContent: Bool - public let verifiedUpgradeLink: String? - public let status: DataLayer.BannerInfoStatus? - - public init( - missedDeadlines: Bool, - contentTypeGatingEnabled: Bool, - missedGatedContent: Bool, - verifiedUpgradeLink: String?, - status: DataLayer.BannerInfoStatus? - ) { - self.missedDeadlines = missedDeadlines - self.contentTypeGatingEnabled = contentTypeGatingEnabled - self.missedGatedContent = missedGatedContent - self.verifiedUpgradeLink = verifiedUpgradeLink - self.status = status - } -} - -public struct CourseDateBanner { - public let datesBannerInfo: DatesBannerInfo - public let hasEnded: Bool - - public init(datesBannerInfo: DatesBannerInfo, hasEnded: Bool) { - self.datesBannerInfo = datesBannerInfo - self.hasEnded = hasEnded - } -} - -public enum BlockStatus { - case completed - case pastDue - case dueNext - case unreleased - case verifiedOnly - case assignment - case verifiedUpgradeDeadline - case courseExpiredDate - case verificationDeadlineDate - case certificateAvailbleDate - case courseStartDate - case courseEndDate - case event - - static func status(of type: String) -> BlockStatus { - switch type { - case "assignment-due-date": return .assignment - case "verified-upgrade-deadline": return .verifiedUpgradeDeadline - case "course-expired-date": return .courseExpiredDate - case "verification-deadline-date": return .verificationDeadlineDate - case "certificate-available-date": return .certificateAvailbleDate - case "course-start-date": return .courseStartDate - case "course-end-date": return .courseEndDate - default: return .event - } - } -} - -public enum CompletionStatus: String { - case completed = "Completed" - case pastDue = "Past Due" - case today = "Today" - case thisWeek = "This Week" - case nextWeek = "Next Week" - case upcoming = "Upcoming" -} - -public extension Array { - mutating func modifyForEach(_ body: (_ element: inout Element) -> Void) { - for index in indices { - modifyElement(atIndex: index) { body(&$0) } - } - } - - mutating func modifyElement(atIndex index: Index, _ modifyElement: (_ element: inout Element) -> Void) { - var element = self[index] - modifyElement(&element) - self[index] = element - } -} diff --git "a/Course/Course/Domain/Model/CourseDates\321\210\320\272\321\2172.swift" "b/Course/Course/Domain/Model/CourseDates\321\210\320\272\321\2172.swift" new file mode 100644 index 00000000..aa595855 --- /dev/null +++ "b/Course/Course/Domain/Model/CourseDates\321\210\320\272\321\2172.swift" @@ -0,0 +1,349 @@ +//// +//// CourseDates.swift +//// Core +//// +//// Created by  Stepanok Ivan on 05.06.2024. лже файл 2 +//// +// +//import Foundation +//import CryptoKit +// +//public struct CourseDates { +// public let datesBannerInfo: DatesBannerInfo +// public let courseDateBlocks: [CourseDateBlock] +// public let hasEnded, learnerIsFullAccess: Bool +// public let userTimezone: String? +// +// public var statusDatesBlocks: [CompletionStatus: [Date: [CourseDateBlock]]] { +// var statusDatesBlocks: [CompletionStatus: [Date: [CourseDateBlock]]] = [:] +// var statusBlocks: [CompletionStatus: [CourseDateBlock]] = [:] +// +// for block in courseDateBlocks { +// let date = block.date +// switch true { +// case block.complete ?? false || block.blockStatus == .courseStartDate: +// statusBlocks[.completed, default: []].append(block) +// case date.isInPast: +// statusBlocks[.pastDue, default: []].append(block) +// case date.isToday: +// if date < Date() { +// statusBlocks[.pastDue, default: []].append(block) +// } else { +// statusBlocks[.today, default: []].append(block) +// } +// case date.isThisWeek: +// statusBlocks[.thisWeek, default: []].append(block) +// case date.isNextWeek: +// statusBlocks[.nextWeek, default: []].append(block) +// case date.isUpcoming: +// statusBlocks[.upcoming, default: []].append(block) +// default: +// statusBlocks[.upcoming, default: []].append(block) +// } +// } +// +// for status in statusBlocks.keys { +// let courseDateBlocks = statusBlocks[status] +// var dateToCourseDateBlockDict: [Date: [CourseDateBlock]] = [:] +// +// for block in courseDateBlocks ?? [] { +// let date = block.date +// dateToCourseDateBlockDict[date, default: []].append(block) +// } +// statusDatesBlocks[status] = dateToCourseDateBlockDict +// } +// +// return statusDatesBlocks +// } +// +// public var dateBlocks: [Date: [CourseDateBlock]] { +// return courseDateBlocks.reduce(into: [:]) { result, block in +// let date = block.date +// result[date, default: []].append(block) +// } +// } +// +// public init( +// datesBannerInfo: DatesBannerInfo, +// courseDateBlocks: [CourseDateBlock], +// hasEnded: Bool, +// learnerIsFullAccess: Bool, +// userTimezone: String? +// ) { +// self.datesBannerInfo = datesBannerInfo +// self.courseDateBlocks = courseDateBlocks +// self.hasEnded = hasEnded +// self.learnerIsFullAccess = learnerIsFullAccess +// self.userTimezone = userTimezone +// } +// +// public var checksum: String { +// var combinedString = "" +// for block in self.courseDateBlocks { +// let assignmentType = block.assignmentType ?? "" +// combinedString += assignmentType + block.firstComponentBlockID + block.date.description +// } +// +// let checksumData = SHA256.hash(data: Data(combinedString.utf8)) +// let checksumString = checksumData.map { String(format: "%02hhx", $0) }.joined() +// return checksumString +// } +//} +// +//public extension Date { +// static var today: Date { +// return Calendar.current.startOfDay(for: Date()) +// } +// +// static func compare(_ fromDate: Date, to toDate: Date) -> ComparisonResult { +// if fromDate > toDate { +// return .orderedDescending +// } else if fromDate < toDate { +// return .orderedAscending +// } +// return .orderedSame +// } +// +// var isInPast: Bool { +// return Date.compare(self, to: .today) == .orderedAscending +// } +// +// var isToday: Bool { +// let calendar = Calendar.current +// let selfComponents = calendar.dateComponents([.year, .month, .day], from: self) +// let todayComponents = calendar.dateComponents([.year, .month, .day], from: .today) +// return selfComponents == todayComponents +// } +// +// var isInFuture: Bool { +// return Date.compare(self, to: .today) == .orderedDescending +// } +// +// var isThisWeek: Bool { +// // Items due within the next 7 days (7*24 hours from now) +// let calendar = Calendar.current +// let nextDay = calendar.date(byAdding: .day, value: 1, to: .today) ?? .distantPast +// let nextSeventhDay = calendar.date(byAdding: .day, value: 8, to: .today) ?? .distantPast +// return (nextDay...nextSeventhDay).contains(self) +// } +// +// var isNextWeek: Bool { +// // Items due within the next 14 days (14*24 hours from now) +// let calendar = Calendar.current +// let nextEighthDay = calendar.date(byAdding: .day, value: 8, to: .today) ?? .distantPast +// let nextFourteenthDay = calendar.date(byAdding: .day, value: 15, to: .today) ?? .distantPast +// return (nextEighthDay...nextFourteenthDay).contains(self) +// } +// +// var isUpcoming: Bool { +// // Items due after the next 14 days (14*24 hours from now) +// let calendar = Calendar.current +// let nextFourteenthDay = calendar.date(byAdding: .day, value: 14, to: .today) ?? .distantPast +// return Date.compare(self, to: nextFourteenthDay) == .orderedDescending +// } +//} +// +//public struct CourseDateBlock: Identifiable { +// public let id: UUID = UUID() +// +// let assignmentType: String? +// let complete: Bool? +// let date: Date +// let dateType, description: String +// let learnerHasAccess: Bool +// let link: String +// let linkText: String? +// let title: String +// let extraInfo: String? +// let firstComponentBlockID: String +// let useRelativeDates: Bool +// +// var formattedDate: String { +// return date.dateToString(style: .shortWeekdayMonthDayYear, useRelativeDates: useRelativeDates) +// } +// +// public var isInPast: Bool { +// return date.isInPast +// } +// +// public var isToday: Bool { +// if dateType.isEmpty { +// return true +// } else { +// return date.isToday +// } +// } +// +// public var isInFuture: Bool { +// return date.isInFuture +// } +// +// public var isThisWeek: Bool { +// return date.isThisWeek +// } +// +// public var isNextWeek: Bool { +// return date.isNextWeek +// } +// +// public var isUpcoming: Bool { +// return date.isUpcoming +// } +// +// public var isAssignment: Bool { +// return BlockStatus.status(of: dateType) == .assignment +// } +// +// public var isVerifiedOnly: Bool { +// return !learnerHasAccess +// } +// +// public var isComplete: Bool { +// return complete ?? false +// } +// +// public var isLearnerAssignment: Bool { +// return learnerHasAccess && isAssignment +// } +// +// public var isPastDue: Bool { +// return !isComplete && (date < .today) +// } +// +// public var isUnreleased: Bool { +// return link.isEmpty +// } +// +// public var canShowLink: Bool { +// return !isUnreleased && isLearnerAssignment +// } +// +// public var isAvailable: Bool { +// return learnerHasAccess && (!isUnreleased || !isLearnerAssignment) +// } +// +// public var blockStatus: BlockStatus { +// if isComplete { +// return .completed +// } +// +// if !learnerHasAccess { +// return .verifiedOnly +// } +// +// if isAssignment { +// if isInPast { +// return isUnreleased ? .unreleased : .pastDue +// } else if isToday || isInFuture { +// return isUnreleased ? .unreleased : .dueNext +// } +// } +// +// return BlockStatus.status(of: dateType) +// } +// +// public var blockImage: ImageAsset? { +// if !learnerHasAccess { +// return CoreAssets.lockIcon +// } +// +// if isAssignment { +// return CoreAssets.assignmentIcon +// } +// +// switch blockStatus { +// case .courseStartDate, .courseEndDate: +// return CoreAssets.schoolCapIcon +// case .verifiedUpgradeDeadline, .verificationDeadlineDate: +// return CoreAssets.calendarIcon +// case .courseExpiredDate: +// return CoreAssets.lockWithWatchIcon +// case .certificateAvailbleDate: +// return CoreAssets.certificateIcon +// default: +// return CoreAssets.calendarIcon +// } +// } +//} +// +//public struct DatesBannerInfo { +// public let missedDeadlines, contentTypeGatingEnabled, missedGatedContent: Bool +// public let verifiedUpgradeLink: String? +// public let status: DataLayer.BannerInfoStatus? +// +// public init( +// missedDeadlines: Bool, +// contentTypeGatingEnabled: Bool, +// missedGatedContent: Bool, +// verifiedUpgradeLink: String?, +// status: DataLayer.BannerInfoStatus? +// ) { +// self.missedDeadlines = missedDeadlines +// self.contentTypeGatingEnabled = contentTypeGatingEnabled +// self.missedGatedContent = missedGatedContent +// self.verifiedUpgradeLink = verifiedUpgradeLink +// self.status = status +// } +//} +// +//public struct CourseDateBanner { +// public let datesBannerInfo: DatesBannerInfo +// public let hasEnded: Bool +// +// public init(datesBannerInfo: DatesBannerInfo, hasEnded: Bool) { +// self.datesBannerInfo = datesBannerInfo +// self.hasEnded = hasEnded +// } +//} +// +//public enum BlockStatus { +// case completed +// case pastDue +// case dueNext +// case unreleased +// case verifiedOnly +// case assignment +// case verifiedUpgradeDeadline +// case courseExpiredDate +// case verificationDeadlineDate +// case certificateAvailbleDate +// case courseStartDate +// case courseEndDate +// case event +// +// static func status(of type: String) -> BlockStatus { +// switch type { +// case "assignment-due-date": return .assignment +// case "verified-upgrade-deadline": return .verifiedUpgradeDeadline +// case "course-expired-date": return .courseExpiredDate +// case "verification-deadline-date": return .verificationDeadlineDate +// case "certificate-available-date": return .certificateAvailbleDate +// case "course-start-date": return .courseStartDate +// case "course-end-date": return .courseEndDate +// default: return .event +// } +// } +//} +// +//public enum CompletionStatus: String { +// case completed = "Completed" +// case pastDue = "Past Due" +// case today = "Today" +// case thisWeek = "This Week" +// case nextWeek = "Next Week" +// case upcoming = "Upcoming" +//} +// +//public extension Array { +// mutating func modifyForEach(_ body: (_ element: inout Element) -> Void) { +// for index in indices { +// modifyElement(atIndex: index) { body(&$0) } +// } +// } +// +// mutating func modifyElement(atIndex index: Index, _ modifyElement: (_ element: inout Element) -> Void) { +// var element = self[index] +// modifyElement(&element) +// self[index] = element +// } +//} diff --git a/Dashboard/Dashboard/Presentation/AllCoursesView.swift b/Dashboard/Dashboard/Presentation/AllCoursesView.swift index 332ae13c..af2743cf 100644 --- a/Dashboard/Dashboard/Presentation/AllCoursesView.swift +++ b/Dashboard/Dashboard/Presentation/AllCoursesView.swift @@ -88,7 +88,8 @@ public struct AllCoursesView: View { courseStartDate: course.courseStart, courseEndDate: course.courseEnd, hasAccess: course.hasAccess, - showProgress: true + showProgress: true, + useRelativeDates: viewModel.storage.useRelativeDates ).padding(8) }) .accessibilityIdentifier("course_item") @@ -196,7 +197,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 750c0936..439f329f 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 2c93c7d3..e493c00d 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 fc18526a..e2d5e24e 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 @@ -125,7 +128,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 +238,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) } @@ -267,7 +270,10 @@ struct PrimaryCardView_Previews: PreviewProvider { progressPossible: 45, canResume: true, resumeTitle: "Course Chapter 1", - assignmentAction: {_ in }, + useRelativeDates: true, + assignmentAction: { + _ in + }, openCourseAction: {}, resumeAction: {} ) diff --git a/Dashboard/Dashboard/Presentation/ListDashboardView.swift b/Dashboard/Dashboard/Presentation/ListDashboardView.swift index 90a3e5eb..ea8ab1b4 100644 --- a/Dashboard/Dashboard/Presentation/ListDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/ListDashboardView.swift @@ -61,7 +61,8 @@ public struct ListDashboardView: View { model: course, type: .dashboard, index: index, - cellsCount: viewModel.courses.count + cellsCount: viewModel.courses.count, + useRelativeDates: viewModel.storage.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 f962824e..112865e8 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 7ac04111..f70d0555 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift @@ -74,7 +74,8 @@ public struct PrimaryCourseDashboardView: View { progressEarned: primary.progressEarned, progressPossible: primary.progressPossible, canResume: primary.lastVisitedBlockID != nil, - resumeTitle: primary.resumeTitle, + resumeTitle: primary.resumeTitle, + useRelativeDates: viewModel.storage.useRelativeDates, assignmentAction: { lastVisitedBlockID in router.showCourseScreens( courseID: primary.courseID, @@ -228,7 +229,8 @@ public struct PrimaryCourseDashboardView: View { courseStartDate: nil, courseEndDate: nil, hasAccess: course.hasAccess, - showProgress: false + showProgress: false, + useRelativeDates: viewModel.storage.useRelativeDates ).frame(width: idiom == .pad ? nil : 120) } ) @@ -330,7 +332,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 60ffe9c0..f1a74f77 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/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift index b8d9aa86..1e82fc70 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift @@ -111,7 +111,8 @@ public struct DiscoveryView: View { model: course, type: .discovery, index: index, - cellsCount: viewModel.courses.count + cellsCount: viewModel.courses.count, + useRelativeDates: viewModel.storage.useRelativeDates ).padding(.horizontal, 24) .onAppear { Task { diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryViewModel.swift b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryViewModel.swift index 8f64f458..9c091f84 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 99d7ecea..fbc68bdb 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift @@ -112,10 +112,13 @@ public struct SearchView: View { let searchResults = viewModel.searchResults.enumerated() 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: viewModel.storage.useRelativeDates + ) .padding(.horizontal, 24) .onAppear { Task { @@ -219,7 +222,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 8f0c6ff1..76f3ea13 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 e1596add..3aa8e939 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 4b33833a..f28e2a53 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 4a955aa2..eacf0010 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 7d8c8b15..f4c56c10 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 3594f26c..3fce895d 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 d4dab646..d03a252d 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: { @@ -104,7 +106,9 @@ public struct ResponsesView: View { ) { index, comment in CommentCell( comment: comment, - addCommentAvailable: false, leftLineEnabled: true, + addCommentAvailable: false, + useRelativeDates: viewModel.storage.useRelativeDates, + leftLineEnabled: true, onAvatarTap: { username in viewModel.router.showUserDetails(username: username) }, @@ -237,7 +241,8 @@ struct ResponsesView_Previews: PreviewProvider { let viewModel = ResponsesViewModel( interactor: DiscussionInteractor(repository: DiscussionRepositoryMock()), router: DiscussionRouterMock(), - config: ConfigMock(), + 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 c8992fd8..b7369726 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 b764ed0d..3d393da6 100644 --- a/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift +++ b/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift @@ -43,7 +43,8 @@ public struct ThreadView: View { if let comments = viewModel.postComments { ParentCommentView( comments: comments, - isThread: true, + isThread: true, + useRelativeDates: viewModel.storage.useRelativeDates, onAvatarTap: { username in viewModel.router.showUserDetails(username: username) }, @@ -96,7 +97,8 @@ public struct ThreadView: View { ForEach(Array(comments.comments.enumerated()), id: \.offset) { index, comment in CommentCell( comment: comment, - addCommentAvailable: true, + addCommentAvailable: true, + useRelativeDates: viewModel.storage.useRelativeDates, onAvatarTap: { username in viewModel.router.showUserDetails(username: username) }, @@ -281,10 +283,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 2fb75b60..452ea98f 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 c9fd88dd..440666e8 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 95afa250..83b7b274 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 9664d1f1..1ab2fc0f 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 b1676c70..8115ea0f 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/OpenEdX/DI/ScreenAssembly.swift b/OpenEdX/DI/ScreenAssembly.swift index f76ef780..578e94df 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 5f8b8f92..e9c23825 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,15 @@ public class AppStorage: CoreStorage, ProfileStorage, WhatsNewStorage, CourseSto } } + public var useRelativeDates: Bool { + get { + 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 +367,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/Data/ProfileRepository.swift b/Profile/Profile/Data/ProfileRepository.swift index 2bd35cdf..a593f738 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 8110ada0..88e8fe48 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 88690960..b93bf4fe 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 + viewModel.relativeDatesToggle } .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 560d1897..cdf3002d 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 @@ -107,6 +106,39 @@ public class DatesAndCalendarViewModel: ObservableObject { return avaliable } + private var useRelativeDatesBinding: Binding { + Binding( + get: { self.profileStorage.useRelativeDates }, + set: { self.profileStorage.useRelativeDates = $0 } + ) + } + + // MARK: - Options Toggle + var relativeDatesToggle: some View { + VStack(alignment: .leading, spacing: 10) { + Text(ProfileLocalization.Options.title) + .font(Theme.Fonts.labelLarge) + .foregroundColor(Theme.Colors.textPrimary) + HStack(spacing: 16) { + Toggle("", isOn: useRelativeDatesBinding) + .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(.top, 14) + .padding(.horizontal, 24) + .frame(minWidth: 0, + maxWidth: .infinity, + alignment: .leading) + .accessibilityIdentifier("relative_dates_toggle") + } + // MARK: - Lifecycle Functions func loadCalendarOptions() { @@ -187,8 +219,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/Models/CalendarSettings.swift b/Profile/Profile/Presentation/DatesAndCalendar/Models/CalendarSettings.swift index 7c1970d0..b4b63bb4 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 82414e26..dcd886b6 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 + viewModel.relativeDatesToggle } .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 From 757f8e352fc3b2e49305e274256de85201d91537 Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Fri, 19 Jul 2024 11:28:14 +0300 Subject: [PATCH 3/8] fix: address feedback --- Core/Core.xcodeproj/project.pbxproj | 4 - ...CourseDates\321\210\320\272\321\217.swift" | 348 ----------------- Core/Core/Extensions/DateExtension.swift | 41 +- Course/Course.xcodeproj/project.pbxproj | 4 - ...ourseDates\321\210\320\272\321\2172.swift" | 349 ------------------ 5 files changed, 3 insertions(+), 743 deletions(-) delete mode 100644 "Core/Core/Data/Model/CourseDates\321\210\320\272\321\217.swift" delete mode 100644 "Course/Course/Domain/Model/CourseDates\321\210\320\272\321\2172.swift" diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index bfcd1d58..0b440083 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -15,7 +15,6 @@ 021D925028DC89D100ACC565 /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021D924F28DC89D100ACC565 /* UserProfile.swift */; }; 021D925728DCF12900ACC565 /* AlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021D925628DCF12900ACC565 /* AlertView.swift */; }; 022020462C11BB2200D15795 /* Data_CourseDates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022020452C11BB2200D15795 /* Data_CourseDates.swift */; }; - 0225440E2C4961A300EEC33F /* CourseDatesшкя.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0225440D2C4961A300EEC33F /* CourseDatesшкя.swift */; }; 02280F5B294B4E6F0032823A /* Connectivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02280F5A294B4E6F0032823A /* Connectivity.swift */; }; 02284C182A3B1AE00007117F /* UIApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02284C172A3B1AE00007117F /* UIApplicationExtension.swift */; }; 02286D162C106393005EEC8D /* CourseDates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02286D152C106393005EEC8D /* CourseDates.swift */; }; @@ -213,7 +212,6 @@ 021D924F28DC89D100ACC565 /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = ""; }; 021D925628DCF12900ACC565 /* AlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertView.swift; sourceTree = ""; }; 022020452C11BB2200D15795 /* Data_CourseDates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_CourseDates.swift; sourceTree = ""; }; - 0225440D2C4961A300EEC33F /* CourseDatesшкя.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CourseDatesшкя.swift"; sourceTree = ""; }; 02280F5A294B4E6F0032823A /* Connectivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connectivity.swift; sourceTree = ""; }; 02284C172A3B1AE00007117F /* UIApplicationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationExtension.swift; sourceTree = ""; }; 02286D152C106393005EEC8D /* CourseDates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDates.swift; sourceTree = ""; }; @@ -627,7 +625,6 @@ 07E0939E2B308D2800F1E4B2 /* Data_Certificate.swift */, 02A8C5802C05DBB4004B91FF /* Data_EnrollmentsStatus.swift */, 022020452C11BB2200D15795 /* Data_CourseDates.swift */, - 0225440D2C4961A300EEC33F /* CourseDatesшкя.swift */, ); path = Model; sourceTree = ""; @@ -1231,7 +1228,6 @@ E0D5861C2B2FF85B009B4BA7 /* RawStringExtactable.swift in Sources */, 02EBC7592C19DE1100BE182C /* CalendarManagerProtocol.swift in Sources */, 027BD3BD2909478B00392132 /* UIView+EnclosingScrollView.swift in Sources */, - 0225440E2C4961A300EEC33F /* CourseDatesшкя.swift in Sources */, BA8FA6682AD59A5700EA029A /* SocialAuthButton.swift in Sources */, 02D400612B0678190029D168 /* SKStoreReviewControllerExtension.swift in Sources */, 02A4833C29B8C57800D33F33 /* DownloadView.swift in Sources */, diff --git "a/Core/Core/Data/Model/CourseDates\321\210\320\272\321\217.swift" "b/Core/Core/Data/Model/CourseDates\321\210\320\272\321\217.swift" deleted file mode 100644 index 10f40aac..00000000 --- "a/Core/Core/Data/Model/CourseDates\321\210\320\272\321\217.swift" +++ /dev/null @@ -1,348 +0,0 @@ -//// -//// CourseDates.swift -//// Core -//// -//// Created by  Stepanok Ivan on 05.06.2024. лже файл -//// -// -//import Foundation -//import CryptoKit -// -//public struct CourseDates { -// public let datesBannerInfo: DatesBannerInfo -// public let courseDateBlocks: [CourseDateBlock] -// public let hasEnded, learnerIsFullAccess: Bool -// public let userTimezone: String? -// -// public var statusDatesBlocks: [CompletionStatus: [Date: [CourseDateBlock]]] { -// var statusDatesBlocks: [CompletionStatus: [Date: [CourseDateBlock]]] = [:] -// var statusBlocks: [CompletionStatus: [CourseDateBlock]] = [:] -// -// for block in courseDateBlocks { -// let date = block.date -// switch true { -// case block.complete ?? false || block.blockStatus == .courseStartDate: -// statusBlocks[.completed, default: []].append(block) -// case date.isInPast: -// statusBlocks[.pastDue, default: []].append(block) -// case date.isToday: -// if date < Date() { -// statusBlocks[.pastDue, default: []].append(block) -// } else { -// statusBlocks[.today, default: []].append(block) -// } -// case date.isThisWeek: -// statusBlocks[.thisWeek, default: []].append(block) -// case date.isNextWeek: -// statusBlocks[.nextWeek, default: []].append(block) -// case date.isUpcoming: -// statusBlocks[.upcoming, default: []].append(block) -// default: -// statusBlocks[.upcoming, default: []].append(block) -// } -// } -// -// for status in statusBlocks.keys { -// let courseDateBlocks = statusBlocks[status] -// var dateToCourseDateBlockDict: [Date: [CourseDateBlock]] = [:] -// -// for block in courseDateBlocks ?? [] { -// let date = block.date -// dateToCourseDateBlockDict[date, default: []].append(block) -// } -// statusDatesBlocks[status] = dateToCourseDateBlockDict -// } -// -// return statusDatesBlocks -// } -// -// public var dateBlocks: [Date: [CourseDateBlock]] { -// return courseDateBlocks.reduce(into: [:]) { result, block in -// let date = block.date -// result[date, default: []].append(block) -// } -// } -// -// public init( -// datesBannerInfo: DatesBannerInfo, -// courseDateBlocks: [CourseDateBlock], -// hasEnded: Bool, -// learnerIsFullAccess: Bool, -// userTimezone: String? -// ) { -// self.datesBannerInfo = datesBannerInfo -// self.courseDateBlocks = courseDateBlocks -// self.hasEnded = hasEnded -// self.learnerIsFullAccess = learnerIsFullAccess -// self.userTimezone = userTimezone -// } -// -// public var checksum: String { -// var combinedString = "" -// for block in self.courseDateBlocks { -// let assignmentType = block.assignmentType ?? "" -// combinedString += assignmentType + block.firstComponentBlockID + block.date.description -// } -// -// let checksumData = SHA256.hash(data: Data(combinedString.utf8)) -// let checksumString = checksumData.map { String(format: "%02hhx", $0) }.joined() -// return checksumString -// } -//} -// -//public extension Date { -// static var today: Date { -// return Calendar.current.startOfDay(for: Date()) -// } -// -// static func compare(_ fromDate: Date, to toDate: Date) -> ComparisonResult { -// if fromDate > toDate { -// return .orderedDescending -// } else if fromDate < toDate { -// return .orderedAscending -// } -// return .orderedSame -// } -// -// var isInPast: Bool { -// return Date.compare(self, to: .today) == .orderedAscending -// } -// -// var isToday: Bool { -// let calendar = Calendar.current -// let selfComponents = calendar.dateComponents([.year, .month, .day], from: self) -// let todayComponents = calendar.dateComponents([.year, .month, .day], from: .today) -// return selfComponents == todayComponents -// } -// -// var isInFuture: Bool { -// return Date.compare(self, to: .today) == .orderedDescending -// } -// -// var isThisWeek: Bool { -// // Items due within the next 7 days (7*24 hours from now) -// let calendar = Calendar.current -// let nextDay = calendar.date(byAdding: .day, value: 1, to: .today) ?? .distantPast -// let nextSeventhDay = calendar.date(byAdding: .day, value: 8, to: .today) ?? .distantPast -// return (nextDay...nextSeventhDay).contains(self) -// } -// -// var isNextWeek: Bool { -// // Items due within the next 14 days (14*24 hours from now) -// let calendar = Calendar.current -// let nextEighthDay = calendar.date(byAdding: .day, value: 8, to: .today) ?? .distantPast -// let nextFourteenthDay = calendar.date(byAdding: .day, value: 15, to: .today) ?? .distantPast -// return (nextEighthDay...nextFourteenthDay).contains(self) -// } -// -// var isUpcoming: Bool { -// // Items due after the next 14 days (14*24 hours from now) -// let calendar = Calendar.current -// let nextFourteenthDay = calendar.date(byAdding: .day, value: 14, to: .today) ?? .distantPast -// return Date.compare(self, to: nextFourteenthDay) == .orderedDescending -// } -//} -// -//public struct CourseDateBlock: Identifiable { -// public let id: UUID = UUID() -// -// public let assignmentType: String? -// public let complete: Bool? -// public let date: Date -// public let dateType, description: String -// public let learnerHasAccess: Bool -// public let link: String -// public let linkText: String? -// public let title: String -// public let extraInfo: String? -// public let firstComponentBlockID: String -// -// public var formattedDate: String { -// return date.dateToString(style: .shortWeekdayMonthDayYear) -// } -// -// public var isInPast: Bool { -// return date.isInPast -// } -// -// public var isToday: Bool { -// if dateType.isEmpty { -// return true -// } else { -// return date.isToday -// } -// } -// -// public var isInFuture: Bool { -// return date.isInFuture -// } -// -// public var isThisWeek: Bool { -// return date.isThisWeek -// } -// -// public var isNextWeek: Bool { -// return date.isNextWeek -// } -// -// public var isUpcoming: Bool { -// return date.isUpcoming -// } -// -// public var isAssignment: Bool { -// return BlockStatus.status(of: dateType) == .assignment -// } -// -// public var isVerifiedOnly: Bool { -// return !learnerHasAccess -// } -// -// public var isComplete: Bool { -// return complete ?? false -// } -// -// public var isLearnerAssignment: Bool { -// return learnerHasAccess && isAssignment -// } -// -// public var isPastDue: Bool { -// return !isComplete && (date < .today) -// } -// -// public var isUnreleased: Bool { -// return link.isEmpty -// } -// -// public var canShowLink: Bool { -// return !isUnreleased && isLearnerAssignment -// } -// -// public var isAvailable: Bool { -// return learnerHasAccess && (!isUnreleased || !isLearnerAssignment) -// } -// -// public var blockStatus: BlockStatus { -// if isComplete { -// return .completed -// } -// -// if !learnerHasAccess { -// return .verifiedOnly -// } -// -// if isAssignment { -// if isInPast { -// return isUnreleased ? .unreleased : .pastDue -// } else if isToday || isInFuture { -// return isUnreleased ? .unreleased : .dueNext -// } -// } -// -// return BlockStatus.status(of: dateType) -// } -// -// public var blockImage: ImageAsset? { -// if !learnerHasAccess { -// return CoreAssets.lockIcon -// } -// -// if isAssignment { -// return CoreAssets.assignmentIcon -// } -// -// switch blockStatus { -// case .courseStartDate, .courseEndDate: -// return CoreAssets.schoolCapIcon -// case .verifiedUpgradeDeadline, .verificationDeadlineDate: -// return CoreAssets.calendarIcon -// case .courseExpiredDate: -// return CoreAssets.lockWithWatchIcon -// case .certificateAvailbleDate: -// return CoreAssets.certificateIcon -// default: -// return CoreAssets.calendarIcon -// } -// } -//} -// -//public struct DatesBannerInfo { -// public let missedDeadlines, contentTypeGatingEnabled, missedGatedContent: Bool -// public let verifiedUpgradeLink: String? -// public let status: DataLayer.BannerInfoStatus? -// -// public init( -// missedDeadlines: Bool, -// contentTypeGatingEnabled: Bool, -// missedGatedContent: Bool, -// verifiedUpgradeLink: String?, -// status: DataLayer.BannerInfoStatus? -// ) { -// self.missedDeadlines = missedDeadlines -// self.contentTypeGatingEnabled = contentTypeGatingEnabled -// self.missedGatedContent = missedGatedContent -// self.verifiedUpgradeLink = verifiedUpgradeLink -// self.status = status -// } -//} -// -//public struct CourseDateBanner { -// public let datesBannerInfo: DatesBannerInfo -// public let hasEnded: Bool -// -// public init(datesBannerInfo: DatesBannerInfo, hasEnded: Bool) { -// self.datesBannerInfo = datesBannerInfo -// self.hasEnded = hasEnded -// } -//} -// -//public enum BlockStatus { -// case completed -// case pastDue -// case dueNext -// case unreleased -// case verifiedOnly -// case assignment -// case verifiedUpgradeDeadline -// case courseExpiredDate -// case verificationDeadlineDate -// case certificateAvailbleDate -// case courseStartDate -// case courseEndDate -// case event -// -// static func status(of type: String) -> BlockStatus { -// switch type { -// case "assignment-due-date": return .assignment -// case "verified-upgrade-deadline": return .verifiedUpgradeDeadline -// case "course-expired-date": return .courseExpiredDate -// case "verification-deadline-date": return .verificationDeadlineDate -// case "certificate-available-date": return .certificateAvailbleDate -// case "course-start-date": return .courseStartDate -// case "course-end-date": return .courseEndDate -// default: return .event -// } -// } -//} -// -//public enum CompletionStatus: String { -// case completed = "Completed" -// case pastDue = "Past Due" -// case today = "Today" -// case thisWeek = "This Week" -// case nextWeek = "Next Week" -// case upcoming = "Upcoming" -//} -// -//public extension Array { -// mutating func modifyForEach(_ body: (_ element: inout Element) -> Void) { -// for index in indices { -// modifyElement(atIndex: index) { body(&$0) } -// } -// } -// -// mutating func modifyElement(atIndex index: Index, _ modifyElement: (_ element: inout Element) -> Void) { -// var element = self[index] -// modifyElement(&element) -// self[index] = element -// } -//} diff --git a/Core/Core/Extensions/DateExtension.swift b/Core/Core/Extensions/DateExtension.swift index 5a3f14ea..dbb8ed97 100644 --- a/Core/Core/Extensions/DateExtension.swift +++ b/Core/Core/Extensions/DateExtension.swift @@ -50,7 +50,7 @@ public extension Date { } } else { let specificFormatter = DateFormatter() - specificFormatter.dateFormat = "MMM d" + specificFormatter.dateFormat = "MMMM d" let yearFormatter = DateFormatter() yearFormatter.dateFormat = "yyyy" @@ -58,7 +58,7 @@ public extension Date { let dateYear = yearFormatter.string(from: self) if currentYear != dateYear { - specificFormatter.dateFormat = "MMM d, yyyy" + specificFormatter.dateFormat = "MMMM d, yyyy" } return specificFormatter.string(from: self) @@ -190,47 +190,12 @@ public extension 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/Course/Course.xcodeproj/project.pbxproj b/Course/Course.xcodeproj/project.pbxproj index f7303c0e..49b8c6e5 100644 --- a/Course/Course.xcodeproj/project.pbxproj +++ b/Course/Course.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ 02197DC52C1AFC0600CC8FF2 /* CalendarSyncStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EBC7542C19CFCF00BE182C /* CalendarSyncStatusView.swift */; }; - 022544102C4961E400EEC33F /* CourseDatesшкя2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0225440F2C4961E400EEC33F /* CourseDatesшкя2.swift */; }; 02280F5E294B4FDA0032823A /* CourseCoreModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 02280F5C294B4FDA0032823A /* CourseCoreModel.xcdatamodeld */; }; 02280F60294B50030032823A /* CoursePersistenceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02280F5F294B50030032823A /* CoursePersistenceProtocol.swift */; }; 022C64D829ACEC48000F532B /* HandoutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022C64D729ACEC48000F532B /* HandoutsView.swift */; }; @@ -109,7 +108,6 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 0225440F2C4961E400EEC33F /* CourseDatesшкя2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CourseDatesшкя2.swift"; sourceTree = ""; }; 02280F5D294B4FDA0032823A /* CourseCoreModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = CourseCoreModel.xcdatamodel; sourceTree = ""; }; 02280F5F294B50030032823A /* CoursePersistenceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoursePersistenceProtocol.swift; sourceTree = ""; }; 022C64D729ACEC48000F532B /* HandoutsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandoutsView.swift; sourceTree = ""; }; @@ -430,7 +428,6 @@ 02B6B3C228E1DCD100232911 /* CourseDetails.swift */, 0276D75C29DDA3F80004CDF8 /* ResumeBlock.swift */, 022C64E129ADEB83000F532B /* CourseUpdate.swift */, - 0225440F2C4961E400EEC33F /* CourseDatesшкя2.swift */, ); path = Model; sourceTree = ""; @@ -888,7 +885,6 @@ 067B7B4F2BED339200D1768F /* PlayerDelegateProtocol.swift in Sources */, 0231124D28EDA804002588FB /* CourseUnitView.swift in Sources */, 027020FC28E7362100F54332 /* Data_CourseOutlineResponse.swift in Sources */, - 022544102C4961E400EEC33F /* CourseDatesшкя2.swift in Sources */, 067B7B532BED339200D1768F /* PlayerServiceProtocol.swift in Sources */, BAD9CA2D2B2736BB00DE790A /* LessonLineProgressView.swift in Sources */, 02197DC52C1AFC0600CC8FF2 /* CalendarSyncStatusView.swift in Sources */, diff --git "a/Course/Course/Domain/Model/CourseDates\321\210\320\272\321\2172.swift" "b/Course/Course/Domain/Model/CourseDates\321\210\320\272\321\2172.swift" deleted file mode 100644 index aa595855..00000000 --- "a/Course/Course/Domain/Model/CourseDates\321\210\320\272\321\2172.swift" +++ /dev/null @@ -1,349 +0,0 @@ -//// -//// CourseDates.swift -//// Core -//// -//// Created by  Stepanok Ivan on 05.06.2024. лже файл 2 -//// -// -//import Foundation -//import CryptoKit -// -//public struct CourseDates { -// public let datesBannerInfo: DatesBannerInfo -// public let courseDateBlocks: [CourseDateBlock] -// public let hasEnded, learnerIsFullAccess: Bool -// public let userTimezone: String? -// -// public var statusDatesBlocks: [CompletionStatus: [Date: [CourseDateBlock]]] { -// var statusDatesBlocks: [CompletionStatus: [Date: [CourseDateBlock]]] = [:] -// var statusBlocks: [CompletionStatus: [CourseDateBlock]] = [:] -// -// for block in courseDateBlocks { -// let date = block.date -// switch true { -// case block.complete ?? false || block.blockStatus == .courseStartDate: -// statusBlocks[.completed, default: []].append(block) -// case date.isInPast: -// statusBlocks[.pastDue, default: []].append(block) -// case date.isToday: -// if date < Date() { -// statusBlocks[.pastDue, default: []].append(block) -// } else { -// statusBlocks[.today, default: []].append(block) -// } -// case date.isThisWeek: -// statusBlocks[.thisWeek, default: []].append(block) -// case date.isNextWeek: -// statusBlocks[.nextWeek, default: []].append(block) -// case date.isUpcoming: -// statusBlocks[.upcoming, default: []].append(block) -// default: -// statusBlocks[.upcoming, default: []].append(block) -// } -// } -// -// for status in statusBlocks.keys { -// let courseDateBlocks = statusBlocks[status] -// var dateToCourseDateBlockDict: [Date: [CourseDateBlock]] = [:] -// -// for block in courseDateBlocks ?? [] { -// let date = block.date -// dateToCourseDateBlockDict[date, default: []].append(block) -// } -// statusDatesBlocks[status] = dateToCourseDateBlockDict -// } -// -// return statusDatesBlocks -// } -// -// public var dateBlocks: [Date: [CourseDateBlock]] { -// return courseDateBlocks.reduce(into: [:]) { result, block in -// let date = block.date -// result[date, default: []].append(block) -// } -// } -// -// public init( -// datesBannerInfo: DatesBannerInfo, -// courseDateBlocks: [CourseDateBlock], -// hasEnded: Bool, -// learnerIsFullAccess: Bool, -// userTimezone: String? -// ) { -// self.datesBannerInfo = datesBannerInfo -// self.courseDateBlocks = courseDateBlocks -// self.hasEnded = hasEnded -// self.learnerIsFullAccess = learnerIsFullAccess -// self.userTimezone = userTimezone -// } -// -// public var checksum: String { -// var combinedString = "" -// for block in self.courseDateBlocks { -// let assignmentType = block.assignmentType ?? "" -// combinedString += assignmentType + block.firstComponentBlockID + block.date.description -// } -// -// let checksumData = SHA256.hash(data: Data(combinedString.utf8)) -// let checksumString = checksumData.map { String(format: "%02hhx", $0) }.joined() -// return checksumString -// } -//} -// -//public extension Date { -// static var today: Date { -// return Calendar.current.startOfDay(for: Date()) -// } -// -// static func compare(_ fromDate: Date, to toDate: Date) -> ComparisonResult { -// if fromDate > toDate { -// return .orderedDescending -// } else if fromDate < toDate { -// return .orderedAscending -// } -// return .orderedSame -// } -// -// var isInPast: Bool { -// return Date.compare(self, to: .today) == .orderedAscending -// } -// -// var isToday: Bool { -// let calendar = Calendar.current -// let selfComponents = calendar.dateComponents([.year, .month, .day], from: self) -// let todayComponents = calendar.dateComponents([.year, .month, .day], from: .today) -// return selfComponents == todayComponents -// } -// -// var isInFuture: Bool { -// return Date.compare(self, to: .today) == .orderedDescending -// } -// -// var isThisWeek: Bool { -// // Items due within the next 7 days (7*24 hours from now) -// let calendar = Calendar.current -// let nextDay = calendar.date(byAdding: .day, value: 1, to: .today) ?? .distantPast -// let nextSeventhDay = calendar.date(byAdding: .day, value: 8, to: .today) ?? .distantPast -// return (nextDay...nextSeventhDay).contains(self) -// } -// -// var isNextWeek: Bool { -// // Items due within the next 14 days (14*24 hours from now) -// let calendar = Calendar.current -// let nextEighthDay = calendar.date(byAdding: .day, value: 8, to: .today) ?? .distantPast -// let nextFourteenthDay = calendar.date(byAdding: .day, value: 15, to: .today) ?? .distantPast -// return (nextEighthDay...nextFourteenthDay).contains(self) -// } -// -// var isUpcoming: Bool { -// // Items due after the next 14 days (14*24 hours from now) -// let calendar = Calendar.current -// let nextFourteenthDay = calendar.date(byAdding: .day, value: 14, to: .today) ?? .distantPast -// return Date.compare(self, to: nextFourteenthDay) == .orderedDescending -// } -//} -// -//public struct CourseDateBlock: Identifiable { -// public let id: UUID = UUID() -// -// let assignmentType: String? -// let complete: Bool? -// let date: Date -// let dateType, description: String -// let learnerHasAccess: Bool -// let link: String -// let linkText: String? -// let title: String -// let extraInfo: String? -// let firstComponentBlockID: String -// let useRelativeDates: Bool -// -// var formattedDate: String { -// return date.dateToString(style: .shortWeekdayMonthDayYear, useRelativeDates: useRelativeDates) -// } -// -// public var isInPast: Bool { -// return date.isInPast -// } -// -// public var isToday: Bool { -// if dateType.isEmpty { -// return true -// } else { -// return date.isToday -// } -// } -// -// public var isInFuture: Bool { -// return date.isInFuture -// } -// -// public var isThisWeek: Bool { -// return date.isThisWeek -// } -// -// public var isNextWeek: Bool { -// return date.isNextWeek -// } -// -// public var isUpcoming: Bool { -// return date.isUpcoming -// } -// -// public var isAssignment: Bool { -// return BlockStatus.status(of: dateType) == .assignment -// } -// -// public var isVerifiedOnly: Bool { -// return !learnerHasAccess -// } -// -// public var isComplete: Bool { -// return complete ?? false -// } -// -// public var isLearnerAssignment: Bool { -// return learnerHasAccess && isAssignment -// } -// -// public var isPastDue: Bool { -// return !isComplete && (date < .today) -// } -// -// public var isUnreleased: Bool { -// return link.isEmpty -// } -// -// public var canShowLink: Bool { -// return !isUnreleased && isLearnerAssignment -// } -// -// public var isAvailable: Bool { -// return learnerHasAccess && (!isUnreleased || !isLearnerAssignment) -// } -// -// public var blockStatus: BlockStatus { -// if isComplete { -// return .completed -// } -// -// if !learnerHasAccess { -// return .verifiedOnly -// } -// -// if isAssignment { -// if isInPast { -// return isUnreleased ? .unreleased : .pastDue -// } else if isToday || isInFuture { -// return isUnreleased ? .unreleased : .dueNext -// } -// } -// -// return BlockStatus.status(of: dateType) -// } -// -// public var blockImage: ImageAsset? { -// if !learnerHasAccess { -// return CoreAssets.lockIcon -// } -// -// if isAssignment { -// return CoreAssets.assignmentIcon -// } -// -// switch blockStatus { -// case .courseStartDate, .courseEndDate: -// return CoreAssets.schoolCapIcon -// case .verifiedUpgradeDeadline, .verificationDeadlineDate: -// return CoreAssets.calendarIcon -// case .courseExpiredDate: -// return CoreAssets.lockWithWatchIcon -// case .certificateAvailbleDate: -// return CoreAssets.certificateIcon -// default: -// return CoreAssets.calendarIcon -// } -// } -//} -// -//public struct DatesBannerInfo { -// public let missedDeadlines, contentTypeGatingEnabled, missedGatedContent: Bool -// public let verifiedUpgradeLink: String? -// public let status: DataLayer.BannerInfoStatus? -// -// public init( -// missedDeadlines: Bool, -// contentTypeGatingEnabled: Bool, -// missedGatedContent: Bool, -// verifiedUpgradeLink: String?, -// status: DataLayer.BannerInfoStatus? -// ) { -// self.missedDeadlines = missedDeadlines -// self.contentTypeGatingEnabled = contentTypeGatingEnabled -// self.missedGatedContent = missedGatedContent -// self.verifiedUpgradeLink = verifiedUpgradeLink -// self.status = status -// } -//} -// -//public struct CourseDateBanner { -// public let datesBannerInfo: DatesBannerInfo -// public let hasEnded: Bool -// -// public init(datesBannerInfo: DatesBannerInfo, hasEnded: Bool) { -// self.datesBannerInfo = datesBannerInfo -// self.hasEnded = hasEnded -// } -//} -// -//public enum BlockStatus { -// case completed -// case pastDue -// case dueNext -// case unreleased -// case verifiedOnly -// case assignment -// case verifiedUpgradeDeadline -// case courseExpiredDate -// case verificationDeadlineDate -// case certificateAvailbleDate -// case courseStartDate -// case courseEndDate -// case event -// -// static func status(of type: String) -> BlockStatus { -// switch type { -// case "assignment-due-date": return .assignment -// case "verified-upgrade-deadline": return .verifiedUpgradeDeadline -// case "course-expired-date": return .courseExpiredDate -// case "verification-deadline-date": return .verificationDeadlineDate -// case "certificate-available-date": return .certificateAvailbleDate -// case "course-start-date": return .courseStartDate -// case "course-end-date": return .courseEndDate -// default: return .event -// } -// } -//} -// -//public enum CompletionStatus: String { -// case completed = "Completed" -// case pastDue = "Past Due" -// case today = "Today" -// case thisWeek = "This Week" -// case nextWeek = "Next Week" -// case upcoming = "Upcoming" -//} -// -//public extension Array { -// mutating func modifyForEach(_ body: (_ element: inout Element) -> Void) { -// for index in indices { -// modifyElement(atIndex: index) { body(&$0) } -// } -// } -// -// mutating func modifyElement(atIndex index: Index, _ modifyElement: (_ element: inout Element) -> Void) { -// var element = self[index] -// modifyElement(&element) -// self[index] = element -// } -//} From 9fbb3b271a9b72c384e6080244edea1b0bcdbb60 Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Fri, 19 Jul 2024 13:26:35 +0300 Subject: [PATCH 4/8] fix: update tests --- .../Unit/CourseDateViewModelTests.swift | 54 ++++++---- .../DashboardViewModelTests.swift | 28 +++++- .../Base/BaseResponsesViewModelTests.swift | 98 ++++++++++++++++--- .../Comment/ThreadViewModelTests.swift | 8 ++ ...DiscussionSearchTopicsViewModelTests.swift | 6 +- .../Posts/PostViewModelTests.swift | 30 +++++- .../Responses/ResponsesViewModelTests.swift | 7 ++ .../ProfileTests/ProfileMock.generated.swift | 20 ++-- 8 files changed, 200 insertions(+), 51 deletions(-) diff --git a/Course/CourseTests/Presentation/Unit/CourseDateViewModelTests.swift b/Course/CourseTests/Presentation/Unit/CourseDateViewModelTests.swift index c8863798..841c56bb 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/DashboardTests/Presentation/DashboardViewModelTests.swift b/Dashboard/DashboardTests/Presentation/DashboardViewModelTests.swift index 1d3ab3db..a5fb4e9b 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/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift b/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift index b6940c03..0928bd37 100644 --- a/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift @@ -54,7 +54,12 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock() + ) var result = false viewModel.postComments = post @@ -76,7 +81,12 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock() + ) var result = false @@ -100,7 +110,12 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock() + ) var result = false @@ -123,7 +138,12 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock() + ) var result = false @@ -148,7 +168,12 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock() + ) var result = false @@ -170,7 +195,12 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock() + ) var result = false @@ -190,7 +220,12 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock() + ) var result = false @@ -213,7 +248,12 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock() + ) var result = false @@ -236,7 +276,12 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock() + ) var result = false @@ -258,7 +303,12 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock() + ) var result = false @@ -278,7 +328,12 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock() + ) var result = false @@ -301,7 +356,12 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock() + ) var result = false @@ -323,7 +383,12 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock() + ) var result = false @@ -343,7 +408,12 @@ final class BaseResponsesViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + let viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock() + ) viewModel.postComments = post diff --git a/Discussion/DiscussionTests/Presentation/Comment/ThreadViewModelTests.swift b/Discussion/DiscussionTests/Presentation/Comment/ThreadViewModelTests.swift index 7da4cdf5..97ab5f71 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 f94484fc..33a98349 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 052930f5..ca7ddb18 100644 --- a/Discussion/DiscussionTests/Presentation/Posts/PostViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/Posts/PostViewModelTests.swift @@ -109,7 +109,12 @@ final class PostViewModelTests: XCTestCase { let router = DiscussionRouterMock() let config = ConfigMock() var result = false - let viewModel = PostsViewModel(interactor: interactor, router: router, config: config) + let viewModel = PostsViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock() + ) viewModel.courseID = "1" viewModel.type = .allPosts @@ -149,7 +154,13 @@ final class PostViewModelTests: XCTestCase { let router = DiscussionRouterMock() let config = ConfigMock() var result = false - let viewModel = PostsViewModel(interactor: interactor, router: router, config: config) + let viewModel = PostsViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock() + ) + viewModel.isBlackedOut = false let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -174,7 +185,13 @@ final class PostViewModelTests: XCTestCase { let router = DiscussionRouterMock() let config = ConfigMock() var result = false - let viewModel = PostsViewModel(interactor: interactor, router: router, config: config) + let viewModel = PostsViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock() + ) + viewModel.isBlackedOut = false Given(interactor, .getThreadsList(courseID: .any, type: .any, sort: .any, filter: .any, page: .any, willThrow: NSError())) @@ -196,7 +213,12 @@ final class PostViewModelTests: XCTestCase { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() let config = ConfigMock() - let viewModel = PostsViewModel(interactor: interactor, router: router, config: config) + let viewModel = PostsViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock() + ) Given(interactor, .getThreadsList(courseID: .any, type: .any, sort: .any, filter: .any, page: .any, willReturn: threads)) diff --git a/Discussion/DiscussionTests/Presentation/Responses/ResponsesViewModelTests.swift b/Discussion/DiscussionTests/Presentation/Responses/ResponsesViewModelTests.swift index a7c59806..2b010617 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/Profile/ProfileTests/ProfileMock.generated.swift b/Profile/ProfileTests/ProfileMock.generated.swift index 534a1dde..843268a3 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) From 1eeec48d5a82c12453a6b1baf99ae8b6b3db9a0a Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Mon, 22 Jul 2024 10:47:55 +0300 Subject: [PATCH 5/8] fix: address feedback --- .../Presentation/AllCoursesView.swift | 3 +- .../Presentation/ListDashboardView.swift | 4 +- .../PrimaryCourseDashboardView.swift | 9 ++-- .../NativeDiscovery/DiscoveryView.swift | 3 +- .../NativeDiscovery/SearchView.swift | 3 +- .../Comments/Responses/ResponsesView.swift | 9 ++-- .../Comments/Thread/ThreadView.swift | 7 +-- OpenEdX/Data/AppStorage.swift | 1 + Profile/Profile.xcodeproj/project.pbxproj | 4 ++ .../DatesAndCalendarView.swift | 2 +- .../DatesAndCalendarViewModel.swift | 35 +------------- .../Elements/RelativeDatesToggleView.swift | 46 +++++++++++++++++++ .../SyncCalendarOptionsView.swift | 2 +- 13 files changed, 76 insertions(+), 52 deletions(-) create mode 100644 Profile/Profile/Presentation/DatesAndCalendar/Elements/RelativeDatesToggleView.swift diff --git a/Dashboard/Dashboard/Presentation/AllCoursesView.swift b/Dashboard/Dashboard/Presentation/AllCoursesView.swift index af2743cf..4960ae42 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()), @@ -89,7 +90,7 @@ public struct AllCoursesView: View { courseEndDate: course.courseEnd, hasAccess: course.hasAccess, showProgress: true, - useRelativeDates: viewModel.storage.useRelativeDates + useRelativeDates: useRelativeDates ).padding(8) }) .accessibilityIdentifier("course_item") diff --git a/Dashboard/Dashboard/Presentation/ListDashboardView.swift b/Dashboard/Dashboard/Presentation/ListDashboardView.swift index ea8ab1b4..a2678b3f 100644 --- a/Dashboard/Dashboard/Presentation/ListDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/ListDashboardView.swift @@ -54,15 +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, - useRelativeDates: viewModel.storage.useRelativeDates + useRelativeDates: useRelativeDates ) .padding(.horizontal, 20) .listRowBackground(Color.clear) diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift index f70d0555..d182a6a4 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift @@ -74,7 +74,7 @@ public struct PrimaryCourseDashboardView: View { progressEarned: primary.progressEarned, progressPossible: primary.progressPossible, canResume: primary.lastVisitedBlockID != nil, - resumeTitle: primary.resumeTitle, + resumeTitle: primary.resumeTitle, useRelativeDates: viewModel.storage.useRelativeDates, assignmentAction: { lastVisitedBlockID in router.showCourseScreens( @@ -200,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 @@ -229,8 +230,8 @@ public struct PrimaryCourseDashboardView: View { courseStartDate: nil, courseEndDate: nil, hasAccess: course.hasAccess, - showProgress: false, - useRelativeDates: viewModel.storage.useRelativeDates + showProgress: false, + useRelativeDates: useRelativeDates ).frame(width: idiom == .pad ? nil : 120) } ) @@ -332,7 +333,7 @@ struct PrimaryCourseDashboardView_Previews: PreviewProvider { interactor: DashboardInteractor.mock, connectivity: Connectivity(), analytics: DashboardAnalyticsMock(), - config: ConfigMock(), + config: ConfigMock(), storage: CoreStorageMock() ) diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift index 1e82fc70..5816da08 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift @@ -106,13 +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, - useRelativeDates: viewModel.storage.useRelativeDates + useRelativeDates: useRelativeDates ).padding(.horizontal, 24) .onAppear { Task { diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift index fbc68bdb..531a7db3 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift @@ -110,6 +110,7 @@ public struct SearchView: View { LazyVStack { let searchResults = viewModel.searchResults.enumerated() + let useRelativeDates = viewModel.storage.useRelativeDates ForEach( Array(searchResults), id: \.offset) { index, course in CourseCellView( @@ -117,7 +118,7 @@ public struct SearchView: View { type: .discovery, index: index, cellsCount: viewModel.searchResults.count, - useRelativeDates: viewModel.storage.useRelativeDates + useRelativeDates: useRelativeDates ) .padding(.horizontal, 24) .onAppear { diff --git a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift index d03a252d..91a5d9dd 100644 --- a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift +++ b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift @@ -59,7 +59,7 @@ public struct ResponsesView: View { if let comments = viewModel.postComments { ParentCommentView( comments: comments, - isThread: false, + isThread: false, useRelativeDates: viewModel.storage.useRelativeDates, onAvatarTap: { username in viewModel.router.showUserDetails(username: username) @@ -101,13 +101,14 @@ 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, - useRelativeDates: viewModel.storage.useRelativeDates, + addCommentAvailable: false, + useRelativeDates: useRelativeDates, leftLineEnabled: true, onAvatarTap: { username in viewModel.router.showUserDetails(username: username) @@ -241,7 +242,7 @@ struct ResponsesView_Previews: PreviewProvider { let viewModel = ResponsesViewModel( interactor: DiscussionInteractor(repository: DiscussionRepositoryMock()), router: DiscussionRouterMock(), - config: ConfigMock(), + config: ConfigMock(), storage: CoreStorageMock(), threadStateSubject: .init(nil) ) diff --git a/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift b/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift index 3d393da6..1d39962f 100644 --- a/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift +++ b/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift @@ -43,7 +43,7 @@ public struct ThreadView: View { if let comments = viewModel.postComments { ParentCommentView( comments: comments, - isThread: true, + isThread: true, useRelativeDates: viewModel.storage.useRelativeDates, onAvatarTap: { username in viewModel.router.showUserDetails(username: username) @@ -93,12 +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: viewModel.storage.useRelativeDates, + addCommentAvailable: true, + useRelativeDates: useRelativeDates, onAvatarTap: { username in viewModel.router.showUserDetails(username: username) }, diff --git a/OpenEdX/Data/AppStorage.swift b/OpenEdX/Data/AppStorage.swift index e9c23825..d064da7a 100644 --- a/OpenEdX/Data/AppStorage.swift +++ b/OpenEdX/Data/AppStorage.swift @@ -332,6 +332,7 @@ 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 { diff --git a/Profile/Profile.xcodeproj/project.pbxproj b/Profile/Profile.xcodeproj/project.pbxproj index afc5ff35..05a1d025 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 = ""; }; @@ -341,6 +343,7 @@ 022301E72BF4C4E70028A287 /* ToggleWithDescriptionView.swift */, 02F81DDE2BF4D83E002D3604 /* CalendarDialogView.swift */, 02F81DE22BF502B9002D3604 /* SyncSelector.swift */, + 029498792C4E4332008FD0E7 /* RelativeDatesToggleView.swift */, ); path = Elements; sourceTree = ""; @@ -676,6 +679,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/Presentation/DatesAndCalendar/DatesAndCalendarView.swift b/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift index b93bf4fe..35ea4a7f 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 - viewModel.relativeDatesToggle + RelativeDatesToggleView(useRelativeDates: $viewModel.profileStorage.useRelativeDates) } .padding(.horizontal, isHorizontal ? 48 : 0) } diff --git a/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarViewModel.swift b/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarViewModel.swift index cdf3002d..821879f7 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarViewModel.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarViewModel.swift @@ -72,7 +72,7 @@ public class DatesAndCalendarViewModel: ObservableObject { var router: ProfileRouter private var interactor: ProfileInteractorProtocol - private var profileStorage: ProfileStorage + var profileStorage: ProfileStorage private var persistence: ProfilePersistenceProtocol private var calendarManager: CalendarManagerProtocol private var connectivity: ConnectivityProtocol @@ -106,39 +106,6 @@ public class DatesAndCalendarViewModel: ObservableObject { return avaliable } - private var useRelativeDatesBinding: Binding { - Binding( - get: { self.profileStorage.useRelativeDates }, - set: { self.profileStorage.useRelativeDates = $0 } - ) - } - - // MARK: - Options Toggle - var relativeDatesToggle: some View { - VStack(alignment: .leading, spacing: 10) { - Text(ProfileLocalization.Options.title) - .font(Theme.Fonts.labelLarge) - .foregroundColor(Theme.Colors.textPrimary) - HStack(spacing: 16) { - Toggle("", isOn: useRelativeDatesBinding) - .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(.top, 14) - .padding(.horizontal, 24) - .frame(minWidth: 0, - maxWidth: .infinity, - alignment: .leading) - .accessibilityIdentifier("relative_dates_toggle") - } - // MARK: - Lifecycle Functions func loadCalendarOptions() { diff --git a/Profile/Profile/Presentation/DatesAndCalendar/Elements/RelativeDatesToggleView.swift b/Profile/Profile/Presentation/DatesAndCalendar/Elements/RelativeDatesToggleView.swift new file mode 100644 index 00000000..d5965ecc --- /dev/null +++ b/Profile/Profile/Presentation/DatesAndCalendar/Elements/RelativeDatesToggleView.swift @@ -0,0 +1,46 @@ +// +// 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(ProfileLocalization.Options.showRelativeDates) + .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") + } +} + +// Usage example +struct ContentView: View { + + var body: some View { + RelativeDatesToggleView(useRelativeDates: .constant(true)) + } +} diff --git a/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift b/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift index dcd886b6..154c869e 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) } - viewModel.relativeDatesToggle + RelativeDatesToggleView(useRelativeDates: $viewModel.profileStorage.useRelativeDates) } .padding(.horizontal, isHorizontal ? 48 : 0) .frameLimit(width: proxy.size.width) From 6959f3c63ea646150156b383b81d4758f8422341 Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Mon, 29 Jul 2024 12:11:01 +0300 Subject: [PATCH 6/8] fix: address feedback --- Core/Core/Extensions/DateExtension.swift | 74 ++++++++++++------- Core/Core/SwiftGen/Strings.swift | 10 +++ Core/Core/en.lproj/Localizable.strings | 3 + .../DatesAndCalendarViewModel.swift | 2 +- .../Elements/RelativeDatesToggleView.swift | 14 ++-- Profile/Profile/SwiftGen/Strings.swift | 2 + Profile/Profile/en.lproj/Localizable.strings | 1 + 7 files changed, 71 insertions(+), 35 deletions(-) diff --git a/Core/Core/Extensions/DateExtension.swift b/Core/Core/Extensions/DateExtension.swift index dbb8ed97..848d0a9e 100644 --- a/Core/Core/Extensions/DateExtension.swift +++ b/Core/Core/Extensions/DateExtension.swift @@ -34,35 +34,59 @@ public extension Date { } func timeAgoDisplay() -> String { - let formatter = RelativeDateTimeFormatter() - formatter.locale = .current - formatter.unitsStyle = .full - let currentDate = Date() - let sevenDaysAgo = Calendar.current.date(byAdding: .day, value: -7, to: currentDate)! - let sevenDaysAhead = Calendar.current.date(byAdding: .day, value: 7, to: currentDate)! - - if self >= sevenDaysAgo && self <= sevenDaysAhead { - if self.description == currentDate.description { - return CoreLocalization.Date.justNow + let calendar = Calendar.current + + let startOfCurrentDate = calendar.startOfDay(for: currentDate) + let startOfSelfDate = calendar.startOfDay(for: self) + + // Calculate date ranges + let sevenDaysAgo = calendar.date(byAdding: .day, value: -7, to: startOfCurrentDate)! + let sevenDaysAhead = calendar.date(byAdding: .day, value: 7, to: startOfCurrentDate)! + + let isCurrentYear = calendar.component(.year, from: self) == calendar.component(.year, from: currentDate) + + if calendar.isDateInToday(self) { + return CoreLocalization.Date.today + } + + if calendar.isDateInYesterday(self) { + return CoreLocalization.yesterday + } + + if calendar.isDateInTomorrow(self) { + return CoreLocalization.tomorrow + } + + if startOfSelfDate > startOfCurrentDate && startOfSelfDate <= sevenDaysAhead { + let weekdayFormatter = DateFormatter() + weekdayFormatter.dateFormat = "EEEE" + if startOfSelfDate == calendar.date(byAdding: .day, value: 1, to: startOfCurrentDate) { + return CoreLocalization.tomorrow + } else if startOfSelfDate == calendar.date(byAdding: .day, value: 7, to: startOfCurrentDate) { + return CoreLocalization.Date.next(weekdayFormatter.string(from: startOfSelfDate)) } else { - return formatter.localizedString(for: self, relativeTo: currentDate) + return weekdayFormatter.string(from: startOfSelfDate) } - } else { - let specificFormatter = DateFormatter() - specificFormatter.dateFormat = "MMMM d" - - let yearFormatter = DateFormatter() - yearFormatter.dateFormat = "yyyy" - let currentYear = yearFormatter.string(from: currentDate) - let dateYear = yearFormatter.string(from: self) - - if currentYear != dateYear { - specificFormatter.dateFormat = "MMMM d, yyyy" - } - - return specificFormatter.string(from: self) } + + if startOfSelfDate < startOfCurrentDate && startOfSelfDate >= sevenDaysAgo { + let daysAgo = calendar.dateComponents([.day], from: startOfSelfDate, to: startOfCurrentDate).day! + return CoreLocalization.Date.daysAgo(daysAgo) + } + + let specificFormatter = DateFormatter() + specificFormatter.dateFormat = isCurrentYear ? "MMMM d" : "MMMM d, yyyy" + return 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) { diff --git a/Core/Core/SwiftGen/Strings.swift b/Core/Core/SwiftGen/Strings.swift index 8cdf97b6..09a0a4ed 100644 --- a/Core/Core/SwiftGen/Strings.swift +++ b/Core/Core/SwiftGen/Strings.swift @@ -125,14 +125,24 @@ 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") + } /// 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/en.lproj/Localizable.strings b/Core/Core/en.lproj/Localizable.strings index b4ca1bc6..3d6ee8c3 100644 --- a/Core/Core/en.lproj/Localizable.strings +++ b/Core/Core/en.lproj/Localizable.strings @@ -53,6 +53,9 @@ "DATE.START" = "Start"; "DATE.STARTED" = "Started"; "DATE.JUST_NOW" = "Just now"; +"DATE.TODAY" = "Today"; +"DATE.NEXT" = "Next %@"; +"DATE.DAYS_AGO" = "%@ Days Ago"; "ALERT.ACCEPT" = "ACCEPT"; "ALERT.CANCEL" = "CANCEL"; diff --git a/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarViewModel.swift b/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarViewModel.swift index 821879f7..667546c2 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarViewModel.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarViewModel.swift @@ -72,7 +72,7 @@ public class DatesAndCalendarViewModel: ObservableObject { var router: ProfileRouter private var interactor: ProfileInteractorProtocol - var profileStorage: ProfileStorage + @Published var profileStorage: ProfileStorage private var persistence: ProfilePersistenceProtocol private var calendarManager: CalendarManagerProtocol private var connectivity: ConnectivityProtocol diff --git a/Profile/Profile/Presentation/DatesAndCalendar/Elements/RelativeDatesToggleView.swift b/Profile/Profile/Presentation/DatesAndCalendar/Elements/RelativeDatesToggleView.swift index d5965ecc..967e5224 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/Elements/RelativeDatesToggleView.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/Elements/RelativeDatesToggleView.swift @@ -24,7 +24,11 @@ struct RelativeDatesToggleView: View { .font(Theme.Fonts.bodyLarge) .foregroundColor(Theme.Colors.textPrimary) } - Text(ProfileLocalization.Options.showRelativeDates) + Text( + useRelativeDates + ? ProfileLocalization.Options.showRelativeDates + : ProfileLocalization.Options.showFullDates + ) .font(Theme.Fonts.labelMedium) .foregroundColor(Theme.Colors.textPrimary) } @@ -36,11 +40,3 @@ struct RelativeDatesToggleView: View { .accessibilityIdentifier("relative_dates_toggle") } } - -// Usage example -struct ContentView: View { - - var body: some View { - RelativeDatesToggleView(useRelativeDates: .constant(true)) - } -} diff --git a/Profile/Profile/SwiftGen/Strings.swift b/Profile/Profile/SwiftGen/Strings.swift index d83527f9..91d19492 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: "Comfirm 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 d94e1152..5bbc119e 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"; From 8ddd6992b1a82241cdd9550d91eeb1266132a02d Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Thu, 8 Aug 2024 19:02:32 +0300 Subject: [PATCH 7/8] fix: address feedback --- Core/Core/Extensions/DateExtension.swift | 10 +- .../Elements/PrimaryCardView.swift | 4 +- .../Base/BaseResponsesViewModelTests.swift | 143 +++--------------- .../Posts/PostViewModelTests.swift | 55 +++---- 4 files changed, 48 insertions(+), 164 deletions(-) diff --git a/Core/Core/Extensions/DateExtension.swift b/Core/Core/Extensions/DateExtension.swift index 848d0a9e..1510d35f 100644 --- a/Core/Core/Extensions/DateExtension.swift +++ b/Core/Core/Extensions/DateExtension.swift @@ -41,8 +41,10 @@ public extension Date { let startOfSelfDate = calendar.startOfDay(for: self) // Calculate date ranges - let sevenDaysAgo = calendar.date(byAdding: .day, value: -7, to: startOfCurrentDate)! - let sevenDaysAhead = calendar.date(byAdding: .day, value: 7, to: startOfCurrentDate)! + guard let sevenDaysAgo = calendar.date(byAdding: .day, value: -7, to: startOfCurrentDate), + let sevenDaysAhead = calendar.date(byAdding: .day, value: 7, to: startOfCurrentDate) else { + return self.dateToString(style: .mmddyy, useRelativeDates: false) + } let isCurrentYear = calendar.component(.year, from: self) == calendar.component(.year, from: currentDate) @@ -71,7 +73,9 @@ public extension Date { } if startOfSelfDate < startOfCurrentDate && startOfSelfDate >= sevenDaysAgo { - let daysAgo = calendar.dateComponents([.day], from: startOfSelfDate, to: startOfCurrentDate).day! + guard let daysAgo = calendar.dateComponents([.day], from: startOfSelfDate, to: startOfCurrentDate).day else { + return self.dateToString(style: .mmddyy, useRelativeDates: false) + } return CoreLocalization.Date.daysAgo(daysAgo) } diff --git a/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift b/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift index e2d5e24e..1e58a4fa 100644 --- a/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift +++ b/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift @@ -271,9 +271,7 @@ struct PrimaryCardView_Previews: PreviewProvider { canResume: true, resumeTitle: "Course Chapter 1", useRelativeDates: true, - assignmentAction: { - _ in - }, + assignmentAction: { _ in }, openCourseAction: {}, resumeAction: {} ) diff --git a/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift b/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift index 0928bd37..d862278f 100644 --- a/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift @@ -50,16 +50,27 @@ final class BaseResponsesViewModelTests: XCTestCase { abuseFlagged: false, closed: false) - func testVoteThreadSuccess() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel( + 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 { + var result = false viewModel.postComments = post @@ -78,15 +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, - storage: CoreStorageMock() - ) + var result = false @@ -107,15 +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, - storage: CoreStorageMock() - ) + var result = false @@ -135,15 +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, - storage: CoreStorageMock() - ) + var result = false @@ -165,15 +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, - storage: CoreStorageMock() - ) + var result = false @@ -192,15 +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, - storage: CoreStorageMock() - ) + var result = false @@ -217,15 +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, - storage: CoreStorageMock() - ) var result = false @@ -245,15 +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, - storage: CoreStorageMock() - ) var result = false @@ -273,15 +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, - storage: CoreStorageMock() - ) var result = false @@ -300,15 +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, - storage: CoreStorageMock() - ) var result = false @@ -325,15 +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, - storage: CoreStorageMock() - ) var result = false @@ -353,15 +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, - storage: CoreStorageMock() - ) var result = false @@ -380,15 +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, - storage: CoreStorageMock() - ) var result = false @@ -405,15 +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, - storage: CoreStorageMock() - ) viewModel.postComments = post diff --git a/Discussion/DiscussionTests/Presentation/Posts/PostViewModelTests.swift b/Discussion/DiscussionTests/Presentation/Posts/PostViewModelTests.swift index ca7ddb18..9b9275d5 100644 --- a/Discussion/DiscussionTests/Presentation/Posts/PostViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/Posts/PostViewModelTests.swift @@ -102,19 +102,29 @@ final class PostViewModelTests: XCTestCase { ]) let discussionInfo = DiscussionInfo(discussionID: "1", blackouts: []) - - - func testGetThreadListSuccess() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - var result = false - let viewModel = PostsViewModel( + + 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 { + var result = false viewModel.courseID = "1" viewModel.type = .allPosts @@ -150,16 +160,7 @@ 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, - storage: CoreStorageMock() - ) viewModel.isBlackedOut = false @@ -181,16 +182,7 @@ 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, - storage: CoreStorageMock() - ) viewModel.isBlackedOut = false @@ -210,16 +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, - storage: CoreStorageMock() - ) - + Given(interactor, .getThreadsList(courseID: .any, type: .any, sort: .any, filter: .any, page: .any, willReturn: threads)) Given(interactor, .getCourseDiscussionInfo(courseID: "1", willReturn: discussionInfo)) From b8b54cd482d7ffa6c2ee2b6271d64f9830168297 Mon Sep 17 00:00:00 2001 From: stepanokdev Date: Thu, 22 Aug 2024 21:17:09 +0300 Subject: [PATCH 8/8] fix: address feedback --- Core/Core/Extensions/DateExtension.swift | 43 ++++++++++++------- Core/Core/SwiftGen/Strings.swift | 8 ++++ Core/Core/en.lproj/Localizable.strings | 3 ++ .../Elements/PrimaryCardView.swift | 20 ++++++--- 4 files changed, 54 insertions(+), 20 deletions(-) diff --git a/Core/Core/Extensions/DateExtension.swift b/Core/Core/Extensions/DateExtension.swift index 1510d35f..cb07e81f 100644 --- a/Core/Core/Extensions/DateExtension.swift +++ b/Core/Core/Extensions/DateExtension.swift @@ -33,42 +33,53 @@ public extension Date { self.init(timeInterval: 0, since: date) } - func timeAgoDisplay() -> String { + 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 self.dateToString(style: .mmddyy, useRelativeDates: false) + return dueInString + self.dateToString(style: .mmddyy, useRelativeDates: false) } - let isCurrentYear = calendar.component(.year, from: self) == calendar.component(.year, from: currentDate) + let isCurrentYear = calendar.component(.year, from: self) == calendar.component(.year, from: startOfCurrentDate) - if calendar.isDateInToday(self) { - return CoreLocalization.Date.today + if calendar.isDateInToday(startOfSelfDate) { + return dueString + CoreLocalization.Date.today } - if calendar.isDateInYesterday(self) { - return CoreLocalization.yesterday + if calendar.isDateInYesterday(startOfSelfDate) { + return dueString + CoreLocalization.yesterday } - if calendar.isDateInTomorrow(self) { - return CoreLocalization.tomorrow + 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 CoreLocalization.tomorrow + 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 weekdayFormatter.string(from: startOfSelfDate) + return dueIn ? ( + CoreLocalization.Date.dueInDays(daysRemaining) + ) : weekdayFormatter.string(from: startOfSelfDate) } } @@ -81,7 +92,7 @@ public extension Date { let specificFormatter = DateFormatter() specificFormatter.dateFormat = isCurrentYear ? "MMMM d" : "MMMM d, yyyy" - return specificFormatter.string(from: self) + return dueInString + specificFormatter.string(from: self) } func isDateInNextWeek(date: Date, currentDate: Date) -> Bool { @@ -148,13 +159,13 @@ public extension Date { return totalSeconds } - func dateToString(style: DateStringStyle, useRelativeDates: Bool) -> String { + func dateToString(style: DateStringStyle, useRelativeDates: Bool, dueIn: Bool = false) -> String { let dateFormatter = DateFormatter() dateFormatter.locale = .current if useRelativeDates { - return timeAgoDisplay() + return timeAgoDisplay(dueIn: dueIn) } else { switch style { case .courseStartsMonthDDYear: @@ -213,7 +224,9 @@ public extension Date { case .iso8601: return date case .shortWeekdayMonthDayYear: - return getShortWeekdayMonthDayYear(dateFormatterString: date) + return ( + dueIn ? CoreLocalization.Date.dueIn : "" + ) + getShortWeekdayMonthDayYear(dateFormatterString: date) } } diff --git a/Core/Core/SwiftGen/Strings.swift b/Core/Core/SwiftGen/Strings.swift index ba641c99..ed9aa825 100644 --- a/Core/Core/SwiftGen/Strings.swift +++ b/Core/Core/SwiftGen/Strings.swift @@ -141,6 +141,14 @@ public enum CoreLocalization { 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 diff --git a/Core/Core/en.lproj/Localizable.strings b/Core/Core/en.lproj/Localizable.strings index 806b411a..b7bc68aa 100644 --- a/Core/Core/en.lproj/Localizable.strings +++ b/Core/Core/en.lproj/Localizable.strings @@ -56,6 +56,9 @@ "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/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift b/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift index 1e58a4fa..83680dc7 100644 --- a/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift +++ b/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift @@ -113,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, @@ -264,13 +265,22 @@ 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", - useRelativeDates: true, + useRelativeDates: false, assignmentAction: { _ in }, openCourseAction: {}, resumeAction: {}