diff --git a/Classes/Repository/RepositoryOverviewHeaderViewModels.swift b/Classes/Repository/RepositoryOverviewHeaderViewModels.swift new file mode 100644 index 000000000..76fd629a6 --- /dev/null +++ b/Classes/Repository/RepositoryOverviewHeaderViewModels.swift @@ -0,0 +1,62 @@ +// +// RepositoryOverviewHeaderViewModels.swift +// Freetime +// +// Created by Viktoras Laukevicius on 23/01/2019. +// Copyright © 2019 Ryan Nystrom. All rights reserved. +// + +import UIKit +import IGListKit +import StyledTextKit + +struct RepositoryOverviewHeaderModel { + let watchersCount: Int + let starsCount: Int + let forksCount: Int +} + +private extension Int { + var localizedDecimal: String { + return NumberFormatter.localizedString(from: NSNumber(value: self), number: .decimal) + } +} + +func RepositoryOverviewHeaderViewModels( + model: RepositoryOverviewHeaderModel, + width: CGFloat, + contentSizeCategory: UIContentSizeCategory + ) -> [ListDiffable] { + let watchersCount = model.watchersCount.localizedDecimal + let starsCount = model.starsCount.localizedDecimal + let forksCount = model.forksCount.localizedDecimal + let iconsStyle = Styles.Text.h2 + let textStyle = Styles.Text.secondaryBold + let builder = StyledTextBuilder(styledText: StyledText( + style: iconsStyle.with(foreground: Styles.Colors.Gray.medium.color) + )) + .save() + let baselineOffset = (iconsStyle.preferredFont.lineHeight - textStyle.preferredFont.lineHeight) / 4.0 + let elements = [ + (UIImage(named: "eye")!, watchersCount), + (UIImage(named: "star")!, starsCount), + (UIImage(named: "repo-forked")!, forksCount) + ] + for (image, countText) in elements { + builder + .add(image: image) + .add(styledText: StyledText(text: " \(countText) ", style: textStyle.with(attributes: [ + .baselineOffset: baselineOffset + ]) + )) + .restore() + .save() + } + + let hdr = StyledTextRenderer( + string: builder.build(), + contentSizeCategory: contentSizeCategory, + inset: .zero + ).warm(width: width) + return [hdr] +} diff --git a/Classes/Repository/RepositoryOverviewViewController.swift b/Classes/Repository/RepositoryOverviewViewController.swift index 36fe39d87..ce77c72e9 100644 --- a/Classes/Repository/RepositoryOverviewViewController.swift +++ b/Classes/Repository/RepositoryOverviewViewController.swift @@ -22,11 +22,13 @@ IndicatorInfoProvider { private let client: RepositoryClient private var readme: RepositoryReadmeModel? private var branch: String + private let headerModel: RepositoryOverviewHeaderModel - init(client: GithubClient, repo: RepositoryDetails, branch: String) { + init(client: GithubClient, repo: RepositoryDetails, branch: String, headerModel: RepositoryOverviewHeaderModel) { self.repo = repo self.branch = branch self.client = RepositoryClient(githubClient: client, owner: repo.owner, name: repo.name) + self.headerModel = headerModel super.init( emptyErrorMessage: NSLocalizedString("Cannot load README.", comment: "") ) @@ -57,6 +59,7 @@ IndicatorInfoProvider { let width = view.safeContentWidth(with: feed.collectionView) let contentSizeCategory = UIContentSizeCategory.preferred let branch = self.branch + let headerModel = self.headerModel client.githubClient.client .send(V3RepositoryReadmeRequest(owner: repo.owner, repo: repo.name, branch: branch)) { [weak self] result in @@ -64,6 +67,11 @@ IndicatorInfoProvider { case .success(let response): DispatchQueue.global().async { + let header = RepositoryOverviewHeaderViewModels( + model: headerModel, + width: width, + contentSizeCategory: contentSizeCategory + ) let models = MarkdownModels( response.data.content, owner: repo.owner, @@ -74,7 +82,7 @@ IndicatorInfoProvider { isRoot: false, branch: branch ) - let model = RepositoryReadmeModel(models: models) + let model = RepositoryReadmeModel(models: header + models) DispatchQueue.main.async { [weak self] in self?.readme = model self?.update(animated: trueUnlessReduceMotionEnabled) diff --git a/Classes/Repository/RepositoryViewController.swift b/Classes/Repository/RepositoryViewController.swift index 474badc4e..61e1b0b3b 100644 --- a/Classes/Repository/RepositoryViewController.swift +++ b/Classes/Repository/RepositoryViewController.swift @@ -22,6 +22,7 @@ EmptyViewDelegate { let hasIssuesEnabled: Bool let defaultBranch: String let graphQLID: String + let headerModel: RepositoryOverviewHeaderModel } private enum State { @@ -128,7 +129,8 @@ EmptyViewDelegate { controllers.append(RepositoryOverviewViewController( client: client, repo: repo, - branch: branch ?? details.defaultBranch + branch: branch ?? details.defaultBranch, + headerModel: details.headerModel )) if details.hasIssuesEnabled { controllers.append(RepositoryIssuesViewController( @@ -173,10 +175,16 @@ EmptyViewDelegate { Squawk.show(error: error) case .success(let data): if let repo = data.repository { + let headerModel = RepositoryOverviewHeaderModel( + watchersCount: repo.watchers.totalCount, + starsCount: repo.stargazers.totalCount, + forksCount: repo.forkCount + ) let details = Details( hasIssuesEnabled: repo.hasIssuesEnabled, defaultBranch: repo.defaultBranchRef?.name ?? "master", - graphQLID: repo.id + graphQLID: repo.id, + headerModel: headerModel ) self?.state = .value(details) } else { diff --git a/Freetime.xcodeproj/project.pbxproj b/Freetime.xcodeproj/project.pbxproj index 6b545a431..a4f474930 100644 --- a/Freetime.xcodeproj/project.pbxproj +++ b/Freetime.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 03A0EC6321F8F60B008C9CE2 /* RepositoryOverviewHeaderViewModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A0EC6221F8F60B008C9CE2 /* RepositoryOverviewHeaderViewModels.swift */; }; 0F9440FD32236514CD7215E9 /* Pods_Freetime.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ECF622FFD773FDA73297C0D0 /* Pods_Freetime.framework */; }; 15F28F992108DBA6006421B6 /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15F28F982108DBA6006421B6 /* SplashView.swift */; }; 290056F3210028B20046EAE5 /* UIViewController+MenuDone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 290056F2210028B20046EAE5 /* UIViewController+MenuDone.swift */; }; @@ -593,6 +594,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 03A0EC6221F8F60B008C9CE2 /* RepositoryOverviewHeaderViewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryOverviewHeaderViewModels.swift; sourceTree = ""; }; 07C7DC5A7A907BE73BCA95AC /* Pods-FreetimeTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FreetimeTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-FreetimeTests/Pods-FreetimeTests.debug.xcconfig"; sourceTree = ""; }; 0F26A46A43D11F8D04E17D4A /* Pods-FreetimeWatch Extension.testflight.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FreetimeWatch Extension.testflight.xcconfig"; path = "Pods/Target Support Files/Pods-FreetimeWatch Extension/Pods-FreetimeWatch Extension.testflight.xcconfig"; sourceTree = ""; }; 15F28F982108DBA6006421B6 /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = ""; }; @@ -2249,6 +2251,7 @@ 986B87331F2CAE9800AAB55C /* RepositoryIssueSummaryType.swift */, 2905AFAC1F7357C50015AE32 /* RepositoryIssuesViewController.swift */, 29FE635E21AE2E2F00A07A86 /* RepositoryLoadingViewController.swift */, + 03A0EC6221F8F60B008C9CE2 /* RepositoryOverviewHeaderViewModels.swift */, 2905AFAA1F7357B40015AE32 /* RepositoryOverviewViewController.swift */, 29EDFE811F661562005BCCEB /* RepositoryReadmeModel.swift */, 29EDFE831F661776005BCCEB /* RepositoryReadmeSectionController.swift */, @@ -2981,6 +2984,7 @@ 986B87191F2B875800AAB55C /* GithubClient+Search.swift in Sources */, 29C0E7071ECBC6C50051D756 /* GithubClient.swift in Sources */, 15F28F992108DBA6006421B6 /* SplashView.swift in Sources */, + 03A0EC6321F8F60B008C9CE2 /* RepositoryOverviewHeaderViewModels.swift in Sources */, 2986B35A1FD30F0B00E3CFC6 /* IssueManagingModel.swift in Sources */, 2981A8A41EFE9FC700E25EF1 /* GithubEmoji.swift in Sources */, 2924C18B20D5B3A100FCFCFF /* LabelMenuCell.swift in Sources */, diff --git a/gql/API.swift b/gql/API.swift index e20f93b8c..fc1085ea6 100644 --- a/gql/API.swift +++ b/gql/API.swift @@ -16961,7 +16961,7 @@ public final class RepoSearchPagesQuery: GraphQLQuery { public final class RepositoryInfoQuery: GraphQLQuery { public static let operationString = - "query RepositoryInfo($owner: String!, $name: String!) {\n repository(owner: $owner, name: $name) {\n __typename\n id\n defaultBranchRef {\n __typename\n name\n }\n hasIssuesEnabled\n }\n}" + "query RepositoryInfo($owner: String!, $name: String!) {\n repository(owner: $owner, name: $name) {\n __typename\n id\n defaultBranchRef {\n __typename\n name\n }\n hasIssuesEnabled\n watchers {\n __typename\n totalCount\n }\n stargazers {\n __typename\n totalCount\n }\n forkCount\n }\n}" public var owner: String public var name: String @@ -17010,6 +17010,9 @@ public final class RepositoryInfoQuery: GraphQLQuery { GraphQLField("id", type: .nonNull(.scalar(GraphQLID.self))), GraphQLField("defaultBranchRef", type: .object(DefaultBranchRef.selections)), GraphQLField("hasIssuesEnabled", type: .nonNull(.scalar(Bool.self))), + GraphQLField("watchers", type: .nonNull(.object(Watcher.selections))), + GraphQLField("stargazers", type: .nonNull(.object(Stargazer.selections))), + GraphQLField("forkCount", type: .nonNull(.scalar(Int.self))), ] public var snapshot: Snapshot @@ -17018,8 +17021,8 @@ public final class RepositoryInfoQuery: GraphQLQuery { self.snapshot = snapshot } - public init(id: GraphQLID, defaultBranchRef: DefaultBranchRef? = nil, hasIssuesEnabled: Bool) { - self.init(snapshot: ["__typename": "Repository", "id": id, "defaultBranchRef": defaultBranchRef.flatMap { (value: DefaultBranchRef) -> Snapshot in value.snapshot }, "hasIssuesEnabled": hasIssuesEnabled]) + public init(id: GraphQLID, defaultBranchRef: DefaultBranchRef? = nil, hasIssuesEnabled: Bool, watchers: Watcher, stargazers: Stargazer, forkCount: Int) { + self.init(snapshot: ["__typename": "Repository", "id": id, "defaultBranchRef": defaultBranchRef.flatMap { (value: DefaultBranchRef) -> Snapshot in value.snapshot }, "hasIssuesEnabled": hasIssuesEnabled, "watchers": watchers.snapshot, "stargazers": stargazers.snapshot, "forkCount": forkCount]) } public var __typename: String { @@ -17060,6 +17063,36 @@ public final class RepositoryInfoQuery: GraphQLQuery { } } + /// A list of users watching the repository. + public var watchers: Watcher { + get { + return Watcher(snapshot: snapshot["watchers"]! as! Snapshot) + } + set { + snapshot.updateValue(newValue.snapshot, forKey: "watchers") + } + } + + /// A list of users who have starred this starrable. + public var stargazers: Stargazer { + get { + return Stargazer(snapshot: snapshot["stargazers"]! as! Snapshot) + } + set { + snapshot.updateValue(newValue.snapshot, forKey: "stargazers") + } + } + + /// Returns how many forks there are of this repository in the whole network. + public var forkCount: Int { + get { + return snapshot["forkCount"]! as! Int + } + set { + snapshot.updateValue(newValue, forKey: "forkCount") + } + } + public struct DefaultBranchRef: GraphQLSelectionSet { public static let possibleTypes = ["Ref"] @@ -17097,6 +17130,82 @@ public final class RepositoryInfoQuery: GraphQLQuery { } } } + + public struct Watcher: GraphQLSelectionSet { + public static let possibleTypes = ["UserConnection"] + + public static let selections: [GraphQLSelection] = [ + GraphQLField("__typename", type: .nonNull(.scalar(String.self))), + GraphQLField("totalCount", type: .nonNull(.scalar(Int.self))), + ] + + public var snapshot: Snapshot + + public init(snapshot: Snapshot) { + self.snapshot = snapshot + } + + public init(totalCount: Int) { + self.init(snapshot: ["__typename": "UserConnection", "totalCount": totalCount]) + } + + public var __typename: String { + get { + return snapshot["__typename"]! as! String + } + set { + snapshot.updateValue(newValue, forKey: "__typename") + } + } + + /// Identifies the total count of items in the connection. + public var totalCount: Int { + get { + return snapshot["totalCount"]! as! Int + } + set { + snapshot.updateValue(newValue, forKey: "totalCount") + } + } + } + + public struct Stargazer: GraphQLSelectionSet { + public static let possibleTypes = ["StargazerConnection"] + + public static let selections: [GraphQLSelection] = [ + GraphQLField("__typename", type: .nonNull(.scalar(String.self))), + GraphQLField("totalCount", type: .nonNull(.scalar(Int.self))), + ] + + public var snapshot: Snapshot + + public init(snapshot: Snapshot) { + self.snapshot = snapshot + } + + public init(totalCount: Int) { + self.init(snapshot: ["__typename": "StargazerConnection", "totalCount": totalCount]) + } + + public var __typename: String { + get { + return snapshot["__typename"]! as! String + } + set { + snapshot.updateValue(newValue, forKey: "__typename") + } + } + + /// Identifies the total count of items in the connection. + public var totalCount: Int { + get { + return snapshot["totalCount"]! as! Int + } + set { + snapshot.updateValue(newValue, forKey: "totalCount") + } + } + } } } } diff --git a/gql/RepositoryInfo.graphql b/gql/RepositoryInfo.graphql index 0d60fec56..aff2927a2 100644 --- a/gql/RepositoryInfo.graphql +++ b/gql/RepositoryInfo.graphql @@ -5,5 +5,12 @@ query RepositoryInfo($owner: String!, $name: String!) { name } hasIssuesEnabled + watchers { + totalCount + } + stargazers { + totalCount + } + forkCount } }