Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
4f787c1
feat: design
IvanStepanok Feb 17, 2025
ef59ab2
feat: add endpoints and update models
IvanStepanok Mar 12, 2025
c662442
feat: add routing logic
IvanStepanok Mar 18, 2025
da6f649
feat: update course routing
IvanStepanok Mar 21, 2025
d961e61
feat: add pagination
IvanStepanok Mar 24, 2025
265d3d9
feat: add offline mode, feature flag, pagination
IvanStepanok Mar 25, 2025
3524c8e
feat: add refreshable extension and fix minor bugs
IvanStepanok Mar 25, 2025
3d78afe
feat: add analytics
IvanStepanok Mar 25, 2025
1fddc22
feat: update to swift 6
IvanStepanok Mar 25, 2025
dc69bf9
feat: add error handling
IvanStepanok Mar 25, 2025
8fbaf84
feat: add shift due dates
IvanStepanok Mar 25, 2025
fe50bff
feat: update reset date deadlines logic
IvanStepanok Mar 28, 2025
4ac0a7c
feat: add cache-first logic
IvanStepanok Mar 28, 2025
a61a74a
feat: prepare for tests
IvanStepanok Mar 28, 2025
8cbf9ad
feat: update logic
IvanStepanok Mar 29, 2025
d99bd0f
feat: add index to core data model
IvanStepanok Mar 29, 2025
3828fea
Merge branch 'develop' into feat/app_level_dates
IvanStepanok Mar 29, 2025
6c206c4
feat: update navbar icon
IvanStepanok Mar 29, 2025
e2ab504
feat: update library version
IvanStepanok Mar 29, 2025
3649d72
feat: add AppDatesTests
IvanStepanok Mar 31, 2025
51df2cc
feat: remove prints
IvanStepanok Mar 31, 2025
9852c55
fix: address feedback
IvanStepanok Mar 31, 2025
cd95691
feat: remove dates sorting
IvanStepanok Apr 3, 2025
a17c47f
Merge branch 'develop' into feat/app_level_dates
IvanStepanok Apr 3, 2025
82be032
fix: merge conflicts and tests
IvanStepanok Apr 3, 2025
eddc065
fix: update feature flag
IvanStepanok Apr 3, 2025
6d2186b
fix: address feedback
IvanStepanok Apr 6, 2025
62dc849
fix: convert folders to groups
IvanStepanok Apr 8, 2025
2776525
feat: address feedback (design)
IvanStepanok Apr 16, 2025
9964cc7
feat: address feedback
IvanStepanok Apr 16, 2025
b73c0c3
fix: resolve issue 581
Demian-Yushyn Jul 30, 2025
cc0e655
fix: update adjustments
Demian-Yushyn Jul 30, 2025
84a2156
fix: add timeout constant
Demian-Yushyn Jul 30, 2025
cd349b3
fix: adjust naming
Demian-Yushyn Jul 30, 2025
5aa8e55
fix: update connectivity
Demian-Yushyn Jul 30, 2025
64c59a1
fix: remove unused functions and hardcoded variables
Demian-Yushyn Jul 31, 2025
b4fcf31
fix: Removed unused functions and hardcoded variables
Demian-Yushyn Jul 31, 2025
253341d
Merge remote-tracking branch 'origin/fix/issue-581' into fix/issue-581
Demian-Yushyn Jul 31, 2025
6121c2a
fix: issue 581
Demian-Yushyn Jul 31, 2025
b26c3d7
fix: progress on issue 581
Demian-Yushyn Jul 31, 2025
f21a460
fix: apply updates
Demian-Yushyn Jul 31, 2025
14441a6
fix: update wi DI Config, thread fixes
Demian-Yushyn Jul 31, 2025
327bb30
fix: connectivity logic update
Demian-Yushyn Jul 31, 2025
a9074d0
fix: cacheValidity change
Demian-Yushyn Jul 31, 2025
dabc744
fix: unit tests update
Demian-Yushyn Aug 6, 2025
48d55bc
fix: removed team and coma
Demian-Yushyn Aug 6, 2025
786fa30
fix: removed extra comas
Demian-Yushyn Aug 6, 2025
a7475cd
Merge branch 'develop' into fix/issue-581
IvanStepanok Sep 16, 2025
0fce30b
fix: unit tests
Demian-Yushyn Sep 16, 2025
1de79e4
Merge branch 'develop' into feat/app_level_dates
IvanStepanok Oct 8, 2025
e6470d7
fix: issues after merge request
IvanStepanok Oct 8, 2025
cac59af
fix: update mocks for tests
IvanStepanok Oct 8, 2025
b3ca735
fix: update logic for duplicates
IvanStepanok Oct 13, 2025
635c716
Merge branch 'develop' into fix/issue-581
Demian-Yushyn Oct 29, 2025
ad3f260
fix: removed state object warning
Demian-Yushyn Oct 29, 2025
39ace76
fix: update config
Demian-Yushyn Oct 29, 2025
8465734
Merge pull request #2 from raccoongang/fix/issue-581
IvanStepanok Nov 10, 2025
cd1d48f
fix: update shift date logic
IvanStepanok Dec 4, 2025
fd1a68d
fix: address QA feedback
IvanStepanok Dec 16, 2025
77c3bec
feat: update shift due date description
IvanStepanok Dec 22, 2025
4284f61
Merge branch 'develop' into feat/app_level_dates
IvanStepanok Dec 24, 2025
2c222c4
feat: update colors
IvanStepanok Jan 6, 2026
6ace7ac
fix: update swiftymocky to master branch
IvanStepanok Jan 6, 2026
3dce160
fix: update testLoadDatesOfflineSuccess test
IvanStepanok Jan 7, 2026
fa9e011
Merge branch 'develop' into feat/app_level_dates
IvanStepanok Jan 14, 2026
9f20aa4
fix: add connectivity mock
IvanStepanok Jan 14, 2026
00c6ecf
fix: prevent double unit opening
IvanStepanok Jan 19, 2026
83841c9
feat: move app level dates key to features config
IvanStepanok Jan 20, 2026
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
3 changes: 3 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ excluded: # paths to ignore during linting. Takes precedence over `included`.
- Carthage
- DerivedData
- Pods
- DerivedData
- Core/CoreTests
- Authorization/AuthorizationTests
- Course/CourseTests
Expand All @@ -30,6 +31,7 @@ excluded: # paths to ignore during linting. Takes precedence over `included`.
- Profile/ProfileTests
- WhatsNew/WhatsNewTests
- Theme/ThemeTests
- AppDates/AppDatesTests
- vendor
- Core/Core/SwiftGen
- Authorization/Authorization/SwiftGen
Expand All @@ -41,6 +43,7 @@ excluded: # paths to ignore during linting. Takes precedence over `included`.
- Profile/Profile/SwiftGen
- WhatsNew/WhatsNew/SwiftGen
- Theme/Theme/SwiftGen
- AppDates/AppDates/SwiftGen
# - Source/ExcludedFile.swift
# - Source/*/ExcludedFile.swift # Exclude files with a wildcard
#analyzer_rules: # Rules run by `swiftlint analyze` (experimental)
Expand Down
99 changes: 99 additions & 0 deletions AppDates/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore

## User settings
xcuserdata/*
/AppDates.xcodeproj/xcuserdata/
/AppDates.xcworkspace/xcuserdata/

## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
*.xcscmblueprint
*.xccheckout

## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
build/
DerivedData/
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3

## Obj-C/Swift specific
*.hmap

## App packaging
*.ipa
*.dSYM.zip
*.dSYM

## R.swift
R.generated.swift

## Playgrounds
timeline.xctimeline
playground.xcworkspace

# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
# *.xcodeproj
#
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm

.build/

# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
Pods/
#
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace

# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts

Carthage/Build/

# Accio dependency management
Dependencies/
.accio/

# fastlane
#
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control

fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output

# Code Injection
#
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode

iOSInjectionProject/

.DS_Store
.idea
xcode-frameworks
1,628 changes: 1,628 additions & 0 deletions AppDates/AppDates.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions AppDates/AppDates/AppDates.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// AppDates.h
// AppDates
//
// Created by Ivan Stepanok on 15.02.2025.
//

#import <Foundation/Foundation.h>

//! Project version number for AppDates.
FOUNDATION_EXPORT double AppDatesVersionNumber;

//! Project version string for AppDates.
FOUNDATION_EXPORT const unsigned char AppDatesVersionString[];

// In this header, you should import all the public headers of your framework using statements like #import <AppDates/PublicHeader.h>


28 changes: 28 additions & 0 deletions AppDates/AppDates/Data/DatesPersistenceProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// DatesPersistenceProtocol.swift
// AppDates
//
// Created by Ivan Stepanok on 15.02.2025.
//

import Foundation
import Core

//sourcery: AutoMockable
public protocol DatesPersistenceProtocol: Sendable {
func loadCourseDates(limit: Int?, offset: Int?) async throws -> [CourseDate]
func saveCourseDates(dates: [CourseDate], startIndex: Int) async
func clearAllCourseDates() async
}

#if DEBUG
public struct DatesPersistenceMock: DatesPersistenceProtocol {
public func loadCourseDates(limit: Int?, offset: Int?) async throws -> [CourseDate] {[]}
public func saveCourseDates(dates: [CourseDate], startIndex: Int) async {}
public func clearAllCourseDates() async {}
}
#endif

public final class AppDatesBundle {
private init() {}
}
184 changes: 184 additions & 0 deletions AppDates/AppDates/Data/DatesRepository.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
//
// DatesRepository.swift
// AppDates
//
// Created by Ivan Stepanok on 15.02.2025.
//

import Foundation
import Core
import OEXFoundation

public protocol DatesRepositoryProtocol: Sendable {
func getCourseDates(page: Int) async throws -> ([CourseDate], String?)
func getCourseDatesOffline(limit: Int?, offset: Int?) async throws -> [CourseDate]
func resetAllRelativeCourseDeadlines() async throws
}

public actor DatesRepository: DatesRepositoryProtocol {

private let api: API
private let storage: CoreStorage
private let config: ConfigProtocol
private let persistence: DatesPersistenceProtocol
private var totalItemsCount: Int = 0

public init(api: API, storage: CoreStorage, config: ConfigProtocol, persistence: DatesPersistenceProtocol) {
self.api = api
self.storage = storage
self.config = config
self.persistence = persistence
}

public func getCourseDates(page: Int) async throws -> ([CourseDate], String?) {
let response = try await api.requestData(
DatesEndpoint.getCourseDates(username: storage.user?.username ?? "", page: page)
)
.mapResponse(DataLayer.CourseDatesResponse.self)

let dates = response.domain

if page == 1 {
await persistence.clearAllCourseDates()
totalItemsCount = 0
}

let startIndex = totalItemsCount
let indexedDates = dates.enumerated().map { offset, date in
CourseDate(
date: date.date,
title: date.title,
courseName: date.courseName,
courseId: date.courseId,
blockId: date.blockId,
hasAccess: date.hasAccess,
order: startIndex + offset
)
}
totalItemsCount += indexedDates.count

await persistence.saveCourseDates(dates: indexedDates, startIndex: startIndex)

return (indexedDates, response.next)
}

public func getCourseDatesOffline(limit: Int? = nil, offset: Int? = nil) async throws -> [CourseDate] {
return try await persistence.loadCourseDates(limit: limit, offset: offset)
}

public func resetAllRelativeCourseDeadlines() async throws {
try await api.request(DatesEndpoint.resetAllRelativeCourseDeadlines)
}
}

// Mark - For testing and SwiftUI preview
#if DEBUG
public final class DatesRepositoryMock: DatesRepositoryProtocol {

public init() {}

public func getCourseDates(page: Int) async throws -> ([CourseDate], String?) {
let dates = [
CourseDate(
date: Date().addingTimeInterval(-86400 * 2),
title: "Assignment from the Day Before Yesterday",
courseName: "Course 6",
courseId: "course-v1:1+1+daybeforeyesterday",
blockId: "block-v1:1+1+daybeforeyesterday+type@sequential+block@assignment",
hasAccess: true
),
CourseDate(
date: Date().addingTimeInterval(-86400),
title: "Assignment from Yesterday",
courseName: "Course 7",
courseId: "course-v1:1+1+yesterday",
blockId: "block-v1:1+1+yesterday+type@sequential+block@assignment",
hasAccess: true
),
CourseDate(
date: Date().addingTimeInterval(60 * 10),
title: "Today's Assignment 1",
courseName: "Course 1",
courseId: "course-v1:1+1+today1",
blockId: "block-v1:1+1+today1+type@sequential+block@assignment1",
hasAccess: true
),
CourseDate(
date: Date().addingTimeInterval(60 * 20),
title: "Today's Assignment 2",
courseName: "Course 1",
courseId: "course-v1:1+1+today2",
blockId: "block-v1:1+1+today2+type@sequential+block@assignment2",
hasAccess: true
),
CourseDate(
date: Date().addingTimeInterval(86400), // 1 day
title: "Tomorrow's Assignment",
courseName: "Course 2",
courseId: "course-v1:1+1+tomorrow1",
blockId: "block-v1:1+1+tomorrow1+type@sequential+block@assignment3",
hasAccess: true
),
CourseDate(
date: Date().addingTimeInterval(86400 * 5),
title: "Assignment in 5 Days",
courseName: "Course 3",
courseId: "course-v1:1+1+5days1",
blockId: "block-v1:1+1+5days1+type@sequential+block@assignment4",
hasAccess: true
),
CourseDate(
date: Date().addingTimeInterval(86400 * 5),
title: "Assignment in 5 Days (Part 2)",
courseName: "Course 3",
courseId: "course-v1:1+1+5days2",
blockId: "block-v1:1+1+5days2+type@sequential+block@assignment5",
hasAccess: true
),
CourseDate(
date: Date().addingTimeInterval(86400 * 10),
title: "Assignment in 10 Days",
courseName: "Course 4",
courseId: "course-v1:1+1+10days1",
blockId: "block-v1:1+1+10days1+type@sequential+block@assignment6",
hasAccess: true
),
CourseDate(
date: Date().addingTimeInterval(86400 * 20),
title: "Assignment in 20 Days 1",
courseName: "Course 5",
courseId: "course-v1:1+1+20days1",
blockId: "block-v1:1+1+20days1+type@sequential+block@assignment7",
hasAccess: true
),
CourseDate(
date: Date().addingTimeInterval(86400 * 20),
title: "Assignment in 20 Days 2",
courseName: "Course 5",
courseId: "course-v1:1+1+20days2",
blockId: "block-v1:1+1+20days2+type@sequential+block@assignment8",
hasAccess: true
)
]

return (dates, nil)
}

public func getCourseDatesOffline(limit: Int? = nil, offset: Int? = nil) async throws -> [CourseDate] {
return [
CourseDate(
date: Date().addingTimeInterval(-86400 * 3),
title: "Offline Assignment",
courseName: "Cached Course",
courseId: "course-v1:1+1+offline",
blockId: "block-v1:1+1+offline+type@sequential+block@bafd854414124f6db42fee42ca8acc19",
hasAccess: true
)
]
}

public func resetAllRelativeCourseDeadlines() async throws {}

public func clearAllCourseDates() async {}
}
#endif
Loading
Loading