-
Notifications
You must be signed in to change notification settings - Fork 381
Show release notes UI in inbox when notable new features #2473
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
// | ||
// NewFeaturesCell.swift | ||
// Freetime | ||
// | ||
// Created by Ryan Nystrom on 11/22/18. | ||
// Copyright © 2018 Ryan Nystrom. All rights reserved. | ||
// | ||
|
||
import UIKit | ||
import SnapKit | ||
|
||
protocol NewFeaturesCellDelegate: class { | ||
func didTapClose(for cell: NewFeaturesCell) | ||
} | ||
|
||
final class NewFeaturesCell: UICollectionViewCell { | ||
|
||
static let inset = UIEdgeInsets( | ||
top: Styles.Sizes.rowSpacing, | ||
left: Styles.Sizes.gutter, | ||
bottom: Styles.Sizes.rowSpacing, | ||
right: Styles.Sizes.gutter | ||
) | ||
|
||
weak var delegate: NewFeaturesCellDelegate? | ||
|
||
private let label = UILabel() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could be a bit more descriptive, I think? 😄 |
||
private let closeButton = UIButton() | ||
|
||
override init(frame: CGRect) { | ||
super.init(frame: frame) | ||
|
||
isAccessibilityElement = true | ||
|
||
backgroundColor = .white | ||
contentView.backgroundColor = Styles.Colors.Blue.light.color | ||
contentView.layer.cornerRadius = Styles.Sizes.cardCornerRadius | ||
contentView.layer.borderWidth = 1 / UIScreen.main.scale | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks to be a 1px thin line rather than a 1pt thin line! I've used this a couple times 😄 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, a |
||
contentView.layer.borderColor = Styles.Colors.blueGray.color.cgColor | ||
|
||
contentView.addSubview(label) | ||
contentView.addSubview(closeButton) | ||
|
||
let tint = Styles.Colors.Blue.medium.color | ||
label.textColor = tint | ||
label.font = Styles.Text.secondaryBold.preferredFont | ||
label.isAccessibilityElement = false | ||
|
||
let closeButtonSize = Styles.Sizes.icon.width | ||
closeButton.setImage(UIImage(named: "x-small")?.withRenderingMode(.alwaysTemplate), for: .normal) | ||
closeButton.tintColor = tint | ||
closeButton.layer.cornerRadius = closeButtonSize / 2 | ||
closeButton.addTarget(self, action: #selector(onCloseButton), for: .touchUpInside) | ||
closeButton.accessibilityLabel = NSLocalizedString("Dismiss new feature modal", comment: "") | ||
|
||
label.snp.makeConstraints { make in | ||
make.centerY.equalToSuperview() | ||
make.left.equalTo(Styles.Sizes.gutter) | ||
make.right.lessThanOrEqualTo(closeButton.snp.left).offset(-Styles.Sizes.columnSpacing) | ||
} | ||
closeButton.snp.makeConstraints { make in | ||
make.centerY.equalToSuperview() | ||
make.right.equalTo(-Styles.Sizes.gutter) | ||
make.size.equalTo(closeButtonSize) | ||
} | ||
} | ||
|
||
required init?(coder aDecoder: NSCoder) { | ||
fatalError("init(coder:) has not been implemented") | ||
} | ||
|
||
override func layoutSubviews() { | ||
super.layoutSubviews() | ||
contentView.frame = UIEdgeInsetsInsetRect(CGRect( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this the same as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That’s for x and y insetting (not There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh TIL! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. :D |
||
x: safeAreaInsets.left, | ||
y: bounds.minY, | ||
width: bounds.width - safeAreaInsets.left - safeAreaInsets.right, | ||
height: bounds.height | ||
), NewFeaturesCell.inset) | ||
} | ||
|
||
@objc private func onCloseButton() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we also add an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I’m actually not sure what that is, but this probably needs better AX There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It lets you use a z-swipe to dismiss the view controller. |
||
delegate?.didTapClose(for: self) | ||
} | ||
|
||
func configure(with version: String) { | ||
label.text = String.localizedStringWithFormat( | ||
NSLocalizedString("New features in %@!", comment: ""), | ||
version | ||
) | ||
} | ||
|
||
override func accessibilityPerformEscape() -> Bool { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💪 |
||
delegate?.didTapClose(for: self) | ||
return true | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
// | ||
// NewFeaturesController.swift | ||
// Freetime | ||
// | ||
// Created by Ryan Nystrom on 11/22/18. | ||
// Copyright © 2018 Ryan Nystrom. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
import GitHubAPI | ||
import IGListKit | ||
|
||
final class NewFeaturesController { | ||
|
||
// change to true for hardcoded testing | ||
private let testing = false | ||
|
||
var version: String { | ||
if testing { | ||
return "test" | ||
} | ||
return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pretty sure this is safe; maybe use implicitly unwrapped optionals or an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Want to avoid any implicit unwraps for stuff that can’t be tested. If this changed out from under us it’d stink (however unlikely). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An |
||
} | ||
|
||
private var fetchURL: URL? { | ||
return URLBuilder(host: "raw.githubusercontent.com") | ||
.add(paths: ["GitHawkApp", "Release-Notes", "master", "versions", "\(version).md"]) | ||
.url | ||
} | ||
|
||
private let session: URLSession = { | ||
let config = URLSessionConfiguration.default | ||
config.requestCachePolicy = .reloadIgnoringLocalCacheData | ||
config.urlCache = nil | ||
return URLSession.init(configuration: config) | ||
}() | ||
|
||
private let userDefaultsKey = "com.freetime.new-features-controller.has-fetched" | ||
|
||
private var hasFetchedLatest: Bool { | ||
get { | ||
guard let last = UserDefaults.standard.string(forKey: userDefaultsKey) else { | ||
return false | ||
} | ||
// value kept to latest version when seen | ||
// pinned to old versions (or empty) when out of date | ||
return last >= version | ||
} | ||
set { | ||
if newValue { | ||
UserDefaults.standard.set(version, forKey: userDefaultsKey) | ||
} else { | ||
UserDefaults.standard.removeObject(forKey: userDefaultsKey) | ||
} | ||
} | ||
} | ||
|
||
func fetch(success: @escaping (String) -> Void) { | ||
guard let url = fetchURL, | ||
(testing == true || hasFetchedLatest == false) | ||
else { return } | ||
hasFetchedLatest = true | ||
|
||
let task = session.dataTask(with: url) { (data, response, _) in | ||
if let response = response as? HTTPURLResponse, | ||
response.statusCode == 200, | ||
let data = data, | ||
let string = String(data: data, encoding: .utf8) { | ||
DispatchQueue.main.async { | ||
success(string) | ||
} | ||
} | ||
} | ||
task.resume() | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
// | ||
// NewFeaturesSectionController.swift | ||
// Freetime | ||
// | ||
// Created by Ryan Nystrom on 11/22/18. | ||
// Copyright © 2018 Ryan Nystrom. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
import IGListKit | ||
import ContextMenu | ||
|
||
final class NewFeaturesSectionController: ListSwiftSectionController<String>, | ||
NewFeaturesCellDelegate { | ||
|
||
private var markdown: String? | ||
private let controller = NewFeaturesController() | ||
private var closed = false | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What kind of state does this represent? And super-nit: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the user tapped the X to dismiss |
||
|
||
override init() { | ||
super.init() | ||
controller.fetch { [weak self] markdown in | ||
self?.markdown = markdown | ||
self?.update() | ||
} | ||
} | ||
|
||
override func createBinders(from value: String) -> [ListBinder] { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is some... interesting code. Having difficulties parsing/understanding this completely :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Takes some getting used to. Heavy use of generics but result is completely type safe. |
||
guard let markdown = self.markdown, | ||
closed == false | ||
else { return [] } | ||
return [ | ||
binder( | ||
markdown, | ||
cellType: ListCellType.class(NewFeaturesCell.self), | ||
size: { | ||
return $0.collection.cellSize( | ||
with: Styles.Sizes.tableCellHeight | ||
+ NewFeaturesCell.inset.top | ||
+ NewFeaturesCell.inset.bottom | ||
) | ||
}, | ||
configure: { [weak self] (cell, _) in | ||
guard let `self` = self else { return } | ||
cell.configure(with: self.controller.version) | ||
cell.delegate = self | ||
}, | ||
didSelect: { [weak self] _ in | ||
self?.showChanges(markdown: markdown) | ||
}) | ||
] | ||
} | ||
|
||
private func showChanges(markdown: String) { | ||
viewController?.showContextualMenu(IssuePreviewViewController( | ||
markdown: markdown, | ||
owner: "GitHawkApp", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think someone added the GitHawk owner/repo combination in a constant somewhere? Not sure who/where. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @BasThomas Yup I did! But its in the Templates pull request which hasn't been merged |
||
repo: "GitHawk", | ||
title: String.localizedStringWithFormat( | ||
NSLocalizedString("New in %@", comment: ""), | ||
controller.version | ||
), | ||
asMenu: true | ||
)) | ||
} | ||
|
||
// MARK: NewFeaturesCellDelegate | ||
|
||
func didTapClose(for cell: NewFeaturesCell) { | ||
closed = true | ||
update() | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what this does/when it is used? What is this
ViewController
showing again?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previewing markdown