diff --git a/media/ios/Classes/NewsBlurAppDelegate.h b/media/ios/Classes/NewsBlurAppDelegate.h index 157638a76..9bcefd645 100644 --- a/media/ios/Classes/NewsBlurAppDelegate.h +++ b/media/ios/Classes/NewsBlurAppDelegate.h @@ -293,6 +293,12 @@ - (UIView *)makeFeedTitleGradient:(NSDictionary *)feed withRect:(CGRect)rect; - (UIView *)makeFeedTitle:(NSDictionary *)feed; - (UIButton *)makeRightFeedTitle:(NSDictionary *)feed; + +- (void)toggleAuthorClassifier:(NSString *)author feedId:(NSString *)feedId; +- (void)toggleTagClassifier:(NSString *)tag feedId:(NSString *)feedId; +- (void)toggleTitleClassifier:(NSString *)title feedId:(NSString *)feedId score:(int)score; +- (void)toggleFeedClassifier:(NSString *)feedId; + @end @interface UnreadCounts : NSObject { diff --git a/media/ios/Classes/NewsBlurAppDelegate.m b/media/ios/Classes/NewsBlurAppDelegate.m index bf775ddf1..5fbc20c4c 100644 --- a/media/ios/Classes/NewsBlurAppDelegate.m +++ b/media/ios/Classes/NewsBlurAppDelegate.m @@ -679,7 +679,7 @@ for (NSString *feed in [classifiers objectForKey:@"feeds"]) { if ([[intelligence objectForKey:@"feed"] intValue] <= 0 && - [[story objectForKey:@"story_feed_id"] isEqualToString:feed]) { + [storyFeedId isEqualToString:feed]) { int score = [[[classifiers objectForKey:@"feeds"] objectForKey:feed] intValue]; [intelligence setObject:[NSNumber numberWithInt:score] forKey:@"feed"]; } @@ -1669,6 +1669,181 @@ return titleImageButton; } +#pragma mark - +#pragma mark Classifiers + +- (void)toggleAuthorClassifier:(NSString *)author feedId:(NSString *)feedId { + int authorScore = [[[[self.activeClassifiers objectForKey:feedId] + objectForKey:@"authors"] + objectForKey:author] intValue]; + if (authorScore > 0) { + authorScore = -1; + } else if (authorScore < 0) { + authorScore = 0; + } else { + authorScore = 1; + } + NSMutableDictionary *feedClassifiers = [[self.activeClassifiers objectForKey:feedId] + mutableCopy]; + NSMutableDictionary *authors = [[feedClassifiers objectForKey:@"authors"] mutableCopy]; + [authors setObject:[NSNumber numberWithInt:authorScore] forKey:author]; + [feedClassifiers setObject:authors forKey:@"authors"]; + [self.activeClassifiers setObject:feedClassifiers forKey:feedId]; + [self.storyPageControl refreshHeaders]; + [self.trainerViewController refresh]; + + NSString *urlString = [NSString stringWithFormat:@"http://%@/classifier/save", + NEWSBLUR_URL]; + NSURL *url = [NSURL URLWithString:urlString]; + __block ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url]; + [request setPostValue:author + forKey:authorScore >= 1 ? @"like_author" : + authorScore <= -1 ? @"dislike_author" : + @"remove_like_author"]; + [request setPostValue:feedId forKey:@"feed_id"]; + [request setCompletionBlock:^{ + [self.feedsViewController refreshFeedList:feedId]; + }]; + [request setDidFailSelector:@selector(requestFailed:)]; + [request setDelegate:self]; + [request startAsynchronous]; + + [self recalculateIntelligenceScores:feedId]; + [self.feedDetailViewController.storyTitlesTable reloadData]; +} + +- (void)toggleTagClassifier:(NSString *)tag feedId:(NSString *)feedId { + NSLog(@"toggleTagClassifier: %@", tag); + int tagScore = [[[[self.activeClassifiers objectForKey:feedId] + objectForKey:@"tags"] + objectForKey:tag] intValue]; + + if (tagScore > 0) { + tagScore = -1; + } else if (tagScore < 0) { + tagScore = 0; + } else { + tagScore = 1; + } + + NSMutableDictionary *feedClassifiers = [[self.activeClassifiers objectForKey:feedId] + mutableCopy]; + NSMutableDictionary *tags = [[feedClassifiers objectForKey:@"tags"] mutableCopy]; + [tags setObject:[NSNumber numberWithInt:tagScore] forKey:tag]; + [feedClassifiers setObject:tags forKey:@"tags"]; + [self.activeClassifiers setObject:feedClassifiers forKey:feedId]; + [self.storyPageControl refreshHeaders]; + [self.trainerViewController refresh]; + + NSString *urlString = [NSString stringWithFormat:@"http://%@/classifier/save", + NEWSBLUR_URL]; + NSURL *url = [NSURL URLWithString:urlString]; + __block ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url]; + [request setPostValue:tag + forKey:tagScore >= 1 ? @"like_tag" : + tagScore <= -1 ? @"dislike_tag" : + @"remove_like_tag"]; + [request setPostValue:feedId forKey:@"feed_id"]; + [request setCompletionBlock:^{ + [self.feedsViewController refreshFeedList:feedId]; + }]; + [request setDidFailSelector:@selector(requestFailed:)]; + [request setDelegate:self]; + [request startAsynchronous]; + + [self recalculateIntelligenceScores:feedId]; + [self.feedDetailViewController.storyTitlesTable reloadData]; +} + +- (void)toggleTitleClassifier:(NSString *)title feedId:(NSString *)feedId score:(int)score { + NSLog(@"toggle Title: %@ (%@) / %d", title, feedId, score); + int titleScore = [[[[self.activeClassifiers objectForKey:feedId] + objectForKey:@"titles"] + objectForKey:title] intValue]; + + if (score) { + titleScore = score; + } else { + if (titleScore > 0) { + titleScore = -1; + } else if (titleScore < 0) { + titleScore = 0; + } else { + titleScore = 1; + } + } + + NSMutableDictionary *feedClassifiers = [[self.activeClassifiers objectForKey:feedId] + mutableCopy]; + NSMutableDictionary *titles = [[feedClassifiers objectForKey:@"titles"] mutableCopy]; + [titles setObject:[NSNumber numberWithInt:titleScore] forKey:title]; + [feedClassifiers setObject:titles forKey:@"titles"]; + [self.activeClassifiers setObject:feedClassifiers forKey:feedId]; + [self.storyPageControl refreshHeaders]; + [self.trainerViewController refresh]; + + NSString *urlString = [NSString stringWithFormat:@"http://%@/classifier/save", + NEWSBLUR_URL]; + NSURL *url = [NSURL URLWithString:urlString]; + __block ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url]; + [request setPostValue:title + forKey:titleScore >= 1 ? @"like_title" : + titleScore <= -1 ? @"dislike_title" : + @"remove_like_title"]; + [request setPostValue:feedId forKey:@"feed_id"]; + [request setCompletionBlock:^{ + [self.feedsViewController refreshFeedList:feedId]; + }]; + [request setDidFailSelector:@selector(requestFailed:)]; + [request setDelegate:self]; + [request startAsynchronous]; + + [self recalculateIntelligenceScores:feedId]; + [self.feedDetailViewController.storyTitlesTable reloadData]; +} + +- (void)toggleFeedClassifier:(NSString *)feedId { + int feedScore = [[[[self.activeClassifiers objectForKey:feedId] + objectForKey:@"feeds"] + objectForKey:feedId] intValue]; + + if (feedScore > 0) { + feedScore = -1; + } else if (feedScore < 0) { + feedScore = 0; + } else { + feedScore = 1; + } + + NSMutableDictionary *feedClassifiers = [[self.activeClassifiers objectForKey:feedId] + mutableCopy]; + NSMutableDictionary *feeds = [[feedClassifiers objectForKey:@"feeds"] mutableCopy]; + [feeds setObject:[NSNumber numberWithInt:feedScore] forKey:feedId]; + [feedClassifiers setObject:feeds forKey:@"feeds"]; + [self.activeClassifiers setObject:feedClassifiers forKey:feedId]; + [self.storyPageControl refreshHeaders]; + [self.trainerViewController refresh]; + + NSString *urlString = [NSString stringWithFormat:@"http://%@/classifier/save", + NEWSBLUR_URL]; + NSURL *url = [NSURL URLWithString:urlString]; + __block ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url]; + [request setPostValue:feedId + forKey:feedScore >= 1 ? @"like_feed" : + feedScore <= -1 ? @"dislike_feed" : + @"remove_like_feed"]; + [request setPostValue:feedId forKey:@"feed_id"]; + [request setCompletionBlock:^{ + [self.feedsViewController refreshFeedList:feedId]; + }]; + [request setDidFailSelector:@selector(requestFailed:)]; + [request setDelegate:self]; + [request startAsynchronous]; + + [self recalculateIntelligenceScores:feedId]; + [self.feedDetailViewController.storyTitlesTable reloadData]; +} + @end diff --git a/media/ios/Classes/NewsBlurViewController.m b/media/ios/Classes/NewsBlurViewController.m index 66559cc5e..c7129fbf8 100644 --- a/media/ios/Classes/NewsBlurViewController.m +++ b/media/ios/Classes/NewsBlurViewController.m @@ -287,8 +287,12 @@ static const CGFloat kFolderTitleHeight = 28; if ([request responseStatusCode] == 403) { return [appDelegate showLogin]; } else if ([request responseStatusCode] == 404 || + [request responseStatusCode] == 429 || [request responseStatusCode] >= 500) { [pull finishedLoading]; + if ([request responseStatusCode] == 429) { + return [self informError:@"Slow down. You're rate-limited."]; + } return [self informError:@"The server barfed!"]; } diff --git a/media/ios/Classes/StoryDetailViewController.h b/media/ios/Classes/StoryDetailViewController.h index 58cc72bee..e464257aa 100644 --- a/media/ios/Classes/StoryDetailViewController.h +++ b/media/ios/Classes/StoryDetailViewController.h @@ -66,9 +66,6 @@ - (NSString *)getAvatars:(NSString *)key; - (NSDictionary *)getUser:(int)user_id; -- (void)toggleAuthorClassifier:(NSString *)author; -- (void)toggleTagClassifier:(NSString *)tag; -- (void)finishTrain:(ASIHTTPRequest *)request; - (void)refreshHeader; diff --git a/media/ios/Classes/StoryDetailViewController.m b/media/ios/Classes/StoryDetailViewController.m index ce1cee632..cac436496 100644 --- a/media/ios/Classes/StoryDetailViewController.m +++ b/media/ios/Classes/StoryDetailViewController.m @@ -69,6 +69,10 @@ self.webView.scalesPageToFit = YES; self.webView.multipleTouchEnabled = NO; + + [self.webView.scrollView setDelaysContentTouches:NO]; + [self.webView.scrollView setDecelerationRate:UIScrollViewDecelerationRateNormal]; + self.pageIndex = -2; } @@ -831,6 +835,8 @@ shouldStartLoadWithRequest:(NSURLRequest *)request NSURL *url = [request URL]; NSArray *urlComponents = [url pathComponents]; NSString *action = @""; + NSString *feedId = [NSString stringWithFormat:@"%@", [self.activeStory + objectForKey:@"story_feed_id"]]; if ([urlComponents count] > 1) { action = [NSString stringWithFormat:@"%@", [urlComponents objectAtIndex:1]]; } @@ -904,11 +910,11 @@ shouldStartLoadWithRequest:(NSURLRequest *)request return NO; } else if ([action isEqualToString:@"classify-author"]) { NSString *author = [NSString stringWithFormat:@"%@", [urlComponents objectAtIndex:2]]; - [self toggleAuthorClassifier:author]; + [self.appDelegate toggleAuthorClassifier:author feedId:feedId]; return NO; } else if ([action isEqualToString:@"classify-tag"]) { NSString *tag = [NSString stringWithFormat:@"%@", [urlComponents objectAtIndex:2]]; - [self toggleTagClassifier:tag]; + [self.appDelegate toggleTagClassifier:tag feedId:feedId]; return NO; } else if ([action isEqualToString:@"show-profile"]) { appDelegate.activeUserProfileId = [NSString stringWithFormat:@"%@", [urlComponents objectAtIndex:2]]; @@ -1279,95 +1285,6 @@ shouldStartLoadWithRequest:(NSURLRequest *)request // self.webView.hidden = NO; } -#pragma mark - -#pragma mark Classifiers - -- (void)toggleAuthorClassifier:(NSString *)author { - NSString *feedId = [NSString stringWithFormat:@"%@", [self.activeStory - objectForKey:@"story_feed_id"]]; - int authorScore = [[[[appDelegate.activeClassifiers objectForKey:feedId] - objectForKey:@"authors"] - objectForKey:author] intValue]; - if (authorScore > 0) { - authorScore = -1; - } else if (authorScore < 0) { - authorScore = 0; - } else { - authorScore = 1; - } - NSMutableDictionary *feedClassifiers = [[appDelegate.activeClassifiers objectForKey:feedId] - mutableCopy]; - NSMutableDictionary *authors = [[feedClassifiers objectForKey:@"authors"] mutableCopy]; - [authors setObject:[NSNumber numberWithInt:authorScore] forKey:author]; - [feedClassifiers setObject:authors forKey:@"authors"]; - [appDelegate.activeClassifiers setObject:feedClassifiers forKey:feedId]; - [appDelegate.storyPageControl refreshHeaders]; - - NSString *urlString = [NSString stringWithFormat:@"http://%@/classifier/save", - NEWSBLUR_URL]; - NSURL *url = [NSURL URLWithString:urlString]; - ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url]; - [request setPostValue:author - forKey:authorScore >= 1 ? @"like_author" : - authorScore <= -1 ? @"dislike_author" : - @"remove_like_author"]; - [request setPostValue:feedId forKey:@"feed_id"]; - [request setDidFinishSelector:@selector(finishTrain:)]; - [request setDidFailSelector:@selector(requestFailed:)]; - [request setDelegate:self]; - [request startAsynchronous]; - - [appDelegate recalculateIntelligenceScores:feedId]; - [appDelegate.feedDetailViewController.storyTitlesTable reloadData]; -} - -- (void)toggleTagClassifier:(NSString *)tag { - NSLog(@"toggleTagClassifier: %@", tag); - NSString *feedId = [NSString stringWithFormat:@"%@", [self.activeStory - objectForKey:@"story_feed_id"]]; - int tagScore = [[[[appDelegate.activeClassifiers objectForKey:feedId] - objectForKey:@"tags"] - objectForKey:tag] intValue]; - - if (tagScore > 0) { - tagScore = -1; - } else if (tagScore < 0) { - tagScore = 0; - } else { - tagScore = 1; - } - - NSMutableDictionary *feedClassifiers = [[appDelegate.activeClassifiers objectForKey:feedId] - mutableCopy]; - NSMutableDictionary *tags = [[feedClassifiers objectForKey:@"tags"] mutableCopy]; - [tags setObject:[NSNumber numberWithInt:tagScore] forKey:tag]; - [feedClassifiers setObject:tags forKey:@"tags"]; - [appDelegate.activeClassifiers setObject:feedClassifiers forKey:feedId]; - [appDelegate.storyPageControl refreshHeaders]; - - NSString *urlString = [NSString stringWithFormat:@"http://%@/classifier/save", - NEWSBLUR_URL]; - NSURL *url = [NSURL URLWithString:urlString]; - ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url]; - [request setPostValue:tag - forKey:tagScore >= 1 ? @"like_tag" : - tagScore <= -1 ? @"dislike_tag" : - @"remove_like_tag"]; - [request setPostValue:feedId forKey:@"feed_id"]; - [request setDidFinishSelector:@selector(finishTrain:)]; - [request setDidFailSelector:@selector(requestFailed:)]; - [request setDelegate:self]; - [request startAsynchronous]; - - [appDelegate recalculateIntelligenceScores:feedId]; - [appDelegate.feedDetailViewController.storyTitlesTable reloadData]; -} - -- (void)finishTrain:(ASIHTTPRequest *)request { - id feedId = [self.activeStory objectForKey:@"story_feed_id"]; - [appDelegate.feedsViewController refreshFeedList:feedId]; -} - - (void)refreshHeader { NSString *headerString = [[[self getHeader] stringByReplacingOccurrencesOfString:@"\'" withString:@"\\'"] stringByReplacingOccurrencesOfString:@"\n" withString:@" "]; diff --git a/media/ios/Classes/TrainerViewController.h b/media/ios/Classes/TrainerViewController.h index 55851221e..c6273a716 100644 --- a/media/ios/Classes/TrainerViewController.h +++ b/media/ios/Classes/TrainerViewController.h @@ -13,7 +13,8 @@ @interface TrainerWebView : UIWebView {} -- (void)changeTitle:(id)sender; +- (void)focusTitle:(id)sender; +- (void)hideTitle:(id)sender; @end @@ -37,6 +38,8 @@ @property (nonatomic, assign) BOOL feedTrainer; @property (nonatomic, assign) BOOL storyTrainer; +- (void)refresh; +- (NSString *)makeTrainerHTML; - (NSString *)makeTrainerSections; - (NSString *)makeStoryAuthor; - (NSString *)makeFeedAuthors; @@ -47,6 +50,5 @@ - (NSString *)makeClassifier:(NSString *)classifierName withType:(NSString *)classifierType score:(int)score; - (IBAction)doCloseDialog:(id)sender; -- (void)changeTitle:(id)sender; @end \ No newline at end of file diff --git a/media/ios/Classes/TrainerViewController.m b/media/ios/Classes/TrainerViewController.m index 17f2eeb7b..58f292017 100644 --- a/media/ios/Classes/TrainerViewController.m +++ b/media/ios/Classes/TrainerViewController.m @@ -34,6 +34,8 @@ navBar.tintColor = UIColorFromRGB(0x183353); [self hideGradientBackground:webView]; + [self.webView.scrollView setDelaysContentTouches:YES]; + [self.webView.scrollView setDecelerationRate:UIScrollViewDecelerationRateNormal]; } - (void) hideGradientBackground:(UIView*)theView { @@ -48,18 +50,33 @@ - (void)viewWillAppear:(BOOL)animated { [[UIMenuController sharedMenuController] setMenuItems:[NSArray arrayWithObjects: - [[UIMenuItem alloc] initWithTitle:@"👎 Hide" action:@selector(changeTitle:)], - [[UIMenuItem alloc] initWithTitle:@"👍 Focus" action:@selector(changeTitle:)], + [[UIMenuItem alloc] initWithTitle:@"👎 Hide" action:@selector(hideTitle:)], + [[UIMenuItem alloc] initWithTitle:@"👍 Focus" action:@selector(focusTitle:)], nil]]; UILabel *titleLabel = (UILabel *)[appDelegate makeFeedTitle:appDelegate.activeFeed]; titleLabel.shadowColor = UIColorFromRGB(0x306070); navBar.topItem.titleView = titleLabel; - + NSString *path = [[NSBundle mainBundle] bundlePath]; NSURL *baseURL = [NSURL fileURLWithPath:path]; + [self.webView loadHTMLString:[self makeTrainerHTML] baseURL:baseURL]; +} + +- (void)refresh { + if (self.view.hidden || self.view.superview == nil) { + NSLog(@"Trainer hidden, ignoring redraw."); + return; + } + NSString *headerString = [[[self makeTrainerSections] + stringByReplacingOccurrencesOfString:@"\'" withString:@"\\'"] + stringByReplacingOccurrencesOfString:@"\n" withString:@" "]; + NSString *jsString = [NSString stringWithFormat:@"document.getElementById('NB-trainer').innerHTML = '%@';", + headerString]; - [self.webView loadHTMLString:[self makeTrainerSections] baseURL:baseURL]; + [self.webView stringByEvaluatingJavaScriptFromString:jsString]; + + [self.webView stringByEvaluatingJavaScriptFromString:@"attachFastClick({skipEvent: true});"]; } - (void)viewDidAppear:(BOOL)animated { @@ -74,11 +91,8 @@ #pragma mark - #pragma mark Story layout -- (NSString *)makeTrainerSections { - NSString *storyAuthor = self.feedTrainer ? [self makeFeedAuthors] : [self makeStoryAuthor]; - NSString *storyTags = self.feedTrainer ? [self makeFeedTags] : [self makeStoryTags]; - NSString *storyTitle = self.feedTrainer ? @"" : [self makeTitle]; - NSString *storyPublisher = [self makePublisher]; +- (NSString *)makeTrainerHTML { + NSString *trainerSections = [self makeTrainerSections]; int contentWidth = self.view.frame.size.width; NSString *contentWidthClass; @@ -105,21 +119,13 @@ "" "%@" // header string "" - "
" - "
%@
" - "
%@
" - "
%@
" - "
%@
" - "
" + "
%@
" "%@" // footer "" "", headerString, contentWidthClass, - storyTitle, - storyAuthor, - storyTags, - storyPublisher, + trainerSections, footerString ]; @@ -128,6 +134,26 @@ return htmlString; } +- (NSString *)makeTrainerSections { + NSString *storyAuthor = self.feedTrainer ? [self makeFeedAuthors] : [self makeStoryAuthor]; + NSString *storyTags = self.feedTrainer ? [self makeFeedTags] : [self makeStoryTags]; + NSString *storyTitle = self.feedTrainer ? @"" : [self makeTitle]; + NSString *storyPublisher = [self makePublisher]; + + NSString *htmlString = [NSString stringWithFormat:@ + "
" + "
%@
" + "
%@
" + "
%@
" + "
%@
" + "
", + storyTitle, + storyAuthor, + storyTags, + storyPublisher]; + + return htmlString; +} - (NSString *)makeStoryAuthor { NSString *feedId = [NSString stringWithFormat:@"%@", [appDelegate.activeStory objectForKey:@"story_feed_id"]]; @@ -292,13 +318,13 @@ "
Publisher
" "
" "
" - " %@" + " %@" "
" "
" " 0 ? @"NB-story-publisher-positive" : publisherScore < 0 ? @"NB-story-publisher-negative" : @"", + publisherScore > 0 ? @"positive" : publisherScore < 0 ? @"negative" : @"", [self makeClassifier:feedTitle withType:@"publisher" score:publisherScore]]; return storyPublisher; @@ -308,31 +334,42 @@ NSString *feedId = [NSString stringWithFormat:@"%@", [appDelegate.activeStory objectForKey:@"story_feed_id"]]; NSString *storyTitle = [appDelegate.activeStory objectForKey:@"story_title"]; - + if (!storyTitle) { return @""; } - NSMutableDictionary *titleClassifiers = [[appDelegate.activeClassifiers objectForKey:feedId] + NSMutableDictionary *classifiers = [[appDelegate.activeClassifiers objectForKey:feedId] objectForKey:@"titles"]; - for (NSString *titleClassifier in titleClassifiers) { - if ([storyTitle containsString:titleClassifier]) { - int titleScore = [[titleClassifiers objectForKey:titleClassifier] intValue]; - storyTitle = [storyTitle - stringByReplacingOccurrencesOfString:titleClassifier - withString:[NSString stringWithFormat:@" %@", - titleScore > 0 ? @"positive" : titleScore < 0 ? @"negative" : @"", - titleClassifier]]; + NSMutableArray *titleStrings = [NSMutableArray array]; + for (NSString *title in classifiers) { + if ([storyTitle containsString:title]) { + int titleScore = [[classifiers objectForKey:title] intValue]; + NSString *titleClassifier = [NSString stringWithFormat:@ + "
" + " %@" + "
", + title, + titleScore > 0 ? @"positive" : titleScore < 0 ? @"negative" : @"", + [self makeClassifier:title withType:@"title" score:titleScore]]; + [titleStrings addObject:titleClassifier]; } } + NSString *titleClassifiers; + if ([titleStrings count]) { + titleClassifiers = [titleStrings componentsJoinedByString:@""]; + } else { + titleClassifiers = @"
Tap and hold the title
"; + } NSString *titleTrainer = [NSString stringWithFormat:@"
" "
Story Title
" "
" "
%@
" - "
Tap and hold the title
" + " %@" "
" - "
", storyTitle]; + "", storyTitle, titleClassifiers]; return titleTrainer; } @@ -359,17 +396,51 @@ [appDelegate.trainerViewController dismissModalViewControllerAnimated:YES]; } -- (BOOL)canPerformAction:(SEL)action withSender:(id)sender { - if (action == @selector(changeTitle:)) { - return YES; - } else { - return NO; - } +- (void)changeTitle:(id)sender score:(int)score { + NSString *feedId = [NSString stringWithFormat:@"%@", [appDelegate.activeStory + objectForKey:@"story_feed_id"]]; + NSString *selectedTitle = [self.webView + stringByEvaluatingJavaScriptFromString:@"window.getSelection().toString()"]; + + [self.appDelegate toggleTitleClassifier:selectedTitle feedId:feedId score:score]; } -- (void)changeTitle:(id)sender { - NSString *selectedTitle = [self.webView stringByEvaluatingJavaScriptFromString:@"window.getSelection().toString()"]; - NSLog(@"Selected: %@", selectedTitle); + +- (BOOL)webView:(UIWebView *)webView +shouldStartLoadWithRequest:(NSURLRequest *)request + navigationType:(UIWebViewNavigationType)navigationType { + NSURL *url = [request URL]; + NSArray *urlComponents = [url pathComponents]; + NSString *action = @""; + NSString *feedId = [NSString stringWithFormat:@"%@", [appDelegate.activeFeed + objectForKey:@"id"]]; + if ([urlComponents count] > 1) { + action = [NSString stringWithFormat:@"%@", [urlComponents objectAtIndex:1]]; + } + + NSLog(@"Tapped url: %@", url); + if ([[url host] isEqualToString: @"ios.newsblur.com"]){ + + if ([action isEqualToString:@"classify-author"]) { + NSString *author = [NSString stringWithFormat:@"%@", [urlComponents objectAtIndex:2]]; + [self.appDelegate toggleAuthorClassifier:author feedId:feedId]; + return NO; + } else if ([action isEqualToString:@"classify-tag"]) { + NSString *tag = [NSString stringWithFormat:@"%@", [urlComponents objectAtIndex:2]]; + [self.appDelegate toggleTagClassifier:tag feedId:feedId]; + return NO; + } else if ([action isEqualToString:@"classify-title"]) { + NSString *title = [NSString stringWithFormat:@"%@", [urlComponents objectAtIndex:2]]; + [self.appDelegate toggleTitleClassifier:title feedId:feedId score:0]; + return NO; + } else if ([action isEqualToString:@"classify-feed"]) { + NSString *feedId = [NSString stringWithFormat:@"%@", [urlComponents objectAtIndex:2]]; + [self.appDelegate toggleFeedClassifier:feedId]; + return NO; + } + } + + return YES; } @end @@ -378,16 +449,21 @@ @implementation TrainerWebView - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { - if (action == @selector(changeTitle:)) { + if (action == @selector(focusTitle:) || action == @selector(hideTitle:)) { return YES; } else { return NO; } } -- (void)changeTitle:(id)sender { +- (void)focusTitle:(id)sender { NewsBlurAppDelegate *appDelegate = [NewsBlurAppDelegate sharedAppDelegate]; - [appDelegate.trainerViewController changeTitle:sender]; + [appDelegate.trainerViewController changeTitle:sender score:1]; +} + +- (void)hideTitle:(id)sender { + NewsBlurAppDelegate *appDelegate = [NewsBlurAppDelegate sharedAppDelegate]; + [appDelegate.trainerViewController changeTitle:sender score:-1]; } @end diff --git a/media/ios/NewsBlur.xcodeproj/xcuserdata/sclay.xcuserdatad/xcdebugger/Breakpoints.xcbkptlist b/media/ios/NewsBlur.xcodeproj/xcuserdata/sclay.xcuserdatad/xcdebugger/Breakpoints.xcbkptlist index 4a1d0981f..9c93bf1b3 100644 --- a/media/ios/NewsBlur.xcodeproj/xcuserdata/sclay.xcuserdatad/xcdebugger/Breakpoints.xcbkptlist +++ b/media/ios/NewsBlur.xcodeproj/xcuserdata/sclay.xcuserdatad/xcdebugger/Breakpoints.xcbkptlist @@ -16,10 +16,23 @@ landmarkName = "-applyNewIndex:pageController:" landmarkType = "5"> + + 25 + + + doCloseDialog: + + + + 26 + delegate @@ -4603,7 +4611,7 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE - 25 + 26 diff --git a/media/ios/static/fastTouch.js b/media/ios/static/fastTouch.js index 7af5abfbd..21e6bde06 100644 --- a/media/ios/static/fastTouch.js +++ b/media/ios/static/fastTouch.js @@ -1,85 +1,578 @@ +/** + * @preserve FastClick: polyfill to remove click delays on browsers with touch UIs. + * + * @version 0.4.6 + * @codingstandard ftlabs-jsv2 + * @copyright The Financial Times Limited [All Rights Reserved] + * @license MIT License (see LICENSE.txt) + */ -function NoClickDelay(el) { - this.element = typeof el == 'object' ? el : document.getElementById(el); - if( window.Touch ) { - this.element.removeEventListener('touchstart', this.element.notouch, false); - this.element.notouch = this; - this.element.addEventListener('touchstart', this.element.notouch, false); - } -} -NoClickDelay.prototype = { -handleEvent: function(e) { - switch(e.type) { - case 'touchstart': this.onTouchStart(e); break; - case 'touchmove': this.onTouchMove(e); break; - case 'touchend': this.onTouchEnd(e); break; - } -}, -onTouchStart: function(e) { - e.preventDefault(); - this.moved = false; - this.x = e.targetTouches[0].clientX; - this.y = e.targetTouches[0].clientY; - this.theTarget = document.elementFromPoint(e.targetTouches[0].clientX, e.targetTouches[0].clientY); - this.theTarget = $(this.theTarget).closest('a').get(0); - // if(this.theTarget.nodeType == 3) this.theTarget = theTarget.parentNode; - this.theTarget.className+= ' pressed'; - this.element.addEventListener('touchmove', this, false); - this.element.addEventListener('touchend', this, false); -}, -onTouchMove: function(e) { - var x = e.targetTouches[0].clientX; - var y = e.targetTouches[0].clientY; - if( Math.sqrt(Math.pow(x-this.x,2)+Math.pow(y-this.y,2))>50){ - this.moved = true; - this.theTarget.className = this.theTarget.className.replace(/ ?pressed/gi, ''); - this.theTarget.className = this.theTarget.className.replace(/ ?active/gi, ''); - } else { - if(this.moved==true){ - this.moved=false; - this.theTarget.className+= ' pressed'; - } - } -}, -onTouchEnd: function(e) { - this.element.removeEventListener('touchmove', this, false); - this.element.removeEventListener('touchend', this, false); - if( !this.moved && this.theTarget ) { - this.theTarget.className = this.theTarget.className.replace(/ ?pressed/gi, ''); - this.theTarget.className+= ' active'; - var theEvent = document.createEvent('MouseEvents'); - theEvent.initEvent('click', true, true); - this.theTarget.dispatchEvent(theEvent); - } - this.theTarget = undefined; +/*jslint browser:true, node:true*/ +/*global define, Event, Node*/ + + +/** + * Instantiate fast-clicking listeners on the specificed layer. + * + * @constructor + * @param {Element} layer The layer to listen on + */ +function FastClick(layer) { + 'use strict'; + var oldOnClick, self = this; + + + /** + * Whether a click is currently being tracked. + * + * @type boolean + */ + this.trackingClick = false; + + + /** + * Timestamp for when when click tracking started. + * + * @type number + */ + this.trackingClickStart = 0; + + + /** + * The element being tracked for a click. + * + * @type EventTarget + */ + this.targetElement = null; + + + /** + * X-coordinate of touch start event. + * + * @type number + */ + this.touchStartX = 0; + + + /** + * Y-coordinate of touch start event. + * + * @type number + */ + this.touchStartY = 0; + + + /** + * ID of the last touch, retrieved from Touch.identifier. + * + * @type number + */ + this.lastTouchIdentifier = 0; + + + /** + * The FastClick layer. + * + * @type Element + */ + this.layer = layer; + + if (!layer || !layer.nodeType) { + throw new TypeError('Layer must be a document node'); + } + + /** @type function() */ + this.onClick = function() { FastClick.prototype.onClick.apply(self, arguments); }; + + /** @type function() */ + this.onTouchStart = function() { FastClick.prototype.onTouchStart.apply(self, arguments); }; + + /** @type function() */ + this.onTouchMove = function() { FastClick.prototype.onTouchMove.apply(self, arguments); }; + + /** @type function() */ + this.onTouchEnd = function() { FastClick.prototype.onTouchEnd.apply(self, arguments); }; + + /** @type function() */ + this.onTouchCancel = function() { FastClick.prototype.onTouchCancel.apply(self, arguments); }; + + // Devices that don't support touch don't need FastClick + if (typeof window.ontouchstart === 'undefined') { + return; + } + + // Set up event handlers as required + layer.addEventListener('click', this.onClick, true); + layer.addEventListener('touchstart', this.onTouchStart, false); + layer.addEventListener('touchmove', this.onTouchMove, false); + layer.addEventListener('touchend', this.onTouchEnd, false); + layer.addEventListener('touchcancel', this.onTouchCancel, false); + + // Hack is required for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2) + // which is how FastClick normally stops click events bubbling to callbacks registered on the FastClick + // layer when they are cancelled. + if (!Event.prototype.stopImmediatePropagation) { + layer.removeEventListener = function(type, callback, capture) { + var rmv = Node.prototype.removeEventListener; + if (type === 'click') { + rmv.call(layer, type, callback.hijacked || callback, capture); + } else { + rmv.call(layer, type, callback, capture); + } + }; + + layer.addEventListener = function(type, callback, capture) { + var adv = Node.prototype.addEventListener; + if (type === 'click') { + adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) { + if (!event.propagationStopped) { + callback(event); + } + }), capture); + } else { + adv.call(layer, type, callback, capture); + } + }; + } + + // If a handler is already declared in the element's onclick attribute, it will be fired before + // FastClick's onClick handler. Fix this by pulling out the user-defined handler function and + // adding it as listener. + if (typeof layer.onclick === 'function') { + + // Android browser on at least 3.2 requires a new reference to the function in layer.onclick + // - the old one won't work if passed to addEventListener directly. + oldOnClick = layer.onclick; + layer.addEventListener('click', function(event) { + oldOnClick(event); + }, false); + layer.onclick = null; + } } + + +/** + * Android requires an exception for labels. + * + * @type boolean + */ +FastClick.prototype.deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0; + + +/** + * iOS requires an exception for alert confirm dialogs. + * + * @type boolean + */ +FastClick.prototype.deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent); + + +/** + * iOS 4 requires an exception for select elements. + * + * @type boolean + */ +FastClick.prototype.deviceIsIOS4 = FastClick.prototype.deviceIsIOS && (/OS 4_\d(_\d)?/).test(navigator.userAgent); + + +/** + * Determine whether a given element requires a native click. + * + * @param {EventTarget|Element} target Target DOM element + * @returns {boolean} Returns true if the element needs a native click + */ +FastClick.prototype.needsClick = function(target) { + 'use strict'; + switch (target.nodeName.toLowerCase()) { + case 'label': + case 'video': + return true; + default: + return (/\bneedsclick\b/).test(target.className); + } }; -function attachFastClick() { + +/** + * Determine whether a given element requires a call to focus to simulate click into element. + * + * @param {EventTarget|Element} target Target DOM element + * @returns {boolean} Returns true if the element requires a call to focus to simulate native click. + */ +FastClick.prototype.needsFocus = function(target) { + 'use strict'; + switch (target.nodeName.toLowerCase()) { + case 'textarea': + case 'select': + return true; + case 'input': + switch (target.type) { + case 'button': + case 'checkbox': + case 'file': + case 'image': + case 'radio': + case 'submit': + return false; + } + return true; + default: + return (/\bneedsfocus\b/).test(target.className); + } +}; + + +/** + * Send a click event to the specified element. + * + * @param {EventTarget|Element} targetElement + * @param {Event} event + */ +FastClick.prototype.sendClick = function(targetElement, event) { + 'use strict'; + var clickEvent, touch; + + // On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24) + if (document.activeElement && document.activeElement !== targetElement) { + document.activeElement.blur(); + } + + touch = event.changedTouches[0]; + + // Synthesise a click event, with an extra attribute so it can be tracked + clickEvent = document.createEvent('MouseEvents'); + clickEvent.initMouseEvent('click', true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null); + clickEvent.forwardedTouchEvent = true; + targetElement.dispatchEvent(clickEvent); +}; + + +/** + * On touch start, record the position and scroll offset. + * + * @param {Event} event + * @returns {boolean} + */ +FastClick.prototype.onTouchStart = function(event) { + 'use strict'; + var touch = event.targetTouches[0]; + + this.trackingClick = true; + this.trackingClickStart = event.timeStamp; + this.targetElement = event.target; + this.theTarget = $(this.targetElement).closest('a').get(0); + + this.theTarget.className += ' pressed'; + + this.touchStartX = touch.pageX; + this.touchStartY = touch.pageY; + this.startClickTime = new Date; + + // Prevent phantom clicks on fast double-tap (issue #36) + if ((event.timeStamp - this.lastClickTime) < 200) { + event.preventDefault(); + } + + return true; +}; + + +/** + * Based on a touchmove event object, check whether the touch has moved past a boundary since it started. + * + * @param {Event} event + * @returns {boolean} + */ +FastClick.prototype.touchHasMoved = function(event) { + 'use strict'; + var touch = event.targetTouches[0]; + + if (Math.abs(touch.pageX - this.touchStartX) > 10 || Math.abs(touch.pageY - this.touchStartY) > 10) { + return true; + } + + return false; +}; + + +/** + * Update the last position. + * + * @param {Event} event + * @returns {boolean} + */ +FastClick.prototype.onTouchMove = function(event) { + 'use strict'; + if (!this.trackingClick) { + return true; + } + + // If the touch has moved, cancel the click tracking + if (this.targetElement !== event.target || this.touchHasMoved(event)) { + this.trackingClick = false; + this.theTarget.className = this.theTarget.className.replace(/ ?pressed/gi, ''); + this.targetElement = null; + } + + return true; +}; + + +/** + * Attempt to find the labelled control for the given label element. + * + * @param {EventTarget|HTMLLabelElement} labelElement + * @returns {Element|null} + */ +FastClick.prototype.findControl = function(labelElement) { + 'use strict'; + + // Fast path for newer browsers supporting the HTML5 control attribute + if (labelElement.control !== undefined) { + return labelElement.control; + } + + // All browsers under test that support touch events also support the HTML5 htmlFor attribute + if (labelElement.htmlFor) { + return document.getElementById(labelElement.htmlFor); + } + + // If no for attribute exists, attempt to retrieve the first labellable descendant element + // the list of which is defined here: http://www.w3.org/TR/html5/forms.html#category-label + return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea'); +}; + + +/** + * On touch end, determine whether to send a click event at once. + * + * @param {Event} event + * @returns {boolean} + */ +FastClick.prototype.onTouchEnd = function(event) { + 'use strict'; + var forElement, trackingClickStart, targetElement = this.targetElement, touch = event.changedTouches[0]; + + if (!this.trackingClick) { + return true; + } + + // Weird things happen on iOS when an alert or confirm dialog is opened from a click event callback (issue #23): + // when the user next taps anywhere else on the page, new touchstart and touchend events are dispatched + // with the same identifier as the touch event that previously triggered the click that triggered the alert. + if (this.deviceIsIOS) { + if (touch.identifier === this.lastTouchIdentifier) { + event.preventDefault(); + return false; + } + + this.lastTouchIdentifier = touch.identifier; + } + + // Prevent phantom clicks on fast double-tap (issue #36) + if ((event.timeStamp - this.lastClickTime) < 200) { + this.cancelNextClick = true; + return true; + } + + if ((new Date - this.startClickTime) > 115) { + return false; + } + this.lastClickTime = event.timeStamp; + + trackingClickStart = this.trackingClickStart; + this.trackingClick = false; + this.theTarget.className = this.theTarget.className.replace(/ ?pressed/gi, ''); + this.trackingClickStart = 0; + + if (targetElement.nodeName.toLowerCase() === 'label') { + forElement = this.findControl(targetElement); + if (forElement) { + targetElement.focus(); + if (this.deviceIsAndroid) { + return false; + } + + if (!this.needsClick(forElement)) { + event.preventDefault(); + this.sendClick(forElement, event); + } + + return false; + } + } else if (this.needsFocus(targetElement)) { + + // If the touch started a while ago (best guess is 100ms based on tests for issue #36) then focus will be triggered anyway. Return early and unset the target element reference so that the subsequent click will be allowed through. + if ((event.timeStamp - trackingClickStart) > 100) { + this.targetElement = null; + return true; + } + + targetElement.focus(); + + // Select elements need the event to go through on iOS 4, otherwise the selector menu won't open. + if (!this.deviceIsIOS4 || targetElement.tagName.toLowerCase() !== 'select') { + this.targetElement = null; + event.preventDefault(); + } + + return false; + } + + // Prevent the actual click from going though - unless the target node is marked as requiring + // real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted. + if (!this.needsClick(targetElement)) { + event.preventDefault(); + this.sendClick(targetElement, event); + } + + return false; +}; + + +/** + * On touch cancel, stop tracking the click. + * + * @returns {void} + */ +FastClick.prototype.onTouchCancel = function() { + 'use strict'; + this.theTarget.className = this.theTarget.className.replace(/ ?pressed/gi, ''); + this.trackingClick = false; + this.targetElement = null; +}; + + +/** + * On actual clicks, determine whether this is a touch-generated click, a click action occurring + * naturally after a delay after a touch (which needs to be cancelled to avoid duplication), or + * an actual click which should be permitted. + * + * @param {Event} event + * @returns {boolean} + */ +FastClick.prototype.onClick = function(event) { + 'use strict'; + + var oldTargetElement; + + // If a target element was never set (because a touch event was never fired) allow the click + if (!this.targetElement) { + return true; + } + + if (event.forwardedTouchEvent) { + return true; + } + + oldTargetElement = this.targetElement; + this.targetElement = null; + + // It's possible for another FastClick-like library delivered with third-party code to fire a click event before FastClick does (issue #44). In that case, set the click-tracking flag back to false and return early. This will cause onTouchEnd to return early. + if (this.trackingClick) { + this.trackingClick = false; + return true; + } + + // Programmatically generated events targeting a specific element should be permitted + if (!event.cancelable) { + return true; + } + + // Very odd behaviour on iOS (issue #18): if a submit element is present inside a form and the user hits enter in the iOS simulator or clicks the Go button on the pop-up OS keyboard the a kind of 'fake' click event will be triggered with the submit-type input element as the target. + if (event.target.type === 'submit' && event.detail === 0) { + return true; + } + + // Derive and check the target element to see whether the click needs to be permitted; + // unless explicitly enabled, prevent non-touch click events from triggering actions, + // to prevent ghost/doubleclicks. + if (!this.needsClick(oldTargetElement) || this.cancelNextClick) { + this.cancelNextClick = false; + + // Prevent any user-added listeners declared on FastClick element from being fired. + if (event.stopImmediatePropagation) { + event.stopImmediatePropagation(); + } else { + + // Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2) + event.propagationStopped = true; + } + + // Cancel the event + event.stopPropagation(); + event.preventDefault(); + + return false; + } + + // If clicks are permitted, return true for the action to go through. + return true; +}; + + +/** + * Remove all FastClick's event listeners. + * + * @returns {void} + */ +FastClick.prototype.destroy = function() { + 'use strict'; + var layer = this.layer; + + layer.removeEventListener('click', this.onClick, true); + layer.removeEventListener('touchstart', this.onTouchStart, false); + layer.removeEventListener('touchmove', this.onTouchMove, false); + layer.removeEventListener('touchend', this.onTouchEnd, false); + layer.removeEventListener('touchcancel', this.onTouchCancel, false); +}; + + +if (typeof define !== 'undefined' && define.amd) { + + // AMD. Register as an anonymous module. + define(function() { + 'use strict'; + return FastClick; + }); +} + +if (typeof module !== 'undefined' && module.exports) { + module.exports = function(layer) { + 'use strict'; + return new FastClick(layer); + }; + + module.exports.FastClick = FastClick; +} + + +function attachFastClick(options) { + options = options || {}; var avatars = document.getElementsByClassName("NB-show-profile"); Array.prototype.slice.call(avatars, 0).forEach(function(avatar) { - new NoClickDelay(avatar); + new FastClick(avatar, options); }); var tags = document.getElementsByClassName("NB-story-tag"); Array.prototype.slice.call(tags, 0).forEach(function(tag) { - new NoClickDelay(tag); + new FastClick(tag, options); }); var authors = document.getElementsByClassName("NB-story-author"); Array.prototype.slice.call(authors, 0).forEach(function(author) { - new NoClickDelay(author); + new FastClick(author, options); }); var publishers = document.getElementsByClassName("NB-story-publisher"); Array.prototype.slice.call(publishers, 0).forEach(function(publisher) { - new NoClickDelay(publisher); + new FastClick(publisher, options); }); var titles = document.getElementsByClassName("NB-story-title"); Array.prototype.slice.call(titles, 0).forEach(function(title) { - new NoClickDelay(title); + new FastClick(title, options); }); var author = document.getElementById("NB-story-author"); if (author) { - new NoClickDelay(author); + new FastClick(author, options); } } diff --git a/media/ios/static/storyDetailView.css b/media/ios/static/storyDetailView.css index 04dbf2830..34c5206cc 100644 --- a/media/ios/static/storyDetailView.css +++ b/media/ios/static/storyDetailView.css @@ -313,6 +313,8 @@ div + p { text-shadow: 1px 1px 0 #EFEFEF; overflow: hidden; max-width: none; + + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } .NB-iphone .NB-header { diff --git a/media/ios/static/trainer.css b/media/ios/static/trainer.css index 6229b6ce7..da9ed90ff 100644 --- a/media/ios/static/trainer.css +++ b/media/ios/static/trainer.css @@ -53,9 +53,10 @@ } .NB-trainer-title .NB-title-trainer { - padding: 12px 0; + padding: 6px 0 12px; font-size: 18px; - line-height: 22px; + font-weight: bold; + line-height: 24px; -webkit-user-select: auto; -webkit-touch-callout: default; -webkit-highlight: auto; @@ -67,7 +68,7 @@ color: #C0C0C0; font-weight: bold; font-size: 12px; - padding: 12px 0; + padding: 2px 0 4px; } .NB-story-title-positive { @@ -152,10 +153,14 @@ top: 2px; opacity: .2; } -.pressed .NB-classifier .NB-classifier-icon-like, -.pressed .NB-classifier.NB-classifier-dislike .NB-classifier-icon-like { +.NB-classifier.NB-classifier-like .NB-classifier-icon-like { opacity: 1; } +.pressed .NB-classifier .NB-classifier-icon-like, +.pressed .NB-classifier.NB-classifier-dislike .NB-classifier-icon-like, +.pressed .NB-classifier.NB-classifier-like .NB-classifier-icon-dislike { + opacity: .8; +} .pressed .NB-classifier.NB-classifier-like .NB-classifier-icon-like { opacity: 0; } diff --git a/media/ios/static/trainer.js b/media/ios/static/trainer.js index c36cfdd98..ea4fcf796 100644 --- a/media/ios/static/trainer.js +++ b/media/ios/static/trainer.js @@ -14,5 +14,7 @@ Trainer.prototype = { Zepto(function($) { new Trainer(); - attachFastClick(); + attachFastClick({ + skipEvent: true + }); });