Training feeds and stories in feeds. Needs testing on ipad, in river, and on social.

This commit is contained in:
Samuel Clay 2012-12-27 18:37:05 -08:00
parent a5652da4f1
commit 8fe95f47f3
13 changed files with 912 additions and 212 deletions

View file

@ -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 {

View file

@ -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

View file

@ -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!"];
}

View file

@ -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;

View file

@ -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:@" "];

View file

@ -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

View file

@ -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 @@
"<html>"
"<head>%@</head>" // header string
"<body id=\"trainer\" class=\"%@\">"
" <div class=\"NB-trainer\"><div class=\"NB-trainer-inner\">"
" <div class=\"NB-trainer-title NB-trainer-section\">%@</div>"
" <div class=\"NB-trainer-author NB-trainer-section\">%@</div>"
" <div class=\"NB-trainer-tags NB-trainer-section\">%@</div>"
" <div class=\"NB-trainer-publisher NB-trainer-section\">%@</div>"
" </div></div>"
"<div class=\"NB-trainer\" id=\"NB-trainer\">%@</div>"
"%@" // footer
"</body>"
"</html>",
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:@
"<div class=\"NB-trainer-inner\">"
" <div class=\"NB-trainer-title NB-trainer-section\">%@</div>"
" <div class=\"NB-trainer-author NB-trainer-section\">%@</div>"
" <div class=\"NB-trainer-tags NB-trainer-section\">%@</div>"
" <div class=\"NB-trainer-publisher NB-trainer-section\">%@</div>"
"</div>",
storyTitle,
storyAuthor,
storyTags,
storyPublisher];
return htmlString;
}
- (NSString *)makeStoryAuthor {
NSString *feedId = [NSString stringWithFormat:@"%@", [appDelegate.activeStory
objectForKey:@"story_feed_id"]];
@ -292,13 +318,13 @@
" <div class=\"NB-trainer-section-title\">Publisher</div>"
" <div class=\"NB-trainer-section-body\">"
" <div class=\"NB-classifier-container\">"
" <a href=\"http://ios.newsblur.com/classify-publisher/%@\" "
" class=\"NB-story-publisher %@\">%@</a>"
" <a href=\"http://ios.newsblur.com/classify-feed/%@\" "
" class=\"NB-story-publisher NB-story-publisher-%@\">%@</a>"
" </div>"
" </div>"
"</div",
feedId,
publisherScore > 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:@" <span class=\"NB-story-title-%@\">%@</span>",
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:@
"<div class=\"NB-classifier-container\">"
" <a href=\"http://ios.newsblur.com/classify-title/%@\" "
" class=\"NB-story-title NB-story-title-%@\">%@</a>"
"</div>",
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 = @"<div class=\"NB-title-info\">Tap and hold the title</div>";
}
NSString *titleTrainer = [NSString stringWithFormat:@"<div class=\"NB-trainer-section-inner\">"
" <div class=\"NB-trainer-section-title\">Story Title</div>"
" <div class=\"NB-trainer-section-body NB-title\">"
" <div class=\"NB-title-trainer\">%@</div>"
" <div class=\"NB-title-info\">Tap and hold the title</div>"
" %@"
" </div>"
"</div>", storyTitle];
"</div>", 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

View file

@ -16,10 +16,23 @@
landmarkName = "-applyNewIndex:pageController:"
landmarkType = "5">
</FileBreakpoint>
<FileBreakpoint
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "Classes/NewsBlurAppDelegate.m"
timestampString = "378353700.864122"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "681"
endingLineNumber = "681"
landmarkName = "-recalculateIntelligenceScores:"
landmarkType = "5">
</FileBreakpoint>
</FileBreakpoints>
<SymbolicBreakpoints>
<SymbolicBreakpoint
shouldBeEnabled = "Yes"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "objc_exception_throw"
@ -28,7 +41,7 @@
</SymbolicBreakpoints>
<ExceptionBreakpoints>
<ExceptionBreakpoint
shouldBeEnabled = "Yes"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
scope = "0"

View file

@ -4521,6 +4521,14 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
</object>
<int key="connectionID">25</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchEventConnection" key="connection">
<string key="label">doCloseDialog:</string>
<reference key="source" ref="216767292"/>
<reference key="destination" ref="372490531"/>
</object>
<int key="connectionID">26</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">delegate</string>
@ -4603,7 +4611,7 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
<nil key="activeLocalization"/>
<dictionary class="NSMutableDictionary" key="localizations"/>
<nil key="sourceID"/>
<int key="maxID">25</int>
<int key="maxID">26</int>
</object>
<object class="IBClassDescriber" key="IBDocument.Classes">
<array class="NSMutableArray" key="referencedPartialClassDescriptions">

View file

@ -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);
}
}

View file

@ -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 {

View file

@ -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;
}

View file

@ -14,5 +14,7 @@ Trainer.prototype = {
Zepto(function($) {
new Trainer();
attachFastClick();
attachFastClick({
skipEvent: true
});
});