New ContextMenu UX for managing Issues & PRs (#1912)

* wip

* New managing UX using ContextMenu
This commit is contained in:
Ryan Nystrom
2018-07-15 19:50:01 -04:00
committed by GitHub
parent 038600a3a0
commit 1daf87fc86
19 changed files with 582 additions and 47 deletions

View File

@@ -0,0 +1,32 @@
//
// IssueManageButton.swift
// Freetime
//
// Created by Ryan Nystrom on 7/15/18.
// Copyright © 2018 Ryan Nystrom. All rights reserved.
//
import UIKit
func IssueManageButton() -> UIButton {
let button = UIButton()
if let image = UIImage(named: "three-bars")?.withRenderingMode(.alwaysTemplate) {
button.setImage(image, for: .normal)
}
button.tintColor = .white
button.backgroundColor = Styles.Colors.Blue.medium.color
let width = Styles.Sizes.tableCellHeight
button.frame = CGRect(
origin: .zero,
size: CGSize(width: width, height: width)
)
button.layer.cornerRadius = width / 2
button.layer.shadowOpacity = 0.12
button.layer.shadowOffset = CGSize(width: 0, height: 2)
button.layer.shadowColor = UIColor.black.cgColor
button.layer.shadowRadius = 4
button.layer.borderColor = UIColor(white: 1, alpha: 0.2).cgColor
button.layer.borderWidth = 1
button.accessibilityLabel = NSLocalizedString("Show options", comment: "")
return button
}

View File

@@ -0,0 +1,331 @@
//
// IssueManagingContextController.swift
// Freetime
//
// Created by Ryan Nystrom on 7/15/18.
// Copyright © 2018 Ryan Nystrom. All rights reserved.
//
import UIKit
import ContextMenu
import GitHubAPI
final class IssueManagingContextController: NSObject, ContextMenuDelegate {
// Int with lowers-highest permissions to do rank comparisons
enum Permissions: Int {
case none
case author
case collaborator
}
let manageButton: UIView
private var _permissions: Permissions = .none {
didSet {
updateButtonVisibility()
}
}
var permissions: Permissions {
get { return _permissions }
set (newValue) {
// only allow setting to the higher permissions
if newValue.rawValue > _permissions.rawValue {
_permissions = newValue
}
}
}
let model: IssueDetailsModel
var resultID: String? {
didSet {
guard let result = self.result else { return }
if result.viewerCanUpdate {
permissions = .author
}
updateButtonVisibility()
}
}
let client: GithubClient
weak var viewController: UIViewController?
init(model: IssueDetailsModel, client: GithubClient) {
let button = IssueManageButton()
manageButton = button
self.client = client
self.model = model
super.init()
button.isHidden = true
button.addTarget(self, action: #selector(onButton(sender:)), for: .touchUpInside)
}
var result: IssueResult? {
guard let id = resultID else { return nil }
return client.cache.get(id: id) as IssueResult?
}
func updateButtonVisibility() {
let hidden: Bool
if resultID == nil {
hidden = true
} else {
switch permissions {
case .none: hidden = true
case .author, .collaborator: hidden = false
}
}
manageButton.isHidden = hidden
}
enum Action {
case labels
case milestone
case assignees
case reviewers
case unlock
case lock
case reopen
case close
}
func item(_ action: Action) -> ContrastContextMenuItem {
let title: String
let icon: String
switch action {
case .labels:
title = Constants.Strings.labels
icon = "tag"
case .milestone:
title = Constants.Strings.milestone
icon = "milestone"
case .assignees:
title = Constants.Strings.assignees
icon = "person"
case .reviewers:
title = Constants.Strings.reviewers
icon = "reviewer"
case .unlock:
title = NSLocalizedString("Unlock", comment: "")
icon = "key"
case .lock:
title = NSLocalizedString("Lock", comment: "")
icon = "lock"
case .reopen:
title = Constants.Strings.reopen
icon = "sync"
case .close:
title = Constants.Strings.close
icon = "x"
}
let separator: Bool
switch action {
case .reopen, .close: separator = true
default: separator = false
}
return ContrastContextMenuItem(title: title, iconName: icon, separator: separator, action: actionBlock(action))
}
func actionBlock(_ action: Action) -> (ContrastContextMenu) -> Void {
return { [weak self] menu in
menu.dismiss(animated: true)
guard let strongSelf = self else { return }
switch action {
case .labels: strongSelf.presentContextMenu(with: strongSelf.newLabelsController())
case .milestone: strongSelf.presentContextMenu(with: strongSelf.newMilestonesController())
case .assignees: strongSelf.presentContextMenu(with: strongSelf.newPeopleController(type: .assignee))
case .reviewers: strongSelf.presentContextMenu(with: strongSelf.newPeopleController(type: .reviewer))
case .unlock: strongSelf.lock(false)
case .lock: strongSelf.lock(true)
case .reopen: strongSelf.close(false)
case .close: strongSelf.close(true)
}
}
}
@objc func onButton(sender: UIButton) {
guard let result = self.result,
let viewController = self.viewController
else { return }
var items = [ContrastContextMenuItem]()
if case .collaborator = permissions {
items += [ item(.labels), item(.milestone), item(.assignees) ]
if result.pullRequest {
items.append(item(.reviewers))
}
if result.status.locked {
items.append(item(.unlock))
} else {
items.append(item(.lock))
}
}
switch result.status.status {
case .closed:
items.append(item(.reopen))
case .open:
items.append(item(.close))
case .merged: break
}
ContextMenu.shared.show(
sourceViewController: viewController,
viewController: ContrastContextMenu(items: items),
options: ContextMenu.Options(
containerStyle: ContextMenu.ContainerStyle(
xPadding: 0,
yPadding: 4,
backgroundColor: Styles.Colors.menuBackgroundColor.color
),
menuStyle: .minimal
),
sourceView: sender
)
}
func newLabelsController() -> UIViewController {
return LabelsViewController(
selected: result?.labels.labels ?? [],
client: client,
owner: model.owner,
repo: model.repo
)
}
func newMilestonesController() -> UIViewController {
return MilestonesViewController(
client: client,
owner: model.owner,
repo: model.repo,
selected: result?.milestone
)
}
func newPeopleController(type: PeopleViewController.PeopleType) -> UIViewController {
let result = self.result
let selections: [String]
let exclusions: [String]
switch type {
case .assignee:
selections = result?.assignee.users.map { $0.login } ?? []
exclusions = []
case .reviewer:
selections = result?.reviewers?.users.map { $0.login } ?? []
if let isPullRequest = result?.pullRequest,
let pullRequestAuthor = result?.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 presentContextMenu(with controller: UIViewController) {
guard let viewController = self.viewController else { return }
ContextMenu.shared.show(
sourceViewController: viewController,
viewController: controller,
options: ContextMenu.Options(
containerStyle: ContextMenu.ContainerStyle(
backgroundColor: Styles.Colors.menuBackgroundColor.color
)
),
delegate: self
)
}
func close(_ doClose: Bool) {
guard let previous = result 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 = result 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 = result else { return }
client.mutateLabels(
previous: previous,
owner: model.owner,
repo: model.repo,
number: model.number,
labels: labels
)
}
func didDismiss(controller: PeopleViewController) {
guard let previous = result 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
)
}
func didDismiss(controller: MilestonesViewController) {
guard let previous = result else { return }
client.setMilestone(
previous: previous,
owner: model.owner,
repo: model.repo,
number: model.number,
milestone: 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) {}
}

View File

@@ -14,6 +14,8 @@ import SnapKit
import FlatCache
import MessageViewController
import Squawk
import ContextMenu
import GitHubAPI
final class IssuesViewController:
MessageViewController,
@@ -23,7 +25,6 @@ final class IssuesViewController:
FeedSelectionProviding,
IssueNeckLoadSectionControllerDelegate,
FlatCacheListener,
IssueManagingNavSectionControllerDelegate,
IssueCommentSectionControllerDelegate {
private let client: GithubClient
@@ -32,6 +33,7 @@ final class IssuesViewController:
private let textActionsController = TextActionsController()
private var bookmarkNavController: BookmarkNavigationController? = nil
private var autocompleteController: AutocompleteController!
private let manageController: IssueManagingContextController
private var needsScrollToBottom = false
private var lastTimelineElement: ListDiffable?
@@ -67,6 +69,8 @@ final class IssuesViewController:
hidden = true
}
self.setMessageView(hidden: hidden, animated: trueUnlessReduceMotionEnabled)
self.manageController.resultID = resultID
}
}
@@ -94,6 +98,7 @@ final class IssuesViewController:
self.model = model
self.addCommentClient = AddCommentClient(client: client)
self.needsScrollToBottom = scrollToBottom
self.manageController = IssueManagingContextController(model: model, client: client)
super.init(nibName: nil, bundle: nil)
@@ -109,6 +114,8 @@ final class IssuesViewController:
self.addCommentClient.addListener(listener: self)
cacheKey = "issue.\(model.owner).\(model.repo).\(model.number)"
manageController.viewController = self
}
required init?(coder aDecoder: NSCoder) {
@@ -167,10 +174,11 @@ final class IssuesViewController:
actions.frame = CGRect(x: 0, y: 0, width: 0, height: 32)
messageView.add(contentView: actions)
//show disabled bookmark button until issue has finished loading
navigationItem.rightBarButtonItems = [ moreOptionsItem, BookmarkNavigationController.disabledNavigationItem ]
view.addSubview(manageController.manageButton)
}
override func viewDidAppear(_ animated: Bool) {
@@ -198,6 +206,16 @@ final class IssuesViewController:
feed.collectionView.updateSafeInset(container: view, base: Styles.Sizes.threadInset)
}
override func didLayout() {
let manageButtonSize = manageController.manageButton.bounds.size
manageController.manageButton.frame = CGRect(
origin: CGPoint(
x: view.bounds.width - manageButtonSize.width - Styles.Sizes.gutter - view.safeAreaInsets.right,
y: messageView.frame.minY - manageButtonSize.height - Styles.Sizes.gutter),
size: manageButtonSize
)
}
// MARK: Private API
@objc func didPressButton(_ sender: Any?) {
@@ -269,6 +287,9 @@ final class IssuesViewController:
case .read, .none: collab = false
}
self?.viewerIsCollaborator = collab
if collab {
self?.manageController.permissions = .collaborator
}
// avoid finishLoading() so empty view doesn't appear
self?.feed.adapter.performUpdates(animated: trueUnlessReduceMotionEnabled)
case .error:
@@ -397,14 +418,6 @@ final class IssuesViewController:
// side effect so to jump to the last element when auto scrolling
lastTimelineElement = objects.last
if viewerIsCollaborator || current.viewerCanUpdate {
objects.append(IssueManagingModel(
objectId: current.id,
pullRequest: current.pullRequest,
role: viewerIsCollaborator ? .collaborator : .author
))
}
if viewerIsCollaborator,
current.status.status == .open,
let merge = current.mergeModel {
@@ -415,10 +428,6 @@ final class IssuesViewController:
}
func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
if let object = object as? ListDiffable, object === manageKey {
return IssueManagingNavSectionController(delegate: self)
}
switch object {
case is IssueTitleModel: return IssueTitleSectionController()
case is IssueCommentModel:
@@ -448,7 +457,6 @@ final class IssuesViewController:
case is IssueNeckLoadModel: return IssueNeckLoadSectionController(delegate: self)
case is Milestone: return IssueMilestoneSectionController(issueModel: model)
case is IssueFileChangesModel: return IssueViewFilesSectionController(issueModel: model, client: client)
case is IssueManagingModel: return IssueManagingSectionController(model: model, client: client)
case is IssueMergeModel: return IssueMergeSectionController(model: model, client: client, resultID: resultID)
case is IssueTargetBranchModel: return IssueTargetBranchSectionController()
default: fatalError("Unhandled object: \(object)")
@@ -537,12 +545,6 @@ final class IssuesViewController:
}
}
// MARK: IssueManagingNavSectionControllerDelegate
func didSelect(managingNavController: IssueManagingNavSectionController) {
feed.collectionView.scrollToBottom(animated: true)
}
// MARK: IssueCommentSectionControllerDelegate
func didSelectReply(to sectionController: IssueCommentSectionController, commentModel: IssueCommentModel) {

View File

@@ -74,7 +74,9 @@ ContextMenuDelegate {
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

View File

@@ -17,7 +17,8 @@ final class LabelMenuCell: SelectableCell {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
backgroundColor = nil
contentView.backgroundColor = nil
isAccessibilityElement = true
accessibilityTraits |= UIAccessibilityTraitButton
@@ -30,10 +31,8 @@ final class LabelMenuCell: SelectableCell {
}
contentView.addSubview(button)
button.titleLabel?.font = Styles.Text.secondaryBold.preferredFont
button.titleLabel?.font = Styles.Text.bodyBold.preferredFont
button.layer.cornerRadius = Styles.Sizes.avatarCornerRadius
button.layer.borderColor = Styles.Colors.Gray.border.color.cgColor
button.layer.borderWidth = 1 / UIScreen.main.scale
button.clipsToBounds = true
button.isUserInteractionEnabled = false
button.contentEdgeInsets = UIEdgeInsets(
@@ -48,7 +47,8 @@ final class LabelMenuCell: SelectableCell {
make.centerY.equalToSuperview()
}
contentView.addBorder(.bottom, left: Styles.Sizes.gutter, right: -Styles.Sizes.gutter)
let border = contentView.addBorder(.bottom, left: Styles.Sizes.gutter, right: -Styles.Sizes.gutter)
border.backgroundColor = Styles.Colors.Gray.medium.color
}
required init?(coder aDecoder: NSCoder) {

View File

@@ -25,7 +25,7 @@ final class LabelSectionController: ListSwiftSectionController<RepositoryLabel>
size: {
return CGSize(
width: $0.collection.containerSize.width,
height: Styles.Sizes.tableCellHeight
height: Styles.Sizes.tableCellHeightLarge
)
},
configure: { [selected] in

View File

@@ -27,8 +27,9 @@ final class LabelsViewController: BaseListViewController2<String>, BaseListViewC
self.client = client
self.request = RepositoryLabelsQuery(owner: owner, repo: repo)
super.init(emptyErrorMessage: NSLocalizedString("No labels found", comment: ""))
title = NSLocalizedString("Labels", comment: "")
preferredContentSize = CGSize(width: 200, height: 240)
preferredContentSize = Styles.Sizes.contextMenuSize
title = Constants.Strings.labels
feed.collectionView.backgroundColor = Styles.Colors.menuBackgroundColor.color
dataSource = self
}
@@ -36,6 +37,11 @@ final class LabelsViewController: BaseListViewController2<String>, BaseListViewC
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.white]
}
// MARK: Public API
var selected: [RepositoryLabel] {

View File

@@ -18,7 +18,8 @@ final class MilestoneCell2: SelectableCell {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
backgroundColor = nil
contentView.backgroundColor = nil
isAccessibilityElement = true
accessibilityTraits |= UIAccessibilityTraitButton
@@ -31,8 +32,8 @@ final class MilestoneCell2: SelectableCell {
}
contentView.addSubview(label)
label.font = Styles.Text.secondaryBold.preferredFont
label.textColor = Styles.Colors.Gray.dark.color
label.font = Styles.Text.bodyBold.preferredFont
label.textColor = .white
label.snp.makeConstraints { make in
make.left.equalTo(Styles.Sizes.gutter)
make.right.lessThanOrEqualTo(checkedImageView.snp.left)
@@ -41,14 +42,15 @@ final class MilestoneCell2: SelectableCell {
contentView.addSubview(detailLabel)
detailLabel.font = Styles.Text.secondary.preferredFont
detailLabel.textColor = Styles.Colors.Gray.medium.color
detailLabel.textColor = Styles.Colors.Gray.light.color
detailLabel.snp.makeConstraints { make in
make.left.equalTo(label)
make.right.lessThanOrEqualTo(checkedImageView.snp.left)
make.top.equalTo(label.snp.bottom)
}
contentView.addBorder(.bottom, left: Styles.Sizes.gutter, right: -Styles.Sizes.gutter)
let border = contentView.addBorder(.bottom, left: Styles.Sizes.gutter, right: -Styles.Sizes.gutter)
border.backgroundColor = Styles.Colors.Gray.medium.color
}
required init?(coder aDecoder: NSCoder) {

View File

@@ -24,7 +24,7 @@ final class MilestoneSectionController: ListSwiftSectionController<MilestoneView
size: {
return CGSize(
width: $0.collection.containerSize.width,
height: Styles.Sizes.tableCellHeight
height: Styles.Sizes.tableCellHeightLarge
)
},
configure: {
@@ -34,6 +34,7 @@ final class MilestoneSectionController: ListSwiftSectionController<MilestoneView
},
didSelect: { [weak self] context in
guard let strongSelf = self else { return }
context.deselect(animated: true)
strongSelf.delegate?.didSelect(value: context.value, controller: strongSelf)
})
]

View File

@@ -40,8 +40,9 @@ MilestoneSectionControllerDelegate {
self.repo = repo
self.selected = selected
super.init(emptyErrorMessage: NSLocalizedString("No milestones found.", comment: ""))
title = NSLocalizedString("Milestones", comment: "")
preferredContentSize = CGSize(width: 200, height: 240)
title = Constants.Strings.milestone
preferredContentSize = Styles.Sizes.contextMenuSize
feed.collectionView.backgroundColor = Styles.Colors.menuBackgroundColor.color
dataSource = self
}
@@ -49,6 +50,11 @@ MilestoneSectionControllerDelegate {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.white]
}
// MARK: Overrides
override func fetch(page: String?) {

View File

@@ -19,7 +19,8 @@ final class PeopleCell: SelectableCell {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
backgroundColor = nil
contentView.backgroundColor = nil
contentView.addSubview(avatarImageView)
contentView.addSubview(usernameLabel)
@@ -31,7 +32,8 @@ final class PeopleCell: SelectableCell {
make.size.equalTo(Styles.Sizes.avatar)
}
usernameLabel.font = Styles.Text.secondary.preferredFont
usernameLabel.font = Styles.Text.bodyBold.preferredFont
usernameLabel.textColor = .white
usernameLabel.snp.makeConstraints { make in
make.centerY.equalTo(self.contentView)
make.left.equalTo(avatarImageView.snp.right).offset(Styles.Sizes.gutter)
@@ -47,9 +49,10 @@ final class PeopleCell: SelectableCell {
make.size.equalTo(Styles.Sizes.icon)
}
avatarImageView.configureForAvatar()
avatarImageView.configureForAvatar(border: false)
contentView.addBorder(.bottom, left: Styles.Sizes.gutter, right: -Styles.Sizes.gutter)
let border = contentView.addBorder(.bottom, left: Styles.Sizes.gutter, right: -Styles.Sizes.gutter)
border.backgroundColor = Styles.Colors.Gray.medium.color
}
required init?(coder aDecoder: NSCoder) {

View File

@@ -31,7 +31,7 @@ final class PeopleSectionController: ListSwiftSectionController<IssueAssigneeVie
size: {
return CGSize(
width: $0.collection.containerSize.width,
height: Styles.Sizes.tableCellHeight
height: Styles.Sizes.tableCellHeightLarge
)
},
configure: { [selected] in

View File

@@ -54,7 +54,8 @@ PeopleSectionControllerDelegate {
case .reviewer: title = NSLocalizedString("Reviewers", comment: "")
}
preferredContentSize = CGSize(width: 280, height: 240)
feed.collectionView.backgroundColor = Styles.Colors.menuBackgroundColor.color
preferredContentSize = Styles.Sizes.contextMenuSize
updateSelectionCount()
}
@@ -62,6 +63,11 @@ PeopleSectionControllerDelegate {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.white]
}
// MARK: Public API
var selected: [IssueAssigneeViewModel] {

View File

@@ -0,0 +1,108 @@
//
// ContrastContextMenu.swift
// Freetime
//
// Created by Ryan Nystrom on 7/15/18.
// Copyright © 2018 Ryan Nystrom. All rights reserved.
//
import UIKit
struct ContrastContextMenuItem {
let title: String
let iconName: String?
let separator: Bool
let action: ((ContrastContextMenu) -> Void)?
init(
title: String,
iconName: String? = nil,
separator: Bool = false,
action: ((ContrastContextMenu) -> Void)? = nil
) {
self.title = title
self.iconName = iconName
self.separator = separator
self.action = action
}
}
final class ContrastContextMenu: UITableViewController {
private class Cell: UITableViewCell {
static let reuseIdentifier = "cell"
var border: UIView? = nil
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectedBackgroundView = UIView()
selectedBackgroundView?.backgroundColor = Styles.Colors.Gray.medium.color
contentView.backgroundColor = nil
backgroundColor = nil
textLabel?.font = Styles.Text.bodyBold.preferredFont
textLabel?.textColor = .white
imageView?.tintColor = Styles.Colors.Blue.medium.color
border = contentView.addBorder(.top)
border?.backgroundColor = Styles.Colors.Gray.medium.color
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private let items: [ContrastContextMenuItem]
init(items: [ContrastContextMenuItem]) {
self.items = items
super.init(style: .plain)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(Cell.self, forCellReuseIdentifier: Cell.reuseIdentifier)
tableView.rowHeight = Styles.Sizes.tableCellHeight
tableView.separatorStyle = .none
tableView.reloadData()
tableView.layoutIfNeeded()
preferredContentSize = CGSize(width: 180, height: tableView.contentSize.height)
}
// MARK: UITableViewDataSource
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let item = items[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: Cell.reuseIdentifier, for: indexPath)
cell.textLabel?.text = item.title
if let iconName = item.iconName {
cell.imageView?.image = UIImage(named: iconName)?.withRenderingMode(.alwaysTemplate)
}
if let cell = cell as? Cell {
cell.border?.isHidden = !item.separator
}
return cell
}
// MARK: UITableViewDelegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
items[indexPath.row].action?(self)
}
}

View File

@@ -69,4 +69,20 @@ extension UINavigationItem {
}
}
func configure(title: NSAttributedString, accessibilityLabel: String? = nil) {
let label = UILabel()
label.backgroundColor = .clear
label.numberOfLines = 0
label.textAlignment = .center
label.attributedText = title
label.lineBreakMode = .byTruncatingHead
label.adjustsFontSizeToFitWidth = true
label.sizeToFit()
titleView = label
if let accessibilityLabel = accessibilityLabel { // prevent from setting to nil if we don't provide anything, use default instead
titleView?.accessibilityLabel = accessibilityLabel
}
}
}

View File

@@ -49,5 +49,9 @@ enum Constants {
static let bookmark = NSLocalizedString("Bookmark", comment: "")
static let removeBookmark = NSLocalizedString("Remove Bookmark", comment: "")
static let bookmarks = NSLocalizedString("Bookmarks", comment: "")
static let labels = NSLocalizedString("Labels", comment: "")
static let milestone = NSLocalizedString("Milestone", comment: "")
static let assignees = NSLocalizedString("Assignees", comment: "")
static let reviewers = NSLocalizedString("Reviewers", comment: "")
}
}

View File

@@ -52,6 +52,7 @@ enum Styles {
right: Styles.Sizes.commentGutter
)
static let maxImageHeight: CGFloat = 300
static let contextMenuSize: CGSize = CGSize(width: 280, height: 240)
enum HTML {
static let boldWeight = 600
@@ -87,6 +88,7 @@ enum Styles {
static let background = Styles.Colors.Gray.lighter.color
static let purple = "6f42c1"
static let blueGray = "8697af"
static let menuBackgroundColor = "292D35"
enum Red {
static let medium = "cb2431"

View File

@@ -10,12 +10,14 @@ import UIKit
extension UIImageView {
func configureForAvatar() {
func configureForAvatar(border: Bool = true) {
contentMode = .scaleAspectFill
backgroundColor = Styles.Colors.Gray.lighter.color
layer.cornerRadius = Styles.Sizes.avatarCornerRadius
layer.borderColor = Styles.Colors.Gray.light.color.cgColor
layer.borderWidth = 1.0 / UIScreen.main.scale
if border {
layer.borderColor = Styles.Colors.Gray.light.color.cgColor
layer.borderWidth = 1.0 / UIScreen.main.scale
}
clipsToBounds = true
}

View File

@@ -204,6 +204,9 @@
296B4E341F7C80B800C16887 /* GraphQLIDDecodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 296B4E331F7C80B800C16887 /* GraphQLIDDecodeTests.swift */; };
2971722B1F069E6B005E43AC /* SpinnerSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2971722A1F069E6B005E43AC /* SpinnerSectionController.swift */; };
2971722D1F069E96005E43AC /* SpinnerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2971722C1F069E96005E43AC /* SpinnerCell.swift */; };
2973E80D20FBD7BF0050233F /* ContrastContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2973E80C20FBD7BF0050233F /* ContrastContextMenu.swift */; };
2973E80F20FBDB620050233F /* IssueManageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2973E80E20FBDB620050233F /* IssueManageButton.swift */; };
2973E81120FBF73D0050233F /* IssueManagingContextController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2973E81020FBF73D0050233F /* IssueManagingContextController.swift */; };
297403D11F184F8D00ABA95A /* IssueAssigneesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297403D01F184F8D00ABA95A /* IssueAssigneesModel.swift */; };
297403D31F1850DC00ABA95A /* IssueAssigneeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297403D21F1850DC00ABA95A /* IssueAssigneeViewModel.swift */; };
297403D51F18515A00ABA95A /* IssueAssigneeSummaryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297403D41F18515A00ABA95A /* IssueAssigneeSummaryCell.swift */; };
@@ -702,6 +705,9 @@
2971722A1F069E6B005E43AC /* SpinnerSectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpinnerSectionController.swift; sourceTree = "<group>"; };
2971722C1F069E96005E43AC /* SpinnerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpinnerCell.swift; sourceTree = "<group>"; };
2971CED22055C8F600342296 /* Freetime.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Freetime.entitlements; sourceTree = "<group>"; };
2973E80C20FBD7BF0050233F /* ContrastContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContrastContextMenu.swift; sourceTree = "<group>"; };
2973E80E20FBDB620050233F /* IssueManageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueManageButton.swift; sourceTree = "<group>"; };
2973E81020FBF73D0050233F /* IssueManagingContextController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueManagingContextController.swift; sourceTree = "<group>"; };
297403D01F184F8D00ABA95A /* IssueAssigneesModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueAssigneesModel.swift; sourceTree = "<group>"; };
297403D21F1850DC00ABA95A /* IssueAssigneeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueAssigneeViewModel.swift; sourceTree = "<group>"; };
297403D41F18515A00ABA95A /* IssueAssigneeSummaryCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueAssigneeSummaryCell.swift; sourceTree = "<group>"; };
@@ -1116,9 +1122,9 @@
292FCAC71EDFCC510026635E /* Issues */ = {
isa = PBXGroup;
children = (
15245483205DC004005810A6 /* Branches */,
290744A81F24D2DA00FD9E48 /* AddCommentClient.swift */,
297403CF1F184F8400ABA95A /* Assignees */,
15245483205DC004005810A6 /* Branches */,
292FCAC81EDFCC510026635E /* Comments */,
293A45A01F29953800DD1006 /* Commit */,
292CD3B91F0AF26900D3D57B /* DiffHunk */,
@@ -1128,10 +1134,12 @@
294563EB1EE5012100DBCD35 /* Issue+IssueType.swift */,
292CD3C41F0C9EB200D3D57B /* IssueCommentModelHandling.swift */,
291929461F3EAB250012067B /* IssueDetailsModel.swift */,
2973E80E20FBDB620050233F /* IssueManageButton.swift */,
29F7F05B1F2A751B00F6075D /* IssueResult.swift */,
290D2A411F04D3470082E6CC /* IssueStatus.swift */,
295C31CE1F0AA67600521CED /* IssueStatus+ButtonState.swift */,
292FCAE91EDFCC510026635E /* IssuesViewController.swift */,
2973E81020FBF73D0050233F /* IssueManagingContextController.swift */,
292FF8AF1F2FDC33009E63F7 /* IssueTextActionsView.swift */,
294563EF1EE5036A00DBCD35 /* IssueType.swift */,
292FCAEA1EDFCC510026635E /* IssueViewModels.swift */,
@@ -1616,6 +1624,7 @@
2950AB1A2082E47200C6F19A /* AppSplitViewController.swift */,
29CEA5CC1F84DB1B009827DB /* BaseListViewController.swift */,
29C8F9AA208BF64D0075931C /* BaseListViewController2.swift */,
2973E80C20FBD7BF0050233F /* ContrastContextMenu.swift */,
297DD5E01F061BBE006E7E63 /* CreateProfileViewController.swift */,
29316DBE1ECC95DB007CAE3F /* RootViewControllers.swift */,
29C33FDA1F127DBB00EC8D40 /* SplitPlaceholderViewController.swift */,
@@ -2692,6 +2701,7 @@
2949674C1EF9716400B1CF1A /* IssueCommentHrModel.swift in Sources */,
294967531EFC1EDB00B1CF1A /* IssueCommentHtmlCell.swift in Sources */,
294967511EFC1E9E00B1CF1A /* IssueCommentHtmlModel.swift in Sources */,
2973E80D20FBD7BF0050233F /* ContrastContextMenu.swift in Sources */,
292FCAFB1EDFCC510026635E /* IssueCommentImageCell.swift in Sources */,
299997362031DAC000995FFD /* MergeButton.swift in Sources */,
29AF1E8A1F8AB1C30008A0EF /* TextActionsController.swift in Sources */,
@@ -2735,6 +2745,7 @@
292CD3C31F0AF3D400D3D57B /* IssueDiffHunkPreviewCell.swift in Sources */,
29CC294F1FF93AD7006B6DE7 /* IssueCommentEmptyCell.swift in Sources */,
292CD3BD1F0AF3B800D3D57B /* IssueDiffHunkSectionController.swift in Sources */,
2973E80F20FBDB620050233F /* IssueManageButton.swift in Sources */,
291929531F3F9E490012067B /* IssueFileCell.swift in Sources */,
29C8F9AF208C02BE0075931C /* String+ListSwift.swift in Sources */,
98F9F4011F9CCFFE005A0266 /* ImgurClient.swift in Sources */,
@@ -2769,6 +2780,7 @@
29F7F05F1F2A839100F6075D /* IssueNeckLoadSectionController.swift in Sources */,
2919295F1F3FD1F40012067B /* IssuePatchContentViewController.swift in Sources */,
292FF8B91F303DB0009E63F7 /* IssuePreviewModel.swift in Sources */,
2973E81120FBF73D0050233F /* IssueManagingContextController.swift in Sources */,
292FF8B51F303BD0009E63F7 /* IssuePreviewSectionController.swift in Sources */,
292FF8B71F303BD9009E63F7 /* IssuePreviewViewController.swift in Sources */,
292FCB211EDFCF870026635E /* IssueReactionCell.swift in Sources */,