Brion VIBBER has submitted this change and it was merged.

Change subject: Alerts now support duration, position and attributed strings.
......................................................................


Alerts now support duration, position and attributed strings.

Further stability improvements.

Change-Id: I473b5f02319d016b7c4cf8bfd7dea9b1930dcbef
---
M Wikipedia.xcodeproj/project.pbxproj
M wikipedia/AppDelegate.m
M wikipedia/Categories/Alerts/AlertLabel.h
M wikipedia/Categories/Alerts/AlertLabel.m
M wikipedia/Categories/Alerts/UIViewController+Alert.h
M wikipedia/Categories/Alerts/UIViewController+Alert.m
M wikipedia/Categories/NSString+Extras.m
M wikipedia/Categories/NSURLRequest+DictionaryRequest.m
M wikipedia/Categories/TopActionSheet/UINavigationController+TopActionSheet.m
M wikipedia/Categories/UINavigationController+SearchNavStack.m
M wikipedia/Categories/UIView+Debugging.m
M wikipedia/Categories/UIView+RemoveConstraints.m
M wikipedia/Categories/UIView+SearchSubviews.h
M wikipedia/Categories/UIView+SearchSubviews.m
M wikipedia/Categories/UIViewController+HideKeyboard.m
M wikipedia/Categories/UIViewController+SearchChildViewControllers.m
M wikipedia/Categories/UIWebView+HideScrollGradient.m
M wikipedia/Defines/Defines.h
A wikipedia/Images/logo-placeholder-saved.png
A wikipedia/Images/logo-placeholder-sa...@2x.png
M wikipedia/TabularScrollView/TabularScrollView.m
M wikipedia/View Controllers/AccountCreation/AccountCreationViewController.m
M wikipedia/View Controllers/Languages/LanguagesViewController.m
M wikipedia/View Controllers/Login/LoginViewController.m
M wikipedia/View Controllers/Navigation/Bottom/BottomMenuContainerView.m
M wikipedia/View Controllers/Navigation/Bottom/BottomMenuViewController.m
M wikipedia/View Controllers/Navigation/Primary/PrimaryMenuViewController.m
M wikipedia/View Controllers/Navigation/Secondary/SecondaryMenuViewController.m
M wikipedia/View Controllers/Navigation/Top/TopMenuContainerView.m
M wikipedia/View Controllers/Navigation/Top/TopMenuViewController.m
M wikipedia/View Controllers/Nearby/NearbyViewController.m
M wikipedia/View Controllers/Onboarding/OnboardingViewController.m
M wikipedia/View Controllers/PageHistory/PageHistoryViewController.m
M wikipedia/View Controllers/Preview/PreviewAndSaveViewController.m
M wikipedia/View Controllers/Preview/PreviewLicenseView.m
M wikipedia/View Controllers/References/ReferencesVC.m
M wikipedia/View Controllers/SavedPages/SavedPagesViewController.m
M wikipedia/View Controllers/SearchResults/SearchResultsController.m
M wikipedia/View Controllers/SectionEditor/SectionEditorViewController.m
M wikipedia/View Controllers/TableOfContents/TOCViewController.m
M wikipedia/View Controllers/WebView/WebViewController.h
M wikipedia/View Controllers/WebView/WebViewController.m
M wikipedia/en.lproj/Localizable.strings
M wikipedia/qqq.lproj/Localizable.strings
44 files changed, 503 insertions(+), 315 deletions(-)

Approvals:
  Brion VIBBER: Verified; Looks good to me, approved
  jenkins-bot: Verified



diff --git a/Wikipedia.xcodeproj/project.pbxproj 
b/Wikipedia.xcodeproj/project.pbxproj
index fcc6f92..fd95100 100644
--- a/Wikipedia.xcodeproj/project.pbxproj
+++ b/Wikipedia.xcodeproj/project.pbxproj
@@ -27,6 +27,8 @@
                041A3B6218E11ED90079FF1C /* LanguagesViewController.m in 
Sources */ = {isa = PBXBuildFile; fileRef = 041A3B5D18E11ED90079FF1C /* 
LanguagesViewController.m */; };
                041C55D21950B27D006CE0EF /* EditSummaryViewController.m in 
Sources */ = {isa = PBXBuildFile; fileRef = 041C55D11950B27D006CE0EF /* 
EditSummaryViewController.m */; };
                041C6206199ED2A20061516F /* Section+TOC.m in Sources */ = {isa 
= PBXBuildFile; fileRef = 041C6205199ED2A20061516F /* Section+TOC.m */; };
+               041E588219B2D2C500AC140C /* logo-placeholder-saved.png in 
Resources */ = {isa = PBXBuildFile; fileRef = 041E588019B2D2C500AC140C /* 
logo-placeholder-saved.png */; };
+               041E588319B2D2C500AC140C /* logo-placeholder-sa...@2x.png in 
Resources */ = {isa = PBXBuildFile; fileRef = 041E588119B2D2C500AC140C /* 
logo-placeholder-sa...@2x.png */; };
                041EFC371996A1F800B2CB28 /* MapKit.framework in Frameworks */ = 
{isa = PBXBuildFile; fileRef = 041EFC361996A1F800B2CB28 /* MapKit.framework */; 
};
                04224500197F5E09005DD0BF /* AbuseFilterAlert.m in Sources */ = 
{isa = PBXBuildFile; fileRef = 042244FC197F5E09005DD0BF /* AbuseFilterAlert.m 
*/; };
                04224501197F5E09005DD0BF /* BulletedLabel.m in Sources */ = 
{isa = PBXBuildFile; fileRef = 042244FE197F5E09005DD0BF /* BulletedLabel.m */; 
};
@@ -234,6 +236,8 @@
                041C55D11950B27D006CE0EF /* EditSummaryViewController.m */ = 
{isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = 
sourcecode.c.objc; lineEnding = 0; path = EditSummaryViewController.m; 
sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.objc; };
                041C6204199ED2A20061516F /* Section+TOC.h */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = 
"Section+TOC.h"; sourceTree = "<group>"; };
                041C6205199ED2A20061516F /* Section+TOC.m */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path 
= "Section+TOC.m"; sourceTree = "<group>"; };
+               041E588019B2D2C500AC140C /* logo-placeholder-saved.png */ = 
{isa = PBXFileReference; lastKnownFileType = image.png; path = 
"logo-placeholder-saved.png"; sourceTree = "<group>"; };
+               041E588119B2D2C500AC140C /* logo-placeholder-sa...@2x.png */ = 
{isa = PBXFileReference; lastKnownFileType = image.png; path = 
"logo-placeholder-sa...@2x.png"; sourceTree = "<group>"; };
                041EFC361996A1F800B2CB28 /* MapKit.framework */ = {isa = 
PBXFileReference; lastKnownFileType = wrapper.framework; name = 
MapKit.framework; path = System/Library/Frameworks/MapKit.framework; sourceTree 
= SDKROOT; };
                042244FB197F5E09005DD0BF /* AbuseFilterAlert.h */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = 
AbuseFilterAlert.h; sourceTree = "<group>"; };
                042244FC197F5E09005DD0BF /* AbuseFilterAlert.m */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path 
= AbuseFilterAlert.m; sourceTree = "<group>"; };
@@ -1022,6 +1026,8 @@
                                04C91CF119554B310035ED1B /* 
logo-onboard...@2x.png */,
                                045EFF1819A25FEB00D0EDBB /* 
logo-placeholder-search.png */,
                                045EFF1919A25FEB00D0EDBB /* 
logo-placeholder-sea...@2x.png */,
+                               041E588019B2D2C500AC140C /* 
logo-placeholder-saved.png */,
+                               041E588119B2D2C500AC140C /* 
logo-placeholder-sa...@2x.png */,
                                04D3082919991CB60034F106 /* 
logo-placeholder-nearby.png */,
                                04D3082A19991CB60034F106 /* 
logo-placeholder-nea...@2x.png */,
                                04090A32187F53E400577EDF /* clear.png */,
@@ -1811,6 +1817,7 @@
                                04123630189C29EA00E0CF8E /* 
abuse-filter-disallo...@2x.png in Resources */,
                                D4BC22B4181E9E6300CAC673 /* empty.png in 
Resources */,
                                04123636189C29EA00E0CF8E /* 
abuse-filter-flag-white.png in Resources */,
+                               041E588319B2D2C500AC140C /* 
logo-placeholder-sa...@2x.png in Resources */,
                                04090A33187F53E400577EDF /* clear.png in 
Resources */,
                                0412362E189C29EA00E0CF8E /* 
abuse-filter-disallowed.png in Resources */,
                                0485FECF1994D5AE00141361 /* 
NearbyResultCell.xib in Resources */,
@@ -1822,6 +1829,7 @@
                                04C91CEE195520990035ED1B /* 
logo-onboarding-subtitle.png in Resources */,
                                045EFF1A19A25FEB00D0EDBB /* 
logo-placeholder-search.png in Resources */,
                                04224502197F5E09005DD0BF /* BulletedLabel.xib 
in Resources */,
+                               041E588219B2D2C500AC140C /* 
logo-placeholder-saved.png in Resources */,
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
diff --git a/wikipedia/AppDelegate.m b/wikipedia/AppDelegate.m
index a40bc9f..f137d7e 100644
--- a/wikipedia/AppDelegate.m
+++ b/wikipedia/AppDelegate.m
@@ -83,7 +83,8 @@
         @"FakeZeroOn" : @NO,
         @"ShowOnboarding" : @YES,
         @"LastHousekeepingDate" : [NSDate date], //[NSDate 
dateWithDaysBeforeNow:10]
-        @"SendUsageReports": @YES
+        @"SendUsageReports": @YES,
+        @"AccessSavedPagesMessageShown": @NO
     };
     [[NSUserDefaults standardUserDefaults] 
registerDefaults:userDefaultsDefaults];
 }
diff --git a/wikipedia/Categories/Alerts/AlertLabel.h 
b/wikipedia/Categories/Alerts/AlertLabel.h
index 4ac6761..f844072 100644
--- a/wikipedia/Categories/Alerts/AlertLabel.h
+++ b/wikipedia/Categories/Alerts/AlertLabel.h
@@ -4,6 +4,19 @@
 #import <UIKit/UIKit.h>
 #import "PaddedLabel.h"
 
+typedef NS_ENUM(NSInteger, AlertType) {
+    ALERT_TYPE_UNKNOWN,
+    ALERT_TYPE_TOP,
+    ALERT_TYPE_BOTTOM,
+    ALERT_TYPE_FULLSCREEN
+};
+
 @interface AlertLabel : PaddedLabel
 
+-(id)initWithText:(id)text duration:(CGFloat)duration 
padding:(UIEdgeInsets)padding type:(AlertType)type;
+
+-(void)hide;
+
+-(void)fade;
+
 @end
diff --git a/wikipedia/Categories/Alerts/AlertLabel.m 
b/wikipedia/Categories/Alerts/AlertLabel.m
index ce30f09..ece9841 100644
--- a/wikipedia/Categories/Alerts/AlertLabel.m
+++ b/wikipedia/Categories/Alerts/AlertLabel.m
@@ -4,73 +4,125 @@
 #import "AlertLabel.h"
 #import "WMF_Colors.h"
 #import "Defines.h"
+#import "UIView+RemoveConstraints.h"
+
+@interface AlertLabel()
+
+@property (nonatomic) CGFloat duration;
+@property (nonatomic) UIEdgeInsets padding;
+@property (nonatomic, strong) id text;
+@property (nonatomic) AlertType type;
+
+@end
 
 @implementation AlertLabel
 
-- (id)init
+-(id)initWithText:(id)text duration:(CGFloat)duration 
padding:(UIEdgeInsets)padding type:(AlertType)type;
 {
     self = [super init];
     if (self) {
-        self.alpha = 0.0f;
 
-        self.padding = UIEdgeInsetsMake(1, 10, 1, 10);
-
-        self.minimumScaleFactor = 0.2;
-        self.font = [UIFont systemFontOfSize:10];
+        self.font = [UIFont systemFontOfSize:ALERT_FONT_SIZE];
         self.textAlignment = NSTextAlignmentCenter;
         self.textColor = [UIColor colorWithWhite:0.0 alpha:1.0];
+
+        if([text isKindOfClass:[NSAttributedString class]]){
+            self.attributedText = text;
+        }else{
+            self.text = text;
+        }
+
+        self.duration = duration;
+        self.padding = padding;
+        self.type = type;
+        self.minimumScaleFactor = 0.2;
         self.numberOfLines = 0;
         self.lineBreakMode = NSLineBreakByWordWrapping;
-        self.backgroundColor = CHROME_COLOR;
+        self.backgroundColor = ALERT_BACKGROUND_COLOR;
         self.userInteractionEnabled = YES;
-
-        UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] 
initWithTarget:self action:@selector(tap)];
+        UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] 
initWithTarget:self action:@selector(tap:)];
         [self addGestureRecognizer:tap];
     }
     return self;
 }
 
--(void)tap
+-(void)tap:(UITapGestureRecognizer *)recognizer
 {
-    // Hide without delay.
-    self.alpha = 0.0f;
-}
-
--(void)setHidden:(BOOL)hidden
-{
-    if (hidden){
-        [UIView beginAnimations:nil context:NULL];
-        [UIView setAnimationDuration:0.35];
-        [UIView setAnimationDelay:1.0f];
-        [self setAlpha:0.0f];
-        [UIView commitAnimations];
-    }else{
-        [self setAlpha:1.0f];
+    if(recognizer.state == UIGestureRecognizerStateEnded){
+        // Hide without delay.
+        [self hide];
     }
 }
 
--(void)setText:(NSString *)text
+-(void)hide
 {
-    if (text.length == 0){
-        // Just fade out if message is set to empty string
-        self.hidden = YES;
-    }else{
-        super.text = text;
-        self.hidden = NO;
-    }
+    // Don't do anything if this view has yet to move to its superview.
+    if (!self.superview) return;
+
+    // This is important. Without this, rapid taps on save icon followed by 
save icon long press to
+    // bring up the saved pages list followed by selection would cause crash 
on iOS 6. Crash related
+    // to the constraint system causeing the alert view to be retained too 
long.
+    [self removeConstraintsOfViewFromView:self.superview];
+
+    [self removeFromSuperview];
+}
+
+-(void)fade
+{
+    [self fadeAfterDelay:0];
+}
+
+-(void)fadeAfterDelay:(CGFloat)delay
+{
+    // Don't do anything if this view has yet to move to its superview.
+    if (!self.superview) return;
+    
+    [UIView animateWithDuration:0.35
+                          delay:delay
+                        options:0
+                     animations:^{
+                         self.alpha = 0.0;
+                     }
+                     completion:^(BOOL done){
+                         [self hide];
+                     }];
+}
+
+-(void)didMoveToSuperview
+{
+    if (self.duration == -1) return;
+    [self fadeAfterDelay:self.duration];
 }
 
 - (void)drawRect:(CGRect)rect {
     [super drawRect:rect];
     CGContextRef context = UIGraphicsGetCurrentContext();
 
-    CGContextMoveToPoint(context, CGRectGetMinX(rect), CGRectGetMaxY(rect));
-    CGContextAddLineToPoint(context, CGRectGetMaxX(rect), CGRectGetMaxY(rect));
+    switch (self.type) {
+        case ALERT_TYPE_TOP:
+            CGContextMoveToPoint(context, CGRectGetMinX(rect), 
CGRectGetMaxY(rect));
+            CGContextAddLineToPoint(context, CGRectGetMaxX(rect), 
CGRectGetMaxY(rect));
+            break;
+        case ALERT_TYPE_BOTTOM:
+            CGContextMoveToPoint(context, CGRectGetMinX(rect), 
CGRectGetMinY(rect));
+            CGContextAddLineToPoint(context, CGRectGetMaxX(rect), 
CGRectGetMinY(rect));
+            break;
+        default: //ALERT_TYPE_FULLSCREEN
+            return;
+            break;
+    }
 
-    CGContextSetStrokeColorWithColor(context, [[UIColor lightGrayColor] 
CGColor] );
-    CGContextSetLineWidth(context, 1.0f / [UIScreen mainScreen].scale);
+    CGContextSetStrokeColorWithColor(context, CHROME_OUTLINE_COLOR.CGColor);
+    CGContextSetLineWidth(context, CHROME_OUTLINE_WIDTH);
 
     CGContextStrokePath(context);
 }
 
+/*
+-(void)dealloc
+{
+    NSLog(@"DEALLOC'ING ALERT VIEW!");
+}
+*/
+
 @end
diff --git a/wikipedia/Categories/Alerts/UIViewController+Alert.h 
b/wikipedia/Categories/Alerts/UIViewController+Alert.h
index 2004bce..c3cc777 100644
--- a/wikipedia/Categories/Alerts/UIViewController+Alert.h
+++ b/wikipedia/Categories/Alerts/UIViewController+Alert.h
@@ -2,14 +2,13 @@
 //  Copyright (c) 2013 Wikimedia Foundation. Provided under MIT-style license; 
please copy and modify!
 
 #import <UIKit/UIKit.h>
+#import "AlertLabel.h"
 
 // Category for showing alerts from any view controller.
 
 @interface UIViewController (Alert)
 
-// Shows alert text just beneath the nav bar.
-// Fades out alert if alertText set to zero length string.
--(void)showAlert:(NSString *)alertText;
+-(void)showAlert:(id)alertText type:(AlertType)type duration:(CGFloat)duration;
 
 -(void)fadeAlert;
 
diff --git a/wikipedia/Categories/Alerts/UIViewController+Alert.m 
b/wikipedia/Categories/Alerts/UIViewController+Alert.m
index 7fc20ad..b3cd308 100644
--- a/wikipedia/Categories/Alerts/UIViewController+Alert.m
+++ b/wikipedia/Categories/Alerts/UIViewController+Alert.m
@@ -2,57 +2,48 @@
 //  Copyright (c) 2013 Wikimedia Foundation. Provided under MIT-style license; 
please copy and modify!
 
 #import "UIViewController+Alert.h"
-#import "AlertLabel.h"
 #import "UIView+RemoveConstraints.h"
+#import "UIView+SearchSubviews.h"
+#import "WebViewController.h"
+#import "BottomMenuViewController.h"
+#import "Defines.h"
 
 @implementation UIViewController (Alert)
 
--(void)showAlert:(NSString *)alertText
+-(void)showAlert:(id)alertText type:(AlertType)type duration:(CGFloat)duration
 {
     [[NSOperationQueue mainQueue] addOperationWithBlock: ^ {
-        AlertLabel *alertLabel = nil;
+        [self fadeAlert];
         
-        // Tack these alerts to the nav controller's view.
-        UIView *alertContainer = self.view;
+        if ([self shouldHideAlertForViewController:self]) return;
         
-        // Reuse existing alert label if any.
-        alertLabel = [self getExistingViewOfClass:[AlertLabel class] 
inContainer:alertContainer];
+        AlertLabel *newAlertLabel =
+        [[AlertLabel alloc] initWithText: alertText
+                                duration: duration
+                                 padding: ALERT_PADDING
+                                 type:type];
         
-        // If none to reuse, add one.
-        if (!alertLabel) {
-            alertLabel = [[AlertLabel alloc] init];
-            alertLabel.translatesAutoresizingMaskIntoConstraints = NO;
-            //alertLabel.layer.cornerRadius = 3.0f;
-            //alertLabel.clipsToBounds = YES;
-            [alertContainer addSubview:alertLabel];
-            [self constrainAlertView:alertLabel fullScreen:NO];
-        }
-
-        BOOL hide = [self shouldHideAlertForViewController:self];
-
-        if (hide) {
-            alertLabel.alpha = 0.0;
-        }else{
-            alertLabel.text = alertText;
-        }
-
+        newAlertLabel.translatesAutoresizingMaskIntoConstraints = NO;
+        //newAlertLabel.layer.cornerRadius = 3.0f;
+        //newAlertLabel.clipsToBounds = YES;
+        [self.view addSubview:newAlertLabel];
+        
+        [self constrainAlertView:newAlertLabel type:type];
     }];
 }
 
 -(void)fadeAlert
 {
-    [self showAlert:@""];
+    // Fade existing alert labels if any.
+    NSArray *alertLabels = [self.view getSubviewsOfClass:[AlertLabel class]];
+    [alertLabels makeObjectsPerformSelector:@selector(fade)];
 }
 
 -(void)hideAlert
 {
-    // Hide alert immediately. Removes it so any running fade animations don't 
prevent immediate hide.
-    AlertLabel *alertLabel = [self getExistingViewOfClass:[AlertLabel class] 
inContainer:self.view];
-    if (alertLabel) {
-        [alertLabel removeConstraintsOfViewFromView:self.view];
-        [alertLabel removeFromSuperview];
-        alertLabel = nil;
-    }
+    // Hide existing alert labels if any.
+    NSArray *alertLabels = [self.view getSubviewsOfClass:[AlertLabel class]];
+    [alertLabels makeObjectsPerformSelector:@selector(hide)];
 }
 
 -(BOOL)shouldHideAlertForViewController:(UIViewController *)vc
@@ -100,38 +91,55 @@
 }
 */
 
--(void)constrainAlertView:(UIView *)view fullScreen:(BOOL)isFullScreen
+-(void)constrainAlertView: (UIView *)view type:(AlertType)type;
 {
+    [view removeConstraintsOfViewFromView:self.view];
+
     CGFloat margin = 0;
 
-    NSDictionary *views = NSDictionaryOfVariableBindings(view);
+    NSMutableDictionary *views = @{@"view": view}.mutableCopy;
     NSDictionary *metrics = @{@"space": @(margin)};
 
-    [self.view addConstraints:
-     [NSLayoutConstraint constraintsWithVisualFormat: 
@"H:|-(space)-[view]-(space)-|"
-                                             options: 0
-                                             metrics: metrics
-                                               views: views
-      ]
-     ];
-    
-    NSString *verticalConstraint = [NSString 
stringWithFormat:@"V:|-(space)-[view]%@", (isFullScreen) ? @"|": @""];
-    
-    [self.view addConstraints:
-     [NSLayoutConstraint constraintsWithVisualFormat: verticalConstraint
-                                             options: 0
-                                             metrics: metrics
-                                               views: views
-      ]
-     ];
-}
 
--(id)getExistingViewOfClass:(Class)class inContainer:(UIView *)container
-{
-    for (id view in container.subviews.copy) {
-        if ([view isMemberOfClass:class]) return view;
+    UIView *bottomMenuView = nil;
+    if([self isMemberOfClass:[WebViewController class]]){
+        WebViewController *webVC = (WebViewController *)self;
+        bottomMenuView = webVC.bottomMenuViewController.view;
+        if (bottomMenuView) {
+            views[@"bottomMenuView"] = bottomMenuView;
+        }
     }
-    return nil;
+
+    NSString *verticalFormatString = @"";
+    switch (type) {
+        case ALERT_TYPE_BOTTOM:
+            if (bottomMenuView) {
+                verticalFormatString = @"V:[view]-(space)-[bottomMenuView]";
+            }else{
+                verticalFormatString = @"V:[view]-(space)-|";
+            }
+            break;
+        case ALERT_TYPE_FULLSCREEN:
+            verticalFormatString = @"V:|-(space)-[view]-(space)-|";
+            break;
+        default: // ALERT_TYPE_TOP
+            verticalFormatString = @"V:|-(space)-[view]";
+            break;
+    }
+    
+    NSArray *viewConstraintArrays =
+    @[
+      [NSLayoutConstraint constraintsWithVisualFormat: 
@"H:|-(space)-[view]-(space)-|"
+                                              options: 0
+                                              metrics: metrics
+                                                views: views],
+      [NSLayoutConstraint constraintsWithVisualFormat: verticalFormatString
+                                              options: 0
+                                              metrics: metrics
+                                                views: views]
+      ];
+
+    [self.view addConstraints:[viewConstraintArrays 
valueForKeyPath:@"@unionOfArrays.self"]];
 }
 
 @end
diff --git a/wikipedia/Categories/NSString+Extras.m 
b/wikipedia/Categories/NSString+Extras.m
index c1c5ed0..9cf4334 100644
--- a/wikipedia/Categories/NSString+Extras.m
+++ b/wikipedia/Categories/NSString+Extras.m
@@ -75,7 +75,7 @@
     NSData *stringData = [self dataUsingEncoding:NSUTF8StringEncoding];
     TFHpple *parser = [TFHpple hppleWithHTMLData:stringData];
     NSArray *textNodes = [parser searchWithXPathQuery:@"//text()"];
-    NSMutableArray *results = [@[] mutableCopy];
+    NSMutableArray *results = @[].mutableCopy;
     for (TFHppleElement *node in textNodes) {
         if(node.isTextNode) [results addObject:node.raw];
     }
diff --git a/wikipedia/Categories/NSURLRequest+DictionaryRequest.m 
b/wikipedia/Categories/NSURLRequest+DictionaryRequest.m
index 31ea032..2ead3e3 100644
--- a/wikipedia/Categories/NSURLRequest+DictionaryRequest.m
+++ b/wikipedia/Categories/NSURLRequest+DictionaryRequest.m
@@ -15,7 +15,7 @@
 {
     NSMutableString *body = [NSMutableString string];
     
-    for (NSString *key in parameters) {
+    for (NSString *key in parameters.copy) {
         NSString *val = [parameters objectForKey:key];
         if ([body length])
             [body appendString:@"&"];
diff --git 
a/wikipedia/Categories/TopActionSheet/UINavigationController+TopActionSheet.m 
b/wikipedia/Categories/TopActionSheet/UINavigationController+TopActionSheet.m
index 019ea17..59f8d2e 100644
--- 
a/wikipedia/Categories/TopActionSheet/UINavigationController+TopActionSheet.m
+++ 
b/wikipedia/Categories/TopActionSheet/UINavigationController+TopActionSheet.m
@@ -3,6 +3,7 @@
 
 #import "UINavigationController+TopActionSheet.h"
 #import "UIView+RemoveConstraints.h"
+#import "UIView+SearchSubviews.h"
 
 #define ANIMATION_DURATION 0.23f
 
@@ -16,7 +17,7 @@
         UIView *superView = self.view;
         
         // Reuse existing container if any.
-        containerView = [self getExistingViewOfClass:[TabularScrollView class] 
inContainer:superView];
+        containerView = [superView getFirstSubviewOfClass:[TabularScrollView 
class]];
 
         // Remove container view if no views were specified.
         if (!views || (views.count == 0)) {
@@ -66,7 +67,7 @@
 
-(void)topActionSheetChangeOrientation:(TabularScrollViewOrientation)orientation
 {
     [[NSOperationQueue mainQueue] addOperationWithBlock: ^ {
-        TabularScrollView *containerView = [self 
getExistingViewOfClass:[TabularScrollView class] inContainer:self.view];
+        TabularScrollView *containerView = [self.view 
getFirstSubviewOfClass:[TabularScrollView class]];
 
         [containerView setOrientation:orientation];
             
@@ -105,14 +106,6 @@
     [self.view addConstraints:[constraints 
valueForKeyPath:@"@unionOfArrays.self"]];
 
     [self.view setNeedsUpdateConstraints];
-}
-
--(id)getExistingViewOfClass:(Class)class inContainer:(UIView *)container
-{
-    for (id view in container.subviews) {
-        if ([view isMemberOfClass:class]) return view;
-    }
-    return nil;
 }
 
 @end
diff --git a/wikipedia/Categories/UINavigationController+SearchNavStack.m 
b/wikipedia/Categories/UINavigationController+SearchNavStack.m
index e219628..0a158cc 100644
--- a/wikipedia/Categories/UINavigationController+SearchNavStack.m
+++ b/wikipedia/Categories/UINavigationController+SearchNavStack.m
@@ -7,7 +7,7 @@
 
 -(id)searchNavStackForViewControllerOfClass:(Class)aClass
 {
-    for (UIViewController *vc in self.viewControllers) {
+    for (UIViewController *vc in self.viewControllers.copy) {
         if ([vc isMemberOfClass:aClass]) return vc;
     }
     return nil;
@@ -16,7 +16,7 @@
 -(id)getVCBeneathVC:(id)thisVC
 {
     id vcBeneath = nil;
-    for (id vc in self.viewControllers) {
+    for (id vc in self.viewControllers.copy) {
         if (vc == thisVC) return vcBeneath;
         vcBeneath = vc;
     }
diff --git a/wikipedia/Categories/UIView+Debugging.m 
b/wikipedia/Categories/UIView+Debugging.m
index 3fa035d..7c054b7 100644
--- a/wikipedia/Categories/UIView+Debugging.m
+++ b/wikipedia/Categories/UIView+Debugging.m
@@ -12,7 +12,7 @@
     float(^color)() = ^float() {
         return (float)arc4random_uniform(100) / 100.0f;
     };
-    for (UIView *subView in self.subviews) {
+    for (UIView *subView in self.subviews.copy) {
         subView.layer.borderWidth = 1.0f;
         subView.layer.borderColor = [UIColor colorWithWhite:0.0f 
alpha:0.5f].CGColor;
         subView.layer.backgroundColor = [UIColor colorWithRed : color()
diff --git a/wikipedia/Categories/UIView+RemoveConstraints.m 
b/wikipedia/Categories/UIView+RemoveConstraints.m
index d167745..50a154d 100644
--- a/wikipedia/Categories/UIView+RemoveConstraints.m
+++ b/wikipedia/Categories/UIView+RemoveConstraints.m
@@ -7,7 +7,7 @@
 
 -(void)removeConstraintsOfViewFromView:(UIView *)view
 {
-    for (NSLayoutConstraint *c in [view.constraints copy]) {
+    for (NSLayoutConstraint *c in view.constraints.copy) {
         if (c.firstItem == self || c.secondItem == self) {
             [view removeConstraint: c];
         }
diff --git a/wikipedia/Categories/UIView+SearchSubviews.h 
b/wikipedia/Categories/UIView+SearchSubviews.h
index 6a7f940..aea20a6 100644
--- a/wikipedia/Categories/UIView+SearchSubviews.h
+++ b/wikipedia/Categories/UIView+SearchSubviews.h
@@ -7,4 +7,6 @@
 
 -(id)getFirstSubviewOfClass:(Class)class;
 
+-(NSArray *)getSubviewsOfClass:(Class)class;
+
 @end
diff --git a/wikipedia/Categories/UIView+SearchSubviews.m 
b/wikipedia/Categories/UIView+SearchSubviews.m
index 000984d..7ccd815 100644
--- a/wikipedia/Categories/UIView+SearchSubviews.m
+++ b/wikipedia/Categories/UIView+SearchSubviews.m
@@ -7,7 +7,7 @@
 
 -(id)getFirstSubviewOfClass:(Class)class
 {
-    for (id view in self.subviews) {
+    for (id view in self.subviews.copy) {
         if([view isMemberOfClass:class]){
             return view;
         }
@@ -15,4 +15,15 @@
     return nil;
 }
 
+-(NSArray *)getSubviewsOfClass:(Class)class
+{
+    NSMutableArray *output = @[].mutableCopy;
+    for (id view in self.subviews.copy) {
+        if([view isMemberOfClass:class]){
+            [output addObject:view];
+        }
+    }
+    return output;
+}
+
 @end
diff --git a/wikipedia/Categories/UIViewController+HideKeyboard.m 
b/wikipedia/Categories/UIViewController+HideKeyboard.m
index 45893a2..adffc3a 100644
--- a/wikipedia/Categories/UIViewController+HideKeyboard.m
+++ b/wikipedia/Categories/UIViewController+HideKeyboard.m
@@ -16,7 +16,7 @@
 -(void)recurseSubVCs
 {
     [self recurseSubviewsOfView:self.view];
-    for (UIViewController *subVC in self.childViewControllers) {
+    for (UIViewController *subVC in self.childViewControllers.copy) {
         if (subVC.presentedViewController) {
             [subVC.presentedViewController recurseSubVCs];
         }
@@ -35,7 +35,7 @@
             }
         }
     }
-    for (UIView *subView in view.subviews) {
+    for (UIView *subView in view.subviews.copy) {
         [self recurseSubviewsOfView:subView];
     }
 }
diff --git a/wikipedia/Categories/UIViewController+SearchChildViewControllers.m 
b/wikipedia/Categories/UIViewController+SearchChildViewControllers.m
index 42e1c68..3989ce6 100644
--- a/wikipedia/Categories/UIViewController+SearchChildViewControllers.m
+++ b/wikipedia/Categories/UIViewController+SearchChildViewControllers.m
@@ -7,7 +7,7 @@
 
 -(id)searchForChildViewControllerOfClass:(Class)aClass
 {
-    for (UIViewController *vc in self.childViewControllers) {
+    for (UIViewController *vc in self.childViewControllers.copy) {
         if ([vc isMemberOfClass:aClass]) return vc;
     }
     return nil;
diff --git a/wikipedia/Categories/UIWebView+HideScrollGradient.m 
b/wikipedia/Categories/UIWebView+HideScrollGradient.m
index 0c38090..4dbae66 100644
--- a/wikipedia/Categories/UIWebView+HideScrollGradient.m
+++ b/wikipedia/Categories/UIWebView+HideScrollGradient.m
@@ -8,7 +8,7 @@
 -(void)hideScrollGradient
 {
     if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) return;
-    for (UIView *view in self.scrollView.subviews) {
+    for (UIView *view in self.scrollView.subviews.copy) {
         if ([view isKindOfClass:[UIImageView class]]) {
             view.hidden = YES;
         }
diff --git a/wikipedia/Defines/Defines.h b/wikipedia/Defines/Defines.h
index 321026e..6cb69d4 100644
--- a/wikipedia/Defines/Defines.h
+++ b/wikipedia/Defines/Defines.h
@@ -24,3 +24,10 @@
 #define TOP_MENU_INITIAL_HEIGHT 46 
 
 #define CHROME_COLOR [UIColor colorWithRed:0.94 green:0.94 blue:0.96 alpha:1.0]
+
+#define ALERT_FONT_SIZE 12
+#define ALERT_BACKGROUND_COLOR [UIColor colorWithRed:(0.94 * 0.85) green:(0.94 
* 0.85) blue:(0.96 * 0.85) alpha:1.0]
+#define ALERT_PADDING UIEdgeInsetsMake(2, 10, 2, 10)
+
+#define CHROME_OUTLINE_COLOR ALERT_BACKGROUND_COLOR
+#define CHROME_OUTLINE_WIDTH (1.0f / [UIScreen mainScreen].scale)
\ No newline at end of file
diff --git a/wikipedia/Images/logo-placeholder-saved.png 
b/wikipedia/Images/logo-placeholder-saved.png
new file mode 100644
index 0000000..d7e7855
--- /dev/null
+++ b/wikipedia/Images/logo-placeholder-saved.png
Binary files differ
diff --git a/wikipedia/Images/logo-placeholder-sa...@2x.png 
b/wikipedia/Images/logo-placeholder-sa...@2x.png
new file mode 100644
index 0000000..678b5f3
--- /dev/null
+++ b/wikipedia/Images/logo-placeholder-sa...@2x.png
Binary files differ
diff --git a/wikipedia/TabularScrollView/TabularScrollView.m 
b/wikipedia/TabularScrollView/TabularScrollView.m
index 09452b1..588d05b 100644
--- a/wikipedia/TabularScrollView/TabularScrollView.m
+++ b/wikipedia/TabularScrollView/TabularScrollView.m
@@ -112,6 +112,7 @@
     
     if([sender isKindOfClass:[UIGestureRecognizer class]]){
         UIGestureRecognizer *recognizer = (UIGestureRecognizer *)sender;
+        if (recognizer.state != UIGestureRecognizerStateEnded) return;
         tappedView = recognizer.view;
         CGPoint loc = [recognizer locationInView:tappedView];
         tappedChildView = [tappedView hitTest:loc withEvent:nil];
diff --git a/wikipedia/View 
Controllers/AccountCreation/AccountCreationViewController.m b/wikipedia/View 
Controllers/AccountCreation/AccountCreationViewController.m
index 8f71c1c..ffe9272 100644
--- a/wikipedia/View Controllers/AccountCreation/AccountCreationViewController.m
+++ b/wikipedia/View Controllers/AccountCreation/AccountCreationViewController.m
@@ -153,11 +153,13 @@
     self.emailField.textAlignment = [WikipediaAppUtils rtlSafeAlignment];
 }
 
-- (void)loginButtonPushed:(id)sender
+- (void)loginButtonPushed:(UITapGestureRecognizer *)recognizer
 {
-    [self performModalSequeWithID: @"modal_segue_show_login"
-                  transitionStyle: UIModalTransitionStyleCoverVertical
-                            block: nil];
+    if (recognizer.state == UIGestureRecognizerStateEnded) {
+        [self performModalSequeWithID: @"modal_segue_show_login"
+                      transitionStyle: UIModalTransitionStyleCoverVertical
+                                block: nil];
+    }
 }
 
 -(NSAttributedString *)getAttributedPlaceholderForString:(NSString *)string
@@ -350,8 +352,7 @@
 {
     self.captchaViewController.captchaTextBox.text = @"";
 
-    [self showAlert:MWLocalizedString(@"account-creation-captcha-obtaining", 
nil)];
-    [self fadeAlert];
+    [self showAlert:MWLocalizedString(@"account-creation-captcha-obtaining", 
nil) type:ALERT_TYPE_TOP duration:1];
 
     CaptchaResetOp *captchaResetOp =
     [[CaptchaResetOp alloc] initWithDomain: [SessionSingleton 
sharedInstance].domain
@@ -381,7 +382,7 @@
                                [self fadeAlert];
                                
                            } errorBlock: ^(NSError *error){
-                               [self showAlert:error.localizedDescription];
+                               [self showAlert:error.localizedDescription 
type:ALERT_TYPE_TOP duration:-1];
                                
                            }];
     
@@ -424,14 +425,14 @@
     // Create detached loginVC just for logging in.
     LoginViewController *loginVC = [[LoginViewController alloc] init];
     
-    [self showAlert:MWLocalizedString(@"account-creation-logging-in", nil)];
+    [self showAlert:MWLocalizedString(@"account-creation-logging-in", nil) 
type:ALERT_TYPE_TOP duration:-1];
     
     [loginVC loginWithUserName:self.usernameField.text 
password:self.passwordField.text onSuccess:^{
 
         NSString *loggedInMessage = 
MWLocalizedString(@"main-menu-account-title-logged-in", nil);
         loggedInMessage = [loggedInMessage 
stringByReplacingOccurrencesOfString: @"$1"
                                                                      
withString: self.usernameField.text];
-        [self showAlert:loggedInMessage];
+        [self showAlert:loggedInMessage type:ALERT_TYPE_TOP duration:-1];
 
         if (onboardingVC) {
             [self popModalToRoot];
@@ -454,13 +455,13 @@
 
     // Verify passwords fields match.
     if (![self.passwordField.text 
isEqualToString:self.passwordRepeatField.text]) {
-        [self 
showAlert:MWLocalizedString(@"account-creation-passwords-mismatched", nil)];
+        [self 
showAlert:MWLocalizedString(@"account-creation-passwords-mismatched", nil) 
type:ALERT_TYPE_TOP duration:-1];
         isAleadySaving = NO;
         return;
     }
 
     // Save!
-    [self showAlert:MWLocalizedString(@"account-creation-saving", nil)];
+    [self showAlert:MWLocalizedString(@"account-creation-saving", nil) 
type:ALERT_TYPE_TOP duration:-1];
 
     AccountCreationOp *accountCreationOp =
     [[AccountCreationOp alloc] initWithDomain: [SessionSingleton 
sharedInstance].domain
@@ -478,8 +479,7 @@
                                   [self.funnel logSuccess];
 
                                   dispatch_async(dispatch_get_main_queue(), 
^(){
-                                      [self showAlert:result];
-                                      [self fadeAlert];
+                                      [self showAlert:result 
type:ALERT_TYPE_TOP duration:1];
                                       [self performSelector:@selector(login) 
withObject:nil afterDelay:0.6f];
                                       isAleadySaving = NO;
                                   });
@@ -490,7 +490,7 @@
                                   isAleadySaving = NO;
                                   
                               } errorBlock: ^(NSError *error){
-                                  [self showAlert:error.localizedDescription];
+                                  [self showAlert:error.localizedDescription 
type:ALERT_TYPE_TOP duration:-1];
 
                                   [self.funnel 
logError:error.localizedDescription];
 
@@ -522,7 +522,7 @@
                                         isAleadySaving = NO;
                                     }
                                         errorBlock: ^(NSError *error){
-                                            [self 
showAlert:error.localizedDescription];
+                                            [self 
showAlert:error.localizedDescription type:ALERT_TYPE_TOP duration:-1];
                                             isAleadySaving = NO;
                                         }];
 
diff --git a/wikipedia/View Controllers/Languages/LanguagesViewController.m 
b/wikipedia/View Controllers/Languages/LanguagesViewController.m
index 35fd6df..4c6fd76 100644
--- a/wikipedia/View Controllers/Languages/LanguagesViewController.m
+++ b/wikipedia/View Controllers/Languages/LanguagesViewController.m
@@ -176,7 +176,7 @@
 
 -(void)downloadLangLinkData
 {
-    [self showAlert:MWLocalizedString(@"article-languages-downloading", nil)];
+    [self showAlert:MWLocalizedString(@"article-languages-downloading", nil) 
type:ALERT_TYPE_TOP duration:-1];
     AssetsFile *assetsFile = [[AssetsFile alloc] 
initWithFile:ASSETS_FILE_LANGUAGES];
 
     DownloadLangLinksOp *langLinksOp =
@@ -199,7 +199,7 @@
                                       
                                   } errorBlock: ^(NSError *error){
                                       //NSString *errorMsg = 
error.localizedDescription;
-                                      [self 
showAlert:error.localizedDescription];
+                                      [self 
showAlert:error.localizedDescription type:ALERT_TYPE_TOP duration:-1];
                                       
                                   }];
     
diff --git a/wikipedia/View Controllers/Login/LoginViewController.m 
b/wikipedia/View Controllers/Login/LoginViewController.m
index f6347c7..11d9613 100644
--- a/wikipedia/View Controllers/Login/LoginViewController.m
+++ b/wikipedia/View Controllers/Login/LoginViewController.m
@@ -191,8 +191,7 @@
                       NSString *loggedInMessage = 
MWLocalizedString(@"main-menu-account-title-logged-in", nil);
                       loggedInMessage = [loggedInMessage 
stringByReplacingOccurrencesOfString: @"$1"
                                                                                
    withString: self.usernameField.text];
-                      [self showAlert:loggedInMessage];
-                      [self fadeAlert];
+                      [self showAlert:loggedInMessage type:ALERT_TYPE_TOP 
duration:1.0f];
 
                       [self performSelector:(onboardingVC ? 
@selector(popModalToRoot) : @selector(popModal)) withObject:nil 
afterDelay:1.2f];
                       
@@ -255,7 +254,7 @@
                           [SessionSingleton 
sharedInstance].keychainCredentials.password = password;
                           
                           //NSString *result = 
loginResult[@"login"][@"result"];
-                          [self showAlert:loginStatus];
+                          [self showAlert:loginStatus type:ALERT_TYPE_TOP 
duration:-1];
                           
                           [[NSOperationQueue mainQueue] 
addOperationWithBlock:successBlock];
                           
@@ -266,14 +265,14 @@
                           
                       } cancelledBlock: ^(NSError *error){
                           
-                          [self showAlert:error.localizedDescription];
+                          [self showAlert:error.localizedDescription 
type:ALERT_TYPE_TOP duration:-1];
 
                           [[NSOperationQueue mainQueue] 
addOperationWithBlock:failBlock];
 
                           
                       } errorBlock: ^(NSError *error){
                           
-                          [self showAlert:error.localizedDescription];
+                          [self showAlert:error.localizedDescription 
type:ALERT_TYPE_TOP duration:-1];
 
                           [[NSOperationQueue mainQueue] 
addOperationWithBlock:failBlock];
 
@@ -297,7 +296,7 @@
                                
                            } errorBlock: ^(NSError *error){
                                
-                               [self showAlert:error.localizedDescription];
+                               [self showAlert:error.localizedDescription 
type:ALERT_TYPE_TOP duration:-1];
 
                                [[NSOperationQueue mainQueue] 
addOperationWithBlock:failBlock];
 
@@ -338,16 +337,18 @@
      ];
 }
 
-- (void)createAccountButtonPushed:(id)sender
+- (void)createAccountButtonPushed:(UITapGestureRecognizer *)recognizer
 {
-    [self.funnel logCreateAccountAttempt];
+    if (recognizer.state == UIGestureRecognizerStateEnded) {
+        [self.funnel logCreateAccountAttempt];
 
-    [self performModalSequeWithID: @"modal_segue_show_create_account"
-                  transitionStyle: UIModalTransitionStyleCoverVertical
-                            block: ^(AccountCreationViewController 
*createAcctVC){
-                                createAcctVC.funnel = [[CreateAccountFunnel 
alloc] init];
-                                [createAcctVC.funnel 
logStartFromLogin:self.funnel.loginSessionToken];
-                            }];
+        [self performModalSequeWithID: @"modal_segue_show_create_account"
+                      transitionStyle: UIModalTransitionStyleCoverVertical
+                                block: ^(AccountCreationViewController 
*createAcctVC){
+                                    createAcctVC.funnel = 
[[CreateAccountFunnel alloc] init];
+                                    [createAcctVC.funnel 
logStartFromLogin:self.funnel.loginSessionToken];
+                                }];
+    }
 }
 
 @end
diff --git a/wikipedia/View 
Controllers/Navigation/Bottom/BottomMenuContainerView.m b/wikipedia/View 
Controllers/Navigation/Bottom/BottomMenuContainerView.m
index a92a02f..4716f3c 100644
--- a/wikipedia/View Controllers/Navigation/Bottom/BottomMenuContainerView.m
+++ b/wikipedia/View Controllers/Navigation/Bottom/BottomMenuContainerView.m
@@ -2,6 +2,7 @@
 //  Copyright (c) 2013 Wikimedia Foundation. Provided under MIT-style license; 
please copy and modify!
 
 #import "BottomMenuContainerView.h"
+#import "Defines.h"
 
 @implementation BottomMenuContainerView
 
@@ -9,8 +10,8 @@
     CGContextRef context = UIGraphicsGetCurrentContext();
     CGContextMoveToPoint(context, CGRectGetMinX(rect), CGRectGetMinY(rect));
     CGContextAddLineToPoint(context, CGRectGetMaxX(rect), CGRectGetMinY(rect));
-    CGContextSetStrokeColorWithColor(context, [[UIColor lightGrayColor] 
CGColor] );
-    CGContextSetLineWidth(context, 1.0f / [UIScreen mainScreen].scale);
+    CGContextSetStrokeColorWithColor(context, CHROME_OUTLINE_COLOR.CGColor);
+    CGContextSetLineWidth(context, CHROME_OUTLINE_WIDTH);
     CGContextStrokePath(context);
 }
 
diff --git a/wikipedia/View 
Controllers/Navigation/Bottom/BottomMenuViewController.m b/wikipedia/View 
Controllers/Navigation/Bottom/BottomMenuViewController.m
index f820e84..1a9b552 100644
--- a/wikipedia/View Controllers/Navigation/Bottom/BottomMenuViewController.m
+++ b/wikipedia/View Controllers/Navigation/Bottom/BottomMenuViewController.m
@@ -21,6 +21,9 @@
 #import "Defines.h"
 #import "WikipediaAppUtils.h"
 #import "WMF_Colors.h"
+#import "UIViewController+ModalPresent.h"
+#import "UIViewController+ModalsSearch.h"
+#import "UIViewController+ModalPop.h"
 
 typedef NS_ENUM(NSInteger, BottomMenuItemTag) {
     BOTTOM_MENU_BUTTON_UNKNOWN,
@@ -98,6 +101,12 @@
     self.view.backgroundColor = CHROME_COLOR;
 
     [self addTapRecognizersToAllButtons];
+    
+    UILongPressGestureRecognizer *longPressRecognizer =
+    [[UILongPressGestureRecognizer alloc] initWithTarget: self
+                                                  action: 
@selector(saveButtonLongPressed:)];
+    longPressRecognizer.minimumPressDuration = 0.5f;
+    [self.saveButton addGestureRecognizer:longPressRecognizer];
 }
 
 -(void)addTapRecognizersToAllButtons
@@ -111,19 +120,21 @@
 
 #pragma mark Bottom bar button methods
 
-- (void)buttonPushed:(UITapGestureRecognizer *)sender
+- (void)buttonPushed:(UITapGestureRecognizer *)recognizer
 {
-    // If the tapped item was a button, first animate it briefly, then perform 
action.
-    if([sender.view isKindOfClass:[WikiGlyphButton class]]){
-        WikiGlyphButton *button = (WikiGlyphButton *)sender.view;
-        if (!button.enabled)return;
-        CGFloat animationScale = 1.25f;
-        [button.label animateAndRewindXF: 
CATransform3DMakeScale(animationScale, animationScale, 1.0f)
-                              afterDelay: 0.0
-                                duration: 0.06f
-                                    then: ^{
-                                        [self performActionForButton:button];
-                                    }];
+    if (recognizer.state == UIGestureRecognizerStateEnded) {
+        // If the tapped item was a button, first animate it briefly, then 
perform action.
+        if([recognizer.view isKindOfClass:[WikiGlyphButton class]]){
+            WikiGlyphButton *button = (WikiGlyphButton *)recognizer.view;
+            if (!button.enabled)return;
+            CGFloat animationScale = 1.25f;
+            [button.label animateAndRewindXF: 
CATransform3DMakeScale(animationScale, animationScale, 1.0f)
+                                  afterDelay: 0.0
+                                    duration: 0.06f
+                                        then: ^{
+                                            [self 
performActionForButton:button];
+                                        }];
+        }
     }
 }
 
@@ -145,6 +156,15 @@
             break;
         default:
             break;
+    }
+}
+
+-(void)saveButtonLongPressed:(UILongPressGestureRecognizer *)recognizer
+{
+    if (recognizer.state == UIGestureRecognizerStateBegan){
+        [self performModalSequeWithID: @"modal_segue_show_saved_pages"
+                      transitionStyle: UIModalTransitionStyleCoverVertical
+                                block: nil];
     }
 }
 
@@ -211,6 +231,8 @@
 
         WebViewController *webVC = [NAV 
searchNavStackForViewControllerOfClass:[WebViewController class]];
 
+        [webVC showAlert:history.article.titleObj.text type:ALERT_TYPE_BOTTOM 
duration:0.8];
+
         [webVC navigateToPage: history.article.titleObj
                       domain: history.article.domain
              discoveryMethod: DISCOVERY_METHOD_BACKFORWARD
@@ -226,6 +248,8 @@
 
         WebViewController *webVC = [NAV 
searchNavStackForViewControllerOfClass:[WebViewController class]];
 
+        [webVC showAlert:history.article.titleObj.text type:ALERT_TYPE_BOTTOM 
duration:0.8];
+
         [webVC navigateToPage: history.article.titleObj
                       domain: history.article.domain
              discoveryMethod: DISCOVERY_METHOD_BACKFORWARD
diff --git a/wikipedia/View 
Controllers/Navigation/Primary/PrimaryMenuViewController.m b/wikipedia/View 
Controllers/Navigation/Primary/PrimaryMenuViewController.m
index 0a1ae7a..7e1f577 100644
--- a/wikipedia/View Controllers/Navigation/Primary/PrimaryMenuViewController.m
+++ b/wikipedia/View Controllers/Navigation/Primary/PrimaryMenuViewController.m
@@ -82,7 +82,7 @@
 
     [self addTableHeaderView];
     
-    [self.moreButton addGestureRecognizer:[[UITapGestureRecognizer alloc] 
initWithTarget:self action:@selector(moreButtonTapped)]];
+    [self.moreButton addGestureRecognizer:[[UITapGestureRecognizer alloc] 
initWithTarget:self action:@selector(moreButtonTapped:)]];
 }
 
 -(void)addTableHeaderView
@@ -243,9 +243,11 @@
     [self animateView:cell thenPerformActionForItem:tagNumber.integerValue];
 }
 
--(void)moreButtonTapped
+-(void)moreButtonTapped:(UITapGestureRecognizer *)recognizer
 {
-    [self animateView:self.moreButton 
thenPerformActionForItem:PRIMARY_MENU_ITEM_MORE];
+    if (recognizer.state == UIGestureRecognizerStateEnded) {
+        [self animateView:self.moreButton 
thenPerformActionForItem:PRIMARY_MENU_ITEM_MORE];
+    }
 }
 
 -(void)animateView:(UIView *)view 
thenPerformActionForItem:(PrimaryMenuItemTag)tag
@@ -271,13 +273,13 @@
         }
             break;
         case PRIMARY_MENU_ITEM_RANDOM: {
-            //[self showAlert:MWLocalizedString(@"fetching-random-article", 
nil)];
+            //[self showAlert:MWLocalizedString(@"fetching-random-article", 
nil) type:ALERT_TYPE_TOP duration:-1];
             [self fetchRandomArticle];
             [self popModal];
         }
             break;
         case PRIMARY_MENU_ITEM_TODAY: {
-            //[self showAlert:MWLocalizedString(@"fetching-today-article", 
nil)];
+            //[self showAlert:MWLocalizedString(@"fetching-today-article", 
nil) type:ALERT_TYPE_TOP duration:-1];
             [NAV loadTodaysArticle];
             [self popModal];
         }
@@ -328,7 +330,7 @@
                                                  } cancelledBlock: ^(NSError 
*errorCancel) {
                                                     [self fadeAlert];
                                                  } errorBlock: ^(NSError 
*error) {
-                                                    [self 
showAlert:error.localizedDescription];
+                                                    [self 
showAlert:error.localizedDescription type:ALERT_TYPE_TOP duration:-1];
                                                  }];
 
     [[QueuesSingleton sharedInstance].randomArticleQ 
addOperation:downloadTitlesForRandomArticlesOp];
diff --git a/wikipedia/View 
Controllers/Navigation/Secondary/SecondaryMenuViewController.m b/wikipedia/View 
Controllers/Navigation/Secondary/SecondaryMenuViewController.m
index cc4b487..9c2cef2 100644
--- a/wikipedia/View 
Controllers/Navigation/Secondary/SecondaryMenuViewController.m
+++ b/wikipedia/View 
Controllers/Navigation/Secondary/SecondaryMenuViewController.m
@@ -689,8 +689,7 @@
 
     NSDictionary *selectedLangInfo = [notification userInfo];
     
-    [self showAlert:MWLocalizedString(@"main-menu-language-selection-saved", 
nil)];
-    [self fadeAlert];
+    [self showAlert:MWLocalizedString(@"main-menu-language-selection-saved", 
nil) type:ALERT_TYPE_TOP duration:1];
     
     [self switchPreferredLanguageToId:selectedLangInfo[@"code"] 
name:selectedLangInfo[@"name"]];
     
diff --git a/wikipedia/View Controllers/Navigation/Top/TopMenuContainerView.m 
b/wikipedia/View Controllers/Navigation/Top/TopMenuContainerView.m
index ec45490..3970613 100644
--- a/wikipedia/View Controllers/Navigation/Top/TopMenuContainerView.m
+++ b/wikipedia/View Controllers/Navigation/Top/TopMenuContainerView.m
@@ -2,6 +2,7 @@
 //  Copyright (c) 2013 Wikimedia Foundation. Provided under MIT-style license; 
please copy and modify!
 
 #import "TopMenuContainerView.h"
+#import "Defines.h"
 
 @implementation TopMenuContainerView
 
@@ -21,12 +22,13 @@
 }
 
 - (void)drawRect:(CGRect)rect {
+    [super drawRect:rect];
     if (!self.showBottomBorder) return;
     CGContextRef context = UIGraphicsGetCurrentContext();
     CGContextMoveToPoint(context, CGRectGetMinX(rect), CGRectGetMaxY(rect));
     CGContextAddLineToPoint(context, CGRectGetMaxX(rect), CGRectGetMaxY(rect));
-    CGContextSetStrokeColorWithColor(context, [[UIColor lightGrayColor] 
CGColor] );
-    CGContextSetLineWidth(context, 1.0f / [UIScreen mainScreen].scale);
+    CGContextSetStrokeColorWithColor(context, CHROME_OUTLINE_COLOR.CGColor);
+    CGContextSetLineWidth(context, CHROME_OUTLINE_WIDTH);
     CGContextStrokePath(context);
 }
 
diff --git a/wikipedia/View Controllers/Navigation/Top/TopMenuViewController.m 
b/wikipedia/View Controllers/Navigation/Top/TopMenuViewController.m
index 1683588..d02d89a 100644
--- a/wikipedia/View Controllers/Navigation/Top/TopMenuViewController.m
+++ b/wikipedia/View Controllers/Navigation/Top/TopMenuViewController.m
@@ -493,6 +493,9 @@
 {
     UIView *tappedView = nil;
     if([sender isKindOfClass:[UIGestureRecognizer class]]){
+        // We only want to take action when the tap recognizer is in Ended 
state.
+        if (((UIGestureRecognizer *)sender).state != 
UIGestureRecognizerStateEnded) return;
+
         tappedView = ((UIGestureRecognizer *)sender).view;
     }else{
         tappedView = sender;
diff --git a/wikipedia/View Controllers/Nearby/NearbyViewController.m 
b/wikipedia/View Controllers/Nearby/NearbyViewController.m
index a1aca85..649cd61 100644
--- a/wikipedia/View Controllers/Nearby/NearbyViewController.m
+++ b/wikipedia/View Controllers/Nearby/NearbyViewController.m
@@ -231,7 +231,7 @@
             break;
     }
     
-    [self showAlert:errorMessage];
+    [self showAlert:errorMessage type:ALERT_TYPE_TOP duration:-1];
 }
 
 -(void)downloadData
@@ -239,12 +239,12 @@
     CLLocationDegrees lat1 = self.deviceLocation.coordinate.latitude;
     CLLocationDegrees long1 = self.deviceLocation.coordinate.longitude;
 
-    [self showAlert:MWLocalizedString(@"nearby-loading", nil)];
+    [self showAlert:MWLocalizedString(@"nearby-loading", nil) 
type:ALERT_TYPE_TOP duration:-1];
     
     NearbyOp *nearbyOp = [[NearbyOp alloc] initWithLatitude:lat1 
longitude:long1 completionBlock:^(NSArray *result){
         
         [[NSOperationQueue mainQueue] addOperationWithBlock: ^ {
-            //[self showAlert:MWLocalizedString(@"nearby-loaded", nil)];
+            //[self showAlert:MWLocalizedString(@"nearby-loaded", nil) 
type:ALERT_TYPE_TOP duration:-1];
             [self fadeAlert];
             
             self.nearbyDataArray = @[result];
@@ -260,11 +260,11 @@
         
     } cancelledBlock:^(NSError *error){
         NSLog(@"nearby op error = %@", error);
-        //[self showAlert:error.localizedDescription];
+        //[self showAlert:error.localizedDescription type:ALERT_TYPE_TOP 
duration:-1];
 
     } errorBlock:^(NSError *error){
         NSLog(@"nearby op error = %@", error);
-        [self showAlert:error.localizedDescription];
+        [self showAlert:error.localizedDescription type:ALERT_TYPE_TOP 
duration:-1];
     }];
    
     [[QueuesSingleton sharedInstance].nearbyQ cancelAllOperations];
diff --git a/wikipedia/View Controllers/Onboarding/OnboardingViewController.m 
b/wikipedia/View Controllers/Onboarding/OnboardingViewController.m
index 117d508..94904b2 100644
--- a/wikipedia/View Controllers/Onboarding/OnboardingViewController.m
+++ b/wikipedia/View Controllers/Onboarding/OnboardingViewController.m
@@ -117,11 +117,11 @@
     self.loginButton.layer.cornerRadius = cornerRadius;
     self.createAccountButton.layer.cornerRadius = cornerRadius;
     
-    [self.skipButton addGestureRecognizer:[[UITapGestureRecognizer alloc] 
initWithTarget:self action:@selector(skipButtonTapped)]];
+    [self.skipButton addGestureRecognizer:[[UITapGestureRecognizer alloc] 
initWithTarget:self action:@selector(skipButtonTapped:)]];
 
-    [self.createAccountButton addGestureRecognizer:[[UITapGestureRecognizer 
alloc] initWithTarget:self action:@selector(createAccountButtonTapped)]];
+    [self.createAccountButton addGestureRecognizer:[[UITapGestureRecognizer 
alloc] initWithTarget:self action:@selector(createAccountButtonTapped:)]];
 
-    [self.loginButton addGestureRecognizer:[[UITapGestureRecognizer alloc] 
initWithTarget:self action:@selector(loginButtonTapped)]];
+    [self.loginButton addGestureRecognizer:[[UITapGestureRecognizer alloc] 
initWithTarget:self action:@selector(loginButtonTapped:)]];
 
     self.createAccountButton.text = 
MWLocalizedString(@"onboarding-create-account", nil);
     self.skipButton.text = MWLocalizedString(@"onboarding-skip", nil);
@@ -231,23 +231,29 @@
                         substitutionAttributes: 
@[@{NSForegroundColorAttributeName : WMF_COLOR_BLUE}]];
 }
 
--(void)createAccountButtonTapped
+-(void)createAccountButtonTapped:(UITapGestureRecognizer *)recognizer
 {
-    [self performModalSequeWithID: @"modal_segue_show_create_account"
-                  transitionStyle: UIModalTransitionStyleCoverVertical
-                            block: nil];
+    if (recognizer.state == UIGestureRecognizerStateEnded) {
+        [self performModalSequeWithID: @"modal_segue_show_create_account"
+                      transitionStyle: UIModalTransitionStyleCoverVertical
+                                block: nil];
+    }
 }
 
--(void)loginButtonTapped
+-(void)loginButtonTapped:(UITapGestureRecognizer *)recognizer
 {
-    [self performModalSequeWithID: @"modal_segue_show_login"
-                  transitionStyle: UIModalTransitionStyleCoverVertical
-                            block: nil];
+    if (recognizer.state == UIGestureRecognizerStateEnded) {
+        [self performModalSequeWithID: @"modal_segue_show_login"
+                      transitionStyle: UIModalTransitionStyleCoverVertical
+                                block: nil];
+    }
 }
 
--(void)skipButtonTapped
+-(void)skipButtonTapped:(UITapGestureRecognizer *)recognizer
 {
-    [self hide];
+    if (recognizer.state == UIGestureRecognizerStateEnded) {
+        [self hide];
+    }
 }
 
 - (void)didReceiveMemoryWarning
diff --git a/wikipedia/View Controllers/PageHistory/PageHistoryViewController.m 
b/wikipedia/View Controllers/PageHistory/PageHistoryViewController.m
index df077bb..75915f1 100644
--- a/wikipedia/View Controllers/PageHistory/PageHistoryViewController.m
+++ b/wikipedia/View Controllers/PageHistory/PageHistoryViewController.m
@@ -103,7 +103,7 @@
     
     __weak PageHistoryViewController *weakSelf = self;
 
-    [self showAlert:MWLocalizedString(@"page-history-downloading", nil)];
+    [self showAlert:MWLocalizedString(@"page-history-downloading", nil) 
type:ALERT_TYPE_TOP duration:-1];
 
     PageHistoryOp *pageHistoryOp =
     [[PageHistoryOp alloc] initWithDomain: [SessionSingleton 
sharedInstance].currentArticleDomain
@@ -121,7 +121,7 @@
                                [self fadeAlert];
                            }
                                errorBlock: ^(NSError *error){
-                                   [self showAlert:error.localizedDescription];
+                                   [self showAlert:error.localizedDescription 
type:ALERT_TYPE_TOP duration:-1];
                                }];
     pageHistoryOp.delegate = self;
     [[QueuesSingleton sharedInstance].pageHistoryQ addOperation:pageHistoryOp];
diff --git a/wikipedia/View Controllers/Preview/PreviewAndSaveViewController.m 
b/wikipedia/View Controllers/Preview/PreviewAndSaveViewController.m
index 14d15cb..7d98d9f 100644
--- a/wikipedia/View Controllers/Preview/PreviewAndSaveViewController.m
+++ b/wikipedia/View Controllers/Preview/PreviewAndSaveViewController.m
@@ -158,7 +158,7 @@
     
     self.previewLabel.text = 
MWLocalizedString(@"navbar-title-mode-edit-wikitext-preview", nil);
     
-    [self.previewLabel addGestureRecognizer:[[UITapGestureRecognizer alloc] 
initWithTarget:self action:@selector(previewLabelTapped)]];
+    [self.previewLabel addGestureRecognizer:[[UITapGestureRecognizer alloc] 
initWithTarget:self action:@selector(previewLabelTapped:)]];
     
     //self.saveAutomaticallyIfSignedIn = NO;
     
@@ -198,9 +198,11 @@
     [self preview];
 }
 
--(void)previewLabelTapped
+-(void)previewLabelTapped:(UITapGestureRecognizer *)recognizer
 {
-    [self.scrollView scrollSubViewToTop:self.previewLabel animated:YES];
+    if (recognizer.state == UIGestureRecognizerStateEnded) {
+        [self.scrollView scrollSubViewToTop:self.previewLabel animated:YES];
+    }
 }
 
 -(void)dealloc
@@ -298,36 +300,39 @@
 
 -(void)buttonTapped:(UIGestureRecognizer *)recognizer
 {
-    MenuButton *tappedButton = (MenuButton *)recognizer.view;
-    
-    NSString *summaryKey;
-    switch (tappedButton.tag) {
-        case CANNED_SUMMARY_TYPOS:
-            summaryKey = @"typo";
-            break;
-        case CANNED_SUMMARY_GRAMMAR:
-            summaryKey = @"grammar";
-            break;
-        case CANNED_SUMMARY_LINKS:
-            summaryKey = @"links";
-            break;
-        case CANNED_SUMMARY_OTHER:
-            summaryKey = @"other";
-            break;
-        default:
-            NSLog(@"unrecognized button");
-    }
-    [self.funnel logEditSummaryTap:summaryKey];
-
-    switch (tappedButton.tag) {
-        case CANNED_SUMMARY_OTHER:
-            [self showSummaryOverlay];
-            break;
-            
-        default:
-            tappedButton.enabled = !tappedButton.enabled;
-            
-            break;
+    if (recognizer.state == UIGestureRecognizerStateEnded) {
+        
+        MenuButton *tappedButton = (MenuButton *)recognizer.view;
+        
+        NSString *summaryKey;
+        switch (tappedButton.tag) {
+            case CANNED_SUMMARY_TYPOS:
+                summaryKey = @"typo";
+                break;
+            case CANNED_SUMMARY_GRAMMAR:
+                summaryKey = @"grammar";
+                break;
+            case CANNED_SUMMARY_LINKS:
+                summaryKey = @"links";
+                break;
+            case CANNED_SUMMARY_OTHER:
+                summaryKey = @"other";
+                break;
+            default:
+                NSLog(@"unrecognized button");
+        }
+        [self.funnel logEditSummaryTap:summaryKey];
+        
+        switch (tappedButton.tag) {
+            case CANNED_SUMMARY_OTHER:
+                [self showSummaryOverlay];
+                break;
+                
+            default:
+                tappedButton.enabled = !tappedButton.enabled;
+                
+                break;
+        }
     }
 }
 
@@ -390,14 +395,16 @@
 
 -(void)licenseLabelTapped:(UIGestureRecognizer *)recognizer
 {
-    // Call if user taps the blue "Log In" text in the CC text.
-    //self.saveAutomaticallyIfSignedIn = YES;
-    [self performModalSequeWithID: @"modal_segue_show_login"
-                  transitionStyle: UIModalTransitionStyleCoverVertical
-                            block: ^(LoginViewController *loginVC){
-                                loginVC.funnel = [[LoginFunnel alloc] init];
-                                [loginVC.funnel 
logStartFromEdit:self.funnel.editSessionToken];
-                            }];
+    if (recognizer.state == UIGestureRecognizerStateEnded) {        
+        // Call if user taps the blue "Log In" text in the CC text.
+        //self.saveAutomaticallyIfSignedIn = YES;
+        [self performModalSequeWithID: @"modal_segue_show_login"
+                      transitionStyle: UIModalTransitionStyleCoverVertical
+                                block: ^(LoginViewController *loginVC){
+                                    loginVC.funnel = [[LoginFunnel alloc] 
init];
+                                    [loginVC.funnel 
logStartFromEdit:self.funnel.editSessionToken];
+                                }];
+    }
 }
 
 -(void)highlightCaptchaSubmitButton:(BOOL)highlight
@@ -437,7 +444,7 @@
     if (isAleadyPreviewing) return;
     isAleadyPreviewing = YES;
 
-    [self showAlert:MWLocalizedString(@"wikitext-preview-changes", nil)];
+    [self showAlert:MWLocalizedString(@"wikitext-preview-changes", nil) 
type:ALERT_TYPE_TOP duration:-1];
     Section *section = (Section *)[articleDataContext_.mainContext 
objectWithID:self.sectionID];
 
     MWLanguageInfo *languageInfo = [MWLanguageInfo 
languageInfoForCode:section.article.domain];
@@ -470,13 +477,13 @@
         
     } cancelledBlock:^(NSError *error){
         NSString *errorMsg = error.localizedDescription;
-        [self showAlert:errorMsg];
+        [self showAlert:errorMsg type:ALERT_TYPE_TOP duration:-1];
         isAleadyPreviewing = NO;
         
     } errorBlock:^(NSError *error){
         NSString *errorMsg = error.localizedDescription;
         
-        [self showAlert:errorMsg];
+        [self showAlert:errorMsg type:ALERT_TYPE_TOP duration:-1];
         
         isAleadyPreviewing = NO;
     }];
@@ -518,7 +525,7 @@
 
     ArticleDataContextSingleton *articleDataContext_ = 
[ArticleDataContextSingleton sharedInstance];
 
-    [self showAlert:MWLocalizedString(@"wikitext-upload-save", nil)];
+    [self showAlert:MWLocalizedString(@"wikitext-upload-save", nil) 
type:ALERT_TYPE_TOP duration:-1];
     Section *section = (Section *)[articleDataContext_.mainContext 
objectWithID:self.sectionID];
 
     NSManagedObjectID *articleID = section.article.objectID;
@@ -546,13 +553,13 @@
 
     } cancelledBlock:^(NSError *error){
         NSString *errorMsg = error.localizedDescription;
-        [self showAlert:errorMsg];
+        [self showAlert:errorMsg type:ALERT_TYPE_TOP duration:-1];
         isAleadySaving = NO;
         
     } errorBlock:^(NSError *error){
         NSString *errorMsg = error.localizedDescription;
         
-        [self showAlert:errorMsg];
+        [self showAlert:errorMsg type:ALERT_TYPE_TOP duration:-1];
 
         switch (error.code) {
             case WIKITEXT_UPLOAD_ERROR_NEEDS_CAPTCHA:
@@ -577,7 +584,7 @@
                                                                                
 withObject: nil
                                                                                
 afterDelay: 0.4f];
 
-                                [self.captchaViewController 
showAlert:errorMsg];
+                                [self.captchaViewController showAlert:errorMsg 
type:ALERT_TYPE_TOP duration:-1];
 
                                 
self.captchaViewController.captchaImageView.image = nil;
 
@@ -693,7 +700,7 @@
                             
                         } errorBlock: ^(NSError *error){
                             
-                            [self showAlert:error.localizedDescription];
+                            [self showAlert:error.localizedDescription 
type:ALERT_TYPE_TOP duration:-1];
                             isAleadySaving = NO;
                             
                         }];
diff --git a/wikipedia/View Controllers/Preview/PreviewLicenseView.m 
b/wikipedia/View Controllers/Preview/PreviewLicenseView.m
index 7824bf4..1153bef 100644
--- a/wikipedia/View Controllers/Preview/PreviewLicenseView.m
+++ b/wikipedia/View Controllers/Preview/PreviewLicenseView.m
@@ -102,10 +102,11 @@
      ];
 }
 
--(void)termsLicenseLabelTapped:(UILabel *)label
+-(void)termsLicenseLabelTapped:(UITapGestureRecognizer *)recognizer
 {
-    // @todo on iPad position this against the view
-    [self.sheet showInView:ROOT.view];
+    if (recognizer.state == UIGestureRecognizerStateEnded) {
+        [self.sheet showInView:ROOT.view];
+    }
 }
 
 -(UIActionSheet *)sheet
diff --git a/wikipedia/View Controllers/References/ReferencesVC.m 
b/wikipedia/View Controllers/References/ReferencesVC.m
index afb76b1..daab394 100644
--- a/wikipedia/View Controllers/References/ReferencesVC.m
+++ b/wikipedia/View Controllers/References/ReferencesVC.m
@@ -141,13 +141,13 @@
     self.prevButton.hidden = YES;
     [self.topContainerView addSubview:self.prevButton];
 
-    UITapGestureRecognizer *prevTap = [[UITapGestureRecognizer alloc] 
initWithTarget:self action:@selector(prevButtonTap)];
+    UITapGestureRecognizer *prevTap = [[UITapGestureRecognizer alloc] 
initWithTarget:self action:@selector(prevButtonTap:)];
     [self.prevButton addGestureRecognizer:prevTap];
 
-    UITapGestureRecognizer *nextTap = [[UITapGestureRecognizer alloc] 
initWithTarget:self action:@selector(nextButtonTap)];
+    UITapGestureRecognizer *nextTap = [[UITapGestureRecognizer alloc] 
initWithTarget:self action:@selector(nextButtonTap:)];
     [self.nextButton addGestureRecognizer:nextTap];
 
-    UITapGestureRecognizer *xTap = [[UITapGestureRecognizer alloc] 
initWithTarget:self action:@selector(xButtonTap)];
+    UITapGestureRecognizer *xTap = [[UITapGestureRecognizer alloc] 
initWithTarget:self action:@selector(xButtonTap:)];
     [self.xButton addGestureRecognizer:xTap];
 
     //self.nextButton.layer.borderWidth = 1;
@@ -181,9 +181,11 @@
     //self.topContainerView.layer.borderColor = [UIColor whiteColor].CGColor;
 }
 
--(void)xButtonTap
+-(void)xButtonTap:(UITapGestureRecognizer *)recognizer
 {
-    [self.webVC referencesHide];
+    if (recognizer.state == UIGestureRecognizerStateEnded) {
+        [self.webVC referencesHide];
+    }
 }
 
 -(void)setupConstraints
@@ -459,40 +461,46 @@
     }
 }
 
--(void)prevButtonTap
+-(void)prevButtonTap:(UITapGestureRecognizer *)recognizer
 {
-    if (!self.prevButton.enabled) return;
-
-    BOOL isRTL = [WikipediaAppUtils isDeviceLanguageRTL];
-
-    UIPageViewControllerNavigationDirection dir = isRTL
+    if (recognizer.state == UIGestureRecognizerStateEnded) {
+        
+        if (!self.prevButton.enabled) return;
+        
+        BOOL isRTL = [WikipediaAppUtils isDeviceLanguageRTL];
+        
+        UIPageViewControllerNavigationDirection dir = isRTL
         ?
         UIPageViewControllerNavigationDirectionForward
         :
         UIPageViewControllerNavigationDirectionReverse;
-    
-    [self setViewControllers: @[[self 
viewControllerAtIndex:(--self.topPageControl.currentPage)]]
-                   direction: dir
-                    animated: YES
-                  completion: nil];
+        
+        [self setViewControllers: @[[self 
viewControllerAtIndex:(--self.topPageControl.currentPage)]]
+                       direction: dir
+                        animated: YES
+                      completion: nil];
+    }
 }
 
--(void)nextButtonTap
+-(void)nextButtonTap:(UITapGestureRecognizer *)recognizer
 {
-    if (!self.nextButton.enabled) return;
-
-    BOOL isRTL = [WikipediaAppUtils isDeviceLanguageRTL];
-
-    UIPageViewControllerNavigationDirection dir = isRTL
+    if (recognizer.state == UIGestureRecognizerStateEnded) {
+        
+        if (!self.nextButton.enabled) return;
+        
+        BOOL isRTL = [WikipediaAppUtils isDeviceLanguageRTL];
+        
+        UIPageViewControllerNavigationDirection dir = isRTL
         ?
         UIPageViewControllerNavigationDirectionReverse
         :
         UIPageViewControllerNavigationDirectionForward;
-
-    [self setViewControllers: @[[self 
viewControllerAtIndex:(++self.topPageControl.currentPage)]]
-                   direction: dir
-                    animated: YES
-                  completion: nil];
+        
+        [self setViewControllers: @[[self 
viewControllerAtIndex:(++self.topPageControl.currentPage)]]
+                       direction: dir
+                        animated: YES
+                      completion: nil];
+    }
 }
 
 /*
diff --git a/wikipedia/View Controllers/SavedPages/SavedPagesViewController.m 
b/wikipedia/View Controllers/SavedPages/SavedPagesViewController.m
index d8465b9..f9c2e3a 100644
--- a/wikipedia/View Controllers/SavedPages/SavedPagesViewController.m
+++ b/wikipedia/View Controllers/SavedPages/SavedPagesViewController.m
@@ -236,7 +236,7 @@
 
     // Set thumbnail placeholder
 //TODO: don't load thumb from file every time in loop if no image found. fix 
here and in search
-    cell.imageView.image = [UIImage imageNamed:@"logo-placeholder-search.png"];
+    cell.imageView.image = [UIImage imageNamed:@"logo-placeholder-saved.png"];
     cell.useField = NO;
 
     //if (!thumbURL){
diff --git a/wikipedia/View Controllers/SearchResults/SearchResultsController.m 
b/wikipedia/View Controllers/SearchResults/SearchResultsController.m
index dd2b099..cfe10db 100644
--- a/wikipedia/View Controllers/SearchResults/SearchResultsController.m
+++ b/wikipedia/View Controllers/SearchResults/SearchResultsController.m
@@ -135,7 +135,7 @@
     [self clearSearchResults];
     
     // Show "Searching..." message.
-    [self showAlert:MWLocalizedString(@"search-searching", nil)];
+    [self showAlert:MWLocalizedString(@"search-searching", nil) 
type:ALERT_TYPE_TOP duration:-1];
     
     // Search for titles op.
     SearchOp *searchOp =
@@ -160,7 +160,7 @@
                              
                          } errorBlock: ^(NSError *error){
                              
-                             [self showAlert:error.localizedDescription];
+                             [self showAlert:error.localizedDescription 
type:ALERT_TYPE_TOP duration:-1];
                              
                          }];
     
diff --git a/wikipedia/View 
Controllers/SectionEditor/SectionEditorViewController.m b/wikipedia/View 
Controllers/SectionEditor/SectionEditorViewController.m
index 055dfbe..cd8b677 100644
--- a/wikipedia/View Controllers/SectionEditor/SectionEditorViewController.m
+++ b/wikipedia/View Controllers/SectionEditor/SectionEditorViewController.m
@@ -144,8 +144,7 @@
     switch (tappedItem.tag) {
         case NAVBAR_BUTTON_NEXT:
             if (![self changesMade]) {
-                [self 
showAlert:MWLocalizedString(@"wikitext-preview-changes-none", nil)];
-                [self fadeAlert];
+                [self 
showAlert:MWLocalizedString(@"wikitext-preview-changes-none", nil) 
type:ALERT_TYPE_TOP duration:1];
                 break;
             }
             [self preview];
@@ -173,7 +172,7 @@
 
 -(void)loadLatestWikiTextForSectionFromServer
 {
-    [self showAlert:MWLocalizedString(@"wikitext-downloading", nil)];
+    [self showAlert:MWLocalizedString(@"wikitext-downloading", nil) 
type:ALERT_TYPE_TOP duration:-1];
     Section *section = (Section *)[articleDataContext_.mainContext 
objectWithID:self.sectionID];
     NSString *domain = section.article.domain;
     self.protectionStatus = section.article.protectionStatus;
@@ -198,11 +197,9 @@
                 } else {
                     msg = MWLocalizedString(@"page_protected_other", nil);
                 }
-                [self showAlert:msg];
-                
-                [self fadeAlert];
+                [self showAlert:msg type:ALERT_TYPE_TOP duration:1];
             } else {
-                [self 
showAlert:MWLocalizedString(@"wikitext-download-success", nil)];
+                //[self 
showAlert:MWLocalizedString(@"wikitext-download-success", nil) 
type:ALERT_TYPE_TOP duration:1];
                 [self fadeAlert];
             }
             self.unmodifiedWikiText = revision;
@@ -220,11 +217,11 @@
         
     } cancelledBlock:^(NSError *error){
         NSString *errorMsg = error.localizedDescription;
-        [self showAlert:errorMsg];
+        [self showAlert:errorMsg type:ALERT_TYPE_TOP duration:-1];
         
     } errorBlock:^(NSError *error){
         NSString *errorMsg = error.localizedDescription;
-        [self showAlert:errorMsg];
+        [self showAlert:errorMsg type:ALERT_TYPE_TOP duration:-1];
         
     }];
 
diff --git a/wikipedia/View Controllers/TableOfContents/TOCViewController.m 
b/wikipedia/View Controllers/TableOfContents/TOCViewController.m
index d0838d0..eb8cb1e 100644
--- a/wikipedia/View Controllers/TableOfContents/TOCViewController.m
+++ b/wikipedia/View Controllers/TableOfContents/TOCViewController.m
@@ -242,11 +242,14 @@
 {
     // If tapped, go to section/image selection.
     if ([sender isMemberOfClass:[UITapGestureRecognizer class]]) {
-        [self deSelectAllCells];
-        [self unHighlightAllCells];
-        [self navigateToSelection: sender
-                         duration: TOC_SELECTION_SCROLL_DURATION];
-        [self.funnel logClick];
+        // We only want to take action when the tap recognizer is in Ended 
state.
+        if (((UITapGestureRecognizer *)sender).state == 
UIGestureRecognizerStateEnded){
+            [self deSelectAllCells];
+            [self unHighlightAllCells];
+            [self navigateToSelection: sender
+                             duration: TOC_SELECTION_SCROLL_DURATION];
+            [self.funnel logClick];
+        }
     }
 }
 
diff --git a/wikipedia/View Controllers/WebView/WebViewController.h 
b/wikipedia/View Controllers/WebView/WebViewController.h
index b085e0a..fb00508 100644
--- a/wikipedia/View Controllers/WebView/WebViewController.h
+++ b/wikipedia/View Controllers/WebView/WebViewController.h
@@ -14,6 +14,8 @@
 @property (nonatomic) BOOL referencesHidden;
 @property (nonatomic) BOOL scrollingToTop;
 
+@property (weak, nonatomic) BottomMenuViewController *bottomMenuViewController;
+
 -(void)referencesShow:(NSDictionary *)payload;
 -(void)referencesHide;
 
diff --git a/wikipedia/View Controllers/WebView/WebViewController.m 
b/wikipedia/View Controllers/WebView/WebViewController.m
index 1e393ad..c96e4bc 100644
--- a/wikipedia/View Controllers/WebView/WebViewController.m
+++ b/wikipedia/View Controllers/WebView/WebViewController.m
@@ -57,6 +57,7 @@
 #import "WikiGlyphButton.h"
 #import "WikiGlyphLabel.h"
 #import "WikiGlyph_Chars_iOS.h"
+#import "NSString+FormattedAttributedString.h"
 
 //#import "UIView+Debugging.h"
 
@@ -115,7 +116,6 @@
 
 @property (nonatomic) BOOL unsafeToToggleTOC;
 
-@property (weak, nonatomic) BottomMenuViewController *bottomMenuViewController;
 @property (strong, nonatomic) ReferencesVC *referencesVC;
 @property (weak, nonatomic) IBOutlet UIView *referencesContainerView;
 
@@ -291,14 +291,14 @@
     //self.referencesContainerView.layer.borderColor = [UIColor 
redColor].CGColor;
 }
 
--(void)showAlert:(NSString *)alertText
+-(void)showAlert:(id)alertText type:(AlertType)type duration:(CGFloat)duration
 {
     if ([self tocDrawerIsOpen]) return;
 
     // Don't show alerts if onboarding onscreen.
     if ([self shouldShowOnboarding]) return;
 
-    [super showAlert:alertText];
+    [super showAlert:alertText type:type duration:duration];
 }
 
 -(void)viewDidAppear:(BOOL)animated
@@ -1086,9 +1086,10 @@
         Article *article = (Article *)[articleDataContext_.mainContext 
objectWithID:articleID];
         if (!article) return;
         if (article.saved.count == 0) {
-            // Save!
-            [self showAlert:MWLocalizedString(@"share-menu-page-saved", nil)];
-            [self fadeAlert];
+            // Show alert.
+            [self showPageSavedAlertMessageForTitle:article.title];
+
+            // Actually perform the save.
             Saved *saved = [NSEntityDescription 
insertNewObjectForEntityForName:@"Saved" 
inManagedObjectContext:articleDataContext_.mainContext];
             saved.dateSaved = [NSDate date];
             [article addSavedObject:saved];
@@ -1098,11 +1099,51 @@
             for (id obj in article.saved.copy) {
                 [articleDataContext_.mainContext deleteObject:obj];
             }
+            [self fadeAlert];
         }
         NSError *error = nil;
         [articleDataContext_.mainContext save:&error];
         NSLog(@"SAVE PAGE ERROR = %@", error);
     }];
+}
+
+-(void)showPageSavedAlertMessageForTitle:(NSString *)title
+{
+    // First show saved message.
+    NSString *savedMessage = MWLocalizedString(@"share-menu-page-saved", nil);
+    
+    NSMutableAttributedString *attributedSavedMessage =
+    [savedMessage attributedStringWithAttributes: @{}
+                             substitutionStrings: @[title]
+                          substitutionAttributes: @[@{NSFontAttributeName: 
[UIFont italicSystemFontOfSize:ALERT_FONT_SIZE]}]].mutableCopy;
+    
+    CGFloat duration = 2.0;
+    BOOL AccessSavedPagesMessageShown = [[NSUserDefaults standardUserDefaults] 
boolForKey:@"AccessSavedPagesMessageShown"];
+    
+    //AccessSavedPagesMessageShown = NO;
+    
+    if (!AccessSavedPagesMessageShown) {
+        duration = -1;
+        [[NSUserDefaults standardUserDefaults] setObject:@YES 
forKey:@"AccessSavedPagesMessageShown"];
+        [[NSUserDefaults standardUserDefaults] synchronize];
+        
+        NSString *accessMessage = [NSString stringWithFormat:@"\n%@", 
MWLocalizedString(@"share-menu-page-saved-access", nil)];
+        
+        NSDictionary *d = @{
+                            NSFontAttributeName: [UIFont 
fontWithName:@"WikiFontGlyphs-iOS" size:ALERT_FONT_SIZE],
+                            NSBaselineOffsetAttributeName : @2
+                            };
+        
+        NSAttributedString *attributedAccessMessage =
+        [accessMessage attributedStringWithAttributes: @{}
+                                  substitutionStrings: @[IOS_WIKIGLYPH_W, 
IOS_WIKIGLYPH_HEART]
+                               substitutionAttributes: @[d, d]];
+        
+        
+        [attributedSavedMessage 
appendAttributedString:attributedAccessMessage];
+    }
+    
+    [self showAlert:attributedSavedMessage type:ALERT_TYPE_BOTTOM 
duration:duration];
 }
 
 #pragma mark Web view scroll offset recording
@@ -1405,7 +1446,7 @@
     [self hideKeyboard];
     
     // Show loading message
-    //[self showAlert:MWLocalizedString(@"search-loading-section-zero", nil)];
+    //[self showAlert:MWLocalizedString(@"search-loading-section-zero", nil) 
type:ALERT_TYPE_TOP duration:-1];
 
     if (invalidateCache) [self invalidateCacheForPageTitle:title 
domain:domain];
     
@@ -1490,7 +1531,7 @@
         if (article.section.count > 0 && !article.needsRefresh.boolValue) {
             [self.tocVC setTocSectionDataForSections:article.section];
             [self displayArticle:articleID mode:DISPLAY_ALL_SECTIONS];
-            //[self 
showAlert:MWLocalizedString(@"search-loading-article-loaded", nil)];
+            //[self 
showAlert:MWLocalizedString(@"search-loading-article-loaded", nil) 
type:ALERT_TYPE_TOP duration:-1];
             [self fadeAlert];
             return;
         }
@@ -1565,14 +1606,14 @@
         }];
         
         [self displayArticle:articleID mode:DISPLAY_APPEND_NON_LEAD_SECTIONS];
-        //[self showAlert:MWLocalizedString(@"search-loading-article-loaded", 
nil)];
-        [self fadeAlert];
+        //[self showAlert:MWLocalizedString(@"search-loading-article-loaded", 
nil) type:ALERT_TYPE_TOP duration:-1];
+        //[self fadeAlert];
 
     } cancelledBlock:^(NSError *error){
         [self fadeAlert];
     } errorBlock:^(NSError *error){
         NSString *errorMsg = error.localizedDescription;
-        [self showAlert:errorMsg];
+        [self showAlert:errorMsg type:ALERT_TYPE_TOP duration:-1];
     }];
 
     remainingSectionsOp.delegate = self;
@@ -1722,7 +1763,7 @@
         }];
 
         [self displayArticle:articleID mode:DISPLAY_LEAD_SECTION];
-        //[self 
showAlert:MWLocalizedString(@"search-loading-section-remaining", nil)];
+        //[self 
showAlert:MWLocalizedString(@"search-loading-section-remaining", nil) 
type:ALERT_TYPE_TOP duration:-1];
 
     } cancelledBlock:^(NSError *error){
 
@@ -1734,7 +1775,7 @@
 
     } errorBlock:^(NSError *error){
         NSString *errorMsg = error.localizedDescription;
-        [self showAlert:errorMsg];
+        [self showAlert:errorMsg type:ALERT_TYPE_TOP duration:-1];
         if (articleID) {
             // Remove the article so it doesn't get saved.
             Article *article = (Article *)[articleDataContext_.mainContext 
objectWithID:articleID];
@@ -2019,7 +2060,7 @@
                      self.zeroStatusLabel.padding = UIEdgeInsetsMake(3, 10, 3, 
10);
                      self.zeroStatusLabel.backgroundColor = [UIColor 
colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.93];
 
-                     [self showAlert:title];
+                     [self showAlert:title type:ALERT_TYPE_TOP duration:-1];
                      [NAV promptFirstTimeZeroOnWithTitleIfAppropriate:title];
                  });
              }
@@ -2045,7 +2086,7 @@
             self.zeroStatusLabel.padding = UIEdgeInsetsZero;
         });
 
-        [self showAlert:warnVerbiage];
+        [self showAlert:warnVerbiage type:ALERT_TYPE_TOP duration:-1];
         [NAV promptZeroOff];
     }
 }
@@ -2082,13 +2123,7 @@
 {
     NSString *title = [SessionSingleton sharedInstance].currentArticleTitle;
     
-    return
-    ([QueuesSingleton sharedInstance].articleRetrievalQ.operationCount == 0)
-    &&
-    (![self tocDrawerIsOpen])
-    &&
-    (title && (title.length > 0))
-    ;
+    return (![self tocDrawerIsOpen]) && (title && (title.length > 0));
 }
 
 #pragma mark Data migration
diff --git a/wikipedia/en.lproj/Localizable.strings 
b/wikipedia/en.lproj/Localizable.strings
index 7006936..1a8296a 100644
--- a/wikipedia/en.lproj/Localizable.strings
+++ b/wikipedia/en.lproj/Localizable.strings
@@ -156,8 +156,9 @@
 "credits-github-mirror" = "App mirror (GitHub)";
 "credits-external-libraries" = "External repositories";
 
-"share-menu-save-page" = "Save Article";
-"share-menu-page-saved" = "Article Saved";
+"share-menu-save-page" = "Save page";
+"share-menu-page-saved" = "$1 saved for offline reading.";
+"share-menu-page-saved-access" = "Tip: to access your saved pages, tap $1 
above or long-press $2 below.";
 
 "timestamp-just-now" = "just now";
 "timestamp-minutes" = "%d minutes ago";
diff --git a/wikipedia/qqq.lproj/Localizable.strings 
b/wikipedia/qqq.lproj/Localizable.strings
index 67f1154..ae8fc00 100644
--- a/wikipedia/qqq.lproj/Localizable.strings
+++ b/wikipedia/qqq.lproj/Localizable.strings
@@ -146,7 +146,8 @@
 "credits-github-mirror" = "Text for item linking to the app's mirrored GitHub 
repository";
 "credits-external-libraries" = "Title for area of credits page showing 
external open source libraries used by app.\n{{Identical|External}}";
 "share-menu-save-page" = "Button text for saving current page from the share 
menu";
-"share-menu-page-saved" = "Alert text shown when page saved";
+"share-menu-page-saved" = "Alert text shown when page saved for offline 
reading. $1 is the name of the page being saved";
+"share-menu-page-saved-access" = "Reminder to user of ways to access saved 
pages. $1 is the main menu W icon, $2 is the save page heart icon";
 "timestamp-just-now" = "Human-readable approximate timestamp for events in the 
last couple of minutes.\n{{Identical|Just now}}";
 "timestamp-minutes" = "Human-readable approximate timestamp for events in the 
last couple hours, expressed as minutes";
 "timestamp-hours" = "Human-readable approximate timestamp for events in the 
last couple days, expressed as hours";

-- 
To view, visit https://gerrit.wikimedia.org/r/157489
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I473b5f02319d016b7c4cf8bfd7dea9b1930dcbef
Gerrit-PatchSet: 4
Gerrit-Project: apps/ios/wikipedia
Gerrit-Branch: master
Gerrit-Owner: Mhurd <mh...@wikimedia.org>
Gerrit-Reviewer: Brion VIBBER <br...@wikimedia.org>
Gerrit-Reviewer: Chad <ch...@wikimedia.org>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to