Skip to content
This repository was archived by the owner on Sep 20, 2023. It is now read-only.

Commit 7381566

Browse files
authored
New inbox header to select inbox type (all, mentioned, repo, etc) (#2524)
* update contextmenu * setup menu item on inbox * add subscription request * wip networking * add dashboard cell, controller, and model * clean up notification parsing * repo selection working * feature working * update menu to fix centering and revert icon position * adjust ui
1 parent d16fc0c commit 7381566

35 files changed

+6008
-5118
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//
2+
// GithubClient+InboxFilterController.swift
3+
// Freetime
4+
//
5+
// Created by Ryan Nystrom on 12/2/18.
6+
// Copyright © 2018 Ryan Nystrom. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import GitHubAPI
11+
12+
extension GithubClient: InboxFilterControllerClient {
13+
14+
func fetchSubscriptions(completion: @escaping (Result<[RepositoryDetails]>) -> Void) {
15+
guard let username = userSession?.username else {
16+
completion(.error(nil))
17+
return
18+
}
19+
client.send(V3SubscriptionsRequest(username: username)) { result in
20+
switch result {
21+
case .failure(let error):
22+
completion(.error(error))
23+
case .success(let data):
24+
let repos = data.data.map {
25+
RepositoryDetails(owner: $0.owner.login, name: $0.name)
26+
}
27+
completion(.success(repos))
28+
}
29+
}
30+
}
31+
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
//
2+
// InboxFilterController.swift
3+
// Freetime
4+
//
5+
// Created by Ryan Nystrom on 12/2/18.
6+
// Copyright © 2018 Ryan Nystrom. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import ContextMenu
11+
12+
protocol InboxFilterControllerClient {
13+
func fetchSubscriptions(completion: @escaping (Result<[RepositoryDetails]>) -> Void)
14+
}
15+
16+
protocol InboxFilterControllerListener: class {
17+
func didUpdateSelectedFilter(for controller: InboxFilterController)
18+
}
19+
20+
final class InboxFilterController {
21+
22+
let client: InboxFilterControllerClient
23+
let announcer = Announcer<InboxFilterControllerListener>()
24+
25+
private static let filters = [
26+
InboxFilterModel(type: .unread),
27+
InboxFilterModel(type: .all),
28+
InboxFilterModel(type: .mentioned),
29+
InboxFilterModel(type: .assigned),
30+
InboxFilterModel(type: .created)
31+
]
32+
33+
private(set) var selected: InboxFilterModel = InboxFilterController.filters[0] {
34+
didSet {
35+
announcer.enumerate { $0.didUpdateSelectedFilter(for: self) }
36+
}
37+
}
38+
private var fetchedFilters = [InboxFilterModel]()
39+
40+
init(client: InboxFilterControllerClient) {
41+
self.client = client
42+
}
43+
44+
func update(selection: InboxFilterModel) {
45+
selected = selection
46+
}
47+
48+
private func selected(model: InboxFilterModel) {
49+
selected = model
50+
}
51+
52+
private func showRepos(from viewController: UIViewController?) {
53+
guard let viewController = viewController else { return }
54+
ContextMenu.shared.show(
55+
sourceViewController: viewController,
56+
viewController: InboxFilterReposViewController(inboxFilterController: self),
57+
options: ContextMenu.Options(
58+
containerStyle: ContextMenu.ContainerStyle(
59+
backgroundColor: Styles.Colors.menuBackgroundColor.color
60+
)
61+
)
62+
)
63+
}
64+
65+
func showMenu(from viewController: UIViewController) {
66+
var items: [ContrastContextMenuItem] = InboxFilterController.filters.map { model in
67+
ContrastContextMenuItem(
68+
title: model.type.title,
69+
iconName: model.type.iconName,
70+
iconColor: Styles.Colors.Blue.medium.color,
71+
separator: false,
72+
action: { [weak self] menu in
73+
menu.dismiss(animated: trueUnlessReduceMotionEnabled)
74+
self?.selected(model: model)
75+
})
76+
}
77+
items.append(ContrastContextMenuItem(
78+
title: NSLocalizedString("Repos", comment: ""),
79+
iconName: "repo",
80+
iconColor: Styles.Colors.Blue.medium.color,
81+
separator: true,
82+
action: { [weak self, weak viewController] menu in
83+
menu.dismiss(animated: trueUnlessReduceMotionEnabled)
84+
self?.showRepos(from: viewController)
85+
}))
86+
87+
ContextMenu.shared.show(
88+
sourceViewController: viewController,
89+
viewController: ContrastContextMenu(items: items),
90+
options: ContextMenu.Options(
91+
containerStyle: ContextMenu.ContainerStyle(
92+
backgroundColor: Styles.Colors.menuBackgroundColor.color
93+
),
94+
menuStyle: .minimal,
95+
hapticsStyle: .medium,
96+
position: .centerX
97+
),
98+
sourceView: viewController.navigationItem.titleView
99+
)
100+
}
101+
102+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//
2+
// InboxFilterModel.swift
3+
// Freetime
4+
//
5+
// Created by Ryan Nystrom on 12/2/18.
6+
// Copyright © 2018 Ryan Nystrom. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import IGListKit
11+
12+
struct InboxFilterModel: ListSwiftDiffable {
13+
14+
enum FilterType: Equatable {
15+
case unread
16+
case all
17+
case assigned
18+
case created
19+
case mentioned
20+
case repo(owner: String, name: String)
21+
22+
var title: String {
23+
switch self {
24+
case .unread: return Constants.Strings.inbox
25+
case .all: return NSLocalizedString("All", comment: "")
26+
case .assigned: return NSLocalizedString("Assigned", comment: "")
27+
case .created: return NSLocalizedString("Created", comment: "")
28+
case .mentioned: return NSLocalizedString("Mentioned", comment: "")
29+
case let .repo(_, name): return name
30+
}
31+
}
32+
33+
var subtitle: String? {
34+
switch self {
35+
case .unread, .all, .assigned, .created, .mentioned: return nil
36+
case let .repo(owner, _): return owner
37+
}
38+
}
39+
40+
var iconName: String? {
41+
switch self {
42+
case .unread: return "inbox"
43+
case .all: return "bell"
44+
case .assigned: return "person"
45+
case .created: return "plus"
46+
case .mentioned: return "mention"
47+
case .repo: return nil
48+
}
49+
}
50+
51+
// MARK: Equatable
52+
53+
static func == (lhs: FilterType, rhs: FilterType) -> Bool {
54+
return lhs.title == rhs.title
55+
&& lhs.subtitle == rhs.subtitle
56+
}
57+
58+
}
59+
60+
let type: FilterType
61+
62+
// MARK: ListSwiftDiffable
63+
64+
var identifier: String {
65+
return "\(type.title).\(type.subtitle ?? "")"
66+
}
67+
68+
func isEqual(to value: ListSwiftDiffable) -> Bool {
69+
return value is InboxFilterModel
70+
}
71+
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//
2+
// InboxFilterRepoCell.swift
3+
// Freetime
4+
//
5+
// Created by Ryan Nystrom on 12/2/18.
6+
// Copyright © 2018 Ryan Nystrom. All rights reserved.
7+
//
8+
9+
import UIKit
10+
import SnapKit
11+
12+
final class InboxFilterRepoCell: SelectableCell {
13+
14+
private let label = UILabel()
15+
16+
override init(frame: CGRect) {
17+
super.init(frame: frame)
18+
19+
accessibilityIdentifier = "inbox-filter-repo-cell"
20+
21+
label.adjustsFontSizeToFitWidth = true
22+
label.minimumScaleFactor = 0.7
23+
contentView.addSubview(label)
24+
25+
label.snp.makeConstraints { make in
26+
make.centerY.equalToSuperview()
27+
make.left.equalTo(Styles.Sizes.gutter)
28+
make.right.lessThanOrEqualTo(-Styles.Sizes.gutter)
29+
}
30+
31+
addBorder(.bottom, left: Styles.Sizes.gutter)
32+
}
33+
34+
required init?(coder aDecoder: NSCoder) {
35+
fatalError("init(coder:) has not been implemented")
36+
}
37+
38+
func configure(owner: String, name: String) {
39+
let text = NSMutableAttributedString(string: "\(owner)/", attributes: [
40+
.foregroundColor: Styles.Colors.Gray.light.color,
41+
.font: Styles.Text.body.preferredFont
42+
])
43+
text.append(NSAttributedString(string: "\(name)", attributes: [
44+
.foregroundColor: UIColor.white,
45+
.font: Styles.Text.bodyBold.preferredFont
46+
]))
47+
label.attributedText = text
48+
}
49+
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//
2+
// InboxFilterRepoSectionController.swift
3+
// Freetime
4+
//
5+
// Created by Ryan Nystrom on 12/2/18.
6+
// Copyright © 2018 Ryan Nystrom. All rights reserved.
7+
//
8+
9+
import IGListKit
10+
11+
final class InboxFilterRepoSectionController: ListSwiftSectionController<RepositoryDetails> {
12+
13+
private let inboxFilterController: InboxFilterController
14+
15+
init(inboxFilterController: InboxFilterController) {
16+
self.inboxFilterController = inboxFilterController
17+
super.init()
18+
}
19+
20+
override func createBinders(from value: RepositoryDetails) -> [ListBinder] {
21+
return [
22+
binder(
23+
value,
24+
cellType: ListCellType.class(InboxFilterRepoCell.self),
25+
size: {
26+
return $0.collection.cellSize(with: Styles.Sizes.tableCellHeight)
27+
}, configure: {
28+
$0.configure(
29+
owner: $1.value.owner,
30+
name: $1.value.name
31+
)
32+
}, didSelect: { [weak self] context in
33+
self?.didSelect(value: context.value)
34+
})
35+
]
36+
}
37+
38+
private func didSelect(value: RepositoryDetails) {
39+
viewController?.dismiss(animated: trueUnlessReduceMotionEnabled)
40+
inboxFilterController.update(selection: InboxFilterModel(
41+
type: .repo(owner: value.owner, name: value.name))
42+
)
43+
}
44+
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//
2+
// InboxFilterReposViewController.swift
3+
// Freetime
4+
//
5+
// Created by Ryan Nystrom on 12/2/18.
6+
// Copyright © 2018 Ryan Nystrom. All rights reserved.
7+
//
8+
9+
import UIKit
10+
import IGListKit
11+
import Squawk
12+
13+
final class InboxFilterReposViewController: BaseListViewController<String>,
14+
BaseListViewControllerDataSource {
15+
16+
private let inboxFilterController: InboxFilterController
17+
private var repos = [RepositoryDetails]()
18+
19+
init(inboxFilterController: InboxFilterController) {
20+
self.inboxFilterController = inboxFilterController
21+
super.init(emptyErrorMessage: NSLocalizedString("Error loading repos", comment: ""))
22+
dataSource = self
23+
title = NSLocalizedString("Watched Repos", comment: "")
24+
preferredContentSize = Styles.Sizes.contextMenuSize
25+
feed.collectionView.backgroundColor = Styles.Colors.menuBackgroundColor.color
26+
feed.setLoadingSpinnerColor(to: .white)
27+
}
28+
29+
required init?(coder aDecoder: NSCoder) {
30+
fatalError("init(coder:) has not been implemented")
31+
}
32+
33+
override func viewDidLoad() {
34+
super.viewDidLoad()
35+
navigationController?.navigationBar.titleTextAttributes = [
36+
.foregroundColor: UIColor.white
37+
]
38+
}
39+
40+
override func fetch(page: String?) {
41+
inboxFilterController.client.fetchSubscriptions { [weak self] result in
42+
switch result {
43+
case .error(let error):
44+
Squawk.show(error: error)
45+
case .success(let repos):
46+
self?.repos = repos
47+
self?.update()
48+
}
49+
}
50+
}
51+
52+
// MARK: BaseListViewControllerDataSource
53+
54+
func models(adapter: ListSwiftAdapter) -> [ListSwiftPair] {
55+
return repos.map { [inboxFilterController] model in
56+
ListSwiftPair.pair(model, {
57+
InboxFilterRepoSectionController(inboxFilterController: inboxFilterController)
58+
})
59+
}
60+
}
61+
62+
}

0 commit comments

Comments
 (0)