Skip to content
This repository has been archived by the owner on May 10, 2024. It is now read-only.

Ref #2151 New tab page notifications #2214

Merged
merged 50 commits into from
Jan 22, 2020
Merged

Ref #2151 New tab page notifications #2214

merged 50 commits into from
Jan 22, 2020

Conversation

iccub
Copy link
Contributor

@iccub iccub commented Jan 19, 2020

Summary of Changes

This pull request fixes issue #2151

Submitter Checklist:

  • Unit Tests are updated to cover new or changed functionality
  • User-facing strings use NSLocalizableString()

Test Plan:

Screenshots:

Reviewer Checklist:

  • Issues include necessary QA labels:
    • QA/(Yes|No)
    • release-notes/(include|exclude)
    • bug / enhancement
  • Necessary security reviews have taken place.
  • Adequate unit test coverage exists to prevent regressions.
  • Adequate test plan exists for QA to validate (if applicable).
  • Issue is assigned to a milestone (should happen at merge time).

@iccub iccub requested a review from jhreis January 19, 2020 23:12
@@ -129,11 +130,13 @@ class SettingsViewController: UIViewController {
@objc private func rewardsSwitchValueChanged() {
state.ledger.isEnabled = settingsView.rewardsToggleSection.toggleSwitch.isOn
updateVisualStateOfSections(animated: true)
NotificationCenter.default.post(name: .rewardsToggled, object: nil)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to reset NTP notifications when either rewards or ads are toggled

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should use LedgerObserver.rewardsEnabledStateUpdated to manage when rewards status is updated as its status is actually toggled as well in WalletViewController when the user has previous disabled rewards and clicks "Enable rewards" from the panel


// MARK: - New tab page
extension Strings {
public struct NTP {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I experimented a bit here with better strings structure.

@@ -161,6 +177,9 @@ class FavoritesViewController: UIViewController, Themeable {
$0.removeObserver(self, name: .topSitesConversion, object: nil)
$0.removeObserver(self, name: .privacyModeChanged, object: nil)
}

// Navigating away from NTP counts the current notification as showed.
Preferences.NewTabPage.brandedImageShowed.value = true
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's one architectural caveat to this solution.
When you have a tab with website open and another empty new tab, navigating from NTP to the first tab counts as tab dismissal and the popup is cleared.

No simple way to prevent that, would require a bigger refactor

@iccub iccub marked this pull request as ready for review January 20, 2020 10:18
@@ -4,14 +4,14 @@

import BraveShared

internal struct Strings {
public struct Strings {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reason why we made this internal was because it matches the same symbol as in BraveShared module and caused issues. Not sure if any issues will arise in the future due to this change

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can replace it with extension so there's always one struct

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or add duplicate strings, I only took like two small string values from BR-UI

@@ -129,11 +130,13 @@ class SettingsViewController: UIViewController {
@objc private func rewardsSwitchValueChanged() {
state.ledger.isEnabled = settingsView.rewardsToggleSection.toggleSwitch.isOn
updateVisualStateOfSections(animated: true)
NotificationCenter.default.post(name: .rewardsToggled, object: nil)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should use LedgerObserver.rewardsEnabledStateUpdated to manage when rewards status is updated as its status is actually toggled as well in WalletViewController when the user has previous disabled rewards and clicks "Enable rewards" from the panel

}

@objc private func adsToggleValueChanged() {
state.ads.isEnabled = settingsView.adsSection.toggleSwitch.isOn
updateVisualStateOfSections(animated: true)
NotificationCenter.default.post(name: .adsToggled, object: nil)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, but we don't have an AdsObserver unfortunately. Not sure if you this matters, but ads is also toggled in some states of onboarding (ads region becoming available to user who already opted into rewards):

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onboarding doesn't matter

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I need to observe both ads and rewards state changes. So callback for rewards and notification for ads?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that should be good yeah

extension Strings {
public struct NTP {
public static let getPaidToSeeThisImage =
NSL("ntp.getPaidToSeeThisImage",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will the script pick up NSL? Can't remember if it needs to actually parse NSLocalizedString

@@ -150,6 +150,15 @@ extension BrowserViewController {
}
}
}

// Note: at the moment the toggle value does not matter, the same code is run regardless of its state.
@objc func rewardsSettingToggled() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just have the notification registered to 1 selector then if they are going to run the same code? Also how come we don't care about the value of the actual toggle here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can do 1 selector.
We do not care about value because it just resets the NTP notification, and what notification is shown depends on few conditions, rewards state, ads state, image type. Does not need to be shown immediately.


func update() {
// Update dark blur, the more of content view go away the less dark it is.
backgroundOverlayView.alpha = (maxY - yPosition) / maxY
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably check maxY != 0 otherwise you can end up with NaN and an exception thrown when you set the frame of contentView below

}

private var initialDrawerYPosition: CGFloat {
let popupY = (view.frame.height / 2) - (contentView.frame.height / 2)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May want to ceil/floor this value, may end up with half pixels

updateDrawerViewConstraints()
}

private func updateDrawerViewConstraints() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just override updateConstraints and call setNeedsUpdateConstraints() in traitCollectionDidChange ?


// MARK: - Animations

@objc private func didRecognizePanGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Name is a bit misleading, it implies it is only called once when it is recognized and not continuously as the user pans.

if gestureRecognizer.state != .ended { return }

let velocity = gestureRecognizer.velocity(in: contentView).y
let landingYPosition = yPosition + velocity / 10
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use projected end point algorithm:

// Distance travelled after decelerating to zero velocity at a constant rate (credit: a WWDC video)
func project(initialVelocity: Float, decelerationRate: Float) -> Float {
  return (initialVelocity / 1000.0) * decelerationRate / (1.0 - decelerationRate) 
}

Where deceleration rate can be taken from UIScrollView.DecelerationRate.normal

You can see its usage already here:

func project(initialVelocity: CGFloat, decelerationRate: CGFloat) -> CGFloat {
return (initialVelocity / 1000.0) * decelerationRate / (1.0 - decelerationRate)
}
private var panState: CGPoint = .zero
@objc private func dismissPannedAdView(_ pan: UIPanGestureRecognizer) {
guard let adView = pan.view as? AdView else { return }
switch pan.state {
case .began:
panState = adView.center
// Make sure to stop the dismiss timer
dismissTimers[adView]?.invalidate()
case .changed:
adView.transform.ty = min(0, pan.translation(in: adView).y)
case .ended:
let velocity = pan.velocity(in: adView).y
let y = min(panState.y, panState.y + pan.translation(in: adView).y)
let projected = project(initialVelocity: velocity, decelerationRate: UIScrollView.DecelerationRate.normal.rawValue)
if y + projected < 0 {
guard let displayedAd = self.displayedAds[adView] else { return }
hide(adView: adView, velocity: velocity)
displayedAd.handler(displayedAd.ad, .dismissed)
break
}
fallthrough
case .cancelled:
// Re-setup timeout timer
setupTimeoutTimer(for: adView)
adView.layer.springAnimate(property: kPOPLayerTranslationY, key: "translation.y") { animation, _ in
animation.toValue = 0
}
default:
break
}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you for roasting this class, all this logic was taken from firefox and then adjusted by me

extension Strings {
/// "BAT" or "BAT Points" depending on the region
public static var BAT: String {
return Preferences.Rewards.isUsingBAP.value == true ? "BAP" : "BAT"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I duplicated some strings to leave BR-UI strings locked as internal.

Not perfect, we might want to rework that at some point

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this is OK, but in the end we can probably merge them into 1 struct. Reason it didnt start that way was because BraveRewardsUI used to be in its own repo.

@iccub iccub force-pushed the feature/ntp_views branch 4 times, most recently from 44061de to 6249ba1 Compare January 21, 2020 12:31
@iccub iccub force-pushed the feature/ntp_views branch 3 times, most recently from 49cac92 to c26f53b Compare January 21, 2020 15:06
Copy link
Collaborator

@kylehickinson kylehickinson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 more small thing :)

let nextYPosition: CGFloat

let bottomHalfOfChildView = view.frame.height - (contentView.frame.height / 2)
let pannedPastHalfOfViewHeight = yPosition > bottomHalfOfChildView

let closeByVelocity = projectedVelocity > contentView.frame.height
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value of projectedVelocity is distance travelled, not specific endpoint. You'll have to add its current yPosition to this value to compare it against where you want the end point to be (in this case the contentView's maxY)

Copy link
Collaborator

@kylehickinson kylehickinson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UI/Runtime review:

For overall transparent overlay sheet:

  • The black seems a bit harsh, I get that it helps readability but have we considered using a blurred background instead?

For the claim rewards screen:
image

Code wise:

  • Claim my rewards button should execute on touch up instead of touch down

Design wise (assuming there was some design somewhere?)

  • Can we align the title and close button?
  • Can we decrease the font-size of the button (and maybe give it some weight), seems so much bigger than the copy

For get paid to turn on ads screen:
image

Code wise:

  • Hide sponsored images just immediately sets the pref right? Wasn't supposed to open settings?
  • Highlights on this button only highlights the image for some reason. It should probably be set up with .system button type so that it highlights everything uniformly

For get paid to turn on rewards:
image

  • Learn more action should be executed on touch up, not touch down
  • Same issue with highlighting as above except there's no image, so there's no feedback at all when you tap either button

For "you can get paid" screen:
image

  • Button action should be executed on touch up, not touch down
  • Same highlight issue

For landscape iPhone:
image

This overlay takes quite a bit of space, I assume this is fine?

@iccub
Copy link
Contributor Author

iccub commented Jan 21, 2020

The overlay on landscape phone is fine.
We initially planned to replace favorite tiles with the notification view, but there wasn't enough space for small phones.

I will try to add the blur view instead of plain black with alpha

Will fix the code concerns

Rest is just what design team proposed, I will pass this feedback to @jamesmudgett

Add dark blur.
view.addSubview(vc.view)
vc.view.snp.remakeConstraints {
$0.right.top.left.equalToSuperview()
$0.bottom.equalTo(view)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this separate if . view is the superview?
I think $0.edges.equalToSuperview() should work fine.


class NTPNotificationView: UIStackView {

private lazy var titleStackView = UIStackView().then {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These shouldn't need to be lazy vars, using let should be fine.

Copy link
Contributor Author

@iccub iccub Jan 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are, because each of those views don't need to be added to the main view.

Some views have headers and buttons, some don't

let initialY = initialDrawerYPosition

// Only move the view if dragged below initial level.
if yPosition <= initialY {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be reduced to: (but up to you)

// Only move the view if dragged below initial level.
yPosition = max(yPosition, initialY)
// Dragged all way down, remove the view.
yPosition = min(yPosition, maxY)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feels less readable to me, i'll skip

contentView.snp.remakeConstraints {
if showAsPopup || (traitCollection.userInterfaceIdiom == .phone
&& UIApplication.shared.statusBarOrientation.isLandscape) {
$0.bottom.equalToSuperview()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can combine:
$0.bottom.centerX.equalToSuperview()

if yPosition == initialDrawerYPosition { return }

contentView.snp.remakeConstraints {
if showAsPopup || (traitCollection.userInterfaceIdiom == .phone
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe create const for traitCollection.userInterfaceIdiom == .phone && UIApplication.shared.statusBarOrientation.isLandscape

let iPhoneLandscape = traitCollection.userInterfaceIdiom == .phone && UIApplication.shared.statusBarOrientation.isLandscape
if showAsPopup && iPhoneLandscape { }

Probably not needed in this simple situation though.

$0.width.equalTo(maxHorizontalWidth)
} else {
$0.leading.trailing.equalToSuperview()
$0.bottom.equalToSuperview()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can add bottom to above line.

@iccub iccub requested a review from jhreis January 22, 2020 17:08
@jhreis jhreis merged commit 1a4afab into development Jan 22, 2020
@jhreis jhreis deleted the feature/ntp_views branch January 22, 2020 22:08
jhreis pushed a commit that referenced this pull request Jan 22, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants