diff --git a/GitTime/Sources/CompositionRoot.swift b/GitTime/Sources/CompositionRoot.swift index d717f02..0b4d6c6 100644 --- a/GitTime/Sources/CompositionRoot.swift +++ b/GitTime/Sources/CompositionRoot.swift @@ -70,7 +70,9 @@ final class CompositionRoot { let activityController = configureActivityScreen(activityService: activityService, userService: userService, - crawlerService: crawlerService) + crawlerService: crawlerService, + keychainService: keychainService + ) let trendController = configureTrendingScreen(crawlerService: crawlerService, languagesService: languageService, @@ -163,11 +165,14 @@ extension CompositionRoot { static func configureActivityScreen( activityService: ActivityServiceType, userService: UserServiceType, - crawlerService: GitTimeCrawlerServiceType + crawlerService: GitTimeCrawlerServiceType, + keychainService: KeychainServiceType ) -> ActivityViewController { let reactor = ActivityViewReactor(activityService: activityService, userService: userService, - crawlerService: crawlerService) + crawlerService: crawlerService, + keychainService: keychainService + ) let controller = ActivityViewController(reactor: reactor) controller.title = "Activity" controller.tabBarItem.title = "Activity" diff --git a/GitTime/Sources/Models/Event.swift b/GitTime/Sources/Models/Event.swift index 8998093..4a3a3f4 100644 --- a/GitTime/Sources/Models/Event.swift +++ b/GitTime/Sources/Models/Event.swift @@ -73,8 +73,7 @@ struct Event: ModelType { repo = try container.decode(RepositoryInfo.self, forKey: .repo) isPublic = try container.decode(Bool.self, forKey: .isPublic) let dateString = try container.decode(String.self, forKey: .createdAt) - let df = DateFormatter() - df.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" + let df = ISO8601DateFormatter() createdAt = df.date(from: dateString) ?? Date() switch type { diff --git a/GitTime/Sources/Models/Me.swift b/GitTime/Sources/Models/Me.swift index 5e71f17..9af714d 100644 --- a/GitTime/Sources/Models/Me.swift +++ b/GitTime/Sources/Models/Me.swift @@ -11,6 +11,7 @@ import Foundation struct Me: ModelType { let id: Int let name: String + let additionalName: String let profileURL: String let url: String let bio: String? @@ -23,6 +24,7 @@ struct Me: ModelType { enum CodingKeys: String, CodingKey { case id case name = "login" + case additionalName = "name" case profileURL = "avatar_url" case url = "html_url" case bio @@ -32,4 +34,20 @@ struct Me: ModelType { case followers case following } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + id = try container.decode(Int.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + additionalName = try container.decodeIfPresent(String.self, forKey: .additionalName) ?? "" + profileURL = try container.decodeIfPresent(String.self, forKey: .profileURL) ?? "" + url = try container.decode(String.self, forKey: .url) + bio = try? container.decode(String.self, forKey: .bio) + location = try? container.decode(String.self, forKey: .location) + publicRepos = try? container.decode(Int.self, forKey: .publicRepos) + privateRepos = try? container.decode(Int.self, forKey: .privateRepos) + followers = try? container.decode(Int.self, forKey: .followers) + following = try? container.decode(Int.self, forKey: .following) + } } diff --git a/GitTime/Sources/Models/Payload.swift b/GitTime/Sources/Models/Payload.swift index cfecffa..1da74c6 100644 --- a/GitTime/Sources/Models/Payload.swift +++ b/GitTime/Sources/Models/Payload.swift @@ -259,8 +259,7 @@ struct Comment: ModelType { body = try container.decode(String.self, forKey: .body) let dateString = try container.decode(String.self, forKey: .createdAt) - let df = DateFormatter() - df.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" + let df = ISO8601DateFormatter() createdAt = df.date(from: dateString) ?? Date() } diff --git a/GitTime/Sources/Services/ActivityService.swift b/GitTime/Sources/Services/ActivityService.swift index a7d4b49..a9de30d 100644 --- a/GitTime/Sources/Services/ActivityService.swift +++ b/GitTime/Sources/Services/ActivityService.swift @@ -26,6 +26,11 @@ class ActivityService: ActivityServiceType { func fetchActivities(userName: String, page: Int) -> Observable<[Event]> { return self.networking.request(.activityEvent(userName: userName, page: page)) .map([Event].self) + .do(onError: { error in + if let decodingErrorInfo = error.decodingErrorInfo { + log.error(decodingErrorInfo) + } + }) .asObservable() } diff --git a/GitTime/Sources/Utils/GitTimeDecodingError.swift b/GitTime/Sources/Utils/GitTimeDecodingError.swift new file mode 100644 index 0000000..ba3f83b --- /dev/null +++ b/GitTime/Sources/Utils/GitTimeDecodingError.swift @@ -0,0 +1,79 @@ +// +// GitTimeDecodingError.swift +// GitTime +// +// Created by Kanz on 4/13/24. +// + +// https://gist.github.com/nunogoncalves/4852077f4e576872f72b70d9e79942f3 + +import Foundation + +import Moya + +extension Error { + var decodingErrorInfo: DecodingErrorInfomation? { + if let moyaError = self as? MoyaError { + if case .objectMapping(let error, let response) = moyaError { + if let decodeError = error as? DecodingError, + let path = response.request?.url?.getPath() { + return DecodingErrorInfomation( + path: path, + error: GitTimeDecodingError(with: decodeError) + ) + } + } + } + return nil + } +} + +struct DecodingErrorInfomation { + let path: String + let error: GitTimeDecodingError +} + +enum GitTimeDecodingError: CustomStringConvertible { + case dataCorrupted(_ message: String) + case keyNotFound(_ message: String) + case typeMismatch(_ message: String) + case valueNotFound(_ message: String) + case any(_ error: Error) + + init(with error: DecodingError) { + switch error { + case let .dataCorrupted(context): + let debugDescription = (context.underlyingError as NSError?)?.userInfo["NSDebugDescription"] ?? "" + self = .dataCorrupted("Data corrupted. \(context.debugDescription) \(debugDescription)") + case let .keyNotFound(key, context): + self = .keyNotFound("Key not found. Expected -> \(key.stringValue) <- at: \(context.prettyPath())") + case let .typeMismatch(_, context): + self = .typeMismatch("Type mismatch. \(context.debugDescription), at: \(context.prettyPath())") + case let .valueNotFound(_, context): + self = .valueNotFound("Value not found. -> \(context.prettyPath()) <- \(context.debugDescription)") + default: + self = .any(error) + } + } + var description: String { + switch self { + case let .dataCorrupted(message), let .keyNotFound(message), let .typeMismatch(message), let .valueNotFound(message): + return message + case let .any(error): + return error.localizedDescription + } + } +} + +extension DecodingError.Context { + func prettyPath(separatedBy separator: String = ".") -> String { + codingPath.map { $0.stringValue }.joined(separator: ".") + } +} + +extension URL { + func getPath() -> String? { + let components = URLComponents(url: self, resolvingAgainstBaseURL: false) + return components?.path + } +} diff --git a/GitTime/Sources/ViewControllers/Activity/ActivityViewReactor.swift b/GitTime/Sources/ViewControllers/Activity/ActivityViewReactor.swift index 247d45a..87cf8bd 100644 --- a/GitTime/Sources/ViewControllers/Activity/ActivityViewReactor.swift +++ b/GitTime/Sources/ViewControllers/Activity/ActivityViewReactor.swift @@ -8,6 +8,7 @@ import UIKit +import GitHubKit import Moya import ReactorKit import RxCocoa @@ -60,17 +61,21 @@ final class ActivityViewReactor: ReactorKit.Reactor { fileprivate let activityService: ActivityServiceType fileprivate let userService: UserServiceType fileprivate let crawlerService: GitTimeCrawlerServiceType + fileprivate let keychainService: KeychainServiceType private let imageDownloder = ImageDownloader(name: "profileImageDownloder") init( activityService: ActivityServiceType, userService: UserServiceType, - crawlerService: GitTimeCrawlerServiceType + crawlerService: GitTimeCrawlerServiceType, + keychainService: KeychainServiceType ) { self.activityService = activityService self.userService = userService self.crawlerService = crawlerService + self.keychainService = keychainService + self.initialState = State(isLoading: false, page: ActivityViewReactor.INITIAL_PAGE, canLoadMore: true, @@ -192,6 +197,27 @@ final class ActivityViewReactor: ReactorKit.Reactor { guard let me = GlobalStates.shared.currentUser.value else { return .empty() } + return self.fetchContributions() + .observe(on: MainScheduler.instance) + .map { userContribution -> ContributionInfo in + return ContributionInfo( + count: userContribution.totalContributions, + contributions: userContribution.contributions.map { + Contribution( + date: $0.date, + contribution: $0.contributionCount, + hexColor: $0.color + ) + }, + userName: me.name, + additionalName: me.additionalName, + profileImageURL: me.profileURL + ) + } + .flatMap { response -> Observable in + return .just(.setContributionInfo(response)) + } + /* return self.crawlerService.fetchContributionsRawdata(userName: me.name) .map { response -> Mutation in let contributionInfo = self.parseContribution(response: response) @@ -203,6 +229,30 @@ final class ActivityViewReactor: ReactorKit.Reactor { .map { contributionInfo -> Mutation in return .setContributionInfo(contributionInfo)} } + */ + } + + private func fetchContributions() -> Observable { + guard let accessToken = keychainService.getAccessToken() else { return .empty() } + guard let me = GlobalStates.shared.currentUser.value else { return .empty() } + + let githubKit = GitHubKit(config: .init(token: accessToken)) + + return Observable.create { observer -> Disposable in + async { + do { + let contribution = try await githubKit.contributions(userName: me.name) + observer.onNext(contribution) + observer.onCompleted() + } catch { + observer.onError(error) + } + } + + return Disposables.create { + + } + } } private func requestTrialContributions() -> Observable { diff --git a/GitTime/Sources/ViewControllers/Login/LoginViewReactor.swift b/GitTime/Sources/ViewControllers/Login/LoginViewReactor.swift index e41dd4d..b25a766 100644 --- a/GitTime/Sources/ViewControllers/Login/LoginViewReactor.swift +++ b/GitTime/Sources/ViewControllers/Login/LoginViewReactor.swift @@ -6,7 +6,6 @@ // Copyright © 2019 KanzDevelop. All rights reserved. // -import FirebaseRemoteConfig import ReactorKit import RxCocoa import RxSwift @@ -33,7 +32,6 @@ final class LoginViewReactor: Reactor { fileprivate let authService: AuthServiceType fileprivate let keychainService: KeychainServiceType fileprivate let userService: UserServiceType - var remoteConfig: RemoteConfig! init(authService: AuthServiceType, keychainService: KeychainServiceType, diff --git a/GitTime/Sources/Views/Activity/ActivityItemCell.swift b/GitTime/Sources/Views/Activity/ActivityItemCell.swift index 073bff6..df68f4c 100644 --- a/GitTime/Sources/Views/Activity/ActivityItemCell.swift +++ b/GitTime/Sources/Views/Activity/ActivityItemCell.swift @@ -157,8 +157,8 @@ final class ActivityItemCell: BaseTableViewCell, ReactorKit.View { let actorProfile = state.event.actor.profileURL if let actorProfileURL = URL(string: actorProfile) { let cache = ImageCache.default - cache.memoryStorage.config.expiration = .seconds(2) - authorProfileImageView.kf.setImage(with: actorProfileURL, options: [.memoryCacheExpiration(.seconds(5))]) + cache.memoryStorage.config.expiration = .days(1) + authorProfileImageView.kf.setImage(with: actorProfileURL, options: [.memoryCacheExpiration(.days(1))]) } let actorName = state.event.actor.name diff --git a/GitTime/Supporting Files/Info.plist b/GitTime/Supporting Files/Info.plist index da0a7f9..d6fd517 100644 --- a/GitTime/Supporting Files/Info.plist +++ b/GitTime/Supporting Files/Info.plist @@ -85,7 +85,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.2.1 + 2.2.3 CFBundleURLTypes @@ -100,7 +100,7 @@ CFBundleVersion - 9 + 11 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/project.yml b/project.yml index 5da4d17..886c4d0 100644 --- a/project.yml +++ b/project.yml @@ -2,7 +2,7 @@ name: GitTime options: bundleIdPrefix: io.github.87kangsw deploymentTarget: - iOS: '13.0' + iOS: '14.0' usesTabs: true indentWidth: 4 tabWidth: 4 @@ -57,13 +57,16 @@ packages: Toaster: url: https://github.com/devxoul/Toaster.git branch: master + GitHubKit: + url: https://github.com/87kangsw/GitHubKit + from: 1.0.0 fileGroups: - GitTime/Supporting Files targets: GitTime: platform: iOS type: application - deploymentTarget: '13.0' + deploymentTarget: '14.0' entitilements: path: GitTime/Supporting Files/GitTime.entitlements scheme: @@ -118,6 +121,7 @@ targets: - package: Firebase product: FirebasePerformance - package: Toaster + - package: GitHubKit GitTimeTests: platform: iOS type: bundle.unit-test