diff --git a/Classes/BITFeedbackManager.m b/Classes/BITFeedbackManager.m index 42e855454f..a5c8898bbc 100644 --- a/Classes/BITFeedbackManager.m +++ b/Classes/BITFeedbackManager.m @@ -45,6 +45,7 @@ #define kBITFeedbackToken @"HockeyFeedbackToken" #define kBITFeedbackName @"HockeyFeedbackName" #define kBITFeedbackEmail @"HockeyFeedbackEmail" +#define kBITFeedbackLastMessageID @"HockeyFeedbackLastMessageID" @implementation BITFeedbackManager { @@ -144,6 +145,20 @@ [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil]; } +#pragma mark - Private methods + +- (NSString *)uuidString { + CFUUIDRef theToken = CFUUIDCreate(NULL); + CFStringRef stringUUID = CFUUIDCreateString(NULL, theToken); + CFRelease(theToken); + + return [(NSString *)stringUUID autorelease]; +} + +- (NSString *)uuidAsLowerCaseAndShortened { + return [[[self uuidString] lowercaseString] stringByReplacingOccurrencesOfString:@"-" withString:@""]; +} + #pragma mark - Feedback Modal UI - (BITFeedbackListViewController *)feedbackListViewController:(BOOL)modal { @@ -332,13 +347,18 @@ NSDate *date1 = [obj1 date]; NSDate *date2 = [obj2 date]; - // not send and send in progress messages on top, sorted by date + // not send, in conflict and send in progress messages on top, sorted by date // read and unread on bottom, sorted by date + // archived on the very bottom if ([obj1 status] >= BITFeedbackMessageStatusSendInProgress && [obj2 status] < BITFeedbackMessageStatusSendInProgress) { return NSOrderedAscending; } else if ([obj1 status] < BITFeedbackMessageStatusSendInProgress && [obj2 status] >= BITFeedbackMessageStatusSendInProgress) { return NSOrderedDescending; + } else if ([obj1 status] == BITFeedbackMessageStatusArchived && [obj2 status] < BITFeedbackMessageStatusArchived) { + return NSOrderedDescending; + } else if ([obj1 status] < BITFeedbackMessageStatusArchived && [obj2 status] == BITFeedbackMessageStatusArchived) { + return NSOrderedAscending; } else { return (NSInteger)[date2 compare:date1]; } @@ -390,6 +410,14 @@ }]; } +- (void)markSendInProgressMessagesAsInConflict { + // make sure message that may have not been send successfully, get back into the right state to be send again + [_feedbackList enumerateObjectsUsingBlock:^(id objMessage, NSUInteger messagesIdx, BOOL *stop) { + if ([(BITFeedbackMessage *)objMessage status] == BITFeedbackMessageStatusSendInProgress) + [(BITFeedbackMessage *)objMessage setStatus:BITFeedbackMessageStatusInConflict]; + }]; +} + #pragma mark - User @@ -479,19 +507,21 @@ BITFeedbackMessage *thisMessage = [self messageWithID:messageID]; if (!thisMessage) { // check if this is a message that was sent right now - __block BITFeedbackMessage *matchingSendInProgressMessage = nil; + __block BITFeedbackMessage *matchingSendInProgressOrInConflictMessage = nil; + + // TODO: match messages in state conflict [messagesSendInProgress enumerateObjectsUsingBlock:^(id objSendInProgressMessage, NSUInteger messagesSendInProgressIdx, BOOL *stop) { - if ([[(NSDictionary *)objMessage objectForKey:@"text"] isEqualToString:[(BITFeedbackMessage *)objSendInProgressMessage text]]) { - matchingSendInProgressMessage = objSendInProgressMessage; + if ([[(NSDictionary *)objMessage objectForKey:@"token"] isEqualToString:[(BITFeedbackMessage *)objSendInProgressMessage token]]) { + matchingSendInProgressOrInConflictMessage = objSendInProgressMessage; *stop = YES; } }]; - if (matchingSendInProgressMessage) { - matchingSendInProgressMessage.date = [self parseRFC3339Date:[(NSDictionary *)objMessage objectForKey:@"created_at"]]; - matchingSendInProgressMessage.id = messageID; - matchingSendInProgressMessage.status = BITFeedbackMessageStatusRead; + if (matchingSendInProgressOrInConflictMessage) { + matchingSendInProgressOrInConflictMessage.date = [self parseRFC3339Date:[(NSDictionary *)objMessage objectForKey:@"created_at"]]; + matchingSendInProgressOrInConflictMessage.id = messageID; + matchingSendInProgressOrInConflictMessage.status = BITFeedbackMessageStatusRead; } else { BITFeedbackMessage *message = [[[BITFeedbackMessage alloc] init] autorelease]; message.text = [(NSDictionary *)objMessage objectForKey:@"text"]; @@ -554,7 +584,7 @@ return; } -- (void)sendNetworkRequestWithHTTPMethod:(NSString *)httpMethod withText:(NSString *)text completionHandler:(void (^)(NSError *err))completionHandler { +- (void)sendNetworkRequestWithHTTPMethod:(NSString *)httpMethod withMessage:(BITFeedbackMessage *)message completionHandler:(void (^)(NSError *err))completionHandler { NSString *boundary = @"----FOO"; _networkRequestInProgress = YES; @@ -582,7 +612,7 @@ [request setValue:@"Hockey/iOS" forHTTPHeaderField:@"User-Agent"]; [request setValue:@"gzip" forHTTPHeaderField:@"Accept-Encoding"]; - if (text) { + if (message) { NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary]; [request setValue:contentType forHTTPHeaderField:@"Content-type"]; @@ -593,7 +623,8 @@ [postBody appendData:[self appendPostValue:[self getDevicePlatform] forKey:@"model"]]; [postBody appendData:[self appendPostValue:[[[NSBundle mainBundle] preferredLocalizations] objectAtIndex:0] forKey:@"lang"]]; [postBody appendData:[self appendPostValue:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"] forKey:@"bundle_version"]]; - [postBody appendData:[self appendPostValue:text forKey:@"text"]]; + [postBody appendData:[self appendPostValue:[message text] forKey:@"text"]]; + [postBody appendData:[self appendPostValue:[message token] forKey:@"message_token"]]; if (self.userName) { [postBody appendData:[self appendPostValue:self.userName forKey:@"name"]]; @@ -619,6 +650,28 @@ if (statusCode == 404) { // thread has been deleted, we archive it [self updateMessageListFromResponse:nil]; + } else if (statusCode == 409) { + // we submitted a message that is already on the server, mark it as being in conflict and resolve it with another fetch + + if (!self.token) { + // set the token to the first message token, since this is identical + __block NSString *token = nil; + + [_feedbackList enumerateObjectsUsingBlock:^(id objMessage, NSUInteger messagesIdx, BOOL *stop) { + if ([(BITFeedbackMessage *)objMessage status] == BITFeedbackMessageStatusSendInProgress) { + token = [(BITFeedbackMessage *)objMessage token]; + *stop = YES; + } + }]; + + if (token) { + self.token = token; + } + } + + [self markSendInProgressMessagesAsInConflict]; + [self saveMessages]; + [self performSelector:@selector(fetchMessageUpdates) withObject:nil afterDelay:0.2]; } else if ([responseData length]) { NSString *responseString = [[[NSString alloc] initWithBytes:[responseData bytes] length:[responseData length] encoding: NSUTF8StringEncoding] autorelease]; BITHockeyLog(@"INFO: Received API response: %@", responseString); @@ -662,7 +715,7 @@ } [self sendNetworkRequestWithHTTPMethod:@"GET" - withText:nil + withMessage:nil completionHandler:^(NSError *err){ // inform the UI to update its data in case the list is already showing [[NSNotificationCenter defaultCenter] postNotificationName:BITHockeyFeedbackMessagesLoadingFinished object:nil]; @@ -696,7 +749,7 @@ } [self sendNetworkRequestWithHTTPMethod:httpMethod - withText:[messageToSend text] + withMessage:messageToSend completionHandler:^(NSError *err){ if (err) { [self markSendInProgressMessagesAsPending]; @@ -714,6 +767,7 @@ BITFeedbackMessage *message = [[[BITFeedbackMessage alloc] init] autorelease]; message.text = text; [message setStatus:BITFeedbackMessageStatusSendPending]; + [message setToken:[self uuidAsLowerCaseAndShortened]]; [message setUserMessage:YES]; [_feedbackList addObject:message]; diff --git a/Classes/BITFeedbackMessage.h b/Classes/BITFeedbackMessage.h index 0263e40c78..4f8a5d9f4e 100644 --- a/Classes/BITFeedbackMessage.h +++ b/Classes/BITFeedbackMessage.h @@ -32,14 +32,16 @@ typedef enum { // default and new messages from SDK per default BITFeedbackMessageStatusSendPending = 0, + // message is in conflict, happens if the message is already stored on the server and tried sending it again + BITFeedbackMessageStatusInConflict = 1, // sending of message is in progress - BITFeedbackMessageStatusSendInProgress = 1, + BITFeedbackMessageStatusSendInProgress = 2, // new messages from server - BITFeedbackMessageStatusUnread = 2, + BITFeedbackMessageStatusUnread = 3, // messages from server once read and new local messages once successful send from SDK - BITFeedbackMessageStatusRead = 3, + BITFeedbackMessageStatusRead = 4, // message is archived, happens if the thread is deleted from the server - BITFeedbackMessageStatusArchived = 4 + BITFeedbackMessageStatusArchived = 5 } BITFeedbackMessageStatus; @interface BITFeedbackMessage : NSObject { @@ -50,6 +52,7 @@ typedef enum { @property (nonatomic, copy) NSString *email; @property (nonatomic, copy) NSDate *date; @property (nonatomic, copy) NSNumber *id; +@property (nonatomic, copy) NSString *token; @property (nonatomic) BITFeedbackMessageStatus status; @property (nonatomic) BOOL userMessage; diff --git a/Classes/BITFeedbackMessage.m b/Classes/BITFeedbackMessage.m index e19044b7cd..1087027950 100644 --- a/Classes/BITFeedbackMessage.m +++ b/Classes/BITFeedbackMessage.m @@ -40,6 +40,7 @@ _name = nil; _email = nil; _date = nil; + _token = nil; _id = [[NSNumber alloc] initWithInteger:0]; _status = BITFeedbackMessageStatusSendPending; _userMessage = NO; @@ -53,6 +54,7 @@ [_email release], _email = nil; [_date release], _date = nil; [_id release], _id = nil; + [_token release], _token = nil; [super dealloc]; } @@ -68,6 +70,7 @@ [encoder encodeObject:self.id forKey:@"id"]; [encoder encodeInteger:self.status forKey:@"status"]; [encoder encodeBool:self.userMessage forKey:@"userMessage"]; + [encoder encodeObject:self.token forKey:@"token"]; } - (id)initWithCoder:(NSCoder *)decoder { @@ -79,6 +82,7 @@ self.id = [decoder decodeObjectForKey:@"id"]; self.status = [decoder decodeIntegerForKey:@"status"]; self.userMessage = [decoder decodeBoolForKey:@"userMessage"]; + self.token = [decoder decodeObjectForKey:@"token"]; } return self; }