Commit b5ead1da authored by Chris Lu's avatar Chris Lu Committed by Commit Bot

[ios] Add blue dot to displayedBadge to indicate unread badges.

Whenever there are multiple non-fullscreen badges, a blue dot will show
on the top right corner of the overflow badge if the menu has not been opened
yet. Once it is opened, the blue dot will disappear. The dot reappears if
a new badge is added.

A new BadgeConsumer method markDisplayedBadgeAsRead() is added to allow for the
mediator to tell the view controller when to hide/show the dot.

Screenshot: https://drive.google.com/open?id=13TMm0r1mtm_tz55hiMWkr77R0rMEijPp

Bug: 976901
Change-Id: I220cb865e117b19520c37cea2287757b46803df9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1832157
Commit-Queue: Chris Lu <thegreenfrog@chromium.org>
Reviewed-by: default avatarPeter Lee <pkl@chromium.org>
Reviewed-by: default avatarSergio Collazos <sczs@chromium.org>
Cr-Commit-Position: refs/heads/master@{#702047}
parent c8b3af7c
......@@ -33,7 +33,8 @@ void InfobarBadgeTabHelper::SetDelegate(
void InfobarBadgeTabHelper::UpdateBadgeForInfobarAccepted(
InfobarType infobar_type) {
infobar_badge_models_[infobar_type].badgeState |= BadgeStateAccepted;
infobar_badge_models_[infobar_type].badgeState |=
BadgeStateAccepted | BadgeStateRead;
[delegate_ updateInfobarBadge:infobar_badge_models_[infobar_type]];
}
......
......@@ -237,7 +237,10 @@ TEST_F(InfobarBadgeTabHelperTest, TestInfobarBadgeState) {
tab_helper()->UpdateBadgeForInfobarAccepted(
InfobarType::kInfobarTypePasswordSave);
EXPECT_TRUE(infobar_badge_tab_delegate_.badgeIsTappable);
EXPECT_TRUE(infobar_badge_tab_delegate_.badgeState &= BadgeStateAccepted);
EXPECT_EQ(BadgeStateAccepted,
infobar_badge_tab_delegate_.badgeState & BadgeStateAccepted);
EXPECT_EQ(BadgeStateRead,
infobar_badge_tab_delegate_.badgeState & BadgeStateRead);
}
// Test the badge state after doesn't change after adding an Infobar with no
......
......@@ -16,4 +16,7 @@ extern NSString* const kBadgeButtonOverflowAccessibilityIdentifier;
// A11y identifier for the Badge Popup Menu Table View.
extern NSString* const kBadgePopupMenuTableViewAccessibilityIdentifier;
// A11y identifier for the unread indicator above the displayed badge.
extern NSString* const kBadgeUnreadIndicatorAccessibilityIdentifier;
#endif // IOS_CHROME_BROWSER_UI_BADGES_BADGE_CONSTANTS_H_
......@@ -22,3 +22,6 @@ NSString* const kBadgeButtonOverflowAccessibilityIdentifier =
NSString* const kBadgePopupMenuTableViewAccessibilityIdentifier =
@"badgePopupMenuOverflowAXID";
NSString* const kBadgeUnreadIndicatorAccessibilityIdentifier =
@"badgeUnreadIndicatorAXID";
......@@ -17,6 +17,9 @@
// |displayedBadgeItem| and |fullscreenBadgeItem|.
- (void)updateDisplayedBadge:(id<BadgeItem>)displayedBadgeItem
fullScreenBadge:(id<BadgeItem>)fullscreenBadgeItem;
// Notifies the consumer whether or not there are unread badges. See
// BadgeStateRead for more information.
- (void)markDisplayedBadgeAsRead:(BOOL)read;
@end
......
......@@ -11,13 +11,18 @@
// States for the InfobarBadge.
typedef NS_OPTIONS(NSUInteger, BadgeState) {
// The badge is not accepted.
// The badge has not been accepted nor has it been read.
BadgeStateNone = 0,
// This property is set if it is read (i.e. the menu is opened, if it is set
// as the displayed badge, or if the user has accepted the badge action).
// Not set if the user has not seen the badge yet (e.g. the badge is in the
// overflow menu and the user has yet to open the menu).
BadgeStateRead = 1 << 0,
// The badge's banner is currently being presented.
BadgeStatePresented = 1 << 0,
BadgeStatePresented = 1 << 1,
// The Infobar Badge is accepted. e.g. The Infobar was accepted/confirmed, and
// the Infobar action has taken place.
BadgeStateAccepted = 1 << 1,
BadgeStateAccepted = 1 << 2,
};
// Holds properties and values the UI needs to configure a badge button.
......
......@@ -146,10 +146,14 @@ const int kMinimumNonFullScreenBadgesForOverflow = 2;
// Get all non-fullscreen badges.
for (id<BadgeItem> item in self.badges) {
if (!item.fullScreen) {
// Mark each badge as read since the overflow menu is about to be
// displayed.
item.badgeState |= BadgeStateRead;
[popupMenuBadges addObject:item];
}
}
[self.dispatcher displayPopupMenuWithBadgeItems:popupMenuBadges];
[self updateConsumerReadStatus];
// TODO(crbug.com/976901): Add metric for this action.
}
......@@ -178,6 +182,18 @@ const int kMinimumNonFullScreenBadgesForOverflow = 2;
#pragma mark - Private
// Directs consumer to update read status depending on the state of the
// non-fullscreen badges.
- (void)updateConsumerReadStatus {
for (id<BadgeItem> item in self.badges) {
if (!item.fullScreen && item.badgeState & BadgeStateRead) {
[self.consumer markDisplayedBadgeAsRead:NO];
return;
}
}
[self.consumer markDisplayedBadgeAsRead:YES];
}
// Gets the last fullscreen and non-fullscreen badges.
// This assumes that there is only ever one fullscreen badge, so the last badge
// in |badges| should be the only one.
......@@ -198,7 +214,7 @@ const int kMinimumNonFullScreenBadgesForOverflow = 2;
if (item.fullScreen) {
fullScreenBadge = item;
} else {
if (item.badgeState == BadgeStatePresented) {
if (item.badgeState & BadgeStatePresented) {
presentingBadge = item;
}
displayedBadge = item;
......@@ -218,9 +234,14 @@ const int kMinimumNonFullScreenBadgesForOverflow = 2;
? presentingBadge
: [[BadgeTappableItem alloc]
initWithBadgeType:BadgeType::kBadgeTypeOverflow];
} else {
// Since there is only one non-fullscreen badge, it will be fixed as the
// displayed badge, so mark it as read.
displayedBadge.badgeState |= BadgeStateRead;
}
[self.consumer updateDisplayedBadge:displayedBadge
fullScreenBadge:fullScreenBadge];
[self updateConsumerReadStatus];
}
- (void)updateNewWebState:(web::WebState*)newWebState
......
......@@ -59,6 +59,7 @@ class FakeInfobarBadgeTabHelper : public InfobarBadgeTabHelper {
@interface FakeBadgeConsumer : NSObject <BadgeConsumer>
@property(nonatomic, strong) id<BadgeItem> displayedBadge;
@property(nonatomic, assign) BOOL hasIncognitoBadge;
@property(nonatomic, assign) BOOL hasUnreadBadge;
@end
@implementation FakeBadgeConsumer
......@@ -72,6 +73,9 @@ class FakeInfobarBadgeTabHelper : public InfobarBadgeTabHelper {
self.hasIncognitoBadge = fullscreenBadgeItem != nil;
self.displayedBadge = displayedBadgeItem;
}
- (void)markDisplayedBadgeAsRead:(BOOL)read {
self.hasUnreadBadge = !read;
}
@end
class BadgeMediatorTest : public PlatformTest {
......@@ -165,6 +169,15 @@ TEST_F(BadgeMediatorTest, BadgeMediatorTestRemoveInfobar) {
BadgeType::kBadgeTypePasswordSave);
}
TEST_F(BadgeMediatorTest, BadgeMediatorTestMarkAsRead) {
AddAndActivateWebState(/*index=*/0, /*incognito=*/false);
AddInfobar();
AddSecondInfobar();
ASSERT_EQ(BadgeType::kBadgeTypeOverflow,
badge_consumer_.displayedBadge.badgeType);
EXPECT_TRUE(badge_consumer_.hasUnreadBadge);
}
// Test that the BadgeMediator updates the current badges to none when switching
// to a second WebState after an infobar is added to the first WebState.
TEST_F(BadgeMediatorTest, BadgeMediatorTestSwitchWebState) {
......
......@@ -7,9 +7,11 @@
#include "base/logging.h"
#import "ios/chrome/browser/ui/badges/badge_button.h"
#import "ios/chrome/browser/ui/badges/badge_button_factory.h"
#import "ios/chrome/browser/ui/badges/badge_constants.h"
#import "ios/chrome/browser/ui/badges/badge_item.h"
#import "ios/chrome/browser/ui/elements/extended_touch_target_button.h"
#import "ios/chrome/browser/ui/util/named_guide.h"
#import "ios/chrome/common/colors/semantic_color_names.h"
#import "ios/chrome/common/ui_util/constraints_ui_util.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
......@@ -20,7 +22,14 @@ namespace {
// FullScreen progress threshold in which to toggle between full screen on and
// off mode for the badge view.
const double kFullScreenProgressThreshold = 0.85;
const CGFloat kFullScreenProgressThreshold = 0.85;
// Spacing between the top and trailing anchors of |unreadIndicatorView| and
// |displayedBadge|.
const CGFloat kUnreadIndicatorViewSpacing = 10.0;
// Height of |unreadIndicatorView|.
const CGFloat kUnreadIndicatorViewHeight = 6.0;
} // namespace
......@@ -43,6 +52,10 @@ const double kFullScreenProgressThreshold = 0.85;
// StackView holding the displayedBadge and fullScreenBadge.
@property(nonatomic, strong) UIStackView* stackView;
// View that displays a blue dot on the top-right corner of the displayed badge
// if there are unread badges to be shown in the overflow menu.
@property(nonatomic, strong) UIView* unreadIndicatorView;
@end
@implementation BadgeViewController
......@@ -117,6 +130,37 @@ const double kFullScreenProgressThreshold = 0.85;
}
}
- (void)markDisplayedBadgeAsRead:(BOOL)read {
// Lazy init if the unread indicator needs to be shown.
if (!self.unreadIndicatorView && !read) {
// Add unread indicator to the displayed badge.
self.unreadIndicatorView = [[UIView alloc] init];
self.unreadIndicatorView.layer.cornerRadius =
kUnreadIndicatorViewHeight / 2;
self.unreadIndicatorView.backgroundColor =
[UIColor colorNamed:kToolbarButtonColor];
self.unreadIndicatorView.translatesAutoresizingMaskIntoConstraints = NO;
self.unreadIndicatorView.accessibilityIdentifier =
kBadgeUnreadIndicatorAccessibilityIdentifier;
[_displayedBadge addSubview:self.unreadIndicatorView];
[NSLayoutConstraint activateConstraints:@[
[self.unreadIndicatorView.trailingAnchor
constraintEqualToAnchor:_displayedBadge.trailingAnchor
constant:-kUnreadIndicatorViewSpacing],
[self.unreadIndicatorView.topAnchor
constraintEqualToAnchor:_displayedBadge.topAnchor
constant:kUnreadIndicatorViewSpacing],
[self.unreadIndicatorView.heightAnchor
constraintEqualToConstant:kUnreadIndicatorViewHeight],
[self.unreadIndicatorView.heightAnchor
constraintEqualToAnchor:self.unreadIndicatorView.widthAnchor]
]];
}
if (self.unreadIndicatorView) {
self.unreadIndicatorView.hidden = read;
}
}
#pragma mark FullscreenUIElement
- (void)updateForFullscreenProgress:(CGFloat)progress {
......@@ -149,6 +193,7 @@ const double kFullScreenProgressThreshold = 0.85;
[_displayedBadge removeFromSuperview];
if (!badgeButton) {
_displayedBadge = nil;
self.unreadIndicatorView = nil;
return;
}
_displayedBadge = badgeButton;
......
......@@ -81,6 +81,7 @@ NSString* const kSCDisplayedBadgeToggleButton =
initWithBadgeType:BadgeType::kBadgeTypeOverflow];
[self.consumer setupWithDisplayedBadge:displayedBadge
fullScreenBadge:incognitoItem];
[self.consumer markDisplayedBadgeAsRead:NO];
}
@end
......@@ -121,6 +122,7 @@ NSString* const kSCDisplayedBadgeToggleButton =
initWithInfobarType:InfobarType::kInfobarTypePasswordSave] ];
[self.badgePopupMenuCoordinator setBadgeItemsToShow:badgeItems];
[self.badgePopupMenuCoordinator start];
[self.consumer markDisplayedBadgeAsRead:YES];
}
@end
......@@ -59,12 +59,17 @@ using ::showcase_utils::Close;
kSCDisplayedBadgeToggleButton)]
performAction:grey_tap()];
// Assert that overflow badge is shown and tap on it.
// Assert that overflow badge and the unread indicator is shown and tap on it.
[[EarlGrey selectElementWithMatcher:
grey_allOf(grey_accessibilityID(
kBadgeButtonOverflowAccessibilityIdentifier),
grey_sufficientlyVisible(), nil)]
assertWithMatcher:grey_sufficientlyVisible()];
[[EarlGrey selectElementWithMatcher:
grey_allOf(grey_accessibilityID(
kBadgeUnreadIndicatorAccessibilityIdentifier),
grey_sufficientlyVisible(), nil)]
assertWithMatcher:grey_sufficientlyVisible()];
[[EarlGrey
selectElementWithMatcher:grey_accessibilityID(
kBadgeButtonOverflowAccessibilityIdentifier)]
......@@ -77,6 +82,18 @@ using ::showcase_utils::Close;
kBadgePopupMenuTableViewAccessibilityIdentifier),
grey_sufficientlyVisible(), nil)]
assertWithMatcher:grey_sufficientlyVisible()];
// Dismiss popup menu by tapping outside of the menu. Tapping the displayed
// badge is sufficient here. Assert that the unread indicator is not there
// anymore.
[[EarlGrey selectElementWithMatcher:grey_accessibilityID(
kSCDisplayedBadgeToggleButton)]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:
grey_allOf(grey_accessibilityID(
kBadgeUnreadIndicatorAccessibilityIdentifier),
grey_sufficientlyVisible(), nil)]
assertWithMatcher:grey_notVisible()];
}
@end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment