New Inbox design (#1870)

* New Inbox design

* rename ident to number

* use ax animation

* building with old notifications removed

* remove "2" suffix

* use latest IGLK+Swift

* apply IGLK perf fixes

* fix build

* Update with IGLK binding bug fix
This commit is contained in:
Ryan Nystrom
2018-06-17 14:47:59 -04:00
committed by GitHub
parent 76f9b95817
commit faca5497a8
50 changed files with 2000 additions and 2072 deletions

View File

@@ -9,7 +9,7 @@
import Foundation
import IGListKit
final class IssueAssigneeViewModel: ListDiffable, ListSwiftIdentifiable, ListSwiftEquatable {
final class IssueAssigneeViewModel: ListDiffable, ListSwiftDiffable {
let login: String
let avatarURL: URL
@@ -28,14 +28,12 @@ final class IssueAssigneeViewModel: ListDiffable, ListSwiftIdentifiable, ListSwi
return true
}
// MARK: ListSwiftIdentifiable
// MARK: ListSwiftDiffable
var identifier: String {
return avatarURL.absoluteString
}
// MARK: ListSwiftEquatable
func isEqual(to object: ListSwiftDiffable) -> Bool {
return true
}

View File

@@ -231,25 +231,15 @@ final class IssuesViewController:
bookmarkNavController?.configureNavigationItem(bookmarkItem)
}
func viewOwnerAction() -> UIAlertAction? {
weak var weakSelf = self
return AlertAction(AlertActionBuilder { $0.rootViewController = weakSelf })
.view(owner: model.owner)
}
func viewRepoAction() -> UIAlertAction? {
guard let result = result else { return nil }
let repo = RepositoryDetails(
return action(
owner: model.owner,
name: model.repo,
defaultBranch: result.defaultBranch,
hasIssuesEnabled: result.hasIssuesEnabled
repo: model.repo,
branch: result.defaultBranch,
issuesEnabled: result.hasIssuesEnabled,
client: client
)
weak var weakSelf = self
return AlertAction(AlertActionBuilder { $0.rootViewController = weakSelf })
.view(client: client, repo: repo)
}
@objc func onMore(sender: UIBarButtonItem) {
@@ -344,7 +334,7 @@ final class IssuesViewController:
@objc func onNavigationTitle(sender: UIView) {
let alert = UIAlertController.configured(preferredStyle: .actionSheet)
alert.addActions([
viewOwnerAction(),
action(owner: model.owner),
viewRepoAction(),
AlertAction.cancel()
])

View File

@@ -9,7 +9,7 @@
import UIKit
import IGListKit
final class LabelsViewController: BaseListViewController2, BaseListViewController2DataSource {
final class LabelsViewController: BaseListViewController2<String>, BaseListViewController2DataSource {
private let selectedLabels: Set<RepositoryLabel>
private var labels = [RepositoryLabel]()

View File

@@ -9,19 +9,17 @@
import Foundation
import IGListKit
struct MilestoneViewModel: ListSwiftIdentifiable, ListSwiftEquatable {
struct MilestoneViewModel: ListSwiftDiffable {
let title: String
let due: String
let selected: Bool
// MARK: ListSwiftIdentifiable
// MARK: ListSwiftDiffable
var identifier: String {
return title
}
// MARK: ListSwiftEquatable
func isEqual(to value: ListSwiftDiffable) -> Bool {
guard let value = value as? MilestoneViewModel else { return false }
return due == value.due

View File

@@ -9,7 +9,7 @@
import UIKit
import IGListKit
final class MilestonesViewController: BaseListViewController2,
final class MilestonesViewController: BaseListViewController2<String>,
BaseListViewController2DataSource,
MilestoneSectionControllerDelegate {

View File

@@ -0,0 +1,14 @@
//
// Repository.swift
// Freetime
//
// Created by Ryan Nystrom on 6/9/18.
// Copyright © 2018 Ryan Nystrom. All rights reserved.
//
import Foundation
struct Repository {
var owner: String
var name: String
}

View File

@@ -9,7 +9,7 @@
import Foundation
import IGListKit
final class RepositoryLabel: ListDiffable, Hashable, Equatable, ListSwiftIdentifiable, ListSwiftEquatable {
final class RepositoryLabel: ListDiffable, Hashable, Equatable, ListSwiftDiffable {
let color: String
let name: String
@@ -46,14 +46,12 @@ final class RepositoryLabel: ListDiffable, Hashable, Equatable, ListSwiftIdentif
&& lhs.name == rhs.name
}
// MARK: ListSwiftIdentifiable
// MARK: ListSwiftDiffable
var identifier: String {
return name
}
// MARK: ListSwiftEquatable
func isEqual(to object: ListSwiftDiffable) -> Bool {
guard let object = object as? RepositoryLabel else { return false }
return self == object

View File

@@ -9,7 +9,7 @@
import Foundation
import IGListKit
extension String: ListSwiftIdentifiable, ListSwiftEquatable {
extension String: ListSwiftDiffable {
public var identifier: String {
return self

View File

@@ -0,0 +1,24 @@
//
// CreateNotificationTitle.swift
// Freetime
//
// Created by Ryan Nystrom on 6/9/18.
// Copyright © 2018 Ryan Nystrom. All rights reserved.
//
import Foundation
import StyledTextKit
func CreateNotification(title: String, width: CGFloat, contentSizeCategory: UIContentSizeCategory) -> StyledTextRenderer {
// TODO add owner/repo #
let builder = StyledTextBuilder(styledText: StyledText(
text: title,
style: Styles.Text.body
))
.add(attributes: [.foregroundColor: Styles.Colors.Gray.dark.color])
return StyledTextRenderer(
string: builder.build(),
contentSizeCategory: contentSizeCategory,
inset: NotificationCell.inset
).warm(width: width)
}

View File

@@ -0,0 +1,22 @@
//
// InboxType.swift
// Freetime
//
// Created by Ryan Nystrom on 6/16/18.
// Copyright © 2018 Ryan Nystrom. All rights reserved.
//
import Foundation
enum InboxType {
case unread
case repo(Repository)
case all
var showAll: Bool {
switch self {
case .all, .repo: return true
case .unread: return false
}
}
}

View File

@@ -7,41 +7,50 @@
//
import UIKit
import GitHubAPI
func CreateViewModels(
containerWidth: CGFloat,
func CreateNotificationViewModels(
width: CGFloat,
contentSizeCategory: UIContentSizeCategory,
v3notifications: [V3Notification]) -> [NotificationViewModel] {
var viewModels = [NotificationViewModel]()
v3notifications: [V3Notification],
completion: @escaping ([NotificationViewModel]) -> Void
) {
DispatchQueue.global().async {
var models = [NotificationViewModel]()
for notification in v3notifications {
guard let type = NotificationType(rawValue: notification.subject.type.rawValue),
let identifier = notification.subject.identifier
else { continue }
for notification in v3notifications {
guard let type = NotificationType(rawValue: notification.subject.type.rawValue),
let identifier = notification.subject.identifier
else { continue }
let modelIdentifier: NotificationViewModel.Identifier
switch identifier {
case .hash(let h): modelIdentifier = .hash(h)
case .number(let n): modelIdentifier = .number(n)
case .release(let r): modelIdentifier = .release(r)
let number: NotificationViewModel.Number
switch identifier {
case .hash(let h): number = .hash(h)
case .number(let n): number = .number(n)
case .release(let r): number = .release(r)
}
models.append(NotificationViewModel(
v3id: notification.id,
repo: notification.repository.name,
owner: notification.repository.owner.login,
title: CreateNotification(title: notification.subject.title, width: width, contentSizeCategory: contentSizeCategory),
number: number,
state: .pending, // fetched later
date: notification.updatedAt,
ago: notification.updatedAt.agoString(.short),
read: !notification.unread,
comments: 0, // fetched later
watching: true, // assumed based on receiving
type: type,
// TODO get from GQL notification request
branch: "master",
issuesEnabled: true
))
}
let model = NotificationViewModel(
id: notification.id,
title: notification.subject.title,
type: type,
date: notification.updatedAt,
read: !notification.unread,
owner: notification.repository.owner.login,
repo: notification.repository.name,
identifier: modelIdentifier,
containerWidth: containerWidth,
contentSizeCategory: contentSizeCategory
)
viewModels.append(model)
DispatchQueue.main.async {
completion(models)
}
}
return viewModels
}

View File

@@ -1,100 +1,132 @@
//
// NotificationDetailsCell.swift
// NotificationCell.swift
// Freetime
//
// Created by Ryan Nystrom on 5/12/17.
// Copyright © 2017 Ryan Nystrom. All rights reserved.
// Created by Ryan Nystrom on 6/8/18.
// Copyright © 2018 Ryan Nystrom. All rights reserved.
//
import UIKit
import SnapKit
import StyledTextKit
final class NotificationCell: SwipeSelectableCell {
protocol NotificationCellDelegate: class {
func didTapRead(cell: NotificationCell)
func didTapWatch(cell: NotificationCell)
func didTapMore(cell: NotificationCell, sender: UIView)
}
static let labelInset = UIEdgeInsets(
top: Styles.Text.title.preferredFont.lineHeight + 2*Styles.Sizes.rowSpacing,
final class NotificationCell: SelectableCell {
public static let inset = UIEdgeInsets(
top: NotificationCell.topInset + NotificationCell.headerHeight + Styles.Sizes.rowSpacing,
left: Styles.Sizes.icon.width + 2*Styles.Sizes.columnSpacing,
bottom: Styles.Sizes.rowSpacing,
right: Styles.Sizes.gutter + Styles.Sizes.icon.width + Styles.Sizes.columnSpacing
bottom: NotificationCell.actionsHeight,
right: Styles.Sizes.gutter
)
public static let topInset = Styles.Sizes.rowSpacing
public static let headerHeight = ceil(Styles.Text.secondary.preferredFont.lineHeight)
public static let actionsHeight = Styles.Sizes.gutter + 4*Styles.Sizes.rowSpacing
static var minHeight: CGFloat {
// comment icon
return Styles.Sizes.icon.height
// date, comment count labels
+ 2 * Styles.Text.secondary.preferredFont.lineHeight
// padding
+ 3 * Styles.Sizes.rowSpacing
}
private let reasonImageView = UIImageView()
private weak var delegate: NotificationCellDelegate?
private let iconImageView = UIImageView()
private let dateLabel = ShowMoreDetailsLabel()
private let titleLabel = UILabel()
private let detailsLabel = UILabel()
private let textView = StyledTextView()
private let commentLabel = UILabel()
private let commentImageView = UIImageView()
private let stackView = UIStackView()
private let commentButton = UIButton()
private let readButton = UIButton()
private let watchButton = UIButton()
private let moreButton = UIButton()
override init(frame: CGRect) {
super.init(frame: frame)
accessibilityTraits |= UIAccessibilityTraitButton
isAccessibilityElement = true
backgroundColor = .white
contentView.addSubview(titleLabel)
contentView.addSubview(iconImageView)
contentView.addSubview(detailsLabel)
contentView.addSubview(dateLabel)
contentView.addSubview(reasonImageView)
contentView.addSubview(textView)
contentView.addSubview(commentImageView)
contentView.addSubview(commentLabel)
contentView.addSubview(stackView)
stackView.addArrangedSubview(commentButton)
stackView.addArrangedSubview(watchButton)
stackView.addArrangedSubview(readButton)
stackView.addArrangedSubview(moreButton)
titleLabel.numberOfLines = 1
titleLabel.font = Styles.Text.title.preferredFont
titleLabel.textColor = Styles.Colors.Gray.light.color
titleLabel.lineBreakMode = .byTruncatingMiddle
titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
titleLabel.snp.makeConstraints { make in
let grey = Styles.Colors.Gray.light.color
let font = Styles.Text.secondary.preferredFont
let inset = NotificationCell.inset
let actionsHeight = NotificationCell.actionsHeight
iconImageView.snp.makeConstraints { make in
make.top.equalTo(inset.top)
make.centerX.equalTo(inset.left / 2)
}
dateLabel.font = font
dateLabel.textColor = grey
dateLabel.snp.makeConstraints { make in
make.top.equalTo(NotificationCell.topInset)
make.right.equalTo(-inset.right)
}
detailsLabel.lineBreakMode = .byTruncatingMiddle
detailsLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
detailsLabel.snp.makeConstraints { make in
make.top.equalTo(Styles.Sizes.rowSpacing)
make.left.equalTo(NotificationCell.labelInset.left)
make.left.equalTo(NotificationCell.inset.left)
make.right.lessThanOrEqualTo(dateLabel.snp.left).offset(-Styles.Sizes.columnSpacing)
}
dateLabel.backgroundColor = .clear
dateLabel.numberOfLines = 1
dateLabel.font = Styles.Text.secondary.preferredFont
dateLabel.textColor = Styles.Colors.Gray.light.color
dateLabel.textAlignment = .right
dateLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
dateLabel.snp.makeConstraints { make in
make.right.equalTo(-Styles.Sizes.gutter)
make.centerY.equalTo(titleLabel)
stackView.alignment = .center
stackView.distribution = .equalSpacing
stackView.snp.makeConstraints { make in
make.left.equalTo(inset.left)
make.right.equalTo(-inset.right)
make.bottom.equalToSuperview()
make.height.equalTo(actionsHeight)
}
reasonImageView.backgroundColor = .clear
reasonImageView.contentMode = .scaleAspectFit
reasonImageView.snp.makeConstraints { make in
make.size.equalTo(Styles.Sizes.icon)
make.top.equalTo(NotificationCell.labelInset.top)
make.left.equalTo(Styles.Sizes.rowSpacing)
commentButton.titleLabel?.font = font
commentButton.isUserInteractionEnabled = false
commentButton.tintColor = grey
commentButton.setTitleColor(grey, for: .normal)
commentButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: -2, right: 0)
commentButton.titleEdgeInsets = UIEdgeInsets(top: -4, left: 2, bottom: 0, right: 0)
commentButton.setImage(UIImage(named: "comment-small")?.withRenderingMode(.alwaysTemplate), for: .normal)
commentButton.contentHorizontalAlignment = .left
commentButton.snp.makeConstraints { make in
make.width.equalTo(actionsHeight)
}
commentImageView.tintColor = dateLabel.textColor
commentImageView.image = UIImage(named: "comment-small")?.withRenderingMode(.alwaysTemplate)
commentImageView.backgroundColor = .clear
commentImageView.snp.makeConstraints { make in
make.right.equalTo(dateLabel)
make.top.equalTo(dateLabel.snp.bottom).offset(Styles.Sizes.rowSpacing)
watchButton.tintColor = grey
watchButton.addTarget(self, action: #selector(onWatch(sender:)), for: .touchUpInside)
watchButton.contentHorizontalAlignment = .center
watchButton.snp.makeConstraints { make in
make.width.equalTo(actionsHeight)
}
commentLabel.font = dateLabel.font
commentLabel.textColor = dateLabel.textColor
commentLabel.snp.makeConstraints { make in
make.top.equalTo(commentImageView.snp.bottom)
make.centerX.equalTo(commentImageView)
readButton.tintColor = grey
readButton.setImage(UIImage(named: "check-small")?.withRenderingMode(.alwaysTemplate), for: .normal)
readButton.addTarget(self, action: #selector(onRead(sender:)), for: .touchUpInside)
readButton.contentHorizontalAlignment = .center
readButton.snp.makeConstraints { make in
make.width.equalTo(actionsHeight)
}
contentView.addBorder(.bottom, left: NotificationCell.labelInset.left)
moreButton.tintColor = grey
moreButton.setImage(UIImage(named: "bullets-small")?.withRenderingMode(.alwaysTemplate), for: .normal)
moreButton.addTarget(self, action: #selector(onMore(sender:)), for: .touchUpInside)
moreButton.contentHorizontalAlignment = .right
moreButton.snp.makeConstraints { make in
make.width.equalTo(actionsHeight)
}
contentView.addBorder(.bottom, left: inset.left)
}
required init?(coder aDecoder: NSCoder) {
@@ -103,55 +135,65 @@ final class NotificationCell: SwipeSelectableCell {
override func layoutSubviews() {
super.layoutSubviews()
layoutContentViewForSafeAreaInsets()
textView.reposition(for: contentView.bounds.width)
}
// MARK: Public API
var isRead = false {
didSet {
for view in [titleLabel, textView, reasonImageView] {
view.alpha = isRead ? 0.5 : 1
}
}
override var accessibilityLabel: String? {
get { return AccessibilityHelper.generatedLabel(forCell: self)}
set {}
}
func configure(_ viewModel: NotificationViewModel) {
override var canBecomeFirstResponder: Bool {
return true
}
public func configure(with model: NotificationViewModel, delegate: NotificationCellDelegate?) {
textView.configure(with: model.title, width: contentView.bounds.width)
dateLabel.setText(date: model.date, format: .short)
self.delegate = delegate
var titleAttributes = [
NSAttributedStringKey.font: Styles.Text.title.preferredFont,
NSAttributedStringKey.foregroundColor: Styles.Colors.Gray.light.color
]
let title = NSMutableAttributedString(string: "\(viewModel.owner)/\(viewModel.repo) ", attributes: titleAttributes)
let title = NSMutableAttributedString(string: "\(model.owner)/\(model.repo) ", attributes: titleAttributes)
titleAttributes[NSAttributedStringKey.font] = Styles.Text.secondary.preferredFont
switch viewModel.identifier {
switch model.number {
case .number(let number): title.append(NSAttributedString(string: "#\(number)", attributes: titleAttributes))
default: break
}
titleLabel.attributedText = title
textView.configure(with: viewModel.title, width: contentView.bounds.width)
dateLabel.setText(date: viewModel.date)
reasonImageView.image = viewModel.type.icon.withRenderingMode(.alwaysTemplate)
accessibilityLabel = AccessibilityHelper
.generatedLabel(forCell: self)
.appending(".\n\(viewModel.type.localizedString)")
detailsLabel.attributedText = title
let tintColor: UIColor
switch viewModel.state {
switch model.state {
case .closed: tintColor = Styles.Colors.Red.medium.color
case .merged: tintColor = Styles.Colors.purple.color
case .open: tintColor = Styles.Colors.Green.medium.color
case .pending: tintColor = Styles.Colors.Blue.medium.color
}
reasonImageView.tintColor = tintColor
iconImageView.tintColor = tintColor
iconImageView.image = model.type.icon.withRenderingMode(.alwaysTemplate)
let commentHidden = viewModel.commentCount == 0
commentLabel.isHidden = commentHidden
commentImageView.isHidden = commentHidden
commentLabel.text = viewModel.commentCount.abbreviated
let hasComments = model.comments > 0
commentButton.alpha = hasComments ? 1 : 0.3
commentButton.setTitle(hasComments ? model.comments.abbreviated : "", for: .normal)
let watchingImageName = model.watching ? "mute" : "unmute"
watchButton.setImage(UIImage(named: "\(watchingImageName)-small")?.withRenderingMode(.alwaysTemplate), for: .normal)
contentView.alpha = model.read ? 0.5 : 1
}
@objc func onRead(sender: UIView) {
delegate?.didTapRead(cell: self)
}
@objc func onWatch(sender: UIView) {
delegate?.didTapWatch(cell: self)
}
@objc func onMore(sender: UIView) {
delegate?.didTapMore(cell: self, sender: sender)
}
}

View File

@@ -1,19 +1,18 @@
//
// NotificationClient.swift
// NotificationClient2.swift
// Freetime
//
// Created by Ryan Nystrom on 6/30/17.
// Copyright © 2017 Ryan Nystrom. All rights reserved.
// Created by Ryan Nystrom on 6/9/18.
// Copyright © 2018 Ryan Nystrom. All rights reserved.
//
import Foundation
import GitHubAPI
// used to request states via graphQL
extension NotificationViewModel {
var stateAlias: (number: Int, key: String)? {
switch identifier {
switch number {
case .hash, .release:
// commits and releases don't have states, always "merged"
return nil
@@ -24,14 +23,7 @@ extension NotificationViewModel {
}
}
final class NotificationClient {
struct NotificationRepository {
let owner: String
let name: String
}
private var openedNotificationIDs = Set<String>()
final class NotificationModelController {
let githubClient: GithubClient
@@ -43,7 +35,7 @@ final class NotificationClient {
static private let openOnReadKey = "com.freetime.NotificationClient.read-on-open"
static func readOnOpen() -> Bool {
static var readOnOpen: Bool {
return UserDefaults.standard.bool(forKey: openOnReadKey)
}
@@ -53,23 +45,25 @@ final class NotificationClient {
// https://developer.github.com/v3/activity/notifications/#list-your-notifications
func fetchNotifications(
repo: NotificationRepository? = nil,
repo: Repository? = nil,
all: Bool = false,
page: Int = 1,
width: CGFloat,
completion: @escaping (Result<([NotificationViewModel], Int?)>) -> Void
) {
let contentSizeCategory = UIApplication.shared.preferredContentSizeCategory
// TODO move handling + parsing to a single method?
if let repo = repo {
githubClient.client.send(V3RepositoryNotificationRequest(all: all, owner: repo.owner, repo: repo.name)) { result in
switch result {
case .success(let response):
let viewModels = CreateViewModels(
containerWidth: width,
CreateNotificationViewModels(
width: width,
contentSizeCategory: contentSizeCategory,
v3notifications: response.data
)
self.fetchStates(for: viewModels, page: response.next, completion: completion)
) { [weak self] in
self?.fetchStates(for: $0, page: response.next, completion: completion)
}
case .failure(let error):
completion(.error(error))
}
@@ -78,12 +72,13 @@ final class NotificationClient {
githubClient.client.send(V3NotificationRequest(all: all, page: page)) { result in
switch result {
case .success(let response):
let viewModels = CreateViewModels(
containerWidth: width,
CreateNotificationViewModels(
width: width,
contentSizeCategory: contentSizeCategory,
v3notifications: response.data
)
self.fetchStates(for: viewModels, page: response.next, completion: completion)
) { [weak self] in
self?.fetchStates(for: $0, page: response.next, completion: completion)
}
case .failure(let error):
completion(.error(error))
}
@@ -125,7 +120,10 @@ final class NotificationClient {
let state = NotificationViewModel.State(rawValue: stateString),
let commentsJSON = issueOrPullRequest["comments"] as? [String: Any],
let commentCount = commentsJSON["totalCount"] as? Int {
updatedNotifications.append(notification.updated(state: state, commentCount: commentCount))
var newNotification = notification
newNotification.state = state
newNotification.comments = commentCount
updatedNotifications.append(newNotification)
} else {
updatedNotifications.append(notification)
}
@@ -148,7 +146,7 @@ final class NotificationClient {
}
}
func markRepoNotifications(repo: NotificationRepository, completion: @escaping (Bool) -> Void) {
func markRepoNotifications(repo: Repository, completion: @escaping (Bool) -> Void) {
githubClient.client.send(V3MarkRepositoryNotificationsRequest(owner: repo.owner, repo: repo.name)) { result in
switch result {
case .success: completion(true)
@@ -157,32 +155,37 @@ final class NotificationClient {
}
}
func notificationOpened(id: String) -> Bool {
return openedNotificationIDs.contains(id)
}
func markNotificationRead(id: String) {
let cache = githubClient.cache
guard var model = cache.get(id: id) as NotificationViewModel?,
!model.read
else { return }
func markNotificationRead(id: String, isOpen: Bool) {
let oldModel = githubClient.cache.get(id: id) as NotificationViewModel?
model.read = true
cache.set(value: model)
if isOpen {
openedNotificationIDs.insert(id)
} else {
// optimistically set the model to read
// if the request fails, replace this model w/ the old one.
if let old = oldModel {
githubClient.cache.set(value: old.updated(read: true))
}
}
githubClient.client.send(V3MarkThreadsRequest(id: id)) { [weak self] result in
githubClient.client.send(V3MarkThreadsRequest(id: id)) { result in
switch result {
case .success: break
case .failure:
if isOpen {
self?.openedNotificationIDs.remove(id)
} else if let old = oldModel {
self?.githubClient.cache.set(value: old)
}
model.read = false
cache.set(value: model)
}
}
}
func toggleWatch(notification: NotificationViewModel) {
let cache = githubClient.cache
var model = notification
model.watching = !notification.watching
cache.set(value: model)
githubClient.client.send(V3SubscribeThreadRequest(id: model.v3id, ignore: model.watching)) { result in
switch result {
case .success: break
case .failure:
cache.set(value: notification)
}
}
}

View File

@@ -1,52 +0,0 @@
//
// NotificationNextPageCell.swift
// Freetime
//
// Created by Ryan Nystrom on 7/2/17.
// Copyright © 2017 Ryan Nystrom. All rights reserved.
//
import UIKit
import SnapKit
final class NotificationNextPageCell: UICollectionViewCell {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
accessibilityTraits |= UIAccessibilityTraitButton
isAccessibilityElement = true
label.font = Styles.Text.button.preferredFont
label.textColor = Styles.Colors.Gray.light.color
contentView.addSubview(label)
label.snp.makeConstraints { make in
make.center.equalTo(contentView)
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
layoutContentViewForSafeAreaInsets()
}
// MARK: Public API
func configure(page: Int) {
let format = NSLocalizedString("Load page %i", comment: "")
label.text = String(format: format, page)
}
override var accessibilityLabel: String? {
get {
return AccessibilityHelper.generatedLabel(forCell: self)
}
set { }
}
}

View File

@@ -1,46 +0,0 @@
//
// NotificationNextPageSectionController.swift
// Freetime
//
// Created by Ryan Nystrom on 7/2/17.
// Copyright © 2017 Ryan Nystrom. All rights reserved.
//
import UIKit
import IGListKit
protocol NotificationNextPageSectionControllerDelegate: class {
func didSelect(notificationSectionController: NotificationNextPageSectionController)
}
final class NotificationNextPageSectionController: ListGenericSectionController<NSNumber> {
weak var delegate: NotificationNextPageSectionControllerDelegate?
init(delegate: NotificationNextPageSectionControllerDelegate) {
self.delegate = delegate
}
override func sizeForItem(at index: Int) -> CGSize {
guard let width = collectionContext?.containerSize.width else { fatalError("Missing context") }
return CGSize(width: width, height: Styles.Sizes.tableCellHeight)
}
override func cellForItem(at index: Int) -> UICollectionViewCell {
guard let cell = collectionContext?.dequeueReusableCell(
of: NotificationNextPageCell.self,
for: self,
at: index
) as? NotificationNextPageCell,
let object = self.object
else { fatalError("Missing context, object, or cell is wrong type") }
// add one so reads as loading the NEXT page
cell.configure(page: object.intValue)
return cell
}
override func didSelectItem(at index: Int) {
delegate?.didSelect(notificationSectionController: self)
}
}

View File

@@ -1,44 +0,0 @@
//
// NotificationRepoCell.swift
// Freetime
//
// Created by Ryan Nystrom on 5/13/17.
// Copyright © 2017 Ryan Nystrom. All rights reserved.
//
import UIKit
import IGListKit
import SnapKit
final class NotificationRepoCell: UICollectionViewCell, ListBindable {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = Styles.Colors.Gray.lighter.color
label.font = Styles.Text.title.preferredFont
label.textColor = Styles.Colors.Gray.light.color
contentView.addSubview(label)
label.snp.makeConstraints { make in
make.left.equalTo(Styles.Sizes.gutter)
make.centerY.equalTo(self.contentView)
}
contentView.addBorder(.bottom)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: ListBindable
func bindViewModel(_ viewModel: Any) {
guard let viewModel = viewModel as? String else { return }
label.text = viewModel
}
}

View File

@@ -1,127 +1,109 @@
//
// NotificationSectionControllerDelegate.swift
// NotificationSectionController.swift
// Freetime
//
// Created by Ryan Nystrom on 5/13/17.
// Copyright © 2017 Ryan Nystrom. All rights reserved.
// Created by Ryan Nystrom on 6/8/18.
// Copyright © 2018 Ryan Nystrom. All rights reserved.
//
import UIKit
import IGListKit
import SwipeCellKit
import GitHubAPI
final class NotificationSectionController: ListGenericSectionController<NotificationViewModel>,
SwipeCollectionViewCellDelegate {
final class NotificationSectionController: ListSwiftSectionController<NotificationViewModel>, NotificationCellDelegate {
private let client: NotificationClient
private let modelController: NotificationModelController
private let generator = UIImpactFeedbackGenerator()
init(client: NotificationClient) {
self.client = client
init(modelController: NotificationModelController) {
self.modelController = modelController
super.init()
}
override func sizeForItem(at index: Int) -> CGSize {
guard let width = collectionContext?.containerSize.width
else { fatalError("Collection context must be set") }
return CGSize(
width: width,
height: max(ceil(object?.title.viewSize(in: width).height ?? 0), NotificationCell.minHeight)
)
override func createBinders(from value: NotificationViewModel) -> [ListBinder] {
return [
binder(
value,
cellType: ListCellType.class(NotificationCell.self),
size: {
let width = $0.collection.containerSize.width
return CGSize(
width: width,
height: $0.value.title.viewSize(in: width).height
)
},
configure: { [weak self] in
$0.configure(with: $1.value, delegate: self)
},
didSelect: { [weak self] context in
self?.showIssue(model: context.value)
})
]
}
override func cellForItem(at index: Int) -> UICollectionViewCell {
guard let object = self.object,
let cell = collectionContext?.dequeueReusableCell(of: NotificationCell.self, for: self, at: index) as? NotificationCell
else { fatalError("Collection context must be set, missing object, or cell incorrect type") }
cell.delegate = self
cell.configure(object)
cell.isRead = considerObjectRead
return cell
func didTapRead(cell: NotificationCell) {
guard let id = value?.id else { return }
generator.impactOccurred()
modelController.markNotificationRead(id: id)
}
override func didSelectItem(at index: Int) {
guard let object = self.object,
let cell = collectionContext?.cellForItem(at: index, sectionController: self) as? NotificationCell
else { fatalError("Missing object, cell missing, or incorrect type") }
func didTapWatch(cell: NotificationCell) {
guard let value = self.value else { return }
modelController.toggleWatch(notification: value)
}
if NotificationClient.readOnOpen() {
cell.isRead = true
client.markNotificationRead(id: object.id, isOpen: true)
func didTapMore(cell: NotificationCell, sender: UIView) {
guard let value = self.value else { return }
let alert = UIAlertController.configured(preferredStyle: .actionSheet)
alert.addActions([
viewController?.action(owner: value.owner),
viewController?.action(
owner: value.owner,
repo: value.repo,
branch: value.branch,
issuesEnabled: value.issuesEnabled,
client: modelController.githubClient
),
AlertAction.cancel()
])
alert.popoverPresentationController?.setSourceView(sender)
viewController?.present(alert, animated: trueUnlessReduceMotionEnabled)
}
private func showIssue(model: NotificationViewModel) {
if NotificationModelController.readOnOpen {
modelController.markNotificationRead(id: model.id)
}
switch object.identifier {
switch model.number {
case .hash(let hash):
viewController?.presentCommit(owner: object.owner, repo: object.repo, hash: hash)
viewController?.presentCommit(owner: model.owner, repo: model.repo, hash: hash)
case .number(let number):
let model = IssueDetailsModel(owner: object.owner, repo: object.repo, number: number)
let controller = IssuesViewController(
client: client.githubClient,
model: model,
client: modelController.githubClient,
model: IssueDetailsModel(owner: model.owner, repo: model.repo, number: number),
scrollToBottom: true
)
let navigation = UINavigationController(rootViewController: controller)
viewController?.showDetailViewController(navigation, sender: nil)
case .release(let release):
client.githubClient.client
.send(V3ReleaseRequest(owner: object.owner, repo: object.repo, id: release)) { [weak self] result in
switch result {
case .success(let response):
self?.viewController?.presentRelease(
owner: object.owner,
repo: object.repo,
release: response.data.tagName
)
case .failure: ToastManager.showGenericError()
}
}
showRelease(release, model: model)
}
}
// MARK: Private API
func markRead() {
guard let object = object else { fatalError("Should have an object") }
client.markNotificationRead(id: object.id, isOpen: false)
}
var considerObjectRead: Bool {
guard let object = object else { fatalError("Should have an object") }
return object.read || client.notificationOpened(id: object.id)
}
// MARK: SwipeCollectionViewCellDelegate
func collectionView(
_ collectionView: UICollectionView,
editActionsForRowAt indexPath: IndexPath,
for orientation: SwipeActionsOrientation
) -> [SwipeAction]? {
guard orientation == .right, !considerObjectRead else { return nil }
let title = NSLocalizedString("Read", comment: "")
let action = SwipeAction(style: .destructive, title: title) { [weak self] (_, _) in
// swiping-read is an engaging action, system prompt on it
RatingController.prompt(.system)
self?.markRead()
private func showRelease(_ release: String, model: NotificationViewModel) {
modelController.githubClient.client
.send(V3ReleaseRequest(owner: model.owner, repo: model.repo, id: release)) { [weak self] result in
switch result {
case .success(let response):
self?.viewController?.presentRelease(
owner: model.owner,
repo: model.repo,
release: response.data.tagName
)
case .failure:
ToastManager.showGenericError()
}
}
action.backgroundColor = Styles.Colors.Blue.medium.color
action.image = UIImage(named: "check")?.withRenderingMode(.alwaysTemplate)
action.textColor = .white
action.tintColor = .white
action.font = Styles.Text.button.preferredFont
action.transitionDelegate = ScaleTransition.default
return [action]
}
func collectionView(_ collectionView: UICollectionView, editActionsOptionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> SwipeTableOptions {
var options = SwipeTableOptions()
options.expansionStyle = .selection
return options
}
}

View File

@@ -1,32 +0,0 @@
//
// NotificationViewModel+Filterable.swift
// Freetime
//
// Created by Ryan Nystrom on 10/14/17.
// Copyright © 2017 Ryan Nystrom. All rights reserved.
//
import Foundation
extension NotificationViewModel: Filterable {
func match(query: String) -> Bool {
// if query is a number and model is issue/PR, match on the number
switch identifier {
case .number(let id):
if query.rangeOfCharacter(from: CharacterSet.decimalDigits.inverted) == nil {
let ticketId = String(id)
return ticketId.contains(query)
}
default: break
}
let lowerQuery = query.lowercased()
if title.string.allText.lowercased().contains(lowerQuery) { return true }
if owner.lowercased().contains(lowerQuery) { return true }
if repo.lowercased().contains(lowerQuery) { return true }
return false
}
}

View File

@@ -2,19 +2,18 @@
// NotificationViewModel.swift
// Freetime
//
// Created by Ryan Nystrom on 5/12/17.
// Copyright © 2017 Ryan Nystrom. All rights reserved.
// Created by Ryan Nystrom on 6/8/18.
// Copyright © 2018 Ryan Nystrom. All rights reserved.
//
import Foundation
import IGListKit
import FlatCache
import DateAgo
import StyledTextKit
final class NotificationViewModel: ListDiffable, Cachable {
struct NotificationViewModel: ListSwiftDiffable, Cachable {
enum Identifier {
enum Number {
case number(Int)
case hash(String)
case release(String)
@@ -30,123 +29,44 @@ final class NotificationViewModel: ListDiffable, Cachable {
case open = "OPEN"
}
let id: String
let title: StyledTextRenderer
let type: NotificationType
let date: Date
let agoString: String
let read: Bool
let owner: String
let repo: String
let identifier: Identifier
let state: State
let commentCount: Int
var v3id: String
var repo: String
var owner: String
var title: StyledTextRenderer
var number: Number
var state: State
var date: Date
var ago: String // only used for diffing to capture "ago" string at model init
var read: Bool
var comments: Int
var watching: Bool
var type: NotificationType
var branch: String
var issuesEnabled: Bool
init(
id: String,
title: StyledTextRenderer,
type: NotificationType,
date: Date,
read: Bool,
owner: String,
repo: String,
identifier: Identifier,
state: State,
commentCount: Int
) {
self.id = id
self.title = title
self.type = type
self.date = date
self.read = read
self.owner = owner
self.repo = repo
self.identifier = identifier
self.state = state
self.commentCount = commentCount
self.agoString = date.agoString(.long)
// MARK: Identifiable
var id: String {
return v3id
}
convenience init(
id: String,
title: String,
type: NotificationType,
date: Date,
read: Bool,
owner: String,
repo: String,
identifier: Identifier,
containerWidth: CGFloat,
contentSizeCategory: UIContentSizeCategory
) {
let builder = StyledTextBuilder(styledText: StyledText(
text: title,
style: Styles.Text.body
))
.add(attributes: [.foregroundColor: Styles.Colors.Gray.dark.color])
let title = StyledTextRenderer(
string: builder.build(),
contentSizeCategory: contentSizeCategory,
inset: NotificationCell.labelInset
)
self.init(
id: id,
title: title,
type: type,
date: date,
read: read,
owner: owner,
repo: repo,
identifier: identifier,
state: .pending,
commentCount: 0
)
// MARK: ListSwiftDiffable
var identifier: String {
return v3id
}
// MARK: Public API
// MARK: ListSwiftEquatable
func updated(
id: String? = nil,
title: StyledTextRenderer? = nil,
type: NotificationType? = nil,
date: Date? = nil,
read: Bool? = nil,
owner: String? = nil,
repo: String? = nil,
identifier: Identifier? = nil,
state: State? = nil,
commentCount: Int? = nil
) -> NotificationViewModel {
return NotificationViewModel(
id: id ?? self.id,
title: title ?? self.title,
type: type ?? self.type,
date: date ?? self.date,
read: read ?? self.read,
owner: owner ?? self.owner,
repo: repo ?? self.repo,
identifier: identifier ?? self.identifier,
state: state ?? self.state,
commentCount: commentCount ?? self.commentCount
)
}
// MARK: ListDiffable
func diffIdentifier() -> NSObjectProtocol {
return id as NSObjectProtocol
}
func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
if self === object { return true }
guard let object = object as? NotificationViewModel else { return false }
return read == object.read
&& type == object.type
&& agoString == object.agoString
&& repo == object.repo
&& owner == object.owner
&& state == object.state
&& title.string == object.title.string
func isEqual(to value: ListSwiftDiffable) -> Bool {
guard let value = value as? NotificationViewModel else { return false }
// making assumptions that given the v3id, most things don't change
return read == value.read
&& comments == value.comments
&& watching == value.watching
&& state == value.state
&& ago == value.ago
&& title.string == value.title.string
}
}

View File

@@ -2,54 +2,40 @@
// NotificationsViewController.swift
// Freetime
//
// Created by Ryan Nystrom on 5/13/17.
// Copyright © 2017 Ryan Nystrom. All rights reserved.
// Created by Ryan Nystrom on 6/8/18.
// Copyright © 2018 Ryan Nystrom. All rights reserved.
//
import UIKit
import IGListKit
import SnapKit
import FlatCache
class NotificationsViewController: BaseListViewController<NSNumber>,
ForegroundHandlerDelegate,
RatingSectionControllerDelegate,
PrimaryViewController,
TabNavRootViewControllerType,
BaseListViewControllerDataSource,
FlatCacheListener {
final class NotificationsViewController: BaseListViewController2<Int>,
BaseListViewController2DataSource,
ForegroundHandlerDelegate, FlatCacheListener {
enum InboxType {
case unread
case repo(NotificationClient.NotificationRepository)
case all
}
private let client: NotificationClient
private let modelController: NotificationModelController
private let foreground = ForegroundHandler(threshold: 5 * 60)
private let inboxType: InboxType
private var notificationIDs = [String]()
// set to nil and update to dismiss the rating control
private var ratingToken: RatingToken? = RatingController.inFeedToken()
private var notifications: [NotificationViewModel] {
return notificationIDs.compactMap { modelController.githubClient.cache.get(id: $0) }
}
init(client: NotificationClient, inboxType: InboxType) {
self.client = client
init(modelController: NotificationModelController, inboxType: InboxType) {
self.modelController = modelController
self.inboxType = inboxType
super.init(
emptyErrorMessage: NSLocalizedString("Cannot load your inbox.", comment: "")
)
super.init(emptyErrorMessage: NSLocalizedString("Cannot load your inbox.", comment: ""))
self.dataSource = self
self.foreground.delegate = self
switch inboxType {
case .all:
title = NSLocalizedString("All", comment: "")
case .unread:
title = NSLocalizedString("Inbox", comment: "")
case .repo(let repo):
title = repo.name
case .all: title = NSLocalizedString("All", comment: "")
case .unread: title = NSLocalizedString("Inbox", comment: "")
case .repo(let repo): title = repo.name
}
}
@@ -79,7 +65,67 @@ FlatCacheListener {
navigationController?.tabBarItem.badgeColor = Styles.Colors.Red.medium.color
}
// MARK: Private API
override func fetch(page: Int?) {
let width = view.bounds.width
let showAll = inboxType.showAll
let repo: Repository?
switch inboxType {
case .repo(let r): repo = r
case .all, .unread: repo = nil
}
if let page = page {
modelController.fetchNotifications(repo: repo, all: showAll, page: page, width: width) { [weak self] result in
self?.handle(result: result, append: true, animated: false, page: page)
}
} else {
let first = 1
modelController.fetchNotifications(repo: repo, all: showAll, page: first, width: width) { [weak self] result in
self?.handle(result: result, append: false, animated: trueUnlessReduceMotionEnabled, page: first)
}
}
}
private func handle(result: Result<([NotificationViewModel], Int?)>, append: Bool, animated: Bool, page: Int) {
switch result {
case .success(let notifications, let next):
var ids = [String]()
notifications.forEach {
modelController.githubClient.cache.add(listener: self, value: $0)
ids.append($0.id)
}
rebuildAndUpdate(ids: ids, append: append, page: next, animated: animated)
case .error:
error(animated: trueUnlessReduceMotionEnabled)
ToastManager.showNetworkError()
}
// set after updating so self.models has already been changed
updateUnreadState()
}
private func updateUnreadState() {
// don't update tab bar and badges when not showing only new notifications
// prevents archives updating badge and tab #s
switch inboxType {
case .all, .repo: return
case .unread: break
}
var unread = 0
for id in notificationIDs {
guard let model = modelController.githubClient.cache.get(id: id) as NotificationViewModel?,
!model.read
else { continue }
unread += 1
}
let hasUnread = unread > 0
navigationItem.rightBarButtonItem?.isEnabled = hasUnread
navigationController?.tabBarItem.badgeValue = hasUnread ? "\(unread)" : nil
BadgeNotifications.update(count: unread)
}
@objc func onMore(sender: UIBarButtonItem) {
let alert = UIAlertController.configured(preferredStyle: .actionSheet)
@@ -91,33 +137,36 @@ FlatCacheListener {
self?.onViewAll()
}))
let cache = client.githubClient.cache
let cache = modelController.githubClient.cache
var repoNames = Set<String>()
for id in notificationIDs {
guard let notification = cache.get(id: id) as NotificationViewModel?,
!repoNames.contains(notification.repo)
guard let model = cache.get(id: id) as NotificationViewModel?,
!repoNames.contains(model.repo)
else { continue }
repoNames.insert(notification.repo)
alert.add(action: UIAlertAction(title: notification.repo, style: .default, handler: { [weak self] _ in
self?.pushRepoNotifications(owner: notification.owner, repo: notification.repo)
repoNames.insert(model.repo)
alert.add(action: UIAlertAction(title: model.repo, style: .default, handler: { [weak self] _ in
self?.pushRepoNotifications(owner: model.owner, repo: model.repo)
}))
}
alert.add(action: AlertAction.cancel())
alert.popoverPresentationController?.barButtonItem = sender
present(alert, animated: true)
present(alert, animated: trueUnlessReduceMotionEnabled)
}
func pushRepoNotifications(owner: String, repo: String) {
let model = NotificationClient.NotificationRepository(owner: owner, name: repo)
let controller = NotificationsViewController(client: client, inboxType: .repo(model))
let controller = NotificationsViewController(
modelController: modelController,
inboxType: .repo(Repository(owner: owner, name: repo))
)
navigationController?.pushViewController(controller, animated: trueUnlessReduceMotionEnabled)
}
func onViewAll() {
let controller = NotificationsViewController(client: client, inboxType: .all)
let controller = NotificationsViewController(
modelController: modelController,
inboxType: .all
)
navigationController?.pushViewController(controller, animated: trueUnlessReduceMotionEnabled)
}
@@ -131,50 +180,7 @@ FlatCacheListener {
item.accessibilityLabel = NSLocalizedString("Mark notifications read", comment: "")
navigationItem.rightBarButtonItem = item
if updateState {
updateUnreadState(count: notificationIDs.count)
}
}
private func updateUnreadState(count: Int) {
// don't update tab bar and badges when not showing only new notifications
// prevents archives updating badge and tab #s
switch inboxType {
case .all, .repo: return
case .unread: break
}
let hasUnread = count > 0
navigationItem.rightBarButtonItem?.isEnabled = hasUnread
navigationController?.tabBarItem.badgeValue = hasUnread ? "\(count)" : nil
BadgeNotifications.update(count: count)
}
private func markRead() {
self.setRightBarItemSpinning()
let block: (Bool) -> Void = { success in
let generator = UINotificationFeedbackGenerator()
if success {
generator.notificationOccurred(.success)
// clear all badges
BadgeNotifications.update(count: 0)
// change the spinner to the mark all item
// don't update state here; it is managed by `fetch`
self.resetRightBarItem(updatingState: false)
} else {
generator.notificationOccurred(.error)
}
self.fetch(page: nil)
// "mark all" is an engaging action, system prompt on it
RatingController.prompt(.system)
}
switch inboxType {
case .all, .unread: client.markAllNotifications(completion: block)
case .repo(let repo): client.markRepoNotifications(repo: repo, completion: block)
updateUnreadState()
}
}
@@ -202,33 +208,44 @@ FlatCacheListener {
self?.markRead()
}),
AlertAction.cancel()
])
])
present(alert, animated: trueUnlessReduceMotionEnabled)
}
private func handle(result: Result<([NotificationViewModel], Int?)>, append: Bool, animated: Bool, page: Int) {
switch result {
case .success(let notifications, let next):
var ids = [String]()
notifications.forEach {
client.githubClient.cache.add(listener: self, value: $0)
ids.append($0.id)
private func markRead() {
self.setRightBarItemSpinning()
let block: (Bool) -> Void = { success in
let generator = UINotificationFeedbackGenerator()
if success {
generator.notificationOccurred(.success)
// clear all badges
BadgeNotifications.update(count: 0)
// change the spinner to the mark all item
// don't update state here; it is managed by `fetch`
self.resetRightBarItem(updatingState: false)
} else {
generator.notificationOccurred(.error)
}
rebuildAndUpdate(ids: ids, append: append, page: next as NSNumber?, animated: animated)
case .error:
error(animated: trueUnlessReduceMotionEnabled)
ToastManager.showNetworkError()
self.fetch(page: nil)
// "mark all" is an engaging action, system prompt on it
RatingController.prompt(.system)
}
// set after updating so self.models has already been changed
updateUnreadState(count: notificationIDs.count)
switch inboxType {
case .all, .unread: modelController.markAllNotifications(completion: block)
case .repo(let repo): modelController.markRepoNotifications(repo: repo, completion: block)
}
}
private func rebuildAndUpdate(
ids: [String],
append: Bool,
page: NSNumber?,
page: Int?,
animated: Bool
) {
if append {
@@ -239,101 +256,29 @@ FlatCacheListener {
update(page: page, animated: animated)
}
private var showAll: Bool {
switch inboxType {
case .all, .repo: return true
case .unread: return false
}
}
// MARK: BaseListViewController2DataSource
// MARK: Overrides
override func fetch(page: NSNumber?) {
let width = view.bounds.width
let repo: NotificationClient.NotificationRepository?
switch inboxType {
case .repo(let r): repo = r
case .all, .unread: repo = nil
}
if let page = page?.intValue {
client.fetchNotifications(repo: repo, all: showAll, page: page, width: width) { [weak self] result in
self?.handle(result: result, append: true, animated: false, page: page)
}
} else {
let first = 1
client.fetchNotifications(repo: repo, all: showAll, page: first, width: width) { [weak self] result in
self?.handle(result: result, append: false, animated: trueUnlessReduceMotionEnabled, page: first)
func models(adapter: ListSwiftAdapter) -> [ListSwiftPair] {
return notificationIDs.compactMap { id in
guard let model = modelController.githubClient.cache.get(id: id) as NotificationViewModel?
else { return nil }
return ListSwiftPair.pair(model) { [modelController] in
NotificationSectionController(modelController: modelController)
}
}
}
// MARK: BaseListViewControllerDataSource
func headModels(listAdapter: ListAdapter) -> [ListDiffable] {
return []
}
func models(listAdapter: ListAdapter) -> [ListDiffable] {
var models = [NotificationViewModel]()
let showAll = self.showAll
for id in notificationIDs {
if let model = client.githubClient.cache.get(id: id) as NotificationViewModel?,
(showAll || !model.read) {
// swap the model if not read, otherwise exclude it
// this powers the "swipe to archive" feature deleting the cell
models.append(model)
}
}
// every time the list is updated, update bar items and badges
updateUnreadState(count: models.count)
return models
}
func sectionController(model: Any, listAdapter: ListAdapter) -> ListSectionController {
switch model {
case is NotificationViewModel: return NotificationSectionController(client: client)
case is RatingToken: return RatingSectionController(delegate: self)
default: fatalError("Unhandled object: \(model)")
}
}
func emptySectionController(listAdapter: ListAdapter) -> ListSectionController {
return NoNewNotificationSectionController(
topInset: 0,
layoutInsets: view.safeAreaInsets
)
}
// MARK: ForegroundHandlerDelegate
func didForeground(handler: ForegroundHandler) {
feed.refreshHead()
}
// MARK: RatingSectionControllerDelegate
func ratingNeedsDismiss(sectionController: RatingSectionController) {
ratingToken = nil
update(animated: trueUnlessReduceMotionEnabled)
}
// MARK: TabNavRootViewControllerType
func didSingleTapTab() {
feed.collectionView.scrollToTop(animated: trueUnlessReduceMotionEnabled)
}
func didDoubleTapTab() {}
// MARK: FlatCacheListener
func flatCacheDidUpdate(cache: FlatCache, update: FlatCache.Update) {
self.update(animated: trueUnlessReduceMotionEnabled)
updateUnreadState()
}
}

View File

@@ -1,21 +0,0 @@
//
// SegmentedControlModel+Notifications.swift
// Freetime
//
// Created by Ryan Nystrom on 7/6/17.
// Copyright © 2017 Ryan Nystrom. All rights reserved.
//
import Foundation
extension SegmentedControlModel {
static func forNotifications() -> SegmentedControlModel {
return SegmentedControlModel(items: [Constants.Strings.unread, Constants.Strings.all])
}
var unreadSelected: Bool {
return items[selectedIndex] == Constants.Strings.unread
}
}

View File

@@ -10,7 +10,7 @@ import Foundation
import IGListKit
import GitHubAPI
final class PeopleViewController: BaseListViewController2,
final class PeopleViewController: BaseListViewController2<String>,
BaseListViewController2DataSource,
PeopleSectionControllerDelegate {

View File

@@ -9,8 +9,14 @@
import UIKit
import IGListKit
protocol LoadMoreSectionController2Delegate: class {
func didSelect(controller: LoadMoreSectionController2)
}
final class LoadMoreSectionController2: ListSwiftSectionController<String> {
weak var delegate: LoadMoreSectionController2Delegate?
override func createBinders(from value: String) -> [ListBinder] {
return [
binder(value, cellType: .class(LoadMoreCell.self), size: {
@@ -18,6 +24,9 @@ final class LoadMoreSectionController2: ListSwiftSectionController<String> {
width: $0.collection.containerSize.width,
height: Styles.Sizes.tableCellHeight
)
}, didSelect: { [weak self] context in
guard let strongSelf = self else { return }
strongSelf.delegate?.didSelect(controller: strongSelf)
})
]
}

View File

@@ -39,7 +39,7 @@ NewIssueTableViewControllerDelegate {
super.viewDidLoad()
versionLabel.text = Bundle.main.prettyVersionString
markReadSwitch.isOn = NotificationClient.readOnOpen()
markReadSwitch.isOn = NotificationModelController.readOnOpen
apiStatusView.layer.cornerRadius = 7
signatureSwitch.isOn = Signature.enabled
@@ -227,7 +227,7 @@ NewIssueTableViewControllerDelegate {
}
@IBAction func onMarkRead(_ sender: Any) {
NotificationClient.setReadOnOpen(open: markReadSwitch.isOn)
NotificationModelController.setReadOnOpen(open: markReadSwitch.isOn)
}
private func style() {

View File

@@ -13,15 +13,16 @@ protocol BaseListViewController2DataSource: class {
func models(adapter: ListSwiftAdapter) -> [ListSwiftPair]
}
class BaseListViewController2: UIViewController,
class BaseListViewController2<PageType: CustomStringConvertible>: UIViewController,
ListSwiftAdapterDataSource,
FeedDelegate {
FeedDelegate,
LoadMoreSectionController2Delegate {
private let emptyErrorMessage: String
public weak var dataSource: BaseListViewController2DataSource?
public private(set) lazy var feed: Feed = { Feed(viewController: self, delegate: self) }()
private var page: String?
private var page: PageType?
private var hasError = false
private let emptyKey: ListDiffable = "emptyKey" as ListDiffable
@@ -52,7 +53,7 @@ FeedDelegate {
// MARK: Overridable API
func fetch(page: String?) {}
func fetch(page: PageType?) {}
// MARK: Public API
@@ -64,7 +65,7 @@ FeedDelegate {
}
final func update(
page: String?,
page: PageType?,
animated: Bool,
completion: (() -> Void)? = nil
) {
@@ -117,12 +118,22 @@ FeedDelegate {
return []
}
if let page = self.page {
let pagePair = ListSwiftPair.pair(page) { LoadMoreSectionController2() }
if let page = self.page?.description {
let pagePair = ListSwiftPair.pair(page) { [weak self] in
let controller = LoadMoreSectionController2()
controller.delegate = self
return controller
}
return models + [pagePair]
} else {
return models
}
}
// MARK: LoadMoreSectionController2Delegate
func didSelect(controller: LoadMoreSectionController2) {
fetch(page: page)
}
}

View File

@@ -31,7 +31,10 @@ func newSettingsRootViewController(
}
func newNotificationsRootViewController(client: GithubClient) -> UIViewController {
let controller = NotificationsViewController(client: NotificationClient(githubClient: client), inboxType: .unread)
let controller = NotificationsViewController(
modelController: NotificationModelController(githubClient: client),
inboxType: .unread
)
let title = NSLocalizedString("Inbox", comment: "")
controller.title = title
let nav = UINavigationController(rootViewController: controller)

View File

@@ -0,0 +1,37 @@
//
// UIViewController+CommonActionItems.swift
// Freetime
//
// Created by Ryan Nystrom on 6/10/18.
// Copyright © 2018 Ryan Nystrom. All rights reserved.
//
import UIKit
extension UIViewController {
func action(owner: String) -> UIAlertAction? {
weak var weakSelf = self
return AlertAction(AlertActionBuilder { $0.rootViewController = weakSelf })
.view(owner: owner)
}
func action(
owner: String,
repo: String,
branch: String,
issuesEnabled: Bool,
client: GithubClient
) -> UIAlertAction? {
let repo = RepositoryDetails(
owner: owner,
name: repo,
defaultBranch: branch,
hasIssuesEnabled: issuesEnabled
)
weak var weakSelf = self
return AlertAction(AlertActionBuilder { $0.rootViewController = weakSelf })
.view(client: client, repo: repo)
}
}

View File

@@ -11,8 +11,8 @@ import DateAgo
extension ShowMoreDetailsLabel {
func setText(date: Date) {
text = date.agoString(.long)
func setText(date: Date, format: Date.AgoFormat = .long) {
text = date.agoString(format)
detailText = DateDetailsFormatter().string(from: date)
}

View File

@@ -191,8 +191,6 @@
295A77BE1F75C1CC007BC403 /* RepositoryDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 295A77BD1F75C1CC007BC403 /* RepositoryDetails.swift */; };
295B51421FC26B8100C3993B /* PeopleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 295B51411FC26B8100C3993B /* PeopleCell.swift */; };
295B51481FC26F7F00C3993B /* UIImageView+Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 295B51471FC26F7F00C3993B /* UIImageView+Avatar.swift */; };
295C31C71F09E62600521CED /* NotificationNextPageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 295C31C61F09E62600521CED /* NotificationNextPageCell.swift */; };
295C31C91F09E72D00521CED /* NotificationNextPageSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 295C31C81F09E72D00521CED /* NotificationNextPageSectionController.swift */; };
295C31CD1F0AA55400521CED /* IssueStatusEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 295C31CC1F0AA55400521CED /* IssueStatusEvent.swift */; };
295C31CF1F0AA67600521CED /* IssueStatus+ButtonState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 295C31CE1F0AA67600521CED /* IssueStatus+ButtonState.swift */; };
295C31D11F0AA72000521CED /* IssueStatusEvent+ButtonState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 295C31D01F0AA72000521CED /* IssueStatusEvent+ButtonState.swift */; };
@@ -213,7 +211,6 @@
297403D91F18545A00ABA95A /* IssueAssigneesSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297403D81F18545A00ABA95A /* IssueAssigneesSectionController.swift */; };
297403DB1F18550200ABA95A /* IssueAssigneeSummaryModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297403DA1F18550200ABA95A /* IssueAssigneeSummaryModel.swift */; };
297403DD1F185A8700ABA95A /* IssueAssigneeUserCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297403DC1F185A8700ABA95A /* IssueAssigneeUserCell.swift */; };
297406981F0ED1E9003A6BFB /* SegmentedControlModel+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297406971F0ED1E9003A6BFB /* SegmentedControlModel+Notifications.swift */; };
2974069B1F0EDC7C003A6BFB /* IssueCommentTableModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2974069A1F0EDC7C003A6BFB /* IssueCommentTableModel.swift */; };
2974069D1F0EDEAD003A6BFB /* IssueCommentTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2974069C1F0EDEAD003A6BFB /* IssueCommentTableCell.swift */; };
2974069F1F0EDED3003A6BFB /* IssueCommentTableCollectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2974069E1F0EDED3003A6BFB /* IssueCommentTableCollectionCell.swift */; };
@@ -244,7 +241,6 @@
2980033C1F51E82400BE90F4 /* RatingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2980033B1F51E82400BE90F4 /* RatingController.swift */; };
2980033E1F51E93500BE90F4 /* RatingSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2980033D1F51E93500BE90F4 /* RatingSectionController.swift */; };
298003401F51E93B00BE90F4 /* RatingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2980033F1F51E93B00BE90F4 /* RatingCell.swift */; };
2980E0921F073E8B000E02C6 /* NotificationSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2980E0911F073E8B000E02C6 /* NotificationSectionController.swift */; };
2981A8A41EFE9FC700E25EF1 /* GithubEmoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2981A8A31EFE9FC700E25EF1 /* GithubEmoji.swift */; };
2982ED7A1F94EA8F00DBF8EB /* UICollectionViewCell+SafeAreaContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2982ED791F94EA8F00DBF8EB /* UICollectionViewCell+SafeAreaContentView.swift */; };
2986B3581FD30EC400E3CFC6 /* IssueManagingExpansionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2986B3571FD30EC400E3CFC6 /* IssueManagingExpansionModel.swift */; };
@@ -291,10 +287,8 @@
29A195081EC7602500C3E289 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 29A195061EC7601000C3E289 /* Localizable.stringsdict */; };
29A1950A1EC78B4800C3E289 /* NotificationType+Icon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A195091EC78B4800C3E289 /* NotificationType+Icon.swift */; };
29A1950C1EC7901400C3E289 /* NotificationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A1950B1EC7901400C3E289 /* NotificationType.swift */; };
29A195111EC7AC9500C3E289 /* NotificationRepoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A195101EC7AC9500C3E289 /* NotificationRepoCell.swift */; };
29A4768E1ED07A23005D0953 /* DateDetailsFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A4768D1ED07A23005D0953 /* DateDetailsFormatter.swift */; };
29A476A01ED0E6C6005D0953 /* UIColor+Overlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A4769F1ED0E6C6005D0953 /* UIColor+Overlay.swift */; };
29A5AF3F1F9266370065D529 /* NotificationViewModel+Filterable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A5AF3E1F9266370065D529 /* NotificationViewModel+Filterable.swift */; };
29A5AF411F92677D0065D529 /* RepositoryIssueSummaryModel+Filterable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A5AF401F92677D0065D529 /* RepositoryIssueSummaryModel+Filterable.swift */; };
29A5AF431F926C600065D529 /* Filterable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A5AF421F926C600065D529 /* Filterable.swift */; };
29A5AF451F9298360065D529 /* GitHubClient+Repository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A5AF441F9298360065D529 /* GitHubClient+Repository.swift */; };
@@ -308,10 +302,14 @@
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 */; };
29B5D08B20D578DB003DFBE2 /* InboxType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B5D08A20D578DB003DFBE2 /* InboxType.swift */; };
29B94E671FCB2D4600715D7E /* CodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B94E661FCB2D4600715D7E /* CodeView.swift */; };
29B94E691FCB36A000715D7E /* File+ListDiffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B94E681FCB36A000715D7E /* File+ListDiffable.swift */; };
29B94E6D1FCB472400715D7E /* IssueFileChangesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B94E6C1FCB472400715D7E /* IssueFileChangesModel.swift */; };
29B94E6F1FCB743900715D7E /* RepositoryFileCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B94E6E1FCB743900715D7E /* RepositoryFileCell.swift */; };
29BBD82920CAC7D5004D62FE /* NotificationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29BBD82820CAC7D5004D62FE /* NotificationViewModel.swift */; };
29BBD82B20CACB2F004D62FE /* NotificationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29BBD82A20CACB2F004D62FE /* NotificationCell.swift */; };
29BBD82D20CACDFF004D62FE /* NotificationSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29BBD82C20CACDFF004D62FE /* NotificationSectionController.swift */; };
29BE40D32070786400A79C86 /* CMarkParsing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29BE40D22070786400A79C86 /* CMarkParsing.swift */; };
29C0E7071ECBC6C50051D756 /* GithubClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29C0E7061ECBC6C50051D756 /* GithubClient.swift */; };
29C167671ECA005500439D62 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29C167661ECA005500439D62 /* Constants.swift */; };
@@ -328,8 +326,6 @@
29C8F9AD208C02860075931C /* LoadMoreSectionController2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29C8F9AC208C02860075931C /* LoadMoreSectionController2.swift */; };
29C8F9AF208C02BE0075931C /* String+ListSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29C8F9AE208C02BE0075931C /* String+ListSwift.swift */; };
29C8F9B5208C081D0075931C /* LabelSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29C8F9B4208C081D0075931C /* LabelSectionController.swift */; };
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 */; };
29CC29301FF421DC006B6DE7 /* GithubClient+PullRequestReviewComments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29CC292E1FF421DC006B6DE7 /* GithubClient+PullRequestReviewComments.swift */; };
29CC29311FF421DC006B6DE7 /* PullRequestReviewCommentsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29CC292F1FF421DC006B6DE7 /* PullRequestReviewCommentsViewController.swift */; };
@@ -363,7 +359,11 @@
29EE1C1D1F3A33890046A54D /* RepositoryLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29EE1C1C1F3A33890046A54D /* RepositoryLabel.swift */; };
29EE44461F19D5C100B05ED3 /* GithubClient+Issues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29EE44451F19D5C100B05ED3 /* GithubClient+Issues.swift */; };
29EE444A1F19D85800B05ED3 /* ShowErrorStatusBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29EE44491F19D85800B05ED3 /* ShowErrorStatusBar.swift */; };
29EEB22D1F9BF65B00AB237B /* NotificationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29EEB22C1F9BF65B00AB237B /* NotificationsViewController.swift */; };
29F3A18420CADA3A00645CB7 /* NotificationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F3A18320CADA3A00645CB7 /* NotificationsViewController.swift */; };
29F3A18620CBF99E00645CB7 /* NotificationModelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F3A18520CBF99E00645CB7 /* NotificationModelController.swift */; };
29F3A18820CBFF8700645CB7 /* CreateNotificationTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F3A18720CBFF8700645CB7 /* CreateNotificationTitle.swift */; };
29F3A18A20CC017700645CB7 /* Repository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F3A18920CC017700645CB7 /* Repository.swift */; };
29F3A18C20CD790F00645CB7 /* UIViewController+CommonActionItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F3A18B20CD790F00645CB7 /* UIViewController+CommonActionItems.swift */; };
29F7F05C1F2A751B00F6075D /* IssueResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F7F05B1F2A751B00F6075D /* IssueResult.swift */; };
29F7F05F1F2A839100F6075D /* IssueNeckLoadSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F7F05E1F2A839100F6075D /* IssueNeckLoadSectionController.swift */; };
29F7F0611F2A83AA00F6075D /* IssueNeckLoadCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F7F0601F2A83AA00F6075D /* IssueNeckLoadCell.swift */; };
@@ -688,8 +688,6 @@
295A77BD1F75C1CC007BC403 /* RepositoryDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryDetails.swift; sourceTree = "<group>"; };
295B51411FC26B8100C3993B /* PeopleCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeopleCell.swift; sourceTree = "<group>"; };
295B51471FC26F7F00C3993B /* UIImageView+Avatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+Avatar.swift"; sourceTree = "<group>"; };
295C31C61F09E62600521CED /* NotificationNextPageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationNextPageCell.swift; sourceTree = "<group>"; };
295C31C81F09E72D00521CED /* NotificationNextPageSectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationNextPageSectionController.swift; sourceTree = "<group>"; };
295C31CC1F0AA55400521CED /* IssueStatusEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueStatusEvent.swift; sourceTree = "<group>"; };
295C31CE1F0AA67600521CED /* IssueStatus+ButtonState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IssueStatus+ButtonState.swift"; sourceTree = "<group>"; };
295C31D01F0AA72000521CED /* IssueStatusEvent+ButtonState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IssueStatusEvent+ButtonState.swift"; sourceTree = "<group>"; };
@@ -712,7 +710,6 @@
297403D81F18545A00ABA95A /* IssueAssigneesSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueAssigneesSectionController.swift; sourceTree = "<group>"; };
297403DA1F18550200ABA95A /* IssueAssigneeSummaryModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueAssigneeSummaryModel.swift; sourceTree = "<group>"; };
297403DC1F185A8700ABA95A /* IssueAssigneeUserCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueAssigneeUserCell.swift; sourceTree = "<group>"; };
297406971F0ED1E9003A6BFB /* SegmentedControlModel+Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SegmentedControlModel+Notifications.swift"; sourceTree = "<group>"; };
2974069A1F0EDC7C003A6BFB /* IssueCommentTableModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueCommentTableModel.swift; sourceTree = "<group>"; };
2974069C1F0EDEAD003A6BFB /* IssueCommentTableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueCommentTableCell.swift; sourceTree = "<group>"; };
2974069E1F0EDED3003A6BFB /* IssueCommentTableCollectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueCommentTableCollectionCell.swift; sourceTree = "<group>"; };
@@ -747,7 +744,6 @@
2980033B1F51E82400BE90F4 /* RatingController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RatingController.swift; sourceTree = "<group>"; };
2980033D1F51E93500BE90F4 /* RatingSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RatingSectionController.swift; sourceTree = "<group>"; };
2980033F1F51E93B00BE90F4 /* RatingCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RatingCell.swift; sourceTree = "<group>"; };
2980E0911F073E8B000E02C6 /* NotificationSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationSectionController.swift; sourceTree = "<group>"; };
2981A8A31EFE9FC700E25EF1 /* GithubEmoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubEmoji.swift; sourceTree = "<group>"; };
2981A8A61EFEBEF900E25EF1 /* EmojiTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiTests.swift; sourceTree = "<group>"; };
2982ED791F94EA8F00DBF8EB /* UICollectionViewCell+SafeAreaContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UICollectionViewCell+SafeAreaContentView.swift"; sourceTree = "<group>"; };
@@ -795,11 +791,9 @@
29A195061EC7601000C3E289 /* Localizable.stringsdict */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = "<group>"; };
29A195091EC78B4800C3E289 /* NotificationType+Icon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NotificationType+Icon.swift"; sourceTree = "<group>"; };
29A1950B1EC7901400C3E289 /* NotificationType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationType.swift; sourceTree = "<group>"; };
29A195101EC7AC9500C3E289 /* NotificationRepoCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationRepoCell.swift; sourceTree = "<group>"; };
29A4768D1ED07A23005D0953 /* DateDetailsFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateDetailsFormatter.swift; sourceTree = "<group>"; };
29A4769F1ED0E6C6005D0953 /* UIColor+Overlay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Overlay.swift"; sourceTree = "<group>"; };
29A476B11ED24D99005D0953 /* IssueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueTests.swift; sourceTree = "<group>"; };
29A5AF3E1F9266370065D529 /* NotificationViewModel+Filterable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationViewModel+Filterable.swift"; sourceTree = "<group>"; };
29A5AF401F92677D0065D529 /* RepositoryIssueSummaryModel+Filterable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RepositoryIssueSummaryModel+Filterable.swift"; sourceTree = "<group>"; };
29A5AF421F926C600065D529 /* Filterable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filterable.swift; sourceTree = "<group>"; };
29A5AF441F9298360065D529 /* GitHubClient+Repository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GitHubClient+Repository.swift"; sourceTree = "<group>"; };
@@ -813,10 +807,14 @@
29AF1E8B1F8ABC5A0008A0EF /* RepositoryCodeDirectoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryCodeDirectoryViewController.swift; sourceTree = "<group>"; };
29AF1E8D1F8ABC900008A0EF /* RepositoryFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryFile.swift; sourceTree = "<group>"; };
29B0EF861F93DF6C00870291 /* RepositoryCodeBlobViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryCodeBlobViewController.swift; sourceTree = "<group>"; };
29B5D08A20D578DB003DFBE2 /* InboxType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxType.swift; sourceTree = "<group>"; };
29B94E661FCB2D4600715D7E /* CodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeView.swift; sourceTree = "<group>"; };
29B94E681FCB36A000715D7E /* File+ListDiffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "File+ListDiffable.swift"; sourceTree = "<group>"; };
29B94E6C1FCB472400715D7E /* IssueFileChangesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueFileChangesModel.swift; sourceTree = "<group>"; };
29B94E6E1FCB743900715D7E /* RepositoryFileCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryFileCell.swift; sourceTree = "<group>"; };
29BBD82820CAC7D5004D62FE /* NotificationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewModel.swift; sourceTree = "<group>"; };
29BBD82A20CACB2F004D62FE /* NotificationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationCell.swift; sourceTree = "<group>"; };
29BBD82C20CACDFF004D62FE /* NotificationSectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSectionController.swift; sourceTree = "<group>"; };
29BE40D22070786400A79C86 /* CMarkParsing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CMarkParsing.swift; sourceTree = "<group>"; };
29C0E7061ECBC6C50051D756 /* GithubClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GithubClient.swift; sourceTree = "<group>"; };
29C167661ECA005500439D62 /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
@@ -835,8 +833,6 @@
29C8F9AC208C02860075931C /* LoadMoreSectionController2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreSectionController2.swift; sourceTree = "<group>"; };
29C8F9AE208C02BE0075931C /* String+ListSwift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+ListSwift.swift"; sourceTree = "<group>"; };
29C8F9B4208C081D0075931C /* LabelSectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelSectionController.swift; sourceTree = "<group>"; };
29C9FDDA1EC6627200EE3A52 /* NotificationCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationCell.swift; sourceTree = "<group>"; };
29C9FDDC1EC6628200EE3A52 /* NotificationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationViewModel.swift; sourceTree = "<group>"; };
29C9FDE01EC667AE00EE3A52 /* Styles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Styles.swift; sourceTree = "<group>"; };
29CC292E1FF421DC006B6DE7 /* GithubClient+PullRequestReviewComments.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "GithubClient+PullRequestReviewComments.swift"; sourceTree = "<group>"; };
29CC292F1FF421DC006B6DE7 /* PullRequestReviewCommentsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PullRequestReviewCommentsViewController.swift; sourceTree = "<group>"; };
@@ -870,7 +866,11 @@
29EE1C1C1F3A33890046A54D /* RepositoryLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepositoryLabel.swift; sourceTree = "<group>"; };
29EE44451F19D5C100B05ED3 /* GithubClient+Issues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "GithubClient+Issues.swift"; sourceTree = "<group>"; };
29EE44491F19D85800B05ED3 /* ShowErrorStatusBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShowErrorStatusBar.swift; sourceTree = "<group>"; };
29EEB22C1F9BF65B00AB237B /* NotificationsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationsViewController.swift; sourceTree = "<group>"; };
29F3A18320CADA3A00645CB7 /* NotificationsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsViewController.swift; sourceTree = "<group>"; };
29F3A18520CBF99E00645CB7 /* NotificationModelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationModelController.swift; sourceTree = "<group>"; };
29F3A18720CBFF8700645CB7 /* CreateNotificationTitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateNotificationTitle.swift; sourceTree = "<group>"; };
29F3A18920CC017700645CB7 /* Repository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Repository.swift; sourceTree = "<group>"; };
29F3A18B20CD790F00645CB7 /* UIViewController+CommonActionItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+CommonActionItems.swift"; sourceTree = "<group>"; };
29F7F05B1F2A751B00F6075D /* IssueResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueResult.swift; sourceTree = "<group>"; };
29F7F05E1F2A839100F6075D /* IssueNeckLoadSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueNeckLoadSectionController.swift; sourceTree = "<group>"; };
29F7F0601F2A83AA00F6075D /* IssueNeckLoadCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueNeckLoadCell.swift; sourceTree = "<group>"; };
@@ -1558,6 +1558,7 @@
98003D8C1FCAD7FC00755C17 /* LabelDetails.swift */,
29EE1C1C1F3A33890046A54D /* RepositoryLabel.swift */,
29C8F9AE208C02BE0075931C /* String+ListSwift.swift */,
29F3A18920CC017700645CB7 /* Repository.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -1628,6 +1629,7 @@
D8D876F71FB6083200A57E2B /* UIPopoverPresentationController+SourceView.swift */,
297AE8691EC0D5C200B44A1F /* UIViewController+Alerts.swift */,
29AF1E851F8AADD00008A0EF /* UIViewController+CancelAction.swift */,
29F3A18B20CD790F00645CB7 /* UIViewController+CommonActionItems.swift */,
29CEA5CE1F84DCB3009827DB /* UIViewController+EmptyBackBar.swift */,
29136BE2200AAA5A007317BE /* UIViewController+FilePathTitle.swift */,
292CD3D71F0DC52900D3D57B /* UIViewController+IssueCommentHtmlCellNavigationDelegate.swift */,
@@ -1808,22 +1810,19 @@
29C9FDD91EC6613F00EE3A52 /* Notifications */ = {
isa = PBXGroup;
children = (
29F3A18720CBFF8700645CB7 /* CreateNotificationTitle.swift */,
290EF5751F06BA06006A2160 /* NoNewNotificationsCell.swift */,
290EF5781F06BAF4006A2160 /* NoNewNotificationsSectionController.swift */,
290EF5691F06A7E1006A2160 /* Notification+NotificationViewModel.swift */,
29C9FDDA1EC6627200EE3A52 /* NotificationCell.swift */,
297DD5E21F06922A006E7E63 /* NotificationClient.swift */,
29B5D08A20D578DB003DFBE2 /* InboxType.swift */,
29BBD82A20CACB2F004D62FE /* NotificationCell.swift */,
29CF01D21FDDA1EE0084B66F /* NotificationEmptyMessageClient.swift */,
295C31C61F09E62600521CED /* NotificationNextPageCell.swift */,
295C31C81F09E72D00521CED /* NotificationNextPageSectionController.swift */,
29A195101EC7AC9500C3E289 /* NotificationRepoCell.swift */,
2980E0911F073E8B000E02C6 /* NotificationSectionController.swift */,
29EEB22C1F9BF65B00AB237B /* NotificationsViewController.swift */,
29F3A18520CBF99E00645CB7 /* NotificationModelController.swift */,
29BBD82C20CACDFF004D62FE /* NotificationSectionController.swift */,
29F3A18320CADA3A00645CB7 /* NotificationsViewController.swift */,
29A1950B1EC7901400C3E289 /* NotificationType.swift */,
29A195091EC78B4800C3E289 /* NotificationType+Icon.swift */,
29C9FDDC1EC6628200EE3A52 /* NotificationViewModel.swift */,
29A5AF3E1F9266370065D529 /* NotificationViewModel+Filterable.swift */,
297406971F0ED1E9003A6BFB /* SegmentedControlModel+Notifications.swift */,
29BBD82820CAC7D5004D62FE /* NotificationViewModel.swift */,
);
path = Notifications;
sourceTree = "<group>";
@@ -2638,9 +2637,9 @@
29A4768E1ED07A23005D0953 /* DateDetailsFormatter.swift in Sources */,
29AF1E8C1F8ABC5A0008A0EF /* RepositoryCodeDirectoryViewController.swift in Sources */,
291929611F3FD2960012067B /* DiffString.swift in Sources */,
29EEB22D1F9BF65B00AB237B /* NotificationsViewController.swift in Sources */,
290744B51F250A6800FD9E48 /* EmojiAutocomplete.swift in Sources */,
29C167691ECA016500439D62 /* EmptyView.swift in Sources */,
29F3A18420CADA3A00645CB7 /* NotificationsViewController.swift in Sources */,
2957777B200129EB00DDD785 /* Int+Abbreviated.swift in Sources */,
29C1677A1ECA14F700439D62 /* Feed.swift in Sources */,
291929551F3FAADF0012067B /* FeedRefresh.swift in Sources */,
@@ -2649,6 +2648,7 @@
49FE18FD204B5D32001681E8 /* Sequence+Contains.swift in Sources */,
291929421F3EA8CD0012067B /* File.swift in Sources */,
299E86491EFD9DBB00E5FE70 /* FlexController.m in Sources */,
29F3A18620CBF99E00645CB7 /* NotificationModelController.swift in Sources */,
29EB1EEF1F425E5100A200B4 /* ForegroundHandler.swift in Sources */,
29C167741ECA0DBB00439D62 /* GithubAPIDateFormatter.swift in Sources */,
294434E11FB1F2DA00050C06 /* BookmarkNavigationController.swift in Sources */,
@@ -2751,11 +2751,13 @@
2908C5891F6F3EB00071C39D /* IssueLocalReaction.swift in Sources */,
2931892B1F5397E400EF0911 /* IssueMilestoneCell.swift in Sources */,
292EB08F1F1FF5EC0046865D /* IssueMilestoneEventCell.swift in Sources */,
29F3A18820CBFF8700645CB7 /* CreateNotificationTitle.swift in Sources */,
292EB08D1F1FF58D0046865D /* IssueMilestoneEventModel.swift in Sources */,
292EB0911F1FF72D0046865D /* IssueMilestoneEventSectionController.swift in Sources */,
DCA5ED141FAEE8030072F074 /* Bookmark.swift in Sources */,
299F4A89204CEDDC004BA4F0 /* Client+AccessToken.swift in Sources */,
2931892F1F539C0E00EF0911 /* IssueMilestoneSectionController.swift in Sources */,
29BBD82920CAC7D5004D62FE /* NotificationViewModel.swift in Sources */,
299F63E2205DE1470015D901 /* UIView+DateDetails.swift in Sources */,
9870B9031FC73EE70009719C /* Secrets.swift in Sources */,
29F7F0611F2A83AA00F6075D /* IssueNeckLoadCell.swift in Sources */,
@@ -2790,6 +2792,7 @@
29136BE1200A7D3D007317BE /* NavigationTitleDropdownView.swift in Sources */,
299C06D81F0DD17B00C2828E /* IssueReviewEmptyTailCell.swift in Sources */,
292CD3CE1F0DB8E700D3D57B /* IssueReviewModel.swift in Sources */,
29F3A18A20CC017700645CB7 /* Repository.swift in Sources */,
292CD3CC1F0DB89600D3D57B /* IssueReviewSectionController.swift in Sources */,
295C31CF1F0AA67600521CED /* IssueStatus+ButtonState.swift in Sources */,
290D2A421F04D3470082E6CC /* IssueStatus.swift in Sources */,
@@ -2803,9 +2806,11 @@
65A315292044369D0074E3B6 /* TabBarController.swift in Sources */,
295840651EE89F28007723C6 /* IssueStatusEventModel.swift in Sources */,
295840691EE8A328007723C6 /* IssueStatusEventSectionController.swift in Sources */,
29F3A18C20CD790F00645CB7 /* UIViewController+CommonActionItems.swift in Sources */,
294563E61EE4EE6F00DBCD35 /* IssueStatusModel.swift in Sources */,
294563E81EE4EED200DBCD35 /* IssueStatusSectionController.swift in Sources */,
292FCB0F1EDFCC510026635E /* IssuesViewController.swift in Sources */,
29BBD82D20CACDFF004D62FE /* NotificationSectionController.swift in Sources */,
292FF8B01F2FDC33009E63F7 /* IssueTextActionsView.swift in Sources */,
292FCB181EDFCC510026635E /* IssueTitleCell.swift in Sources */,
292FCB191EDFCC510026635E /* IssueTitleSectionController.swift in Sources */,
@@ -2818,6 +2823,8 @@
98835BD21F1A158D005BA24F /* LabelCell.swift in Sources */,
29C8F9B5208C081D0075931C /* LabelSectionController.swift in Sources */,
29136BE3200AAA5A007317BE /* UIViewController+FilePathTitle.swift in Sources */,
29B5D08B20D578DB003DFBE2 /* InboxType.swift in Sources */,
29EE1C191F3A2EDB0046A54D /* LabelTableCell.swift in Sources */,
295B51481FC26F7F00C3993B /* UIImageView+Avatar.swift in Sources */,
297B062A1FB9239E0026FA23 /* IGListCollectionViewLayout+GitHawk.swift in Sources */,
DCA5ED1D1FAEF9EE0072F074 /* BookmarkCell.swift in Sources */,
@@ -2835,14 +2842,8 @@
290EF5791F06BAF4006A2160 /* NoNewNotificationsSectionController.swift in Sources */,
290EF56A1F06A821006A2160 /* Notification+NotificationViewModel.swift in Sources */,
29792B1B1FFB21AD007A0C57 /* AutocompleteController.swift in Sources */,
29C9FDDB1EC6627200EE3A52 /* NotificationCell.swift in Sources */,
297DD5E31F06922A006E7E63 /* NotificationClient.swift in Sources */,
295C31C71F09E62600521CED /* NotificationNextPageCell.swift in Sources */,
295C31C91F09E72D00521CED /* NotificationNextPageSectionController.swift in Sources */,
29A195111EC7AC9500C3E289 /* NotificationRepoCell.swift in Sources */,
29D548CB1FA27FE900F8E46F /* UINavigationItem+TitleSubtitle.swift in Sources */,
299A04A11FAE86B0003C2450 /* IssueReviewViewCommentsCell.swift in Sources */,
2980E0921F073E8B000E02C6 /* NotificationSectionController.swift in Sources */,
7BF2239D1F91056C006CC9A2 /* File+Filename.swift in Sources */,
2924C18120D5B29800FCFCFF /* MilestonesViewController.swift in Sources */,
29A1950A1EC78B4800C3E289 /* NotificationType+Icon.swift in Sources */,
@@ -2862,7 +2863,6 @@
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 */,
986B873E1F2E1CE400AAB55C /* RepositoryClient.swift in Sources */,
295A77BE1F75C1CC007BC403 /* RepositoryDetails.swift in Sources */,
@@ -2918,7 +2918,6 @@
986B871D1F2B8FCD00AAB55C /* SearchViewController.swift in Sources */,
298BA0971EC947F100B01946 /* SegmentedControlCell.swift in Sources */,
297B062C1FB92DCA0026FA23 /* UICollectionViewLayout+Orientation.swift in Sources */,
297406981F0ED1E9003A6BFB /* SegmentedControlModel+Notifications.swift in Sources */,
298BA09A1EC947FC00B01946 /* SegmentedControlModel.swift in Sources */,
299F63EA20603FB80015D901 /* MarkdownStyledTextView.swift in Sources */,
DCA5ED0E1FAED91F0072F074 /* BookmarkViewController.swift in Sources */,
@@ -2938,6 +2937,7 @@
295840711EE9F4D3007723C6 /* ShowMoreDetailsLabel+Date.swift in Sources */,
29CF01D31FDDA1EE0084B66F /* NotificationEmptyMessageClient.swift in Sources */,
29CCB28B1FDDFFA200E23FA0 /* String+GithubDate.swift in Sources */,
29BBD82B20CACB2F004D62FE /* NotificationCell.swift in Sources */,
29C295111EC7B83200D46CD2 /* ShowMoreDetailsLabel.swift in Sources */,
DC6339371F9F567000402A8D /* DeleteSwipeAction.swift in Sources */,
DCA5ED121FAEE3AE0072F074 /* Store.swift in Sources */,

View File

@@ -56,6 +56,7 @@
29C1A9EE204B22A200CB6995 /* GitHubAPITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29C1A9ED204B22A200CB6995 /* GitHubAPITests.swift */; };
29C1A9F0204B22A200CB6995 /* GitHubAPI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 29C1A9C8204B04E700CB6995 /* GitHubAPI.framework */; };
29C1A9F7204B497A00CB6995 /* V3Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29C1A9F6204B497A00CB6995 /* V3Request.swift */; };
29F3A18E20CD7DFC00645CB7 /* V3SubscribeThreadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F3A18D20CD7DFC00645CB7 /* V3SubscribeThreadRequest.swift */; };
29F8BABC204B545F00E5CA32 /* V3User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F8BABB204B545F00E5CA32 /* V3User.swift */; };
29F8BABE204B56BC00E5CA32 /* V3Repository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F8BABD204B56BC00E5CA32 /* V3Repository.swift */; };
29F8BAC0204B577800E5CA32 /* V3NotificationSubject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F8BABF204B577800E5CA32 /* V3NotificationSubject.swift */; };
@@ -130,6 +131,7 @@
29C1A9ED204B22A200CB6995 /* GitHubAPITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHubAPITests.swift; sourceTree = "<group>"; };
29C1A9EF204B22A200CB6995 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
29C1A9F6204B497A00CB6995 /* V3Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = V3Request.swift; sourceTree = "<group>"; };
29F3A18D20CD7DFC00645CB7 /* V3SubscribeThreadRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = V3SubscribeThreadRequest.swift; sourceTree = "<group>"; };
29F8BABB204B545F00E5CA32 /* V3User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = V3User.swift; sourceTree = "<group>"; };
29F8BABD204B56BC00E5CA32 /* V3Repository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = V3Repository.swift; sourceTree = "<group>"; };
29F8BABF204B577800E5CA32 /* V3NotificationSubject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = V3NotificationSubject.swift; sourceTree = "<group>"; };
@@ -195,6 +197,7 @@
290890A7204BC4B400D088BC /* GitHubAPIStatusRequest.swift */,
29580512204B62BE009CFD51 /* HTTPRequest.swift */,
29C1A9CC204B04E700CB6995 /* Info.plist */,
290890C1204BDE2A00D088BC /* JSONResponse.swift */,
2958053C204BAD77009CFD51 /* ManualGraphQLRequest.swift */,
29580510204B629B009CFD51 /* Processing.swift */,
29C1A9D3204B05AE00CB6995 /* Request.swift */,
@@ -207,6 +210,7 @@
290890A9204BC58A00D088BC /* V3CreateIssueRequest.swift */,
29580522204B713C009CFD51 /* V3DataResponse.swift */,
2958052C204B7AB2009CFD51 /* V3DeleteCommentRequest.swift */,
290890C3204BDF9E00D088BC /* V3EditCommentRequest.swift */,
290890B3204BCC3100D088BC /* V3File.swift */,
2958052E204B7B5D009CFD51 /* V3LockIssueRequest.swift */,
29580536204BAAE6009CFD51 /* V3MarkNotificationsRequest.swift */,
@@ -220,7 +224,6 @@
29F8BABF204B577800E5CA32 /* V3NotificationSubject.swift */,
290890BF204BDDD900D088BC /* V3PullRequestCommentsRequest.swift */,
290890B1204BCBD200D088BC /* V3PullRequestFilesRequest.swift */,
290890C1204BDE2A00D088BC /* JSONResponse.swift */,
29580526204B778B009CFD51 /* V3Release.swift */,
29580528204B77B1009CFD51 /* V3ReleaseRequest.swift */,
29F8BABD204B56BC00E5CA32 /* V3Repository.swift */,
@@ -228,12 +231,12 @@
290890A5204BC0ED00D088BC /* V3RepositoryReadmeRequest.swift */,
29C1A9F6204B497A00CB6995 /* V3Request.swift */,
290890BD204BDA8F00D088BC /* V3SendPullRequestCommentRequest.swift */,
290890C3204BDF9E00D088BC /* V3EditCommentRequest.swift */,
290890AF204BCB2100D088BC /* V3SetIssueStatusRequest.swift */,
290890B7204BCD2600D088BC /* V3SetMilestonesRequest.swift */,
2908909D204BB57700D088BC /* V3SetRepositoryLabelsRequest.swift */,
2958053A204BAB1A009CFD51 /* V3StatusCode205.swift */,
2958052A204B785F009CFD51 /* V3StatusCodeResponse.swift */,
29F3A18D20CD7DFC00645CB7 /* V3SubscribeThreadRequest.swift */,
29F8BABB204B545F00E5CA32 /* V3User.swift */,
290890AB204BC7D400D088BC /* V3VerifyPersonalAccessTokenRequest.swift */,
29580530204B7BCD009CFD51 /* V3ViewerIsCollaboratorRequest.swift */,
@@ -509,6 +512,7 @@
2958052F204B7B5D009CFD51 /* V3LockIssueRequest.swift in Sources */,
29580515204B62DC009CFD51 /* Response.swift in Sources */,
29F8BABC204B545F00E5CA32 /* V3User.swift in Sources */,
29F3A18E20CD7DFC00645CB7 /* V3SubscribeThreadRequest.swift in Sources */,
29580511204B629B009CFD51 /* Processing.swift in Sources */,
29C1A9E4204B182200CB6995 /* Alamofire+GitHubAPI.swift in Sources */,
290890B2204BCBD200D088BC /* V3PullRequestFilesRequest.swift in Sources */,

View File

@@ -0,0 +1,30 @@
//
// V3SubscribeThreadRequest.swift
// GitHubAPI
//
// Created by Ryan Nystrom on 6/10/18.
// Copyright © 2018 Ryan Nystrom. All rights reserved.
//
import Foundation
public struct V3SubscribeThreadRequest: V3Request {
public typealias ResponseType = V3StatusCodeResponse<V3StatusCode200>
public var pathComponents: [String] {
return ["notifications", "threads", id, "subscription"]
}
public var method: HTTPMethod { return .put }
public var parameters: [String : Any]? {
return [
"ignored": ignore ? "true" : "false"
]
}
public let id: String
public let ignore: Bool
public init(id: String, ignore: Bool) {
self.id = id
self.ignore = ignore
}
}

View File

@@ -185,7 +185,7 @@ CHECKOUT OPTIONS:
:commit: 4f7e90477619b8dc4b9e641efd10952c22150c5c
:git: https://github.com/GitHawkApp/Highlightr.git
IGListKit:
:commit: 49413f2c37ffb8833d0810c7e641c85e91ffe037
:commit: 5c1c92b600f0d8094f22fff48e26ded323568194
:git: https://github.com/Instagram/IGListKit.git
MessageViewController:
:commit: b39c89ea688b79cc8daeb29c214a925c9a1c4396

View File

@@ -39,7 +39,7 @@ public extension ListAdapter {
@return The `ListSectionController`, if the value is a member of the `IGListAdapter`.
*/
public func sectionController(for value: ListSwiftDiffable) -> ListSectionController? {
return sectionController(for: value.boxed)
return sectionController(for: value.sectionBox)
}
/**
@@ -50,7 +50,7 @@ public extension ListAdapter {
@return The section, if the value is a member of the adapter.
*/
public func section(for value: ListSwiftDiffable) -> Int? {
let section = self.section(for: value.boxed)
let section = self.section(for: value.sectionBox)
return section == NSNotFound ? nil : section
}
@@ -79,7 +79,7 @@ public extension ListAdapter {
@return All currently visible cells in the `UICollectionView`. Array is empty if no cells are visible.
*/
public func visibleCells(for value: ListSwiftDiffable) -> [UICollectionViewCell] {
return visibleCells(for: value.boxed)
return visibleCells(for: value.sectionBox)
}
/**
@@ -99,7 +99,7 @@ public extension ListAdapter {
animated: Bool = true
) {
scroll(
to: value.boxed,
to: value.sectionBox,
supplementaryKinds: supplementaryKinds,
scrollDirection: scrollDirection,
scrollPosition: scrollPosition,

View File

@@ -22,13 +22,22 @@ internal final class ListDiffableBox: ListDiffable {
*/
let value: ListSwiftDiffable
/**
*/
let boxesSectionValue: Bool
private let _diffIdentifier: NSObjectProtocol
/**
Initialize a new `ListDiffableBox` object.
@param value The value to be boxed.
*/
init(value: ListSwiftDiffable) {
init(value: ListSwiftDiffable, boxesSectionValue: Bool) {
self.value = value
self.boxesSectionValue = boxesSectionValue
// namespace the identifier with the value type to help prevent collisions
self._diffIdentifier = "\(type(of: value))\(value.identifier)" as NSObjectProtocol
}
// MARK: ListDiffable
@@ -37,16 +46,18 @@ internal final class ListDiffableBox: ListDiffable {
:nodoc:
*/
func diffIdentifier() -> NSObjectProtocol {
// namespace the identifier with the value type to help prevent collisions
return "\(value.self)\(value.identifier)" as NSObjectProtocol
return _diffIdentifier
}
/**
:nodoc:
*/
func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
// always true since objects are updated with ListSwiftSectionController which handles updates at the cell level
return true
// always true when using section models since ListSwiftSectionController handles updates at the cell level
guard boxesSectionValue == false,
let box = object as? ListDiffableBox
else { return true }
return value.isEqual(to: box.value)
}
}

View File

@@ -84,7 +84,7 @@ public final class ListSwiftAdapter: NSObject, ListAdapterDataSource {
guard let dataSource = self.dataSource else { return [] }
return dataSource.values(adapter: self).map {
let box = ListDiffableBox(value: $0.value)
let box = $0.value.sectionBox
// side effect: store the function for use in listAdapter(:, sectionControllerFor object:)
map[box.functionLookupHash] = $0.constructor
return box

View File

@@ -8,10 +8,14 @@
*/
// Not very clean, but it's not possible to write extensions on a composed Protocols.
internal extension ListSwiftIdentifiable where Self: ListSwiftEquatable {
internal extension ListSwiftDiffable {
var boxed: ListDiffable {
return ListDiffableBox(value: self)
var sectionBox: ListDiffable {
return ListDiffableBox(value: self, boxesSectionValue: true)
}
var viewModelBox: ListDiffable {
return ListDiffableBox(value: self, boxesSectionValue: false)
}
}

View File

@@ -7,12 +7,7 @@
* of patent rights can be found in the PATENTS file in the same directory.
*/
public protocol ListSwiftIdentifiable {
public protocol ListSwiftDiffable {
var identifier: String { get }
}
public protocol ListSwiftEquatable {
func isEqual(to value: ListSwiftDiffable) -> Bool
}
public typealias ListSwiftDiffable = ListSwiftIdentifiable & ListSwiftEquatable

View File

@@ -11,7 +11,7 @@ import UIKit
public enum ListCellType<T: UICollectionViewCell> {
case `class`(T.Type)
case storyboard(T.Type, String, Bundle?)
case storyboard(T.Type, String)
case nib(T.Type, String, Bundle?)
}
@@ -19,7 +19,7 @@ public struct ListBinder {
enum CellType {
case cellClass(UICollectionViewCell.Type)
case storyboard(String, Bundle?)
case storyboard(String)
case nib(String, Bundle?)
}
@@ -82,7 +82,7 @@ open class ListSwiftSectionController<T: ListSwiftDiffable>: ListSectionControll
let nestedCellType: ListBinder.CellType
switch cellType {
case let .class(type): nestedCellType = .cellClass(type)
case let .storyboard(_, type, bundle): nestedCellType = .storyboard(type, bundle)
case let .storyboard(_, type): nestedCellType = .storyboard(type)
case let .nib(_, type, bundle): nestedCellType = .nib(type, bundle)
}
@@ -146,15 +146,17 @@ open class ListSwiftSectionController<T: ListSwiftDiffable>: ListSectionControll
let value = strongSelf.value
else { return }
let fromBoxed = strongSelf.binders.map { $0.value.boxed }
let fromBoxed = strongSelf.binders.map { $0.value.viewModelBox }
let to = strongSelf.createBinders(from: value)
let toBoxed = to.map { $0.value.boxed }
let toBoxed = to.map { $0.value.viewModelBox }
let result = ListDiff(
oldArray: fromBoxed,
newArray: toBoxed,
option: .equality
)
strongSelf.binders = to
for (i, _) in result.updates.enumerated() {
let identifier = fromBoxed[i].diffIdentifier()
let toIndex = result.newIndex(forIdentifier: identifier)
@@ -210,7 +212,7 @@ open class ListSwiftSectionController<T: ListSwiftDiffable>: ListSectionControll
rawCell = collectionContext.dequeueReusableCell(of: type, for: self, at: index)
case let .nib(type, bundle):
rawCell = collectionContext.dequeueReusableCell(withNibName: type, bundle: bundle, for: self, at: index)
case let .storyboard(type, bundle):
case let .storyboard(type):
rawCell = collectionContext.dequeueReusableCellFromStoryboard(withIdentifier: type, for: self, at: index)
}

2
Pods/Manifest.lock generated
View File

@@ -185,7 +185,7 @@ CHECKOUT OPTIONS:
:commit: 4f7e90477619b8dc4b9e641efd10952c22150c5c
:git: https://github.com/GitHawkApp/Highlightr.git
IGListKit:
:commit: 49413f2c37ffb8833d0810c7e641c85e91ffe037
:commit: 5c1c92b600f0d8094f22fff48e26ded323568194
:git: https://github.com/Instagram/IGListKit.git
MessageViewController:
:commit: b39c89ea688b79cc8daeb29c214a925c9a1c4396

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "bullets-small.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "bullets-small@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 B

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "mute-small@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "mute-small@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 B

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "unmute-small@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "unmute-small@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 991 B

View File

@@ -232,7 +232,7 @@
<key>File</key>
<string>com.mono0926.LicensePlist/StyledTextKit</string>
<key>Title</key>
<string>StyledTextKit (0.1.0)</string>
<string>StyledTextKit (0.1.1)</string>
<key>Type</key>
<string>PSChildPaneSpecifier</string>
</dict>