diff --git a/Classes/Issues/Issue+IssueType.swift b/Classes/Issues/Issue+IssueType.swift index e6addc5a..762a1ca6 100644 --- a/Classes/Issues/Issue+IssueType.swift +++ b/Classes/Issues/Issue+IssueType.swift @@ -45,6 +45,24 @@ extension IssueQuery.Data.Repository.Issue: IssueType { ) { results.append(model) } + } else if let unlabeled = node.asUnlabeledEvent { + let model = IssueLabeledModel( + id: unlabeled.id, + actor: unlabeled.actor?.login ?? Strings.unknown, + title: unlabeled.label.name, + color: unlabeled.label.color, + type: .removed + ) + results.append(model) + } else if let labeled = node.asLabeledEvent { + let model = IssueLabeledModel( + id: labeled.id, + actor: labeled.actor?.login ?? Strings.unknown, + title: labeled.label.name, + color: labeled.label.color, + type: .added + ) + results.append(model) } } diff --git a/Classes/Issues/IssuesViewController.swift b/Classes/Issues/IssuesViewController.swift index b1bccb05..b025b600 100644 --- a/Classes/Issues/IssuesViewController.swift +++ b/Classes/Issues/IssuesViewController.swift @@ -62,6 +62,8 @@ final class IssuesViewController: UIViewController, IGListAdapterDataSource, Fee return IssueLabelsSectionController() } else if object is IssueStatusModel { return IssueStatusSectionController() + } else if object is IssueLabeledModel { + return IssueLabeledSectionController() } return IGListSectionController() } diff --git a/Classes/Issues/Labeled/IssueLabeledCell.swift b/Classes/Issues/Labeled/IssueLabeledCell.swift new file mode 100644 index 00000000..afa88f2b --- /dev/null +++ b/Classes/Issues/Labeled/IssueLabeledCell.swift @@ -0,0 +1,83 @@ +// +// IssueLabeledCell.swift +// Freetime +// +// Created by Ryan Nystrom on 6/6/17. +// Copyright © 2017 Ryan Nystrom. All rights reserved. +// + +import UIKit +import SnapKit + +final class IssueLabeledCell: UICollectionViewCell { + + let descriptionLabel = UILabel() + let labelBackgroundView = UIView() + let titleLabel = UILabel() + + override init(frame: CGRect) { + super.init(frame: frame) + + descriptionLabel.backgroundColor = .clear + contentView.addSubview(descriptionLabel) + descriptionLabel.snp.makeConstraints { make in + make.left.equalTo(Styles.Sizes.gutter) + make.centerY.equalTo(contentView) + } + + titleLabel.font = Styles.Fonts.smallTitle + contentView.addSubview(titleLabel) + titleLabel.snp.makeConstraints { make in + make.left.equalTo(descriptionLabel.snp.right).offset(Styles.Sizes.columnSpacing) + make.centerY.equalTo(descriptionLabel) + } + + // even though the background view is behind the title, add it second so that constraints can be setup + labelBackgroundView.layer.cornerRadius = Styles.Sizes.avatarCornerRadius + labelBackgroundView.clipsToBounds = true + contentView.addSubview(labelBackgroundView) + labelBackgroundView.snp.makeConstraints { make in + make.center.equalTo(titleLabel) + make.width.equalTo(titleLabel).offset(Styles.Sizes.columnSpacing) + make.height.equalTo(titleLabel).offset(Styles.Sizes.rowSpacing) + } + + // then swap the z indexes of the label and background + contentView.bringSubview(toFront: titleLabel) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Public API + + func configure(_ model: IssueLabeledModel) { + let actorAttributes = [ + NSForegroundColorAttributeName: Styles.Colors.Gray.dark, + NSFontAttributeName: Styles.Fonts.bodyBold + ] + let actor = NSAttributedString(string: model.actor, attributes: actorAttributes) + + let actionString: String + switch model.type { + case .added: actionString = NSLocalizedString(" added label", comment: "") + case .removed: actionString = NSLocalizedString(" removed label", comment: "") + } + let actionAttributes = [ + NSForegroundColorAttributeName: Styles.Colors.Gray.medium, + NSFontAttributeName: Styles.Fonts.body + ] + let action = NSAttributedString(string: actionString, attributes: actionAttributes) + let descriptionText = NSMutableAttributedString(attributedString: actor) + descriptionText.append(action) + descriptionLabel.attributedText = descriptionText + + let color = UIColor.fromHex(model.color) + labelBackgroundView.backgroundColor = color + + titleLabel.text = model.title + titleLabel.textColor = color.textOverlayColor + } + +} diff --git a/Classes/Issues/Labeled/IssueLabeledModel.swift b/Classes/Issues/Labeled/IssueLabeledModel.swift new file mode 100644 index 00000000..18e913ff --- /dev/null +++ b/Classes/Issues/Labeled/IssueLabeledModel.swift @@ -0,0 +1,48 @@ +// +// IssueLabeledModel.swift +// Freetime +// +// Created by Ryan Nystrom on 6/6/17. +// Copyright © 2017 Ryan Nystrom. All rights reserved. +// + +import Foundation +import IGListKit + +final class IssueLabeledModel: IGListDiffable { + + enum EventType { + case added + case removed + } + + let id: String + let actor: String + let title: String + let color: String + let type: EventType + + init(id: String, actor: String, title: String, color: String, type: EventType) { + self.id = id + self.actor = actor + self.title = title + self.color = color + self.type = type + } + + // MARK: IGListDiffable + + func diffIdentifier() -> NSObjectProtocol { + return id as NSObjectProtocol + } + + func isEqual(toDiffableObject object: IGListDiffable?) -> Bool { + if self === object { return true } + guard let object = object as? IssueLabeledModel else { return false } + return actor == object.actor + && title == object.title + && color == object.color + // skipping type in favor of the event id distinguishing between the two + } + +} diff --git a/Classes/Issues/Labeled/IssueLabeledSectionController.swift b/Classes/Issues/Labeled/IssueLabeledSectionController.swift new file mode 100644 index 00000000..e5c23df5 --- /dev/null +++ b/Classes/Issues/Labeled/IssueLabeledSectionController.swift @@ -0,0 +1,27 @@ +// +// IssueLabeledSectionController.swift +// Freetime +// +// Created by Ryan Nystrom on 6/6/17. +// Copyright © 2017 Ryan Nystrom. All rights reserved. +// + +import Foundation +import IGListKit + +final class IssueLabeledSectionController: IGListGenericSectionController { + + override func sizeForItem(at index: Int) -> CGSize { + guard let width = collectionContext?.containerSize.width else { return .zero } + return CGSize(width: width, height: 30) + } + + override func cellForItem(at index: Int) -> UICollectionViewCell { + guard let cell = collectionContext?.dequeueReusableCell(of: IssueLabeledCell.self, for: self, at: index) as? IssueLabeledCell, + let object = self.object + else { return UICollectionViewCell() } + cell.configure(object) + return cell + } + +} diff --git a/Classes/Issues/PullRequest+IssueType.swift b/Classes/Issues/PullRequest+IssueType.swift index 4a65aba3..58817ec9 100644 --- a/Classes/Issues/PullRequest+IssueType.swift +++ b/Classes/Issues/PullRequest+IssueType.swift @@ -45,6 +45,24 @@ extension PullRequestQuery.Data.Repository.PullRequest: IssueType { ) { results.append(model) } + } else if let unlabeled = node.asUnlabeledEvent { + let model = IssueLabeledModel( + id: unlabeled.id, + actor: unlabeled.actor?.login ?? Strings.unknown, + title: unlabeled.label.name, + color: unlabeled.label.color, + type: .removed + ) + results.append(model) + } else if let labeled = node.asLabeledEvent { + let model = IssueLabeledModel( + id: labeled.id, + actor: labeled.actor?.login ?? Strings.unknown, + title: labeled.label.name, + color: labeled.label.color, + type: .added + ) + results.append(model) } } diff --git a/Classes/Views/Strings.swift b/Classes/Views/Strings.swift index bd35c2a8..0b2aa162 100644 --- a/Classes/Views/Strings.swift +++ b/Classes/Views/Strings.swift @@ -17,5 +17,6 @@ struct Strings { static let signout = NSLocalizedString("Sign out", comment: "") static let open = NSLocalizedString("Open", comment: "") static let closed = NSLocalizedString("Closed", comment: "") + static let unknown = NSLocalizedString("Unknown", comment: "") } diff --git a/Classes/Views/Styles.swift b/Classes/Views/Styles.swift index a07af9f6..48c6dd10 100644 --- a/Classes/Views/Styles.swift +++ b/Classes/Views/Styles.swift @@ -25,6 +25,7 @@ struct Styles { struct Fonts { static let body = UIFont.systemFont(ofSize: 16) + static let bodyBold = UIFont.boldSystemFont(ofSize: 16) static let secondary = UIFont.systemFont(ofSize: 13) static let title = UIFont.boldSystemFont(ofSize: 14) static let button = UIFont.systemFont(ofSize: 17) diff --git a/Freetime.xcodeproj/project.pbxproj b/Freetime.xcodeproj/project.pbxproj index 20c953c3..8913f6e5 100644 --- a/Freetime.xcodeproj/project.pbxproj +++ b/Freetime.xcodeproj/project.pbxproj @@ -141,6 +141,9 @@ 29C9FDDB1EC6627200EE3A52 /* NotificationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29C9FDDA1EC6627200EE3A52 /* NotificationCell.swift */; }; 29C9FDDD1EC6628200EE3A52 /* NotificationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29C9FDDC1EC6628200EE3A52 /* NotificationViewModel.swift */; }; 29C9FDE11EC667AE00EE3A52 /* Styles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29C9FDE01EC667AE00EE3A52 /* Styles.swift */; }; + 29FB942C1EE750DB0016E6D4 /* IssueLabeledModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FB942B1EE750DB0016E6D4 /* IssueLabeledModel.swift */; }; + 29FB942E1EE751F70016E6D4 /* IssueLabeledSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FB942D1EE751F70016E6D4 /* IssueLabeledSectionController.swift */; }; + 29FB94301EE752280016E6D4 /* IssueLabeledCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FB942F1EE752280016E6D4 /* IssueLabeledCell.swift */; }; 29FF85A31EE1CFF7007B8762 /* IssueLabelModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FF85A21EE1CFF7007B8762 /* IssueLabelModel.swift */; }; 29FF85A51EE1EA7A007B8762 /* ReactionContent+ReactionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FF85A41EE1EA7A007B8762 /* ReactionContent+ReactionType.swift */; }; 7C3E80AE871BAB9F2CE23FBA /* Pods_Freetime.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56223772300F4C201A21701F /* Pods_Freetime.framework */; }; @@ -293,6 +296,9 @@ 29C9FDE01EC667AE00EE3A52 /* Styles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Styles.swift; sourceTree = ""; }; 29F63FD01EC530BD007F55E4 /* Github API.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = "Github API.playground"; sourceTree = ""; }; 29F63FD11EC530E7007F55E4 /* TextViews.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = TextViews.playground; sourceTree = ""; }; + 29FB942B1EE750DB0016E6D4 /* IssueLabeledModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueLabeledModel.swift; sourceTree = ""; }; + 29FB942D1EE751F70016E6D4 /* IssueLabeledSectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueLabeledSectionController.swift; sourceTree = ""; }; + 29FB942F1EE752280016E6D4 /* IssueLabeledCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueLabeledCell.swift; sourceTree = ""; }; 29FF85A21EE1CFF7007B8762 /* IssueLabelModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueLabelModel.swift; sourceTree = ""; }; 29FF85A41EE1EA7A007B8762 /* ReactionContent+ReactionType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReactionContent+ReactionType.swift"; sourceTree = ""; }; 56223772300F4C201A21701F /* Pods_Freetime.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Freetime.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -332,6 +338,7 @@ 292FCAE91EDFCC510026635E /* IssuesViewController.swift */, 294563EF1EE5036A00DBCD35 /* IssueType.swift */, 292FCAEA1EDFCC510026635E /* IssueViewModels.swift */, + 29FB942A1EE750720016E6D4 /* Labeled */, 292FCAEC1EDFCC510026635E /* Labels */, 294563ED1EE5012900DBCD35 /* PullRequest+IssueType.swift */, 294563E41EE4EE5B00DBCD35 /* Status */, @@ -676,6 +683,16 @@ path = Notifications; sourceTree = ""; }; + 29FB942A1EE750720016E6D4 /* Labeled */ = { + isa = PBXGroup; + children = ( + 29FB942F1EE752280016E6D4 /* IssueLabeledCell.swift */, + 29FB942B1EE750DB0016E6D4 /* IssueLabeledModel.swift */, + 29FB942D1EE751F70016E6D4 /* IssueLabeledSectionController.swift */, + ); + path = Labeled; + sourceTree = ""; + }; 4AC385A3A25CE8377A0E2188 /* Pods */ = { isa = PBXGroup; children = ( @@ -1007,18 +1024,21 @@ 29C9FDD81EC65FEE00EE3A52 /* User.swift in Sources */, 292FCB0A1EDFCC510026635E /* IssueCommentTextCell.swift in Sources */, 29C167671ECA005500439D62 /* Strings.swift in Sources */, + 29FB942E1EE751F70016E6D4 /* IssueLabeledSectionController.swift in Sources */, 29316DC31ECC981D007CAE3F /* RootNavigationManager.swift in Sources */, 29C9FDCF1EC65FEE00EE3A52 /* Organization.swift in Sources */, 292FCAFF1EDFCC510026635E /* IssueCommentSectionController.swift in Sources */, 297AE87A1EC0D5C200B44A1F /* Authorization.swift in Sources */, 29C1677A1ECA14F700439D62 /* Feed.swift in Sources */, 292FCB171EDFCC510026635E /* IssueLabelSummaryModel.swift in Sources */, + 29FB94301EE752280016E6D4 /* IssueLabeledCell.swift in Sources */, 292FCB051EDFCC510026635E /* MarkdownScanner.swift in Sources */, 29C295171EC7BCDA00D46CD2 /* NotificationsViewController.swift in Sources */, 292FCB101EDFCC510026635E /* IssueViewModels.swift in Sources */, 29316DB11ECC7D89007CAE3F /* SettingsUsersSectionController.swift in Sources */, 294563EE1EE5012900DBCD35 /* PullRequest+IssueType.swift in Sources */, 292FCB0C1EDFCC510026635E /* IssueEvent.swift in Sources */, + 29FB942C1EE750DB0016E6D4 /* IssueLabeledModel.swift in Sources */, 29C295111EC7B83200D46CD2 /* ShowMoreDetailsLabel.swift in Sources */, 29C9FDCE1EC65FEE00EE3A52 /* Notification.swift in Sources */, 297AE8791EC0D5C200B44A1F /* App.swift in Sources */,