diff --git a/Classes/History/Client+History.swift b/Classes/History/Client+History.swift new file mode 100644 index 000000000..98e228cad --- /dev/null +++ b/Classes/History/Client+History.swift @@ -0,0 +1,52 @@ +// +// Client+History.swift +// Freetime +// +// Created by Ryan Nystrom on 10/20/18. +// Copyright © 2018 Ryan Nystrom. All rights reserved. +// + +import Foundation +import GitHubAPI + +extension Client { + + func fetchHistory( + owner: String, + repo: String, + branch: String, + path: String?, + cursor: String?, + width: CGFloat, + contentSizeCategory: UIContentSizeCategory, + completion: @escaping (Result<([PathCommitModel], String?)>) -> Void + ) { + query( + RepoFileHistoryQuery( + owner: owner, + name: repo, + branch: branch, + path: path, + after: cursor, + page_size: 20 + ), + result: { $0 }, + completion: { result in + switch result { + case .failure(let error): + completion(.error(error)) + case .success(let data): + let commits = data.commits(width: width, contentSizeCategory: contentSizeCategory) + let nextPage: String? + if let pageInfo = data.repository?.object?.asCommit?.history.pageInfo, + pageInfo.hasNextPage { + nextPage = pageInfo.endCursor + } else { + nextPage = nil + } + completion(.success((commits, nextPage))) + } + }) + } + +} diff --git a/Classes/History/PathCommitCell.swift b/Classes/History/PathCommitCell.swift new file mode 100644 index 000000000..82fcb4308 --- /dev/null +++ b/Classes/History/PathCommitCell.swift @@ -0,0 +1,56 @@ +// +// PathCommitCell.swift +// Freetime +// +// Created by Ryan Nystrom on 10/20/18. +// Copyright © 2018 Ryan Nystrom. All rights reserved. +// + +import UIKit +import SnapKit + +final class PathCommitCell: SelectableCell { + + static let inset = UIEdgeInsets( + top: Styles.Sizes.rowSpacing, + left: Styles.Sizes.gutter, + bottom: Styles.Sizes.rowSpacing, + right: Styles.Sizes.gutter + Styles.Sizes.columnSpacing + Styles.Sizes.icon.width + ) + + private let textView = MarkdownStyledTextView() + private let disclosureImageView = UIImageView(image: UIImage(named: "chevron-right")?.withRenderingMode(.alwaysTemplate)) + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = .white + contentView.addSubview(textView) + + disclosureImageView.tintColor = Styles.Colors.Gray.light.color + contentView.addSubview(disclosureImageView) + + disclosureImageView.snp.makeConstraints { make in + make.right.equalTo(-Styles.Sizes.gutter) + make.centerY.equalToSuperview() + } + + addBorder(.bottom, left: Styles.Sizes.gutter) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + textView.reposition(for: contentView.bounds.width) + } + + // MARK: Public API + + func configure(with model: PathCommitModel) { + textView.configure(with: model.text, width: contentView.bounds.width) + } + +} diff --git a/Classes/History/PathCommitModel.swift b/Classes/History/PathCommitModel.swift new file mode 100644 index 000000000..e4dcda436 --- /dev/null +++ b/Classes/History/PathCommitModel.swift @@ -0,0 +1,30 @@ +// +// PathCommitModel.swift +// Freetime +// +// Created by Ryan Nystrom on 10/20/18. +// Copyright © 2018 Ryan Nystrom. All rights reserved. +// + +import Foundation +import IGListKit +import StyledTextKit + +struct PathCommitModel: ListSwiftDiffable { + + let oid: String + let text: StyledTextRenderer + let commitURL: URL + + // MARK: ListSwiftDiffable + + var identifier: String { + return oid + } + + func isEqual(to value: ListSwiftDiffable) -> Bool { + guard let value = value as? PathCommitModel else { return false } + return text.string == value.text.string + } + +} diff --git a/Classes/History/PathCommitSectionController.swift b/Classes/History/PathCommitSectionController.swift new file mode 100644 index 000000000..c2434a96f --- /dev/null +++ b/Classes/History/PathCommitSectionController.swift @@ -0,0 +1,37 @@ +// +// PathCommitSectionController.swift +// Freetime +// +// Created by Ryan Nystrom on 10/20/18. +// Copyright © 2018 Ryan Nystrom. All rights reserved. +// + +import Foundation +import IGListKit + +final class PathCommitSectionController: ListSwiftSectionController { + + override func createBinders(from value: PathCommitModel) -> [ListBinder] { + return [ + binder( + value, + cellType: ListCellType.class(PathCommitCell.self), + size: { + return CGSize( + width: $0.collection.containerSize.width, + height: $0.value.text.viewSize(in: $0.collection.insetContainerSize.width).height + ) + }, + configure: { + $0.configure(with: $1.value) + }, + didSelect: { [weak self] context in + guard let `self` = self else { return } + context.deselect(animated: true) + self.viewController?.presentSafari(url: context.value.commitURL) + }) + ] + } + +} + diff --git a/Classes/History/PathHistoryViewController.swift b/Classes/History/PathHistoryViewController.swift new file mode 100644 index 000000000..8489c6914 --- /dev/null +++ b/Classes/History/PathHistoryViewController.swift @@ -0,0 +1,75 @@ +// +// PathHistoryViewController.swift +// Freetime +// +// Created by Ryan Nystrom on 10/20/18. +// Copyright © 2018 Ryan Nystrom. All rights reserved. +// + +import UIKit +import IGListKit +import Squawk + +final class PathHistoryViewController: BaseListViewController2, +BaseListViewController2DataSource { + + private let viewModel: PathHistoryViewModel + private var models = [PathCommitModel]() + + init(viewModel: PathHistoryViewModel) { + self.viewModel = viewModel + super.init(emptyErrorMessage: NSLocalizedString("Cannot load history.", comment: "")) + dataSource = self + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + let titleView = NavigationTitleDropdownView(chevronVisible: false) + titleView.configure( + title: NSLocalizedString("History", comment: ""), + subtitle: viewModel.path?.path + ) + navigationItem.titleView = titleView + } + + override func fetch(page: String?) { + // assumptions here, but the collectionview may not have been laid out or content size found + // assume the collectionview is pinned to the view's bounds + let contentInset = feed.collectionView.contentInset + let width = view.bounds.width - contentInset.left - contentInset.right + + viewModel.client.client.fetchHistory( + owner: viewModel.owner, + repo: viewModel.repo, + branch: viewModel.branch, + path: viewModel.path?.path, + cursor: page, + width: width, + contentSizeCategory: UIApplication.shared.preferredContentSizeCategory + ) { [weak self] result in + switch result { + case .error(let error): + Squawk.show(error: error) + case .success(let commits, let nextPage): + if page == nil { + self?.models = commits + } else { + self?.models += commits + } + self?.update(page: nextPage, animated: trueUnlessReduceMotionEnabled) + } + } + } + + // MARK: BaseListViewController2DataSource + + func models(adapter: ListSwiftAdapter) -> [ListSwiftPair] { + return models.map { ListSwiftPair.pair($0, { PathCommitSectionController() }) } + } + +} diff --git a/Classes/History/PathHistoryViewModel.swift b/Classes/History/PathHistoryViewModel.swift new file mode 100644 index 000000000..72584d915 --- /dev/null +++ b/Classes/History/PathHistoryViewModel.swift @@ -0,0 +1,19 @@ +// +// PathHistoryViewModel.swift +// Freetime +// +// Created by Ryan Nystrom on 10/20/18. +// Copyright © 2018 Ryan Nystrom. All rights reserved. +// + +import Foundation + +struct PathHistoryViewModel { + + let owner: String + let repo: String + let client: GithubClient + let branch: String + let path: FilePath? + +} diff --git a/Classes/History/RepoFileHistoryQueryDataToPathHistoryViewModel.swift b/Classes/History/RepoFileHistoryQueryDataToPathHistoryViewModel.swift new file mode 100644 index 000000000..195d2fa00 --- /dev/null +++ b/Classes/History/RepoFileHistoryQueryDataToPathHistoryViewModel.swift @@ -0,0 +1,75 @@ +// +// HistoryGraphQLToPathCommitModel.swift +// Freetime +// +// Created by Ryan Nystrom on 10/20/18. +// Copyright © 2018 Ryan Nystrom. All rights reserved. +// + +import Foundation +import StyledTextKit + +extension RepoFileHistoryQuery.Data { + + func commits( + width: CGFloat, + contentSizeCategory: UIContentSizeCategory + ) -> [PathCommitModel] { + guard let nodes = repository?.object?.asCommit?.history.nodes + else { return [] } + + return nodes.compactMap { + guard let model = $0, + let author = model.author?.user?.login, + let date = model.committedDate.githubDate, + let url = URL(string: model.url) + else { return nil } + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.paragraphSpacing = 12 + paragraphStyle.lineSpacing = 2 + let attributes: [NSAttributedStringKey: Any] = [ + .foregroundColor: Styles.Colors.Gray.dark.color, + .paragraphStyle: paragraphStyle, + .backgroundColor: UIColor.white + ] + + let builder = StyledTextBuilder(styledText: StyledText( + style: Styles.Text.bodyBold.with(attributes: attributes) + )) + .add(text: "\(model.message.firstLine)\n") + .add(style: Styles.Text.secondary.with(foreground: Styles.Colors.Gray.medium.color)) + .save() + .add(text: author, traits: [.traitBold]) + .restore() + + if let committer = model.committer?.user?.login { + builder.add(text: NSLocalizedString(" authored and ", comment: "")) + .save() + .add(text: committer, traits: [.traitBold]) + .restore() + } + + builder.add(text: NSLocalizedString(" committed ", comment: "")) + .save() + .add(styledText: StyledText( + text: model.oid.hashDisplay, + style: Styles.Text.secondaryCodeBold.with(foreground: Styles.Colors.Blue.medium.color) + )) + .restore() + .add(text: " \(date.agoString(.long))") + + return PathCommitModel( + oid: model.oid, + text: StyledTextRenderer( + string: builder.build(), + contentSizeCategory: contentSizeCategory, + inset: PathCommitCell.inset, + backgroundColor: .white + ).warm(width: width), + commitURL: url + ) + } + } + +} diff --git a/Classes/History/UIViewController+HistoryAction.swift b/Classes/History/UIViewController+HistoryAction.swift new file mode 100644 index 000000000..140d588a1 --- /dev/null +++ b/Classes/History/UIViewController+HistoryAction.swift @@ -0,0 +1,37 @@ +// +// UIViewController+HistoryAction.swift +// Freetime +// +// Created by Ryan Nystrom on 10/20/18. +// Copyright © 2018 Ryan Nystrom. All rights reserved. +// + +import UIKit + +extension UIViewController { + func viewHistoryAction( + owner: String, + repo: String, + branch: String, + client: GithubClient, + path: FilePath? = nil + ) -> UIAlertAction { + return UIAlertAction( + title: NSLocalizedString("View History", comment: ""), + style: .default + ) { [weak self] _ in + self?.navigationController?.pushViewController( + PathHistoryViewController( + viewModel: PathHistoryViewModel( + owner: owner, + repo: repo, + client: client, + branch: branch, + path: path + ) + ), + animated: trueUnlessReduceMotionEnabled + ) + } + } +} diff --git a/Classes/Issues/IssuesViewController.swift b/Classes/Issues/IssuesViewController.swift index 2a60c1c83..d6a7a8ec7 100644 --- a/Classes/Issues/IssuesViewController.swift +++ b/Classes/Issues/IssuesViewController.swift @@ -9,7 +9,6 @@ import UIKit import IGListKit import TUSafariActivity -import SafariServices import SnapKit import FlatCache import MessageViewController diff --git a/Classes/Repository/RepositoryCodeBlobViewController.swift b/Classes/Repository/RepositoryCodeBlobViewController.swift index 40f3ccfb5..6eb86218c 100644 --- a/Classes/Repository/RepositoryCodeBlobViewController.swift +++ b/Classes/Repository/RepositoryCodeBlobViewController.swift @@ -109,6 +109,7 @@ final class RepositoryCodeBlobViewController: UIViewController, EmptyViewDelegat weak var weakSelf = self let alertBuilder = AlertActionBuilder { $0.rootViewController = weakSelf } var actions = [ + viewHistoryAction(owner: repo.owner, repo: repo.name, branch: branch, client: client, path: path), AlertAction(alertBuilder).share([path.path], activities: nil, type: .shareFilePath) { $0.popoverPresentationController?.setSourceView(sender) }, diff --git a/Classes/Repository/RepositoryCodeDirectoryViewController.swift b/Classes/Repository/RepositoryCodeDirectoryViewController.swift index 767031a44..4e0460bcb 100644 --- a/Classes/Repository/RepositoryCodeDirectoryViewController.swift +++ b/Classes/Repository/RepositoryCodeDirectoryViewController.swift @@ -93,6 +93,7 @@ RepositoryBranchUpdatable { weak var weakSelf = self let alertBuilder = AlertActionBuilder { $0.rootViewController = weakSelf } var actions = [ + viewHistoryAction(owner: repo.owner, repo: repo.name, branch: branch, client: client, path: path), AlertAction(alertBuilder).share([path.path], activities: nil, type: .shareFilePath) { $0.popoverPresentationController?.setSourceView(sender) }, diff --git a/Classes/Repository/RepositoryOverviewViewController.swift b/Classes/Repository/RepositoryOverviewViewController.swift index db92ddd70..986755a82 100644 --- a/Classes/Repository/RepositoryOverviewViewController.swift +++ b/Classes/Repository/RepositoryOverviewViewController.swift @@ -28,18 +28,6 @@ RepositoryBranchUpdatable { private var readme: RepositoryReadmeModel? private var branch: String -// lazy var _feed: Feed = { Feed( -// viewController: self, -// delegate: self, -// collectionView: HackScrollIndicatorInsetsCollectionView( -// frame: .zero, -// collectionViewLayout: ListCollectionViewLayout.basic() -// )) -// }() -// override var feed: Feed { -// return _feed -// } - init(client: GithubClient, repo: RepositoryDetails) { self.repo = repo self.client = RepositoryClient(githubClient: client, owner: repo.owner, name: repo.name) @@ -49,12 +37,6 @@ RepositoryBranchUpdatable { ) self.dataSource = self title = NSLocalizedString("Overview", comment: "") -// self.feed.collectionView.contentInset = UIEdgeInsets( -// top: Styles.Sizes.rowSpacing, -// left: Styles.Sizes.gutter, -// bottom: Styles.Sizes.rowSpacing, -// right: Styles.Sizes.gutter -// ) } required init?(coder aDecoder: NSCoder) { diff --git a/Classes/Repository/RepositoryViewController.swift b/Classes/Repository/RepositoryViewController.swift index 8c00ce073..76ba82c6e 100644 --- a/Classes/Repository/RepositoryViewController.swift +++ b/Classes/Repository/RepositoryViewController.swift @@ -10,7 +10,6 @@ import UIKit import Tabman import Pageboy import TUSafariActivity -import SafariServices import Squawk import ContextMenu @@ -25,12 +24,6 @@ ContextMenuDelegate { private var bookmarkNavController: BookmarkNavigationController? public private(set) var branch: String - var moreOptionsItem: UIBarButtonItem { - let rightItem = UIBarButtonItem(image: UIImage(named: "bullets-hollow"), target: self, action: #selector(RepositoryViewController.onMore(sender:))) - rightItem.accessibilityLabel = Constants.Strings.moreOptions - return rightItem - } - init(client: GithubClient, repo: RepositoryDetails) { self.repo = repo self.client = client @@ -79,11 +72,25 @@ ContextMenuDelegate { appearance.indicator.color = Styles.Colors.Blue.medium.color }) - configureNavigationItems() + let moreItem = UIBarButtonItem( + image: UIImage(named: "bullets-hollow"), + target: self, + action: #selector(RepositoryViewController.onMore(sender:)) + ) + moreItem.accessibilityLabel = Constants.Strings.moreOptions + var items = [moreItem] + if let bookmarkItem = bookmarkNavController?.navigationItem { + items.append(bookmarkItem) + } + navigationItem.rightBarButtonItems = items + let navigationTitle = NavigationTitleDropdownView() navigationItem.titleView = navigationTitle navigationTitle.addTarget(self, action: #selector(onNavigationTitle(sender:)), for: .touchUpInside) - let labelFormat = NSLocalizedString("Repository %@ by %@", comment: "Accessibility label for a repository navigation item") + let labelFormat = NSLocalizedString( + "Repository %@ by %@", + comment: "Accessibility label for a repository navigation item" + ) let accessibilityLabel = String(format: labelFormat, arguments: [repo.name, repo.owner]) navigationTitle.configure(title: repo.name, subtitle: repo.owner, accessibilityLabel: accessibilityLabel) } @@ -106,15 +113,7 @@ ContextMenuDelegate { return URL(string: "https://github.com/\(repo.owner)/\(repo.name)")! } - func configureNavigationItems() { - var items = [moreOptionsItem] - if let bookmarkItem = bookmarkNavController?.navigationItem { - items.append(bookmarkItem) - } - navigationItem.rightBarButtonItems = items - } - - func switchBranchAction() -> UIAlertAction { + var switchBranchAction: UIAlertAction { return UIAlertAction( title: NSLocalizedString("Switch Branch", comment: ""), style: .default @@ -167,11 +166,12 @@ ContextMenuDelegate { let alertBuilder = AlertActionBuilder { $0.rootViewController = weakSelf } alert.addActions([ + viewHistoryAction(owner: repo.owner, repo: repo.name, branch: branch, client: client), repo.hasIssuesEnabled ? newIssueAction() : nil, AlertAction(alertBuilder).share([repoUrl], activities: [TUSafariActivity()], type: .shareUrl) { $0.popoverPresentationController?.setSourceView(sender) }, - switchBranchAction(), + switchBranchAction, AlertAction.cancel() ]) alert.popoverPresentationController?.setSourceView(sender) diff --git a/Classes/Section Controllers/LoadMore/LoadMoreSectionController2.swift b/Classes/Section Controllers/LoadMore/LoadMoreSectionController2.swift index 72e9e8adc..07377d8f3 100644 --- a/Classes/Section Controllers/LoadMore/LoadMoreSectionController2.swift +++ b/Classes/Section Controllers/LoadMore/LoadMoreSectionController2.swift @@ -13,20 +13,44 @@ protocol LoadMoreSectionController2Delegate: class { func didSelect(controller: LoadMoreSectionController2) } +private struct LoadMoreModel: ListSwiftDiffable { + let loading: Bool + + var identifier: String { + return "loading" + } + + func isEqual(to value: ListSwiftDiffable) -> Bool { + guard let value = value as? LoadMoreModel else { return false } + return loading == value.loading + } +} + final class LoadMoreSectionController2: ListSwiftSectionController { weak var delegate: LoadMoreSectionController2Delegate? + private var loading = false override func createBinders(from value: String) -> [ListBinder] { return [ - binder(value, cellType: .class(LoadMoreCell.self), size: { - return CGSize( - width: $0.collection.containerSize.width, - height: Styles.Sizes.tableCellHeight - ) - }, didSelect: { [weak self] _ in - guard let strongSelf = self else { return } - strongSelf.delegate?.didSelect(controller: strongSelf) + binder( + LoadMoreModel(loading: loading), + cellType: ListCellType.class(LoadMoreCell.self), + size: { + return CGSize( + width: $0.collection.containerSize.width, + height: Styles.Sizes.tableCellHeight + ) + + }, + configure: { + $0.configure(loading: $1.value.loading) + }, + didSelect: { [weak self] _ in + guard let strongSelf = self else { return } + strongSelf.loading = true + strongSelf.delegate?.didSelect(controller: strongSelf) + strongSelf.update() }) ] } diff --git a/Classes/Settings/SettingsViewController.swift b/Classes/Settings/SettingsViewController.swift index b00ec7937..1cb170daf 100644 --- a/Classes/Settings/SettingsViewController.swift +++ b/Classes/Settings/SettingsViewController.swift @@ -7,7 +7,6 @@ // import UIKit -import SafariServices import GitHubAPI import GitHubSession import Squawk diff --git a/Classes/Utility/AlertAction.swift b/Classes/Utility/AlertAction.swift index 063129552..fddf57f28 100644 --- a/Classes/Utility/AlertAction.swift +++ b/Classes/Utility/AlertAction.swift @@ -7,7 +7,6 @@ // import UIKit -import SafariServices typealias AlertActionBlock = (UIAlertAction) -> Void diff --git a/Classes/Utility/String+FirstLine.swift b/Classes/Utility/String+FirstLine.swift new file mode 100644 index 000000000..91fa296a6 --- /dev/null +++ b/Classes/Utility/String+FirstLine.swift @@ -0,0 +1,17 @@ +// +// String+FirstLine.swift +// Freetime +// +// Created by Ryan Nystrom on 10/20/18. +// Copyright © 2018 Ryan Nystrom. All rights reserved. +// + +import Foundation + +extension String { + + var firstLine: String { + return components(separatedBy: .newlines).first ?? self + } + +} diff --git a/Classes/Views/NavigationTitleDropdownView.swift b/Classes/Views/NavigationTitleDropdownView.swift index 275a91c62..dc46c6e67 100644 --- a/Classes/Views/NavigationTitleDropdownView.swift +++ b/Classes/Views/NavigationTitleDropdownView.swift @@ -16,42 +16,45 @@ final class NavigationTitleDropdownView: UIControl { private let label = UILabel() private let chevron = UIImageView(image: UIImage(named: "chevron-down-small")?.withRenderingMode(.alwaysTemplate)) - override init(frame: CGRect) { - super.init(frame: frame) + init(chevronVisible: Bool = true) { + super.init(frame: .zero) isAccessibilityElement = true accessibilityTraits |= UIAccessibilityTraitButton let chevronSize = chevron.image?.size ?? .zero - chevron.tintColor = Styles.Colors.Gray.medium.color - chevron.setContentCompressionResistancePriority(.required, for: .horizontal) - chevron.setContentCompressionResistancePriority(.required, for: .vertical) - chevron.translatesAutoresizingMaskIntoConstraints = false - addSubview(chevron) + if chevronVisible { + chevron.tintColor = Styles.Colors.Gray.medium.color + chevron.setContentCompressionResistancePriority(.required, for: .horizontal) + chevron.setContentCompressionResistancePriority(.required, for: .vertical) + chevron.translatesAutoresizingMaskIntoConstraints = false + addSubview(chevron) + } label.backgroundColor = .clear label.numberOfLines = 2 label.translatesAutoresizingMaskIntoConstraints = false label.textAlignment = .center - label.lineBreakMode = .byTruncatingMiddle + label.lineBreakMode = .byTruncatingHead label.adjustsFontSizeToFitWidth = true label.minimumScaleFactor = 0.75 - label.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) label.setContentCompressionResistancePriority(.defaultLow, for: .vertical) addSubview(label) - chevron.snp.makeConstraints { make in - make.left.equalTo(label.snp.right).offset(NavigationTitleDropdownView.spacing) - make.right.lessThanOrEqualTo(self) - make.centerY.equalTo(self) - make.size.equalTo(chevronSize) + if chevronVisible { + chevron.snp.makeConstraints { make in + make.left.equalTo(label.snp.right).offset(NavigationTitleDropdownView.spacing) + make.right.lessThanOrEqualTo(self) + make.centerY.equalTo(self) + make.size.equalTo(chevronSize) + } } label.snp.makeConstraints { make in -// make.center.equalTo(self) make.centerY.equalToSuperview() - make.centerX.equalToSuperview().offset(-2) - make.top.bottom.left.lessThanOrEqualTo(self).priority(.high) + make.centerX.equalToSuperview().offset(chevronVisible ? -2 : 0) + make.top.bottom.left.right.lessThanOrEqualTo(self).priority(.high) } } @@ -62,10 +65,12 @@ final class NavigationTitleDropdownView: UIControl { override var intrinsicContentSize: CGSize { let greatest = CGFloat.greatestFiniteMagnitude let labelSize = label.sizeThatFits(CGSize(width: greatest, height: greatest)) - let chevronSize = chevron.image?.size ?? .zero + let chevronSpacing = dropdownEnabled + ? (chevron.image?.size ?? .zero).width + NavigationTitleDropdownView.spacing + : 0 return CGSize( - width: labelSize.width + NavigationTitleDropdownView.spacing + chevronSize.width, - height: max(chevronSize.height, labelSize.height) + width: labelSize.width + chevronSpacing, + height: labelSize.height ) } @@ -100,7 +105,7 @@ final class NavigationTitleDropdownView: UIControl { ] let attributedTitle = NSMutableAttributedString(string: title, attributes: titleAttributes) - if let subtitle = subtitle { + if let subtitle = subtitle, !subtitle.isEmpty { attributedTitle.append(NSAttributedString(string: "\n")) attributedTitle.append(NSAttributedString(string: subtitle, attributes: [ .font: Styles.Text.secondaryBold.preferredFont, @@ -116,7 +121,12 @@ final class NavigationTitleDropdownView: UIControl { // MARK: Private API - func fadeControls(alpha: CGFloat) { + private var dropdownEnabled: Bool { + return chevron.superview != nil + } + + private func fadeControls(alpha: CGFloat) { + guard dropdownEnabled else { return } [label, chevron].forEach { $0.alpha = alpha } } diff --git a/Classes/Views/StyledTextViewCell.swift b/Classes/Views/StyledTextViewCell.swift index 5666b4eba..e0805d42f 100644 --- a/Classes/Views/StyledTextViewCell.swift +++ b/Classes/Views/StyledTextViewCell.swift @@ -18,8 +18,6 @@ class StyledTextViewCell: UICollectionViewCell { textView.gesturableAttributes = MarkdownAttribute.all contentView.addSubview(textView) isAccessibilityElement = true -// clipsToBounds = false -// textView.clipsToBounds = false } required init?(coder aDecoder: NSCoder) { diff --git a/Classes/Views/Styles.swift b/Classes/Views/Styles.swift index 3cbf2f45f..b834c0491 100644 --- a/Classes/Views/Styles.swift +++ b/Classes/Views/Styles.swift @@ -67,6 +67,7 @@ enum Styles { static let code = TextStyle(font: .name("Courier"), size: 16) static let codeBold = TextStyle(font: .name("Courier-Bold"), size: 16) static let secondaryCode = TextStyle(font: .name("Courier"), size: 13) + static let secondaryCodeBold = TextStyle(font: .name("Courier-Bold"), size: 13) static let finePrint = TextStyle(size: 12) static let h1 = TextStyle(font: .system(.bold), size: 24) diff --git a/Freetime.xcodeproj/project.pbxproj b/Freetime.xcodeproj/project.pbxproj index e14e29225..1f8683a38 100644 --- a/Freetime.xcodeproj/project.pbxproj +++ b/Freetime.xcodeproj/project.pbxproj @@ -326,6 +326,15 @@ 29AF1E8C1F8ABC5A0008A0EF /* RepositoryCodeDirectoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29AF1E8B1F8ABC5A0008A0EF /* RepositoryCodeDirectoryViewController.swift */; }; 29AF1E8E1F8ABC900008A0EF /* RepositoryFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29AF1E8D1F8ABC900008A0EF /* RepositoryFile.swift */; }; 29B0EF871F93DF6C00870291 /* RepositoryCodeBlobViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B0EF861F93DF6C00870291 /* RepositoryCodeBlobViewController.swift */; }; + 29B205F2217B7B5A00E4DD9F /* PathHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B205F1217B7B5A00E4DD9F /* PathHistoryViewController.swift */; }; + 29B205F4217B7B8400E4DD9F /* PathCommitSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B205F3217B7B8400E4DD9F /* PathCommitSectionController.swift */; }; + 29B205F6217B7B9C00E4DD9F /* PathCommitModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B205F5217B7B9C00E4DD9F /* PathCommitModel.swift */; }; + 29B205F8217B7E6F00E4DD9F /* PathHistoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B205F7217B7E6F00E4DD9F /* PathHistoryViewModel.swift */; }; + 29B205FA217B8B9100E4DD9F /* PathCommitCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B205F9217B8B9100E4DD9F /* PathCommitCell.swift */; }; + 29B205FC217B912A00E4DD9F /* Client+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B205FB217B912A00E4DD9F /* Client+History.swift */; }; + 29B205FE217B971300E4DD9F /* String+FirstLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B205FD217B971300E4DD9F /* String+FirstLine.swift */; }; + 29B20600217B9C0600E4DD9F /* RepoFileHistoryQueryDataToPathHistoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B205FF217B9C0600E4DD9F /* RepoFileHistoryQueryDataToPathHistoryViewModel.swift */; }; + 29B20602217BC37B00E4DD9F /* UIViewController+HistoryAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B20601217BC37B00E4DD9F /* UIViewController+HistoryAction.swift */; }; 29B5D08B20D578DB003DFBE2 /* InboxType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B5D08A20D578DB003DFBE2 /* InboxType.swift */; }; 29B75AD9210A9A0300C28131 /* IssueLabelStatusCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B75AD8210A9A0300C28131 /* IssueLabelStatusCell.swift */; }; 29B75ADB210A9B3100C28131 /* IssueLabelStatusModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B75ADA210A9B3100C28131 /* IssueLabelStatusModel.swift */; }; @@ -866,6 +875,15 @@ 29AF1E8B1F8ABC5A0008A0EF /* RepositoryCodeDirectoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryCodeDirectoryViewController.swift; sourceTree = ""; }; 29AF1E8D1F8ABC900008A0EF /* RepositoryFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryFile.swift; sourceTree = ""; }; 29B0EF861F93DF6C00870291 /* RepositoryCodeBlobViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryCodeBlobViewController.swift; sourceTree = ""; }; + 29B205F1217B7B5A00E4DD9F /* PathHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathHistoryViewController.swift; sourceTree = ""; }; + 29B205F3217B7B8400E4DD9F /* PathCommitSectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathCommitSectionController.swift; sourceTree = ""; }; + 29B205F5217B7B9C00E4DD9F /* PathCommitModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathCommitModel.swift; sourceTree = ""; }; + 29B205F7217B7E6F00E4DD9F /* PathHistoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathHistoryViewModel.swift; sourceTree = ""; }; + 29B205F9217B8B9100E4DD9F /* PathCommitCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathCommitCell.swift; sourceTree = ""; }; + 29B205FB217B912A00E4DD9F /* Client+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Client+History.swift"; sourceTree = ""; }; + 29B205FD217B971300E4DD9F /* String+FirstLine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+FirstLine.swift"; sourceTree = ""; }; + 29B205FF217B9C0600E4DD9F /* RepoFileHistoryQueryDataToPathHistoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepoFileHistoryQueryDataToPathHistoryViewModel.swift; sourceTree = ""; }; + 29B20601217BC37B00E4DD9F /* UIViewController+HistoryAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+HistoryAction.swift"; sourceTree = ""; }; 29B5D08A20D578DB003DFBE2 /* InboxType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxType.swift; sourceTree = ""; }; 29B75AD8210A9A0300C28131 /* IssueLabelStatusCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueLabelStatusCell.swift; sourceTree = ""; }; 29B75ADA210A9B3100C28131 /* IssueLabelStatusModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueLabelStatusModel.swift; sourceTree = ""; }; @@ -1622,6 +1640,7 @@ isa = PBXGroup; children = ( DCA5ED0C1FAED8FE0072F074 /* Bookmark */, + 29B205F0217B7B4700E4DD9F /* History */, 98F9F3FB1F9CCFFE005A0266 /* Image Upload */, 292FCAC71EDFCC510026635E /* Issues */, 29EE1C131F3A2E440046A54D /* Labels */, @@ -1887,6 +1906,21 @@ path = EditComment; sourceTree = ""; }; + 29B205F0217B7B4700E4DD9F /* History */ = { + isa = PBXGroup; + children = ( + 29B205FB217B912A00E4DD9F /* Client+History.swift */, + 29B205F9217B8B9100E4DD9F /* PathCommitCell.swift */, + 29B205F5217B7B9C00E4DD9F /* PathCommitModel.swift */, + 29B205F3217B7B8400E4DD9F /* PathCommitSectionController.swift */, + 29B205F1217B7B5A00E4DD9F /* PathHistoryViewController.swift */, + 29B205F7217B7E6F00E4DD9F /* PathHistoryViewModel.swift */, + 29B205FF217B9C0600E4DD9F /* RepoFileHistoryQueryDataToPathHistoryViewModel.swift */, + 29B20601217BC37B00E4DD9F /* UIViewController+HistoryAction.swift */, + ); + path = History; + sourceTree = ""; + }; 29BCA7FA212137E000753A3C /* Branches */ = { isa = PBXGroup; children = ( @@ -2111,6 +2145,7 @@ 7B7BBA3D1F8B752A00D6AEDA /* SearchQueryTokenizer.swift */, 49FE18FC204B5D32001681E8 /* Sequence+Contains.swift */, DCA5ED111FAEE3AE0072F074 /* Store.swift */, + 29B205FD217B971300E4DD9F /* String+FirstLine.swift */, 29B75ADC210AA14F00C28131 /* String+Size.swift */, 754488B01F7ADF8D0032D08C /* UIAlertController+Action.swift */, 2967DC55211751CB00FD3683 /* UIContentSizeCategory+Preferred.swift */, @@ -2759,6 +2794,7 @@ 290744B41F250A6800FD9E48 /* AutocompleteCell.swift in Sources */, 290744B81F250A7200FD9E48 /* AutocompleteType.swift in Sources */, 291929671F3FF9C50012067B /* BadgeNotifications.swift in Sources */, + 29B20600217B9C0600E4DD9F /* RepoFileHistoryQueryDataToPathHistoryViewModel.swift in Sources */, 29459A6F1FE61E0500034A04 /* MarkdownCheckboxModel.swift in Sources */, 299997302031227E00995FFD /* IssueMergeButtonModel.swift in Sources */, 29CEA5CD1F84DB1B009827DB /* BaseListViewController.swift in Sources */, @@ -2768,6 +2804,7 @@ D8BAD0661FDF224600C41071 /* WrappingStaticSpacingFlowLayout.swift in Sources */, 98003D8D1FCAD7FC00755C17 /* LabelDetails.swift in Sources */, 297DD5E11F061BBE006E7E63 /* CreateProfileViewController.swift in Sources */, + 29B205FA217B8B9100E4DD9F /* PathCommitCell.swift in Sources */, 29AAB7171FB4A2AE001D5E6A /* BoundedImageSize.swift in Sources */, 29A4768E1ED07A23005D0953 /* DateDetailsFormatter.swift in Sources */, 29AF1E8C1F8ABC5A0008A0EF /* RepositoryCodeDirectoryViewController.swift in Sources */, @@ -2912,6 +2949,7 @@ 2946FA5120367FC100C37435 /* GithubClient+Merge.swift in Sources */, 2999972A20311B3800995FFD /* IssueMergeSectionController.swift in Sources */, 29F7F05F1F2A839100F6075D /* IssueNeckLoadSectionController.swift in Sources */, + 29B20602217BC37B00E4DD9F /* UIViewController+HistoryAction.swift in Sources */, 2919295F1F3FD1F40012067B /* IssuePatchContentViewController.swift in Sources */, 292FF8B91F303DB0009E63F7 /* IssuePreviewModel.swift in Sources */, 2973E81120FBF73D0050233F /* IssueManagingContextController.swift in Sources */, @@ -2970,6 +3008,7 @@ 291929491F3EAD2E0012067B /* IssueViewFilesSectionController.swift in Sources */, 292FCB101EDFCC510026635E /* IssueViewModels.swift in Sources */, 2963A93B1EE25F6F0066509C /* LabelableFields+IssueLabelModel.swift in Sources */, + 29B205F8217B7E6F00E4DD9F /* PathHistoryViewModel.swift in Sources */, 98835BD21F1A158D005BA24F /* LabelCell.swift in Sources */, 29C8F9B5208C081D0075931C /* LabelSectionController.swift in Sources */, 29136BE3200AAA5A007317BE /* UIViewController+FilePathTitle.swift in Sources */, @@ -3082,6 +3121,7 @@ 29AF1E8E1F8ABC900008A0EF /* RepositoryFile.swift in Sources */, 986B871D1F2B8FCD00AAB55C /* SearchViewController.swift in Sources */, 298BA0971EC947F100B01946 /* SegmentedControlCell.swift in Sources */, + 29B205F6217B7B9C00E4DD9F /* PathCommitModel.swift in Sources */, 297B062C1FB92DCA0026FA23 /* UICollectionViewLayout+Orientation.swift in Sources */, 298BA09A1EC947FC00B01946 /* SegmentedControlModel.swift in Sources */, 299F63EA20603FB80015D901 /* MarkdownStyledTextView.swift in Sources */, @@ -3105,6 +3145,7 @@ 292D2F1720FA5CAD00099342 /* Squawk+GitHawk.swift in Sources */, DC6339371F9F567000402A8D /* DeleteSwipeAction.swift in Sources */, 29BCA7FF212137E100753A3C /* IssueBranchesSectionController.swift in Sources */, + 29B205F4217B7B8400E4DD9F /* PathCommitSectionController.swift in Sources */, DCA5ED121FAEE3AE0072F074 /* Store.swift in Sources */, 29973E561F68BFDE0004B693 /* Signature.swift in Sources */, 29BCA800212137E100753A3C /* IssueTargetBranchCell.swift in Sources */, @@ -3114,6 +3155,7 @@ 29459A711FE7153500034A04 /* LogEnvironmentInformation.swift in Sources */, 49AF91B4204B4B6A00DFF325 /* MergeHelper.swift in Sources */, 2930F2731F8A27750082BA26 /* WidthCache.swift in Sources */, + 29B205FC217B912A00E4DD9F /* Client+History.swift in Sources */, 29242814210A51B5001F5980 /* SpacerSectionController.swift in Sources */, 2971722B1F069E6B005E43AC /* SpinnerSectionController.swift in Sources */, 2999972E203120E300995FFD /* IssueMergeButtonCell.swift in Sources */, @@ -3127,10 +3169,12 @@ 29DB264A1FCA10A800C3D0C9 /* GithubHighlighting.swift in Sources */, 2950AB1D2083B1E400C6F19A /* EmptyLoadingView.swift in Sources */, 29A10543216D9381004734A0 /* UNMutableNotificationContent+Routable.swift in Sources */, + 29B205FE217B971300E4DD9F /* String+FirstLine.swift in Sources */, 29B0EF871F93DF6C00870291 /* RepositoryCodeBlobViewController.swift in Sources */, 98F9F4001F9CCFFE005A0266 /* ImageUploadTableViewController.swift in Sources */, 29C167671ECA005500439D62 /* Constants.swift in Sources */, 291929631F3FF0DA0012067B /* StyledTableCell.swift in Sources */, + 29B205F2217B7B5A00E4DD9F /* PathHistoryViewController.swift in Sources */, 29B75ADD210AA14F00C28131 /* String+Size.swift in Sources */, 29C9FDE11EC667AE00EE3A52 /* Styles.swift in Sources */, 29622B45210520E6000C428D /* CardCollectionViewCell.swift in Sources */, diff --git a/gql/API.swift b/gql/API.swift index 55adaf6f3..abeb703cd 100644 --- a/gql/API.swift +++ b/gql/API.swift @@ -14418,6 +14418,533 @@ public final class RepoFileQuery: GraphQLQuery { } } +public final class RepoFileHistoryQuery: GraphQLQuery { + public static let operationString = + "query RepoFileHistory($owner: String!, $name: String!, $branch: String!, $path: String, $after: String, $page_size: Int!) {\n repository(owner: $owner, name: $name) {\n __typename\n object(expression: $branch) {\n __typename\n ... on Commit {\n history(after: $after, path: $path, first: $page_size) {\n __typename\n nodes {\n __typename\n message\n oid\n committedDate\n url\n author {\n __typename\n user {\n __typename\n login\n }\n }\n committer {\n __typename\n user {\n __typename\n login\n }\n }\n }\n pageInfo {\n __typename\n hasNextPage\n endCursor\n }\n }\n }\n }\n }\n}" + + public var owner: String + public var name: String + public var branch: String + public var path: String? + public var after: String? + public var page_size: Int + + public init(owner: String, name: String, branch: String, path: String? = nil, after: String? = nil, page_size: Int) { + self.owner = owner + self.name = name + self.branch = branch + self.path = path + self.after = after + self.page_size = page_size + } + + public var variables: GraphQLMap? { + return ["owner": owner, "name": name, "branch": branch, "path": path, "after": after, "page_size": page_size] + } + + public struct Data: GraphQLSelectionSet { + public static let possibleTypes = ["Query"] + + public static let selections: [GraphQLSelection] = [ + GraphQLField("repository", arguments: ["owner": GraphQLVariable("owner"), "name": GraphQLVariable("name")], type: .object(Repository.selections)), + ] + + public var snapshot: Snapshot + + public init(snapshot: Snapshot) { + self.snapshot = snapshot + } + + public init(repository: Repository? = nil) { + self.init(snapshot: ["__typename": "Query", "repository": repository.flatMap { (value: Repository) -> Snapshot in value.snapshot }]) + } + + /// Lookup a given repository by the owner and repository name. + public var repository: Repository? { + get { + return (snapshot["repository"] as? Snapshot).flatMap { Repository(snapshot: $0) } + } + set { + snapshot.updateValue(newValue?.snapshot, forKey: "repository") + } + } + + public struct Repository: GraphQLSelectionSet { + public static let possibleTypes = ["Repository"] + + public static let selections: [GraphQLSelection] = [ + GraphQLField("__typename", type: .nonNull(.scalar(String.self))), + GraphQLField("object", arguments: ["expression": GraphQLVariable("branch")], type: .object(Object.selections)), + ] + + public var snapshot: Snapshot + + public init(snapshot: Snapshot) { + self.snapshot = snapshot + } + + public init(object: Object? = nil) { + self.init(snapshot: ["__typename": "Repository", "object": object.flatMap { (value: Object) -> Snapshot in value.snapshot }]) + } + + public var __typename: String { + get { + return snapshot["__typename"]! as! String + } + set { + snapshot.updateValue(newValue, forKey: "__typename") + } + } + + /// A Git object in the repository + public var object: Object? { + get { + return (snapshot["object"] as? Snapshot).flatMap { Object(snapshot: $0) } + } + set { + snapshot.updateValue(newValue?.snapshot, forKey: "object") + } + } + + public struct Object: GraphQLSelectionSet { + public static let possibleTypes = ["Commit", "Tree", "Blob", "Tag"] + + public static let selections: [GraphQLSelection] = [ + GraphQLTypeCase( + variants: ["Commit": AsCommit.selections], + default: [ + GraphQLField("__typename", type: .nonNull(.scalar(String.self))), + ] + ) + ] + + public var snapshot: Snapshot + + public init(snapshot: Snapshot) { + self.snapshot = snapshot + } + + public static func makeTree() -> Object { + return Object(snapshot: ["__typename": "Tree"]) + } + + public static func makeBlob() -> Object { + return Object(snapshot: ["__typename": "Blob"]) + } + + public static func makeTag() -> Object { + return Object(snapshot: ["__typename": "Tag"]) + } + + public static func makeCommit(history: AsCommit.History) -> Object { + return Object(snapshot: ["__typename": "Commit", "history": history.snapshot]) + } + + public var __typename: String { + get { + return snapshot["__typename"]! as! String + } + set { + snapshot.updateValue(newValue, forKey: "__typename") + } + } + + public var asCommit: AsCommit? { + get { + if !AsCommit.possibleTypes.contains(__typename) { return nil } + return AsCommit(snapshot: snapshot) + } + set { + guard let newValue = newValue else { return } + snapshot = newValue.snapshot + } + } + + public struct AsCommit: GraphQLSelectionSet { + public static let possibleTypes = ["Commit"] + + public static let selections: [GraphQLSelection] = [ + GraphQLField("__typename", type: .nonNull(.scalar(String.self))), + GraphQLField("history", arguments: ["after": GraphQLVariable("after"), "path": GraphQLVariable("path"), "first": GraphQLVariable("page_size")], type: .nonNull(.object(History.selections))), + ] + + public var snapshot: Snapshot + + public init(snapshot: Snapshot) { + self.snapshot = snapshot + } + + public init(history: History) { + self.init(snapshot: ["__typename": "Commit", "history": history.snapshot]) + } + + public var __typename: String { + get { + return snapshot["__typename"]! as! String + } + set { + snapshot.updateValue(newValue, forKey: "__typename") + } + } + + /// The linear commit history starting from (and including) this commit, in the same order as `git log`. + public var history: History { + get { + return History(snapshot: snapshot["history"]! as! Snapshot) + } + set { + snapshot.updateValue(newValue.snapshot, forKey: "history") + } + } + + public struct History: GraphQLSelectionSet { + public static let possibleTypes = ["CommitHistoryConnection"] + + public static let selections: [GraphQLSelection] = [ + GraphQLField("__typename", type: .nonNull(.scalar(String.self))), + GraphQLField("nodes", type: .list(.object(Node.selections))), + GraphQLField("pageInfo", type: .nonNull(.object(PageInfo.selections))), + ] + + public var snapshot: Snapshot + + public init(snapshot: Snapshot) { + self.snapshot = snapshot + } + + public init(nodes: [Node?]? = nil, pageInfo: PageInfo) { + self.init(snapshot: ["__typename": "CommitHistoryConnection", "nodes": nodes.flatMap { (value: [Node?]) -> [Snapshot?] in value.map { (value: Node?) -> Snapshot? in value.flatMap { (value: Node) -> Snapshot in value.snapshot } } }, "pageInfo": pageInfo.snapshot]) + } + + public var __typename: String { + get { + return snapshot["__typename"]! as! String + } + set { + snapshot.updateValue(newValue, forKey: "__typename") + } + } + + /// A list of nodes. + public var nodes: [Node?]? { + get { + return (snapshot["nodes"] as? [Snapshot?]).flatMap { (value: [Snapshot?]) -> [Node?] in value.map { (value: Snapshot?) -> Node? in value.flatMap { (value: Snapshot) -> Node in Node(snapshot: value) } } } + } + set { + snapshot.updateValue(newValue.flatMap { (value: [Node?]) -> [Snapshot?] in value.map { (value: Node?) -> Snapshot? in value.flatMap { (value: Node) -> Snapshot in value.snapshot } } }, forKey: "nodes") + } + } + + /// Information to aid in pagination. + public var pageInfo: PageInfo { + get { + return PageInfo(snapshot: snapshot["pageInfo"]! as! Snapshot) + } + set { + snapshot.updateValue(newValue.snapshot, forKey: "pageInfo") + } + } + + public struct Node: GraphQLSelectionSet { + public static let possibleTypes = ["Commit"] + + public static let selections: [GraphQLSelection] = [ + GraphQLField("__typename", type: .nonNull(.scalar(String.self))), + GraphQLField("message", type: .nonNull(.scalar(String.self))), + GraphQLField("oid", type: .nonNull(.scalar(String.self))), + GraphQLField("committedDate", type: .nonNull(.scalar(String.self))), + GraphQLField("url", type: .nonNull(.scalar(String.self))), + GraphQLField("author", type: .object(Author.selections)), + GraphQLField("committer", type: .object(Committer.selections)), + ] + + public var snapshot: Snapshot + + public init(snapshot: Snapshot) { + self.snapshot = snapshot + } + + public init(message: String, oid: String, committedDate: String, url: String, author: Author? = nil, committer: Committer? = nil) { + self.init(snapshot: ["__typename": "Commit", "message": message, "oid": oid, "committedDate": committedDate, "url": url, "author": author.flatMap { (value: Author) -> Snapshot in value.snapshot }, "committer": committer.flatMap { (value: Committer) -> Snapshot in value.snapshot }]) + } + + public var __typename: String { + get { + return snapshot["__typename"]! as! String + } + set { + snapshot.updateValue(newValue, forKey: "__typename") + } + } + + /// The Git commit message + public var message: String { + get { + return snapshot["message"]! as! String + } + set { + snapshot.updateValue(newValue, forKey: "message") + } + } + + /// The Git object ID + public var oid: String { + get { + return snapshot["oid"]! as! String + } + set { + snapshot.updateValue(newValue, forKey: "oid") + } + } + + /// The datetime when this commit was committed. + public var committedDate: String { + get { + return snapshot["committedDate"]! as! String + } + set { + snapshot.updateValue(newValue, forKey: "committedDate") + } + } + + /// The HTTP URL for this commit + public var url: String { + get { + return snapshot["url"]! as! String + } + set { + snapshot.updateValue(newValue, forKey: "url") + } + } + + /// Authorship details of the commit. + public var author: Author? { + get { + return (snapshot["author"] as? Snapshot).flatMap { Author(snapshot: $0) } + } + set { + snapshot.updateValue(newValue?.snapshot, forKey: "author") + } + } + + /// Committership details of the commit. + public var committer: Committer? { + get { + return (snapshot["committer"] as? Snapshot).flatMap { Committer(snapshot: $0) } + } + set { + snapshot.updateValue(newValue?.snapshot, forKey: "committer") + } + } + + public struct Author: GraphQLSelectionSet { + public static let possibleTypes = ["GitActor"] + + public static let selections: [GraphQLSelection] = [ + GraphQLField("__typename", type: .nonNull(.scalar(String.self))), + GraphQLField("user", type: .object(User.selections)), + ] + + public var snapshot: Snapshot + + public init(snapshot: Snapshot) { + self.snapshot = snapshot + } + + public init(user: User? = nil) { + self.init(snapshot: ["__typename": "GitActor", "user": user.flatMap { (value: User) -> Snapshot in value.snapshot }]) + } + + public var __typename: String { + get { + return snapshot["__typename"]! as! String + } + set { + snapshot.updateValue(newValue, forKey: "__typename") + } + } + + /// The GitHub user corresponding to the email field. Null if no such user exists. + public var user: User? { + get { + return (snapshot["user"] as? Snapshot).flatMap { User(snapshot: $0) } + } + set { + snapshot.updateValue(newValue?.snapshot, forKey: "user") + } + } + + public struct User: GraphQLSelectionSet { + public static let possibleTypes = ["User"] + + public static let selections: [GraphQLSelection] = [ + GraphQLField("__typename", type: .nonNull(.scalar(String.self))), + GraphQLField("login", type: .nonNull(.scalar(String.self))), + ] + + public var snapshot: Snapshot + + public init(snapshot: Snapshot) { + self.snapshot = snapshot + } + + public init(login: String) { + self.init(snapshot: ["__typename": "User", "login": login]) + } + + public var __typename: String { + get { + return snapshot["__typename"]! as! String + } + set { + snapshot.updateValue(newValue, forKey: "__typename") + } + } + + /// The username used to login. + public var login: String { + get { + return snapshot["login"]! as! String + } + set { + snapshot.updateValue(newValue, forKey: "login") + } + } + } + } + + public struct Committer: GraphQLSelectionSet { + public static let possibleTypes = ["GitActor"] + + public static let selections: [GraphQLSelection] = [ + GraphQLField("__typename", type: .nonNull(.scalar(String.self))), + GraphQLField("user", type: .object(User.selections)), + ] + + public var snapshot: Snapshot + + public init(snapshot: Snapshot) { + self.snapshot = snapshot + } + + public init(user: User? = nil) { + self.init(snapshot: ["__typename": "GitActor", "user": user.flatMap { (value: User) -> Snapshot in value.snapshot }]) + } + + public var __typename: String { + get { + return snapshot["__typename"]! as! String + } + set { + snapshot.updateValue(newValue, forKey: "__typename") + } + } + + /// The GitHub user corresponding to the email field. Null if no such user exists. + public var user: User? { + get { + return (snapshot["user"] as? Snapshot).flatMap { User(snapshot: $0) } + } + set { + snapshot.updateValue(newValue?.snapshot, forKey: "user") + } + } + + public struct User: GraphQLSelectionSet { + public static let possibleTypes = ["User"] + + public static let selections: [GraphQLSelection] = [ + GraphQLField("__typename", type: .nonNull(.scalar(String.self))), + GraphQLField("login", type: .nonNull(.scalar(String.self))), + ] + + public var snapshot: Snapshot + + public init(snapshot: Snapshot) { + self.snapshot = snapshot + } + + public init(login: String) { + self.init(snapshot: ["__typename": "User", "login": login]) + } + + public var __typename: String { + get { + return snapshot["__typename"]! as! String + } + set { + snapshot.updateValue(newValue, forKey: "__typename") + } + } + + /// The username used to login. + public var login: String { + get { + return snapshot["login"]! as! String + } + set { + snapshot.updateValue(newValue, forKey: "login") + } + } + } + } + } + + public struct PageInfo: GraphQLSelectionSet { + public static let possibleTypes = ["PageInfo"] + + public static let selections: [GraphQLSelection] = [ + GraphQLField("__typename", type: .nonNull(.scalar(String.self))), + GraphQLField("hasNextPage", type: .nonNull(.scalar(Bool.self))), + GraphQLField("endCursor", type: .scalar(String.self)), + ] + + public var snapshot: Snapshot + + public init(snapshot: Snapshot) { + self.snapshot = snapshot + } + + public init(hasNextPage: Bool, endCursor: String? = nil) { + self.init(snapshot: ["__typename": "PageInfo", "hasNextPage": hasNextPage, "endCursor": endCursor]) + } + + public var __typename: String { + get { + return snapshot["__typename"]! as! String + } + set { + snapshot.updateValue(newValue, forKey: "__typename") + } + } + + /// When paginating forwards, are there more items? + public var hasNextPage: Bool { + get { + return snapshot["hasNextPage"]! as! Bool + } + set { + snapshot.updateValue(newValue, forKey: "hasNextPage") + } + } + + /// When paginating forwards, the cursor to continue. + public var endCursor: String? { + get { + return snapshot["endCursor"] as? String + } + set { + snapshot.updateValue(newValue, forKey: "endCursor") + } + } + } + } + } + } + } + } +} + public final class RepoFilesQuery: GraphQLQuery { public static let operationString = "query RepoFiles($owner: String!, $name: String!, $branchAndPath: String!) {\n repository(owner: $owner, name: $name) {\n __typename\n object(expression: $branchAndPath) {\n __typename\n ... on Tree {\n entries {\n __typename\n name\n type\n }\n }\n }\n }\n}" diff --git a/gql/RepoFileHistory.graphql b/gql/RepoFileHistory.graphql new file mode 100644 index 000000000..50b467f92 --- /dev/null +++ b/gql/RepoFileHistory.graphql @@ -0,0 +1,30 @@ +query RepoFileHistory($owner: String!, $name: String!, $branch: String!, $path: String, $after: String, $page_size: Int!) { + repository(owner: $owner, name: $name) { + object(expression: $branch) { + ... on Commit { + history(after: $after, path: $path, first: $page_size) { + nodes { + message + oid + committedDate + url + author { + user { + login + } + } + committer { + user { + login + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } +}