From 512db251aaa9d5e8f40ac9e0a5eabcaf3a747d2e Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Fri, 5 Oct 2012 21:48:01 +0200 Subject: [PATCH] More feedback improvements - Mark messages as archived that got deleted on the server - Send message even if the thread got deleted on the server, so create a new thread - Support Pull-To-Refresh in iOS6 - Update feedback and send pending message when the app gets into the foreground again - Always update user data via delegates before sending a new message, since those could have changed in the app --- Classes/BITHockeyManager.m | 8 + .../Feedback/BITFeedbackListViewController.m | 35 +-- Classes/Feedback/BITFeedbackManager.m | 214 +++++++++++++----- Classes/Feedback/BITFeedbackManagerPrivate.h | 10 +- Classes/Feedback/BITFeedbackMessage.h | 4 +- Classes/Helper/BITHockeyBaseViewController.h | 2 +- Classes/Helper/BITHockeyBaseViewController.m | 8 +- 7 files changed, 201 insertions(+), 80 deletions(-) diff --git a/Classes/BITHockeyManager.m b/Classes/BITHockeyManager.m index fd031bad16..a5579e0cb0 100644 --- a/Classes/BITHockeyManager.m +++ b/Classes/BITHockeyManager.m @@ -217,6 +217,14 @@ } +- (void)setDisableFeedbackManager:(BOOL)disableFeedbackManager { + if (_feedbackManager) { + [_feedbackManager setDisableFeedbackManager:disableFeedbackManager]; + } + _disableFeedbackManager = disableFeedbackManager; +} + + - (void)setServerURL:(NSString *)aServerURL { // ensure url ends with a trailing slash if (![aServerURL hasSuffix:@"/"]) { diff --git a/Classes/Feedback/BITFeedbackListViewController.m b/Classes/Feedback/BITFeedbackListViewController.m index e652d8445b..04fd7a64e3 100644 --- a/Classes/Feedback/BITFeedbackListViewController.m +++ b/Classes/Feedback/BITFeedbackListViewController.m @@ -42,7 +42,6 @@ @interface BITFeedbackListViewController () @property (nonatomic, assign) BITFeedbackManager *manager; -@property (nonatomic, retain) UITableView *tableView; @property (nonatomic, retain) NSDateFormatter *lastUpdateDateFormatter; @end @@ -63,7 +62,8 @@ - (void)dealloc { - [_tableView release], _tableView = nil; + [[NSNotificationCenter defaultCenter] removeObserver:self name:BITHockeyFeedbackMessagesUpdated object:nil]; + [_lastUpdateDateFormatter release]; _lastUpdateDateFormatter = nil; [super dealloc]; @@ -82,28 +82,29 @@ self.title = BITHockeyLocalizedString(@"HockeyFeedbackListTitle"); - self.tableView = [[[UITableView alloc] initWithFrame:self.view.bounds] autorelease]; self.tableView.delegate = self; self.tableView.dataSource = self; self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; [self.tableView setAutoresizingMask:UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth]; [self.tableView setBackgroundColor:[UIColor colorWithRed:0.82 green:0.84 blue:0.84 alpha:1]]; [self.tableView setSeparatorColor:[UIColor colorWithRed:0.79 green:0.79 blue:0.79 alpha:1]]; - [self.view addSubview:self.tableView]; - self.navigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh - target:self - action:@selector(reloadList)] autorelease]; - -} - -- (void)viewDidUnload { - [[NSNotificationCenter defaultCenter] removeObserver:self name:BITHockeyFeedbackMessagesUpdated object:nil]; - - [super viewDidUnload]; + id refreshClass = NSClassFromString(@"UIRefreshControl"); + if (refreshClass) { + self.refreshControl = [[[UIRefreshControl alloc] init] autorelease]; + [self.refreshControl addTarget:self action:@selector(reloadList) forControlEvents:UIControlEventValueChanged]; + } else { + self.navigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh + target:self + action:@selector(reloadList)] autorelease]; + } } - (void)reloadList { + id refreshClass = NSClassFromString(@"UIRefreshControl"); + if (refreshClass) { + [self.refreshControl beginRefreshing]; + } [self.manager updateMessagesList]; } @@ -111,6 +112,11 @@ CGSize contentSize = self.tableView.contentSize; CGPoint contentOffset = self.tableView.contentOffset; + id refreshClass = NSClassFromString(@"UIRefreshControl"); + if (refreshClass) { + [self.refreshControl endRefreshing]; + } + [self.tableView reloadData]; if (self.tableView.contentSize.height > contentSize.height) [self.tableView setContentOffset:CGPointMake(contentOffset.x, self.tableView.contentSize.height - contentSize.height + contentOffset.y) animated:NO]; @@ -304,7 +310,6 @@ BITFeedbackMessage *message = [self.manager messageAtIndex:indexPath.row]; if (!message) return 44; - // BITFeedbackListViewCell *cell = (BITFeedbackListViewCell *)[tableView cellForRowAtIndexPath:indexPath]; return [BITFeedbackListViewCell heightForRowWithText:message.text tableViewWidth:self.view.frame.size.width]; } diff --git a/Classes/Feedback/BITFeedbackManager.m b/Classes/Feedback/BITFeedbackManager.m index d6023ac6f7..dd1a9f40d8 100644 --- a/Classes/Feedback/BITFeedbackManager.m +++ b/Classes/Feedback/BITFeedbackManager.m @@ -36,7 +36,6 @@ #import "BITHockeyManagerPrivate.h" -#import "BITFeedbackMessage.h" #import "BITHockeyHelper.h" @@ -52,6 +51,8 @@ NSFileManager *_fileManager; NSString *_feedbackDir; NSString *_settingsFile; + + BOOL _didSetupDidBecomeActiveNotifications; } #pragma mark - Initialization @@ -66,6 +67,8 @@ _requireUserEmail = BITFeedbackUserDataElementRequired; _showAlertOnIncomingMessages = YES; + _disableFeedbackManager = NO; + _didSetupDidBecomeActiveNotifications = NO; _networkRequestInProgress = NO; _incomingMessagesAlertShowing = NO; _lastCheck = nil; @@ -96,6 +99,10 @@ } - (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self name:BITHockeyNetworkDidBecomeReachableNotification object:nil]; + + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil]; + [_currentFeedbackListViewController release], _currentFeedbackListViewController = nil; [_currentFeedbackComposeViewController release], _currentFeedbackComposeViewController = nil; @@ -114,6 +121,27 @@ } +- (void)didBecomeActiveActions { + if (![self isFeedbackManagerDisabled]) { + [self updateAppDefinedUserData]; + [self updateMessagesList]; + } +} + +- (void)setupDidBecomeActiveNotifications { + if (!_didSetupDidBecomeActiveNotifications) { + NSNotificationCenter *dnc = [NSNotificationCenter defaultCenter]; + [dnc addObserver:self selector:@selector(didBecomeActiveActions) name:UIApplicationDidBecomeActiveNotification object:nil]; + [dnc addObserver:self selector:@selector(didBecomeActiveActions) name:BITHockeyNetworkDidBecomeReachableNotification object:nil]; + _didSetupDidBecomeActiveNotifications = YES; + } +} + +- (void)cleanupDidBecomeActiveNotifications { + [[NSNotificationCenter defaultCenter] removeObserver:self name:BITHockeyNetworkDidBecomeReachableNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil]; +} + #pragma mark - Feedback Modal UI - (BITFeedbackListViewController *)feedbackListViewController:(BOOL)modal { @@ -148,20 +176,44 @@ - (void)startManager { if ([self.feedbackList count] == 0) { [self loadMessages]; + } else { + [self updateAppDefinedUserData]; } [self updateMessagesList]; + + [self setupDidBecomeActiveNotifications]; } - (void)updateMessagesList { if (_networkRequestInProgress) return; - if ([self nextPendingMessage]) { + NSArray *pendingMessages = [self messagesWithStatus:BITFeedbackMessageStatusSendPending]; + if ([pendingMessages count] > 0) { [self submitPendingMessages]; } else { [self fetchMessageUpdates]; } } +- (void)updateAppDefinedUserData { + if ([BITHockeyManager sharedHockeyManager].delegate && + [[BITHockeyManager sharedHockeyManager].delegate respondsToSelector:@selector(userNameForHockeyManager:componentManager:)]) { + self.userName = [[BITHockeyManager sharedHockeyManager].delegate + userNameForHockeyManager:[BITHockeyManager sharedHockeyManager] + componentManager:self]; + self.requireUserName = BITFeedbackUserDataElementDontShow; + self.requireUserEmail = BITFeedbackUserDataElementDontShow; + } + if ([BITHockeyManager sharedHockeyManager].delegate && + [[BITHockeyManager sharedHockeyManager].delegate respondsToSelector:@selector(userEmailForHockeyManager:componentManager:)]) { + self.userEmail = [[BITHockeyManager sharedHockeyManager].delegate + userEmailForHockeyManager:[BITHockeyManager sharedHockeyManager] + componentManager:self]; + self.requireUserName = BITFeedbackUserDataElementDontShow; + self.requireUserEmail = BITFeedbackUserDataElementDontShow; + } +} + #pragma mark - Local Storage - (void)loadMessages { @@ -328,30 +380,24 @@ return message; } -- (BITFeedbackMessage *)sendInProgressMessage { - __block BITFeedbackMessage *message = nil; +- (NSArray *)messagesWithStatus:(BITFeedbackMessageStatus)status { + NSMutableArray *resultMessages = [[NSMutableArray alloc] initWithCapacity:[self.feedbackList count]]; [self.feedbackList enumerateObjectsUsingBlock:^(BITFeedbackMessage *objMessage, NSUInteger messagesIdx, BOOL *stop) { - if ([objMessage status] == BITFeedbackMessageStatusSendInProgress) { - message = objMessage; - *stop = YES; + if ([objMessage status] == status) { + [resultMessages addObject: objMessage]; } }]; - return message; + return [NSArray arrayWithArray:resultMessages];; } -- (BITFeedbackMessage *)nextPendingMessage { - __block BITFeedbackMessage *message = nil; - - [self.feedbackList enumerateObjectsUsingBlock:^(BITFeedbackMessage *objMessage, NSUInteger messagesIdx, BOOL *stop) { - if ([objMessage status] == BITFeedbackMessageStatusSendPending) { - message = objMessage; - *stop = YES; - } +- (void)markSendInProgressMessagesAsPending { + // make sure message that may have not been send successfully, get back into the right state to be send again + [self.feedbackList enumerateObjectsUsingBlock:^(id objMessage, NSUInteger messagesIdx, BOOL *stop) { + if ([(BITFeedbackMessage *)objMessage status] == BITFeedbackMessageStatusSendInProgress) + [(BITFeedbackMessage *)objMessage setStatus:BITFeedbackMessageStatusSendPending]; }]; - - return message; } @@ -386,7 +432,36 @@ #pragma mark - Networking -- (BOOL)updateMessageListFromResponse:(NSDictionary *)jsonDictionary { +- (void)updateMessageListFromResponse:(NSDictionary *)jsonDictionary { + if (!jsonDictionary) { + // nil is used when the server returns 404, so we need to mark all existing threads as archives and delete the discussion token + + NSArray *messagesSendInProgress = [self messagesWithStatus:BITFeedbackMessageStatusSendInProgress]; + NSInteger pendingMessagesCount = [messagesSendInProgress count] + [[self messagesWithStatus:BITFeedbackMessageStatusSendPending] count]; + + [self markSendInProgressMessagesAsPending]; + + [self.feedbackList enumerateObjectsUsingBlock:^(id objMessage, NSUInteger messagesIdx, BOOL *stop) { + if ([(BITFeedbackMessage *)objMessage status] != BITFeedbackMessageStatusSendPending) + [(BITFeedbackMessage *)objMessage setStatus:BITFeedbackMessageStatusArchived]; + }]; + + if ([self token]) { + self.token = nil; + } + + NSInteger pendingMessagesCountAfterProcessing = [[self messagesWithStatus:BITFeedbackMessageStatusSendPending] count]; + + [self saveMessages]; + + // check if this request was successful and we have more messages pending and continue if positive + if (pendingMessagesCount > pendingMessagesCountAfterProcessing && pendingMessagesCountAfterProcessing > 0) { + [self performSelector:@selector(submitPendingMessages) withObject:nil afterDelay:0.1]; + } + + return; + } + NSDictionary *feedback = [jsonDictionary objectForKey:@"feedback"]; NSString *token = [jsonDictionary objectForKey:@"token"]; NSDictionary *feedbackObject = [jsonDictionary objectForKey:@"feedback"]; @@ -400,8 +475,10 @@ NSArray *feedMessages = [feedbackObject objectForKey:@"messages"]; // get the message that was currently sent if available - __block BITFeedbackMessage *sendInProgressMessage = [self sendInProgressMessage]; - __block BOOL messagesUpdated = NO; + NSArray *messagesSendInProgress = [self messagesWithStatus:BITFeedbackMessageStatusSendInProgress]; + + NSInteger pendingMessagesCount = [messagesSendInProgress count] + [[self messagesWithStatus:BITFeedbackMessageStatusSendPending] count]; + __block BOOL newResponseMessage = NO; __block NSMutableSet *returnedMessageIDs = [[[NSMutableSet alloc] init] autorelease]; @@ -409,12 +486,22 @@ NSNumber *messageID = [(NSDictionary *)objMessage objectForKey:@"id"]; [returnedMessageIDs addObject:messageID]; - if (![self messageWithID:messageID]) { - // check if this is the message that was sent right now - if (sendInProgressMessage && [[sendInProgressMessage text] isEqualToString:[(NSDictionary *)objMessage objectForKey:@"text"]]) { - sendInProgressMessage.date = [self parseRFC3339Date:[(NSDictionary *)objMessage objectForKey:@"created_at"]]; - sendInProgressMessage.id = messageID; - sendInProgressMessage.status = BITFeedbackMessageStatusRead; + BITFeedbackMessage *thisMessage = [self messageWithID:messageID]; + if (!thisMessage) { + // check if this is a message that was sent right now + __block BITFeedbackMessage *matchingSendInProgressMessage = nil; + + [messagesSendInProgress enumerateObjectsUsingBlock:^(id objSendInProgressMessage, NSUInteger messagesSendInProgressIdx, BOOL *stop) { + if ([[(NSDictionary *)objMessage objectForKey:@"text"] isEqualToString:[(BITFeedbackMessage *)objSendInProgressMessage text]]) { + matchingSendInProgressMessage = objSendInProgressMessage; + *stop = YES; + } + }]; + + if (matchingSendInProgressMessage) { + matchingSendInProgressMessage.date = [self parseRFC3339Date:[(NSDictionary *)objMessage objectForKey:@"created_at"]]; + matchingSendInProgressMessage.id = messageID; + matchingSendInProgressMessage.status = BITFeedbackMessageStatusRead; } else { BITFeedbackMessage *message = [[[BITFeedbackMessage alloc] init] autorelease]; message.text = [(NSDictionary *)objMessage objectForKey:@"text"]; @@ -429,28 +516,22 @@ newResponseMessage = YES; } - messagesUpdated = YES; + } else { + // TODO: update message } }]; + // TODO: implement todo defined above - // remove all messages that are removed on the server - NSMutableSet *removedMessageIDs = [[[NSMutableSet alloc] init] autorelease]; + [self markSendInProgressMessagesAsPending]; + + // mark all messages as archived that are removed on the server [self.feedbackList enumerateObjectsUsingBlock:^(id objMessage, NSUInteger messagesIdx, BOOL *stop) { - if (![returnedMessageIDs member:[(BITFeedbackMessage *)objMessage id]]) { - [removedMessageIDs addObject:[(BITFeedbackMessage *)objMessage id]]; + if (![returnedMessageIDs member:[(BITFeedbackMessage *)objMessage id]] && + [(BITFeedbackMessage *)objMessage status] != BITFeedbackMessageStatusSendPending + ) { + [(BITFeedbackMessage *)objMessage setStatus:BITFeedbackMessageStatusArchived]; } }]; - - if ([removedMessageIDs count] > 0) { - [removedMessageIDs enumerateObjectsUsingBlock:^(id objID, BOOL *stop) { - [self.feedbackList removeObject:[self messageWithID:objID]]; - }]; - } - - // new data arrived, so save it - if (messagesUpdated) { - [self saveMessages]; - } // we got a new incoming message, trigger user notification system if (newResponseMessage) { @@ -467,11 +548,20 @@ } } - return YES; + NSInteger pendingMessagesCountAfterProcessing = [[self messagesWithStatus:BITFeedbackMessageStatusSendPending] count]; + + // check if this request was successful and we have more messages pending and continue if positive + if (pendingMessagesCount > pendingMessagesCountAfterProcessing && pendingMessagesCountAfterProcessing > 0) { + [self performSelector:@selector(submitPendingMessages) withObject:nil afterDelay:0.1]; + } + + } else { + [self markSendInProgressMessagesAsPending]; } - // quit - return NO; + [self saveMessages]; + + return; } - (void)sendNetworkRequestWithHTTPMethod:(NSString *)httpMethod withText:(NSString *)text completionHandler:(void (^)(NSError *err))completionHandler { @@ -529,9 +619,14 @@ _networkRequestInProgress = NO; if (err) { + [self markSendInProgressMessagesAsPending]; completionHandler(err); } else { - if ([responseData length]) { + NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode]; + if (statusCode == 404) { + // thread has been deleted, we archive it + [self updateMessageListFromResponse:nil]; + } else if ([responseData length]) { NSString *responseString = [[[NSString alloc] initWithBytes:[responseData bytes] length:[responseData length] encoding: NSUTF8StringEncoding] autorelease]; BITHockeyLog(@"INFO: Received API response: %@", responseString); @@ -557,9 +652,10 @@ [self updateMessageListFromResponse:feedDict]; } } - - completionHandler(err); } + + [self markSendInProgressMessagesAsPending]; + completionHandler(err); } }]; } @@ -578,14 +674,20 @@ } - (void)submitPendingMessages { - BITFeedbackMessage *message = [self nextPendingMessage]; + // app defined user data may have changed, update it + [self updateAppDefinedUserData]; - if (message) { - [message setStatus:BITFeedbackMessageStatusSendInProgress]; + NSArray *pendingMessages = [self messagesWithStatus:BITFeedbackMessageStatusSendPending]; + + if ([pendingMessages count] > 0) { + // we send one message at a time + BITFeedbackMessage *messageToSend = [pendingMessages objectAtIndex:0]; + + [messageToSend setStatus:BITFeedbackMessageStatusSendInProgress]; if (self.userName) - [message setName:self.userName]; + [messageToSend setName:self.userName]; if (self.userEmail) - [message setName:self.userEmail]; + [messageToSend setName:self.userEmail]; NSString *httpMethod = @"POST"; if ([self token]) { @@ -593,10 +695,10 @@ } [self sendNetworkRequestWithHTTPMethod:httpMethod - withText:[message text] + withText:[messageToSend text] completionHandler:^(NSError *err){ if (err) { - [message setStatus:BITFeedbackMessageStatusSendPending]; + [self markSendInProgressMessagesAsPending]; [self saveMessages]; } diff --git a/Classes/Feedback/BITFeedbackManagerPrivate.h b/Classes/Feedback/BITFeedbackManagerPrivate.h index 510588ee5f..520e5c3851 100644 --- a/Classes/Feedback/BITFeedbackManagerPrivate.h +++ b/Classes/Feedback/BITFeedbackManagerPrivate.h @@ -29,6 +29,7 @@ */ #import +#import "BITFeedbackMessage.h" @interface BITFeedbackManager () { @@ -42,15 +43,18 @@ @property (nonatomic, retain) NSMutableArray *feedbackList; +// used by BITHockeyManager if disable status is changed +@property (nonatomic, getter = isFeedbackManagerDisabled) BOOL disableFeedbackManager; + - (BITFeedbackMessage *)messageWithID:(NSNumber *)messageID; -- (BITFeedbackMessage *)sendInProgressMessage; -- (BITFeedbackMessage *)nextPendingMessage; + +- (NSArray *)messagesWithStatus:(BITFeedbackMessageStatus)status; - (void)saveMessages; - (void)fetchMessageUpdates; -- (BOOL)updateMessageListFromResponse:(NSDictionary *)jsonDictionary; +- (void)updateMessageListFromResponse:(NSDictionary *)jsonDictionary; @end diff --git a/Classes/Feedback/BITFeedbackMessage.h b/Classes/Feedback/BITFeedbackMessage.h index 2dfc81bc0b..0263e40c78 100644 --- a/Classes/Feedback/BITFeedbackMessage.h +++ b/Classes/Feedback/BITFeedbackMessage.h @@ -37,7 +37,9 @@ typedef enum { // new messages from server BITFeedbackMessageStatusUnread = 2, // messages from server once read and new local messages once successful send from SDK - BITFeedbackMessageStatusRead = 3 + BITFeedbackMessageStatusRead = 3, + // message is archived, happens if the thread is deleted from the server + BITFeedbackMessageStatusArchived = 4 } BITFeedbackMessageStatus; @interface BITFeedbackMessage : NSObject { diff --git a/Classes/Helper/BITHockeyBaseViewController.h b/Classes/Helper/BITHockeyBaseViewController.h index 5196ec95a6..ded912d33c 100644 --- a/Classes/Helper/BITHockeyBaseViewController.h +++ b/Classes/Helper/BITHockeyBaseViewController.h @@ -8,7 +8,7 @@ #import -@interface BITHockeyBaseViewController : UIViewController +@interface BITHockeyBaseViewController : UITableViewController @property (nonatomic, readwrite) BOOL modalAnimated; diff --git a/Classes/Helper/BITHockeyBaseViewController.m b/Classes/Helper/BITHockeyBaseViewController.m index 8e65ec3d7b..48b38d288e 100644 --- a/Classes/Helper/BITHockeyBaseViewController.m +++ b/Classes/Helper/BITHockeyBaseViewController.m @@ -8,16 +8,16 @@ #import "BITHockeyBaseViewController.h" + @interface BITHockeyBaseViewController () + @property (nonatomic) BOOL modal; @property (nonatomic) UIStatusBarStyle statusBarStyle; + @end -@implementation BITHockeyBaseViewController -@synthesize modalAnimated = _modalAnimated; -@synthesize modal = _modal; -@synthesize statusBarStyle = _statusBarStyle; +@implementation BITHockeyBaseViewController - (id)init {