Files
GitHawk/Classes/Issues/Managing/IssueManagingSectionController.swift
Ryan Nystrom e56c29a9a6 Redesigned Issues interface (#1983)
* clean up title model

* update title style

* organize issue SC method

* remove background colors from styled text in issues

* collection view bg white

* remove card inset and clean up snap superview code

* clean up label design

* thread is root and change base font size

* remove borders on comment cells

* adjust collapse design

* adjust comment line spacing and make non-root body smaller

* refactor detail view

* viewer header background color

* more buttons in reaction cell

* vertical spacers working

* design basically finished;

* scroll to bottom accounts for inset

* PR review background color white

* fixup PR review comments

* spacing on merge and review

* readme background white

* rename to just "spacer"

* horizontal spacing

* fix tests
2018-07-27 11:03:09 -04:00

335 lines
11 KiB
Swift

//
// IssueManagingSectionController.swift
// Freetime
//
// Created by Ryan Nystrom on 11/13/17.
// Copyright © 2017 Ryan Nystrom. All rights reserved.
//
import Foundation
import IGListKit
import GitHubAPI
import ContextMenu
final class IssueManagingSectionController: ListBindingSectionController<IssueManagingModel>,
ListBindingSectionControllerDataSource,
ListBindingSectionControllerSelectionDelegate,
ContextMenuDelegate {
private enum Action {
static let labels = IssueManagingActionModel(
key: "tag",
label: NSLocalizedString("Labels", comment: ""),
imageName: "tag",
color: "3f88f7".color
)
static let milestone = IssueManagingActionModel(
key: "milestone",
label: NSLocalizedString("Milestone", comment: ""),
imageName: "milestone",
color: "6847ba".color
)
static let assignees = IssueManagingActionModel(
key: "person",
label: NSLocalizedString("Assignees", comment: ""),
imageName: "person",
color: "e77230".color
)
static let reviewers = IssueManagingActionModel(
key: "reviewer",
label: NSLocalizedString("Reviewers", comment: ""),
imageName: "reviewer",
color: "50a451".color
)
static let lock = IssueManagingActionModel(
key: "lock", // share key so lock/unlock just updates cell
label: NSLocalizedString("Lock", comment: ""),
imageName: "lock",
color: Styles.Colors.Gray.dark.color
)
static let unlock = IssueManagingActionModel(
key: "lock", // share key so lock/unlock just updates cell
label: NSLocalizedString("Unlock", comment: ""),
imageName: "key",
color: Styles.Colors.Gray.dark.color
)
static let reopen = IssueManagingActionModel(
key: "open", // share key so open/close just updates cell
label: Constants.Strings.reopen,
imageName: "sync",
color: Styles.Colors.Green.medium.color
)
static let close = IssueManagingActionModel(
key: "open", // share key so open/close just updates cell
label: Constants.Strings.close,
imageName: "x",
color: Styles.Colors.Red.medium.color
)
}
private let model: IssueDetailsModel
private let client: GithubClient
private var updating = false
init(model: IssueDetailsModel, client: GithubClient) {
self.model = model
self.client = client
super.init()
inset = UIEdgeInsets(top: Styles.Sizes.gutter, left: 0, bottom: Styles.Sizes.gutter, right: 0)
minimumInteritemSpacing = Styles.Sizes.rowSpacing
minimumLineSpacing = Styles.Sizes.rowSpacing
selectionDelegate = self
dataSource = self
}
// MARK: Private API
var issueResult: IssueResult? {
guard let id = object?.objectId else { return nil }
return client.cache.get(id: id)
}
func newLabelsController() -> UIViewController {
return LabelsViewController(
selected: issueResult?.labels.labels ?? [],
client: client,
owner: model.owner,
repo: model.repo
)
}
func newMilestonesController() -> UIViewController {
return MilestonesViewController(
client: client,
owner: model.owner,
repo: model.repo,
selected: issueResult?.milestone
)
}
func newPeopleController(type: PeopleViewController.PeopleType) -> UIViewController {
let selections: [String]
let exclusions: [String]
switch type {
case .assignee:
selections = issueResult?.assignee.users.map { $0.login } ?? []
exclusions = []
case .reviewer:
selections = issueResult?.reviewers?.users.map { $0.login } ?? []
if let isPullRequest = issueResult?.pullRequest,
let pullRequestAuthor = issueResult?.rootComment?.details.login,
isPullRequest {
exclusions = [pullRequestAuthor]
} else {
exclusions = []
}
}
return PeopleViewController(
selections: selections,
exclusions: exclusions,
type: type,
client: client,
owner: model.owner,
repo: model.repo
)
}
func present(controller: UIViewController, from cell: UICollectionViewCell) {
guard let viewController = self.viewController else { return }
ContextMenu.shared.show(
sourceViewController: viewController,
viewController: controller,
options: ContextMenu.Options(menuStyle: .minimal),
sourceView: cell,
delegate: self
)
}
func close(_ doClose: Bool) {
guard let previous = issueResult else { return }
client.setStatus(
previous: previous,
owner: model.owner,
repo: model.repo,
number: model.number,
close: doClose
)
Haptic.triggerNotification(.success)
}
func lock(_ doLock: Bool) {
guard let previous = issueResult else { return }
client.setLocked(
previous: previous,
owner: model.owner,
repo: model.repo,
number: model.number,
locked: doLock
)
Haptic.triggerNotification(.success)
}
func didDismiss(selected labels: [RepositoryLabel]) {
guard let previous = issueResult else { return }
client.mutateLabels(
previous: previous,
owner: model.owner,
repo: model.repo,
number: model.number,
labels: labels
)
}
// MARK: ListBindingSectionControllerDataSource
func sectionController(
_ sectionController: ListBindingSectionController<ListDiffable>,
viewModelsFor object: Any
) -> [ListDiffable] {
guard let object = object as? IssueManagingModel,
let result = issueResult
else { fatalError("Object not correct type") }
var models = [ListDiffable]()
if object.role == .collaborator {
models += [
Action.labels,
Action.milestone,
Action.assignees,
]
if object.pullRequest {
models.append(Action.reviewers)
}
if result.labels.locked {
models.append(Action.unlock)
} else {
models.append(Action.lock)
}
}
switch result.labels.status.status {
case .closed: models.append(Action.reopen)
case .open: models.append(Action.close)
case .merged: break // can't do anything
}
return models
}
func sectionController(
_ sectionController: ListBindingSectionController<ListDiffable>,
sizeForViewModel viewModel: Any,
at index: Int
) -> CGSize {
guard let containerWidth = collectionContext?.insetContainerSize.width
else { fatalError("Collection context must be set") }
let height = IssueManagingActionCell.height
let rawRowCount = min(CGFloat(viewModels.count), floor(containerWidth / (height + minimumInteritemSpacing)))
return CGSize(
width: floor((containerWidth - (rawRowCount - 1) * minimumInteritemSpacing) / rawRowCount),
height: height
)
}
func sectionController(
_ sectionController: ListBindingSectionController<ListDiffable>,
cellForViewModel viewModel: Any,
at index: Int
) -> UICollectionViewCell & ListBindable {
guard let cell = collectionContext?.dequeueReusableCell(of: IssueManagingActionCell.self, for: self, at: index) as? IssueManagingActionCell
else { fatalError("Cell not bindable") }
return cell
}
// MARK: ListBindingSectionControllerSelectionDelegate
func sectionController(
_ sectionController: ListBindingSectionController<ListDiffable>,
didSelectItemAt index: Int,
viewModel: Any
) {
collectionContext?.deselectItem(at: index, sectionController: self, animated: trueUnlessReduceMotionEnabled)
guard updating == false,
let viewModel = viewModel as? ListDiffable,
let cell = collectionContext?.cellForItem(at: index, sectionController: self)
else { return }
if viewModel === Action.labels {
present(controller: newLabelsController(), from: cell)
} else if viewModel === Action.milestone {
let controller = newMilestonesController()
present(controller: controller, from: cell)
} else if viewModel === Action.assignees {
let controller = newPeopleController(type: .assignee)
present(controller: controller, from: cell)
} else if viewModel === Action.reviewers {
let controller = newPeopleController(type: .reviewer)
present(controller: controller, from: cell)
} else if viewModel === Action.close {
close(true)
} else if viewModel === Action.reopen {
close(false)
} else if viewModel === Action.lock {
lock(true)
} else if viewModel === Action.unlock {
lock(false)
}
}
// MARK: MilestonesViewControllerDelegate
func didDismiss(controller: MilestonesViewController) {
guard let previous = issueResult else { return }
client.setMilestone(
previous: previous,
owner: model.owner,
repo: model.repo,
number: model.number,
milestone: controller.selected
)
}
// MARK: PeopleViewControllerDelegate
func didDismiss(controller: PeopleViewController) {
guard let previous = issueResult else { return }
let mutationType: V3AddPeopleRequest.PeopleType
switch controller.type {
case .assignee: mutationType = .assignees
case .reviewer: mutationType = .reviewers
}
client.addPeople(
type: mutationType,
previous: previous,
owner: model.owner,
repo: model.repo,
number: model.number,
people: controller.selected
)
}
// MARK: ContextMenuDelegate
func contextMenuWillDismiss(viewController: UIViewController, animated: Bool) {
if let milestones = viewController as? MilestonesViewController {
didDismiss(controller: milestones)
} else if let people = viewController as? PeopleViewController {
didDismiss(controller: people)
} else if let labels = viewController as? LabelsViewController {
didDismiss(selected: labels.selected)
}
}
func contextMenuDidDismiss(viewController: UIViewController, animated: Bool) {}
}