Reply to Pull Request Review comments (#1483)

* wip

* reply cell shows up in comments

* focus on reply model and scroll to reply

* api to create PR comment replies

* reply working

* clean and code reuse

* even more reuse

* organize
This commit is contained in:
Ryan Nystrom
2018-02-04 19:08:34 -05:00
committed by GitHub
parent 2423e353ec
commit ec4807d415
11 changed files with 392 additions and 80 deletions

View File

@@ -13,16 +13,18 @@ final class IssueDiffHunkModel: ListDiffable {
let path: String
let preview: NSAttributedStringSizing
let offset: Int
init(path: String, preview: NSAttributedStringSizing) {
init(path: String, preview: NSAttributedStringSizing, offset: Int) {
self.path = path
self.preview = preview
self.offset = offset
}
// MARK: ListDiffable
func diffIdentifier() -> NSObjectProtocol {
return preview.attributedText.string as NSObjectProtocol
return "\(preview.attributedText.string)-\(offset)" as NSObjectProtocol
}
func isEqual(toDiffableObject object: ListDiffable?) -> Bool {

View File

@@ -136,20 +136,7 @@ IssueManagingNavSectionControllerDelegate {
view.backgroundColor = Styles.Colors.background
// setup message view properties
borderColor = Styles.Colors.Gray.border.color
messageView.textView.placeholderText = NSLocalizedString("Leave a comment", comment: "")
messageView.textView.placeholderTextColor = Styles.Colors.Gray.light.color
messageView.keyboardType = .twitter
messageView.set(buttonIcon: UIImage(named: "send")?.withRenderingMode(.alwaysTemplate), for: .normal)
messageView.buttonTint = Styles.Colors.Blue.medium.color
messageView.font = Styles.Fonts.body
messageView.inset = UIEdgeInsets(
top: Styles.Sizes.gutter,
left: Styles.Sizes.gutter,
bottom: Styles.Sizes.rowSpacing / 2,
right: Styles.Sizes.gutter
)
messageView.addButton(target: self, action: #selector(didPressButton(_:)))
configure(target: self, action: #selector(didPressButton(_:)))
let getMarkdownBlock = { [weak self] () -> (String) in
return self?.messageView.text ?? ""
@@ -332,31 +319,12 @@ IssueManagingNavSectionControllerDelegate {
func scrollToLastContentElement() {
let adapter = feed.adapter
let collectionView = feed.collectionView
let objects = adapter.objects()
guard objects.count > 1 else { return }
// assuming the last element is the "actions" when collaborator
let lastContent = objects[objects.count - (viewerIsCollaborator ? 2 : 1)]
guard let sectionController = adapter.sectionController(for: lastContent) else { return }
let lastItemIndex = sectionController.numberOfItems() - 1
let path = IndexPath(item: lastItemIndex, section: sectionController.section)
guard let attributes = feed.collectionView.layoutAttributesForItem(at: path) else { return }
let paddedMaxY = min(attributes.frame.maxY + Styles.Sizes.rowSpacing, collectionView.contentSize.height)
let viewportHeight = collectionView.bounds.height
// make sure not already at the top
guard paddedMaxY > viewportHeight else { return }
let offset = paddedMaxY - viewportHeight
collectionView.setContentOffset(
CGPoint(x: collectionView.contentOffset.x, y: offset),
animated: trueUnlessReduceMotionEnabled
)
adapter.scroll(to: lastContent, padding: Styles.Sizes.rowSpacing)
}
func onPreview() {

View File

@@ -383,7 +383,7 @@ extension IssueOrPullRequestQuery.Data.Repository.IssueOrPullRequest.AsPullReque
guard let node = thread.comments.nodes?.first, let firstComment = node else { return nil }
let code = CreateDiffString(code: firstComment.diffHunk, limit: true)
let text = NSAttributedStringSizing(containerWidth: 0, attributedText: code, inset: IssueDiffHunkPreviewCell.textViewInset)
return IssueDiffHunkModel(path: firstComment.path, preview: text)
return IssueDiffHunkModel(path: firstComment.path, preview: text, offset: 0)
}
private func commentModels(

View File

@@ -83,18 +83,19 @@ extension GithubClient {
attributedText: code,
inset: IssueDiffHunkPreviewCell.textViewInset
)
models.append(IssueDiffHunkModel(path: thread.path, preview: text))
models.append(IssueDiffHunkModel(path: thread.path, preview: text, offset: models.count))
for (i, comment) in thread.comments.enumerated() {
for comment in thread.comments {
models.append(createReviewComment(
owner: owner,
repo: repo,
model: comment,
viewer: viewerUsername,
width: width,
isLast: i == thread.comments.count - 1
width: width
))
}
models.append(PullRequestReviewReplyModel(replyID: id))
}
DispatchQueue.main.async {
@@ -104,6 +105,42 @@ extension GithubClient {
}))
}
func sendComment(
body: String,
inReplyTo: Int,
owner: String,
repo: String,
number: Int,
width: CGFloat,
completion: @escaping (Result<IssueCommentModel>) -> ()
) {
let viewer = userSession?.username
// https://developer.github.com/v3/pulls/comments/#alternative-input
request(Request(
path: "repos/\(owner)/\(repo)/pulls/\(number)/comments",
method: .post,
parameters: ["body": body, "in_reply_to": inReplyTo],
completion: { (response, _) in
if response.response?.statusCode == 201,
let json = response.value as? [String: Any],
let id = json["id"] as? Int,
let model = createReviewCommentModel(id: id, json: json) {
let comment = createReviewComment(
owner: owner,
repo: repo,
model: model,
viewer: viewer,
width: width
)
completion(.success(comment))
} else {
ToastManager.showGenericError()
completion(.error(response.error))
}
}))
}
}
private func createReviewCommentModel(id: Int, json: [String: Any]) -> GithubClient.ReviewComment? {
@@ -128,8 +165,7 @@ private func createReviewComment(
repo: String,
model: GithubClient.ReviewComment,
viewer: String?,
width: CGFloat,
isLast: Bool
width: CGFloat
) -> IssueCommentModel {
let details = IssueCommentDetailsViewModel(
date: model.created,
@@ -150,7 +186,7 @@ private func createReviewComment(
bodyModels: bodies,
reactions: reactions,
collapse: nil,
threadState: isLast ? .tail : .neck,
threadState: .neck,
rawMarkdown: model.body,
viewerCanUpdate: false,
viewerCanDelete: false,

View File

@@ -8,24 +8,34 @@
import UIKit
import IGListKit
import MessageViewController
final class PullRequestReviewCommentsViewController: BaseListViewController<NSNumber>,
BaseListViewControllerDataSource {
final class PullRequestReviewCommentsViewController: MessageViewController,
ListAdapterDataSource,
FeedDelegate,
PullRequestReviewReplySectionControllerDelegate {
private let model: IssueDetailsModel
private let client: GithubClient
private let autocomplete: IssueCommentAutocomplete
private var models = [ListDiffable]()
private var results = [ListDiffable]()
private let textActionsController = TextActionsController()
private var autocompleteController: AutocompleteController!
private var focusedReplyModel: PullRequestReviewReplyModel?
lazy private var feed: Feed = {
let f = Feed(viewController: self, delegate: self, managesLayout: false)
f.collectionView.contentInset = Styles.Sizes.threadInset
return f
}()
init(model: IssueDetailsModel, client: GithubClient, autocomplete: IssueCommentAutocomplete) {
self.model = model
self.client = client
self.autocomplete = autocomplete
super.init(
emptyErrorMessage: NSLocalizedString("Error loading review comments.", comment: ""),
dataSource: self
)
feed.collectionView.contentInset = Styles.Sizes.threadInset
super.init(nibName: nil, bundle: nil)
title = NSLocalizedString("Review Comments", comment: "")
}
@@ -33,6 +43,48 @@ BaseListViewControllerDataSource {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
feed.viewDidLoad()
feed.adapter.dataSource = self
// setup after feed is lazy loaded
setup(scrollView: feed.collectionView)
setMessageView(hidden: true, animated: false)
// override Feed bg color setting
view.backgroundColor = Styles.Colors.background
// setup message view properties
configure(target: self, action: #selector(didPressButton(_:)))
let getMarkdownBlock = { [weak self] () -> (String) in
return self?.messageView.text ?? ""
}
let actions = IssueTextActionsView.forMarkdown(
viewController: self,
getMarkdownBlock: getMarkdownBlock,
repo: model.repo,
owner: model.owner,
addBorder: false,
supportsImageUpload: true
)
// text input bar uses UIVisualEffectView, don't try to match it
actions.backgroundColor = .clear
textActionsController.configure(client: client, textView: messageView.textView, actions: actions)
textActionsController.viewController = self
actions.frame = CGRect(x: 0, y: 0, width: 0, height: 40)
messageView.add(contentView: actions)
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
feed.viewWillLayoutSubviews(view: view)
}
override func viewSafeAreaInsetsDidChange() {
if #available(iOS 11.0, *) {
super.viewSafeAreaInsetsDidChange()
@@ -40,36 +92,79 @@ BaseListViewControllerDataSource {
}
}
// MARK: Overrides
// MARK: FeedDelegate
override func fetch(page: NSNumber?) {
func loadFromNetwork(feed: Feed) {
fetch()
}
func loadNextPage(feed: Feed) -> Bool {
return false
}
// MARK: Private API
var insetWidth: CGFloat {
let contentInset = feed.collectionView.contentInset
return view.bounds.width - contentInset.left - contentInset.right
}
func fetch() {
client.fetchPRComments(
owner: model.owner,
repo: model.repo,
number: model.number,
width: view.bounds.width
width: insetWidth
) { [weak self] (result) in
switch result {
case .error: ToastManager.showGenericError()
case .success(let models, let page):
self?.models = models
self?.update(page: page as NSNumber?, animated: trueUnlessReduceMotionEnabled)
case .success(let models, _):
self?.results = models
self?.feed.finishLoading(dismissRefresh: true, animated: true)
}
}
}
// MARK: BaseListViewControllerDataSource
@objc func didPressButton(_ sender: Any) {
guard let reply = focusedReplyModel else { return }
let text = messageView.text
messageView.text = ""
messageView.textView.resignFirstResponder()
setMessageView(hidden: true, animated: true)
client.sendComment(
body: text,
inReplyTo: reply.replyID,
owner: model.owner,
repo: model.repo,
number: model.number,
width: insetWidth
) { [weak self] result in
switch result {
case .error: break
case .success(let comment): self?.insertComment(model: comment, reply: reply)
}
}
func headModels(listAdapter: ListAdapter) -> [ListDiffable] {
return []
}
func models(listAdapter: ListAdapter) -> [ListDiffable] {
return models
func insertComment(model: IssueCommentModel, reply: PullRequestReviewReplyModel) {
let section = feed.adapter.section(for: reply)
guard section < NSNotFound && section < results.count else { return }
results.insert(model, at: section)
feed.adapter.performUpdates(animated: true)
}
func sectionController(model: Any, listAdapter: ListAdapter) -> ListSectionController {
switch model {
// MARK: ListAdapterDataSource
func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
return results
}
func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
switch object {
case is NSAttributedStringSizing: return IssueTitleSectionController()
case is IssueCommentModel: return IssueCommentSectionController(
model: self.model,
@@ -77,25 +172,31 @@ BaseListViewControllerDataSource {
autocomplete: autocomplete
)
case is IssueDiffHunkModel: return IssueDiffHunkSectionController()
case is PullRequestReviewReplyModel: return PullRequestReviewReplySectionController(delegate: self)
// add case for reply model + SC. connect SC.delegate = self
default: fatalError("Unhandled object: \(model)")
}
}
func emptySectionController(listAdapter: ListAdapter) -> ListSectionController {
return ListSingleSectionController(cellClass: LabelCell.self, configureBlock: { (_, cell: UICollectionViewCell) in
guard let cell = cell as? LabelCell else { return }
cell.label.text = NSLocalizedString("No review comments found.", comment: "")
}, sizeBlock: { [weak self] (_, context: ListCollectionContext?) -> CGSize in
guard let context = context,
let strongSelf = self
else { return .zero }
return CGSize(
width: context.containerSize.width,
height: context.containerSize.height - strongSelf.topLayoutGuide.length - strongSelf.bottomLayoutGuide.length
)
})
func emptyView(for listAdapter: ListAdapter) -> UIView? {
switch feed.status {
case .idle:
let emptyView = EmptyView()
emptyView.label.text = NSLocalizedString("Error loading review comments.", comment: "")
return emptyView
case .loading, .loadingNext:
return nil
}
}
// MARK: IssueCommentSectionControllerDelegate
// MARK: PullRequestReviewReplySectionControllerDelegate
func didSelect(replySectionController: PullRequestReviewReplySectionController, reply: PullRequestReviewReplyModel) {
setMessageView(hidden: false, animated: true)
messageView.textView.becomeFirstResponder()
feed.adapter.scroll(to: reply, padding: Styles.Sizes.rowSpacing)
focusedReplyModel = reply
}
}

View File

@@ -0,0 +1,44 @@
//
// PullRequestReviewReplyCell.swift
// Freetime
//
// Created by Ryan Nystrom on 1/29/18.
// Copyright © 2018 Ryan Nystrom. All rights reserved.
//
import UIKit
import SnapKit
final class PullRequestReviewReplyCell: IssueCommentBaseCell {
private let button = UIButton()
override init(frame: CGRect) {
super.init(frame: frame)
let color = Styles.Colors.Blue.medium.color
button.setTitle(NSLocalizedString("Reply", comment: ""), for: .normal)
button.setImage(UIImage(named: "reply")?.withRenderingMode(.alwaysTemplate), for: .normal)
button.tintColor = color
button.setTitleColor(color, for: .normal)
button.titleLabel?.font = Styles.Fonts.body
button.isUserInteractionEnabled = false
let spacing = Styles.Sizes.columnSpacing / 2
button.imageEdgeInsets = UIEdgeInsets(top: 0, left: -spacing, bottom: 0, right: spacing)
button.titleEdgeInsets = UIEdgeInsets(top: 0, left: spacing, bottom: 0, right: -spacing)
button.contentEdgeInsets = UIEdgeInsets(top: 0, left: spacing, bottom: 0, right: spacing)
contentView.addSubview(button)
button.snp.makeConstraints { make in
make.center.equalToSuperview()
}
border = .tail
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@@ -0,0 +1,32 @@
//
// PullRequestReviewReplyModel.swift
// Freetime
//
// Created by Ryan Nystrom on 1/29/18.
// Copyright © 2018 Ryan Nystrom. All rights reserved.
//
import Foundation
import IGListKit
final class PullRequestReviewReplyModel: ListDiffable {
let replyID: Int
init(replyID: Int) {
self.replyID = replyID
}
// MARK: ListDiffable
func diffIdentifier() -> NSObjectProtocol {
return "reply-\(replyID)" as NSObjectProtocol
}
func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
if self === object { return true }
guard let object = object as? PullRequestReviewReplyModel else { return false }
return replyID == object.replyID
}
}

View File

@@ -0,0 +1,40 @@
//
// PullRequestReviewReplySectionController.swift
// Freetime
//
// Created by Ryan Nystrom on 1/29/18.
// Copyright © 2018 Ryan Nystrom. All rights reserved.
//
import Foundation
import IGListKit
protocol PullRequestReviewReplySectionControllerDelegate: class {
func didSelect(replySectionController: PullRequestReviewReplySectionController, reply: PullRequestReviewReplyModel)
}
final class PullRequestReviewReplySectionController: ListGenericSectionController<PullRequestReviewReplyModel> {
private weak var delegate: PullRequestReviewReplySectionControllerDelegate?
init(delegate: PullRequestReviewReplySectionControllerDelegate) {
self.delegate = delegate
}
override func sizeForItem(at index: Int) -> CGSize {
guard let width = collectionContext?.insetContainerSize.width else { return .zero }
return CGSize(width: width, height: Styles.Sizes.tableCellHeight)
}
override func cellForItem(at index: Int) -> UICollectionViewCell {
guard let cell = collectionContext?.dequeueReusableCell(of: PullRequestReviewReplyCell.self, for: self, at: index) as? PullRequestReviewReplyCell
else { fatalError("Missing context or wrong cell") }
return cell
}
override func didSelectItem(at index: Int) {
guard let object = self.object else { return }
delegate?.didSelect(replySectionController: self, reply: object)
}
}

View File

@@ -0,0 +1,37 @@
//
// ListAdapter+Scrolling.swift
// Freetime
//
// Created by Ryan Nystrom on 2/4/18.
// Copyright © 2018 Ryan Nystrom. All rights reserved.
//
import UIKit
import IGListKit
extension ListAdapter {
func scroll(to object: Any, padding: CGFloat) {
guard let collectionView = self.collectionView,
let sectionController = sectionController(for: object)
else { return }
let itemIndex = sectionController.numberOfItems() - 1
let path = IndexPath(item: itemIndex, section: sectionController.section)
guard let attributes = collectionView.layoutAttributesForItem(at: path) else { return }
let paddedMaxY = min(attributes.frame.maxY + padding, collectionView.contentSize.height)
let viewportHeight = collectionView.bounds.height
// make sure not already at the top
guard paddedMaxY > viewportHeight else { return }
let offset = paddedMaxY - viewportHeight
collectionView.setContentOffset(
CGPoint(x: collectionView.contentOffset.x, y: offset),
animated: trueUnlessReduceMotionEnabled
)
}
}

View File

@@ -0,0 +1,32 @@
//
// MessageView+Styles.swift
// Freetime
//
// Created by Ryan Nystrom on 2/4/18.
// Copyright © 2018 Ryan Nystrom. All rights reserved.
//
import Foundation
import MessageViewController
extension MessageViewController {
func configure(target: Any, action: Selector) {
// setup message view properties
borderColor = Styles.Colors.Gray.border.color
messageView.textView.placeholderText = NSLocalizedString("Leave a comment", comment: "")
messageView.textView.placeholderTextColor = Styles.Colors.Gray.light.color
messageView.keyboardType = .twitter
messageView.set(buttonIcon: UIImage(named: "send")?.withRenderingMode(.alwaysTemplate), for: .normal)
messageView.buttonTint = Styles.Colors.Blue.medium.color
messageView.font = Styles.Fonts.body
messageView.inset = UIEdgeInsets(
top: Styles.Sizes.gutter,
left: Styles.Sizes.gutter,
bottom: Styles.Sizes.rowSpacing / 2,
right: Styles.Sizes.gutter
)
messageView.addButton(target: target, action: action)
}
}

View File

@@ -208,6 +208,8 @@
297A372C1F1700BC0081C04E /* IssueRequestModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297A372B1F1700BC0081C04E /* IssueRequestModel.swift */; };
297A372E1F17018F0081C04E /* IssueRequestCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297A372D1F17018F0081C04E /* IssueRequestCell.swift */; };
297A37301F1704C10081C04E /* IssueRequestSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297A372F1F1704C10081C04E /* IssueRequestSectionController.swift */; };
297A6CE4202774830027E03B /* ListAdapter+Scrolling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297A6CE3202774830027E03B /* ListAdapter+Scrolling.swift */; };
297A6CE62027880C0027E03B /* MessageView+Styles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297A6CE52027880C0027E03B /* MessageView+Styles.swift */; };
297AE8791EC0D5C200B44A1F /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297AE85F1EC0D5C100B44A1F /* App.swift */; };
297AE87A1EC0D5C200B44A1F /* Authorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297AE8601EC0D5C100B44A1F /* Authorization.swift */; };
297AE87E1EC0D5C200B44A1F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297AE8671EC0D5C200B44A1F /* AppDelegate.swift */; };
@@ -326,6 +328,9 @@
29DA1E881F5E2B8A0050C64B /* ClearAllHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29DA1E871F5E2B8A0050C64B /* ClearAllHeaderCell.swift */; };
29DA1E8A1F5E2DEC0050C64B /* SearchRecentHeaderSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29DA1E891F5E2DEC0050C64B /* SearchRecentHeaderSectionController.swift */; };
29DA1E8C1F5F8CC40050C64B /* MarkdownAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29DA1E8B1F5F8CC40050C64B /* MarkdownAttribute.swift */; };
29DAA7AB20202A1A0029277A /* PullRequestReviewReplySectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29DAA7AA20202A1A0029277A /* PullRequestReviewReplySectionController.swift */; };
29DAA7AD20202A320029277A /* PullRequestReviewReplyCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29DAA7AC20202A320029277A /* PullRequestReviewReplyCell.swift */; };
29DAA7AF20202BEA0029277A /* PullRequestReviewReplyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29DAA7AE20202BEA0029277A /* PullRequestReviewReplyModel.swift */; };
29DB264A1FCA10A800C3D0C9 /* GithubHighlighting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29DB26491FCA10A800C3D0C9 /* GithubHighlighting.swift */; };
29EB1EEF1F425E5100A200B4 /* ForegroundHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29EB1EEE1F425E5100A200B4 /* ForegroundHandler.swift */; };
29EDFE7C1F65C580005BCCEB /* SplitViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29EDFE7B1F65C580005BCCEB /* SplitViewTests.swift */; };
@@ -628,6 +633,8 @@
297A372B1F1700BC0081C04E /* IssueRequestModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueRequestModel.swift; sourceTree = "<group>"; };
297A372D1F17018F0081C04E /* IssueRequestCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueRequestCell.swift; sourceTree = "<group>"; };
297A372F1F1704C10081C04E /* IssueRequestSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueRequestSectionController.swift; sourceTree = "<group>"; };
297A6CE3202774830027E03B /* ListAdapter+Scrolling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ListAdapter+Scrolling.swift"; sourceTree = "<group>"; };
297A6CE52027880C0027E03B /* MessageView+Styles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageView+Styles.swift"; sourceTree = "<group>"; };
297AE8341EC0D58A00B44A1F /* Freetime.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Freetime.app; sourceTree = BUILT_PRODUCTS_DIR; };
297AE8481EC0D58A00B44A1F /* FreetimeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FreetimeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
297AE84C1EC0D58A00B44A1F /* DateDisplayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateDisplayTests.swift; sourceTree = "<group>"; };
@@ -755,6 +762,9 @@
29DA1E871F5E2B8A0050C64B /* ClearAllHeaderCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClearAllHeaderCell.swift; sourceTree = "<group>"; };
29DA1E891F5E2DEC0050C64B /* SearchRecentHeaderSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchRecentHeaderSectionController.swift; sourceTree = "<group>"; };
29DA1E8B1F5F8CC40050C64B /* MarkdownAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownAttribute.swift; sourceTree = "<group>"; };
29DAA7AA20202A1A0029277A /* PullRequestReviewReplySectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullRequestReviewReplySectionController.swift; sourceTree = "<group>"; };
29DAA7AC20202A320029277A /* PullRequestReviewReplyCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullRequestReviewReplyCell.swift; sourceTree = "<group>"; };
29DAA7AE20202BEA0029277A /* PullRequestReviewReplyModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullRequestReviewReplyModel.swift; sourceTree = "<group>"; };
29DB26491FCA10A800C3D0C9 /* GithubHighlighting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubHighlighting.swift; sourceTree = "<group>"; };
29EB1EEE1F425E5100A200B4 /* ForegroundHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForegroundHandler.swift; sourceTree = "<group>"; };
29EDFE7B1F65C580005BCCEB /* SplitViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplitViewTests.swift; sourceTree = "<group>"; };
@@ -1418,6 +1428,7 @@
29316DBC1ECC8970007CAE3F /* GithubUserSession.swift */,
296B4E301F7C805600C16887 /* GraphQLIDDecode.swift */,
294B11231F7B37D200E04F2D /* ImageCellHeightCache.swift */,
297A6CE3202774830027E03B /* ListAdapter+Scrolling.swift */,
298BA08C1EC90A9000B01946 /* NSAttributedStringSizing.swift */,
292CD3D31F0DC12100D3D57B /* PhotoViewHandler.swift */,
2980033A1F51E82400BE90F4 /* Rating */,
@@ -1428,12 +1439,12 @@
3E79A2FE1F8A7DA700E1126B /* ShortcutHandler.swift */,
29973E551F68BFDE0004B693 /* Signature.swift */,
2971722A1F069E6B005E43AC /* SpinnerSectionController.swift */,
29CCB28A1FDDFFA200E23FA0 /* String+GithubDate.swift */,
29416BFC1F118DD700D03E1A /* String+QueryItemValue.swift */,
293971881F904C82002FAC4B /* Toast.swift */,
2939718A1F904D2A002FAC4B /* Toast+GitHawk.swift */,
292CD3CF1F0DBB5C00D3D57B /* WebviewCellHeightCache.swift */,
2930F2721F8A27750082BA26 /* WidthCache.swift */,
29CCB28A1FDDFFA200E23FA0 /* String+GithubDate.swift */,
D8BAD0651FDF224600C41071 /* WrappingStaticSpacingFlowLayout.swift */,
);
path = Systems;
@@ -1492,6 +1503,7 @@
98835BD11F1A158D005BA24F /* LabelCell.swift */,
D8BAD05F1FDA0A1A00C41071 /* LabelListCell.swift */,
D8BAD0631FDF221900C41071 /* LabelListView.swift */,
297A6CE52027880C0027E03B /* MessageView+Styles.swift */,
29136BE0200A7D3D007317BE /* NavigationTitleDropdownView.swift */,
2963A9331EE2118E0066509C /* ResponderButton.swift */,
29C33FDE1F128D4400EC8D40 /* SelectableCell.swift */,
@@ -1640,6 +1652,9 @@
children = (
29CC292E1FF421DC006B6DE7 /* GithubClient+PullRequestReviewComments.swift */,
29CC292F1FF421DC006B6DE7 /* PullRequestReviewCommentsViewController.swift */,
29DAA7AC20202A320029277A /* PullRequestReviewReplyCell.swift */,
29DAA7AE20202BEA0029277A /* PullRequestReviewReplyModel.swift */,
29DAA7AA20202A1A0029277A /* PullRequestReviewReplySectionController.swift */,
);
path = PullRequestReviews;
sourceTree = "<group>";
@@ -2246,6 +2261,7 @@
files = (
D8BAD0601FDA0A1A00C41071 /* LabelListCell.swift in Sources */,
290744A91F24D2DA00FD9E48 /* AddCommentClient.swift in Sources */,
29DAA7AD20202A320029277A /* PullRequestReviewReplyCell.swift in Sources */,
29316DC51ECC9841007CAE3F /* Alamofire+GithubAPI.swift in Sources */,
75A0ACF51F79A82D0062D99A /* AlertAction.swift in Sources */,
75468F7A1F7AFBC800F2BC19 /* AlertActionBuilder.swift in Sources */,
@@ -2402,6 +2418,7 @@
2928C78A1F15D7E00000D06D /* IssueRenamedCell.swift in Sources */,
DC63393B1F9F65EE00402A8D /* RepositoryAttributedString.swift in Sources */,
2928C7881F15D7C50000D06D /* IssueRenamedModel.swift in Sources */,
297A6CE62027880C0027E03B /* MessageView+Styles.swift in Sources */,
2928C78C1F15D80E0000D06D /* IssueRenamedSectionController.swift in Sources */,
2928C78E1F15DF1B0000D06D /* IssueRenamedString.swift in Sources */,
297A372E1F17018F0081C04E /* IssueRequestCell.swift in Sources */,
@@ -2490,6 +2507,7 @@
2980033E1F51E93500BE90F4 /* RatingSectionController.swift in Sources */,
29FF85A51EE1EA7A007B8762 /* ReactionContent+ReactionType.swift in Sources */,
292FCB1D1EDFCD3D0026635E /* ReactionViewModel.swift in Sources */,
29DAA7AF20202BEA0029277A /* PullRequestReviewReplyModel.swift in Sources */,
29A5AF3F1F9266370065D529 /* NotificationViewModel+Filterable.swift in Sources */,
29B94E691FCB36A000715D7E /* File+ListDiffable.swift in Sources */,
29C9FDD31EC65FEE00EE3A52 /* Repository.swift in Sources */,
@@ -2506,6 +2524,7 @@
986B87341F2CAE9800AAB55C /* RepositoryIssueSummaryType.swift in Sources */,
2905AFAD1F7357C50015AE32 /* RepositoryIssuesViewController.swift in Sources */,
29EE1C1D1F3A33890046A54D /* RepositoryLabel.swift in Sources */,
29DAA7AB20202A1A0029277A /* PullRequestReviewReplySectionController.swift in Sources */,
29792B1D1FFB2FC6007A0C57 /* IssueManagingNavSectionController.swift in Sources */,
2905AFAB1F7357B40015AE32 /* RepositoryOverviewViewController.swift in Sources */,
29EDFE821F661562005BCCEB /* RepositoryReadmeModel.swift in Sources */,
@@ -2526,6 +2545,7 @@
29CC29311FF421DC006B6DE7 /* PullRequestReviewCommentsViewController.swift in Sources */,
986B872B1F2C842000AAB55C /* SearchNoResultsSectionController.swift in Sources */,
29DA1E841F5E26310050C64B /* SearchRecentCell.swift in Sources */,
297A6CE4202774830027E03B /* ListAdapter+Scrolling.swift in Sources */,
DC7857101F97F546009BADDA /* Debouncer.swift in Sources */,
98F9F4031F9CD006005A0266 /* Image+Base64.swift in Sources */,
29DA1E881F5E2B8A0050C64B /* ClearAllHeaderCell.swift in Sources */,