Permalink
Browse files

reputation tracker

  • Loading branch information...
1 parent 420f6d8 commit 4e49cb262eb042de624a5bac2c4ddc1b8e93f7a6 @nikitar committed Apr 3, 2011
@@ -60,14 +60,15 @@
8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; };
8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; };
8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; };
+ BCE374901346E01200BF40B0 /* common.m in Sources */ = {isa = PBXBuildFile; fileRef = 110BE7FD131F16F00063066E /* common.m */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
110BE5971319D3250063066E /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */;
proxyType = 1;
- remoteGlobalIDString = 8D1107260486CEB800E47090 /* Newt */;
+ remoteGlobalIDString = 8D1107260486CEB800E47090;
remoteInfo = Newt;
};
/* End PBXContainerItemProxy section */
@@ -106,6 +107,7 @@
110BE70E131E0C280063066E /* GTMNSString+HTML.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMNSString+HTML.m"; sourceTree = "<group>"; };
110BE70F131E0C280063066E /* GTMDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMDefines.h; sourceTree = "<group>"; };
110BE737131E12C00063066E /* common.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = common.h; path = source/common.h; sourceTree = "<group>"; };
+ 110BE7FD131F16F00063066E /* common.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = common.m; path = source/common.m; sourceTree = "<group>"; };
1122294D1302D82E0087EC8C /* PLBlocks.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PLBlocks.framework; path = lib/PLBlocks.framework; sourceTree = "<group>"; wrapsLines = 0; };
1129997C130304210085DFDA /* README */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = README; sourceTree = "<group>"; };
1148F03B12F998BE00C254BB /* JSON.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSON.h; sourceTree = "<group>"; };
@@ -177,7 +179,6 @@
080E96DDFE201D6D7F000001 /* Classes */ = {
isa = PBXGroup;
children = (
- 11CA31AF130620EB003F036C /* interviewers */,
117778A212F8AF6D00C2E645 /* NewtMenulet.h */,
117778A312F8AF6D00C2E645 /* NewtMenulet.m */,
1148F2B912FA5E2200C254BB /* StackExchangeQueryTool.h */,
@@ -195,6 +196,7 @@
11CA30551304B4D6003F036C /* NewtPersistence.h */,
11CA30561304B4D6003F036C /* NewtPersistence.m */,
110BE737131E12C00063066E /* common.h */,
+ 110BE7FD131F16F00063066E /* common.m */,
);
name = Classes;
sourceTree = "<group>";
@@ -257,14 +259,6 @@
path = lib/JSON;
sourceTree = "<group>";
};
- 11CA31AF130620EB003F036C /* interviewers */ = {
- isa = PBXGroup;
- children = (
- );
- name = interviewers;
- path = source/interviewers;
- sourceTree = "<group>";
- };
19C28FACFE9D520D11CA2CBB /* Products */ = {
isa = PBXGroup;
children = (
@@ -425,6 +419,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ BCE374901346E01200BF40B0 /* common.m in Sources */,
8D11072D0486CEB800E47090 /* main.m in Sources */,
117778A412F8AF6D00C2E645 /* NewtMenulet.m in Sources */,
1148F04612F998BE00C254BB /* NSObject+SBJSON.m in Sources */,
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="7.10">
<data>
- <int key="IBDocument.SystemTarget">1060</int>
+ <int key="IBDocument.SystemTarget">1050</int>
<string key="IBDocument.SystemVersion">10J567</string>
<string key="IBDocument.InterfaceBuilderVersion">823</string>
<string key="IBDocument.AppKitVersion">1038.35</string>
@@ -109,6 +109,18 @@
<int key="NSMnemonicLoc">2147483647</int>
<reference key="NSOnImage" ref="134046214"/>
<reference key="NSMixedImage" ref="821662553"/>
+ <int key="NSTag">101</int>
+ </object>
+ <object class="NSMenuItem" id="1004538258">
+ <reference key="NSMenu" ref="627897770"/>
+ <bool key="NSIsDisabled">YES</bool>
+ <bool key="NSIsSeparator">YES</bool>
+ <string key="NSTitle"/>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="134046214"/>
+ <reference key="NSMixedImage" ref="821662553"/>
+ <int key="NSTag">102</int>
</object>
<object class="NSMenuItem" id="1016037814">
<reference key="NSMenu" ref="627897770"/>
@@ -249,6 +261,7 @@
<reference ref="854242624"/>
<reference ref="96838011"/>
<reference ref="710913590"/>
+ <reference ref="1004538258"/>
</object>
<reference key="parent" ref="0"/>
<string key="objectName">theMenu</string>
@@ -298,6 +311,11 @@
<reference key="object" ref="549122079"/>
<reference key="parent" ref="627897770"/>
</object>
+ <object class="IBObjectRecord">
+ <int key="objectID">592</int>
+ <reference key="object" ref="1004538258"/>
+ <reference key="parent" ref="627897770"/>
+ </object>
</object>
</object>
<object class="NSMutableDictionary" key="flattenedProperties">
@@ -317,12 +335,14 @@
<string>584.IBPluginDependency</string>
<string>588.IBPluginDependency</string>
<string>589.IBPluginDependency</string>
+ <string>592.IBPluginDependency</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
- <string>{{445, 373}, {191, 143}}</string>
+ <string>{{445, 363}, {191, 153}}</string>
+ <string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
@@ -351,7 +371,7 @@
</object>
</object>
<nil key="sourceID"/>
- <int key="maxID">591</int>
+ <int key="maxID">592</int>
</object>
<object class="IBClassDescriber" key="IBDocument.Classes">
<object class="NSMutableArray" key="referencedPartialClassDescriptions">
@@ -886,7 +906,7 @@
<string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string>
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencyDefaults">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.macosx</string>
- <integer value="1060" key="NS.object.0"/>
+ <integer value="1050" key="NS.object.0"/>
</object>
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDevelopmentDependencies">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3</string>
@@ -9,12 +9,14 @@
<string>New Question</string>
<string>New Comment</string>
<string>New Answer</string>
+ <string>Reputation Change</string>
</array>
<key>DefaultNotifications</key>
<array>
<string>New Question</string>
<string>New Comment</string>
<string>New Answer</string>
+ <string>Reputation Change</string>
</array>
</dict>
</plist>
View
@@ -29,7 +29,7 @@
#import "PreferencePaneController.h"
#import "StackExchangeQueryTool.h"
#import "NewtPersistence.h"
-#import "common.h"
+
// Performs polling of SE API
@@ -63,6 +63,7 @@
NSTimer *commentsToUserTimer;
NSTimer *commentsOnPostsTimer;
NSTimer *answersOnPostsTimer;
+ NSTimer *userInfoTimer;
NSTimer *sitesDataTimer;
View
@@ -124,7 +124,6 @@ - (void)awakeFromNib {
prefPane = [[PreferencePaneController alloc] initWithBundle:bundle];
[prefPane setPersistence:persistence];
-// [self loadStackExchangeNetworkSites];
sitesDataTimer = [self startTimerWithMethod:@selector(loadStackExchangeNetworkSites) andInterval:60*24];
// initialise Growl
@@ -138,8 +137,10 @@ - (void)awakeFromNib {
questionTimer = [self startTimerWithMethod:@selector(retrieveQuestions:) andInterval:1];
postsByUserTimer = [self startTimerWithMethod:@selector(retrievePosts:) andInterval:5];
-
- // delay retrieval of comments and answers before user posts are fetched
+ userInfoTimer = [self startTimerWithMethod:@selector(updateUserInfo) andInterval:3];
+
+ // delay retrieval of comments and answers before recent user posts are fetched
+ // also, this might help with API requests throttling
[self performSelector:@selector(delayedTimers) withObject:nil afterDelay:10.0];
}
@@ -179,6 +180,130 @@ - (void)loadStackExchangeNetworkSites {
[persistence updateSites:queryTool];
}
+- (void)updateUserInfo {
+ if (!enabled) {
+ // temporary switched off
+ return;
+ }
+ NSLog(@"updateUserInfo");
+
+ NSString *userGlobalId = [persistence objectForKey:@"user_global_id"];
+ if (userGlobalId == nil) {
+ return;
+ }
+
+ // TODO some duplication with PreferencePaneController#updateProfileURL and #updateUserInfoWithProfiles
+
+ // fetch information about user's profiles across Stack Exchange network
+ QueryToolSuccessHandler globalUserDataHandler = ^(NSDictionary *result) {
+ NSArray *profiles = [result objectForKey:@"associated_users"];
+ [self showReputation:profiles];
+ };
+ [queryTool execute:@"http://stackauth.com"
+ withMethod:[NSString stringWithFormat:@"users/%@/associated", userGlobalId]
+ andParameters:[NSDictionary dictionary]
+ onSuccess:globalUserDataHandler];
+}
+
+- (void)showReputation:(NSArray *)profiles {
+ // save current reputation data to calculate the difference
+ NSArray *mostUsed = [persistence objectForKey:@"most_used_sites"];
+ NSMutableDictionary *repMap = [NSMutableDictionary dictionaryWithCapacity:[mostUsed count]];
+ for (NSString *siteKey in mostUsed) {
+ NSObject *rep = [[persistence siteForKey:siteKey] objectForKey:@"user_reputation"];
+ [repMap setObject:rep forKey:siteKey];
+ }
+
+ // update profile data
+ [prefPane updateProfiles:profiles];
+
+ mostUsed = [persistence objectForKey:@"most_used_sites"];
+
+ // calculate reputation change
+ for (NSString *siteUrl in mostUsed) {
+ NSNumber *old = [repMap objectForKey:siteUrl];
+ if (old == nil) {
+ old = [NSNumber numberWithInt:0];
+ }
+
+ NSDictionary *site = [persistence siteForKey:siteUrl];
+ NSNumber *current = [site objectForKey:@"user_reputation"];
+ int dif = [current intValue] - [old intValue];
+ if (dif != 0) {
+ NSObject *userId = [site objectForKey:@"user_id"];
+ NSString *url = [NSString stringWithFormat:@"%@/users/%@?tab=reputation", siteUrl, userId];
+ NSString *title;
+ if (dif > 0) {
+ title = [NSString stringWithFormat:@"+%d", dif];
+ } else {
+ title = [NSString stringWithFormat:@"%d", dif];
+ }
+
+ [GrowlApplicationBridge notifyWithTitle:title
+ description:@""
+ notificationName:@"Reputation Change"
+ iconData:[site objectForKey:@"icon_data"]
+ priority:0
+ isSticky:FALSE
+ clickContext:url];
+ }
+ }
+
+ // present reputation for most used sites
+ int ITEMS_TO_SHOW = 3;
+
+ int startIndex = [theMenu indexOfItemWithTag:101];
+ int endIndex = [theMenu indexOfItemWithTag:102];
+ if (startIndex + 1 == endIndex) {
+ // create menu items first, if there're none
+ [[theMenu itemAtIndex:endIndex] setHidden:FALSE];
+ for (int i = 0; i < ITEMS_TO_SHOW && i < [profiles count]; ++i) {
+ NSMenuItem *item = [theMenu insertItemWithTitle:@"site"
+ action:@selector(clickReputation:)
+ keyEquivalent:@""
+ atIndex:startIndex + i + 1];
+ [item setTarget:self];
+ [item setTag:110 + i];
+ }
+ }
+
+ // set titles
+ for (int i = 0; i < ITEMS_TO_SHOW && i < [mostUsed count]; ++i) {
+ NSMenuItem *item = [theMenu itemAtIndex:startIndex + i + 1];
+ NSString *siteUrl = [mostUsed objectAtIndex:i];
+
+ NSDictionary *site = [persistence siteForKey:siteUrl];
+ if (site == nil) {
+ continue;
+ }
+ NSString *siteRep = [site objectForKey:@"user_reputation"];
+ NSString *title = [NSString stringWithFormat:@"%@", siteRep];
+ NSData *iconData = [site objectForKey:@"icon_data"];
+ if (iconData == nil) {
+ continue;
+ }
+ NSImage *image = [[NSImage alloc] initWithData:iconData];
+ NSSize newSize;
+ newSize.height = 22;
+ newSize.width = 22;
+ [image setSize:newSize];
+
+ [item setTitle:title];
+ [item setImage:image];
+ [image release];
+ }
+}
+
+- (void)clickReputation:(id)sender {
+ NSArray *sites = [persistence objectForKey:@"most_used_sites"];
+ int index = [sender tag] - 110;
+ NSString *siteUrl = [sites objectAtIndex:index];
+ NSDictionary *site = [persistence siteForKey:siteUrl];
+ NSObject *userId = [site objectForKey:@"user_id"];
+
+ NSString *url = [NSString stringWithFormat:@"%@/users/%@?tab=reputation", siteUrl, userId];
+ [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
+}
- (IBAction)retrieveQuestions:(id)sender {
if (!enabled || silent) {
@@ -276,6 +401,7 @@ - (IBAction)openAboutPanel:(id)sender {
}
- (IBAction)quit:(id)sender {
+ [persistence synchronize];
[NSApp terminate:self];
}
View
@@ -41,6 +41,7 @@
- enabled - whether new questions will be polled from the site
- favourite_tags - list of tags for this site user entered in preferences pane
- user_id, user_type, user_name, user_reputation and user_email_hash - from stackauth.com/{v}/users/{user_id}/associated
+ - icon_data - NSData object with site icon.
*/
@interface NewtPersistence : NSObject {
@@ -74,6 +74,7 @@
- (IBAction)updateProfileURL;
- (void)setPersistence:(NewtPersistence *) persistence_;
+- (void)updateProfiles:(NSArray *)profiles;
//- (void)textDidBeginEditing:(NSNotification *)notification;
//- (void)textDidEndEditing:(NSNotification *)aNotification;
Oops, something went wrong.

0 comments on commit 4e49cb2

Please sign in to comment.