Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [FC-0047] Relative Dates #505

Merged
merged 9 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Core/Core/Data/CoreStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand All @@ -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() {}
Expand Down
5 changes: 3 additions & 2 deletions Core/Core/Data/Model/Data_CourseDates.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
7 changes: 3 additions & 4 deletions Core/Core/Data/Repository/AuthRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}

Expand Down
3 changes: 2 additions & 1 deletion Core/Core/Domain/Model/CourseDates.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
167 changes: 99 additions & 68 deletions Core/Core/Extensions/DateExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -33,16 +33,75 @@ public extension Date {
self.init(timeInterval: 0, since: date)
}

func timeAgoDisplay() -> String {
let formatter = RelativeDateTimeFormatter()
formatter.locale = .current
formatter.unitsStyle = .full
formatter.locale = Locale(identifier: "en_US_POSIX")
if description == Date().description {
return CoreLocalization.Date.justNow
} else {
return formatter.localizedString(for: self, relativeTo: Date())
func timeAgoDisplay(dueIn: Bool = false) -> String {
let currentDate = Date()
let calendar = Calendar.current

let dueString = dueIn ? CoreLocalization.Date.due : ""
let dueInString = dueIn ? CoreLocalization.Date.dueIn : ""

let startOfCurrentDate = calendar.startOfDay(for: currentDate)
let startOfSelfDate = calendar.startOfDay(for: self)

let daysRemaining = Calendar.current.dateComponents(
[.day],
from: startOfCurrentDate,
to: self
).day ?? 0

// Calculate date ranges
guard let sevenDaysAgo = calendar.date(byAdding: .day, value: -7, to: startOfCurrentDate),
let sevenDaysAhead = calendar.date(byAdding: .day, value: 7, to: startOfCurrentDate) else {
return dueInString + self.dateToString(style: .mmddyy, useRelativeDates: false)
}

let isCurrentYear = calendar.component(.year, from: self) == calendar.component(.year, from: startOfCurrentDate)

if calendar.isDateInToday(startOfSelfDate) {
return dueString + CoreLocalization.Date.today
}

if calendar.isDateInYesterday(startOfSelfDate) {
return dueString + CoreLocalization.yesterday
}

if calendar.isDateInTomorrow(startOfSelfDate) {
return dueString + CoreLocalization.tomorrow
}

if startOfSelfDate > startOfCurrentDate && startOfSelfDate <= sevenDaysAhead {
let weekdayFormatter = DateFormatter()
weekdayFormatter.dateFormat = "EEEE"
if startOfSelfDate == calendar.date(byAdding: .day, value: 1, to: startOfCurrentDate) {
return dueInString + CoreLocalization.tomorrow
} else if startOfSelfDate == calendar.date(byAdding: .day, value: 7, to: startOfCurrentDate) {
return CoreLocalization.Date.next(weekdayFormatter.string(from: startOfSelfDate))
} else {
return dueIn ? (
CoreLocalization.Date.dueInDays(daysRemaining)
) : weekdayFormatter.string(from: startOfSelfDate)
}
}

if startOfSelfDate < startOfCurrentDate && startOfSelfDate >= sevenDaysAgo {
guard let daysAgo = calendar.dateComponents([.day], from: startOfSelfDate, to: startOfCurrentDate).day else {
return self.dateToString(style: .mmddyy, useRelativeDates: false)
}
return CoreLocalization.Date.daysAgo(daysAgo)
}

let specificFormatter = DateFormatter()
specificFormatter.dateFormat = isCurrentYear ? "MMMM d" : "MMMM d, yyyy"
return dueInString + specificFormatter.string(from: self)
}

func isDateInNextWeek(date: Date, currentDate: Date) -> Bool {
let calendar = Calendar.current
guard let nextWeek = calendar.date(byAdding: .weekOfYear, value: 1, to: currentDate) else { return false }
let startOfNextWeek = calendar.startOfDay(for: nextWeek)
guard let endOfNextWeek = calendar.date(byAdding: .day, value: 6, to: startOfNextWeek) else { return false }
let startOfSelfDate = calendar.startOfDay(for: date)
return startOfSelfDate >= startOfNextWeek && startOfSelfDate <= endOfNextWeek
}

init(subtitleTime: String) {
Expand Down Expand Up @@ -100,29 +159,34 @@ public extension Date {
return totalSeconds
}

func dateToString(style: DateStringStyle) -> String {
func dateToString(style: DateStringStyle, useRelativeDates: Bool, dueIn: Bool = false) -> String {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")

switch style {
case .courseStartsMonthDDYear:
dateFormatter.dateFormat = CoreLocalization.DateFormat.mmmDdYyyy
case .courseEndsMonthDDYear:
dateFormatter.dateFormat = CoreLocalization.DateFormat.mmmDdYyyy
case .endedMonthDay:
dateFormatter.dateFormat = CoreLocalization.DateFormat.mmmmDd
case .mmddyy:
dateFormatter.dateFormat = "dd.MM.yy"
case .monthYear:
dateFormatter.dateFormat = "MMMM yyyy"
case .startDDMonthYear:
dateFormatter.dateFormat = "dd MMM yyyy"
case .lastPost:
dateFormatter.dateFormat = CoreLocalization.DateFormat.mmmDdYyyy
case .iso8601:
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
case .shortWeekdayMonthDayYear:
applyShortWeekdayMonthDayYear(dateFormatter: dateFormatter)
dateFormatter.locale = .current

if useRelativeDates {
return timeAgoDisplay(dueIn: dueIn)
} else {
switch style {
case .courseStartsMonthDDYear:
dateFormatter.dateStyle = .medium
case .courseEndsMonthDDYear:
dateFormatter.dateStyle = .medium
case .endedMonthDay:
dateFormatter.dateFormat = CoreLocalization.DateFormat.mmmmDd
case .mmddyy:
dateFormatter.dateFormat = "dd.MM.yy"
case .monthYear:
dateFormatter.dateFormat = "MMMM yyyy"
case .startDDMonthYear:
dateFormatter.dateFormat = "dd MMM yyyy"
case .lastPost:
dateFormatter.dateFormat = CoreLocalization.DateFormat.mmmDdYyyy
case .iso8601:
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
case .shortWeekdayMonthDayYear:
applyShortWeekdayMonthDayYear(dateFormatter: dateFormatter)
}
}

let date = dateFormatter.string(from: self)
Expand Down Expand Up @@ -160,52 +224,19 @@ public extension Date {
case .iso8601:
return date
case .shortWeekdayMonthDayYear:
return getShortWeekdayMonthDayYear(dateFormatterString: date)
return (
dueIn ? CoreLocalization.Date.dueIn : ""
) + getShortWeekdayMonthDayYear(dateFormatterString: date)
}
}

private func applyShortWeekdayMonthDayYear(dateFormatter: DateFormatter) {
if isCurrentYear() {
let days = Calendar.current.dateComponents([.day], from: self, to: Date())
if let day = days.day, (-6 ... -2).contains(day) {
dateFormatter.dateFormat = "EEEE"
} else {
dateFormatter.dateFormat = "MMMM d"
}
} else {
dateFormatter.dateFormat = "MMMM d, yyyy"
}
}

private func getShortWeekdayMonthDayYear(dateFormatterString: String) -> String {
let days = Calendar.current.dateComponents([.day], from: self, to: Date())

if let day = days.day {
guard isCurrentYear() else {
// It's past year or future year
return dateFormatterString
}

switch day {
case -6...(-2):
return dateFormatterString
case 2...6:
return timeAgoDisplay()
case -1:
return CoreLocalization.tomorrow
case 1:
return CoreLocalization.yesterday
default:
if day > 6 || day < -6 {
return dateFormatterString
} else {
// It means, date is in hours past due or upcoming
return timeAgoDisplay()
}
}
} else {
return dateFormatterString
}
return dateFormatterString
}

func isCurrentYear() -> Bool {
Expand Down
18 changes: 18 additions & 0 deletions Core/Core/SwiftGen/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,14 +137,32 @@ public enum CoreLocalization {
public static let courseEnds = CoreLocalization.tr("Localizable", "DATE.COURSE_ENDS", fallback: "Course Ends")
/// Course Starts
public static let courseStarts = CoreLocalization.tr("Localizable", "DATE.COURSE_STARTS", fallback: "Course Starts")
/// %@ Days Ago
public static func daysAgo(_ p1: Any) -> String {
return CoreLocalization.tr("Localizable", "DATE.DAYS_AGO", String(describing: p1), fallback: "%@ Days Ago")
}
/// Due
public static let due = CoreLocalization.tr("Localizable", "DATE.DUE", fallback: "Due ")
/// Due in
public static let dueIn = CoreLocalization.tr("Localizable", "DATE.DUE_IN", fallback: "Due in ")
/// Due in %@ Days
public static func dueInDays(_ p1: Any) -> String {
return CoreLocalization.tr("Localizable", "DATE.DUE_IN_DAYS", String(describing: p1), fallback: "Due in %@ Days")
}
/// Ended
public static let ended = CoreLocalization.tr("Localizable", "DATE.ENDED", fallback: "Ended")
/// Just now
public static let justNow = CoreLocalization.tr("Localizable", "DATE.JUST_NOW", fallback: "Just now")
/// Next %@
public static func next(_ p1: Any) -> String {
return CoreLocalization.tr("Localizable", "DATE.NEXT", String(describing: p1), fallback: "Next %@")
}
/// Start
public static let start = CoreLocalization.tr("Localizable", "DATE.START", fallback: "Start")
/// Started
public static let started = CoreLocalization.tr("Localizable", "DATE.STARTED", fallback: "Started")
/// Today
public static let today = CoreLocalization.tr("Localizable", "DATE.TODAY", fallback: "Today")
}
public enum DateFormat {
/// MMM dd, yyyy
Expand Down
10 changes: 5 additions & 5 deletions Core/Core/View/Base/CourseCellView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
Expand Down
6 changes: 6 additions & 0 deletions Core/Core/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@
"DATE.START" = "Start";
"DATE.STARTED" = "Started";
"DATE.JUST_NOW" = "Just now";
"DATE.TODAY" = "Today";
"DATE.NEXT" = "Next %@";
"DATE.DAYS_AGO" = "%@ Days Ago";
"DATE.DUE" = "Due ";
"DATE.DUE_IN" = "Due in ";
"DATE.DUE_IN_DAYS" = "Due in %@ Days";

"ALERT.ACCEPT" = "ACCEPT";
"ALERT.CANCEL" = "CANCEL";
Expand Down
4 changes: 2 additions & 2 deletions Course/Course/Data/CourseRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
Loading
Loading