Skip to content

Commit

Permalink
feat: [FC-0047] Relative Dates (#505)
Browse files Browse the repository at this point in the history
* feat: initial commit

* feat: relative dates

* fix: address feedback

* fix: update tests

* fix: address feedback

* fix: address feedback

* fix: address feedback

* fix: address feedback
  • Loading branch information
IvanStepanok authored Aug 23, 2024
1 parent 157ded7 commit a405dce
Show file tree
Hide file tree
Showing 54 changed files with 548 additions and 326 deletions.
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

0 comments on commit a405dce

Please sign in to comment.