Improve feedback handling

- Messages deleted on the server will be internally marked as being archived
- Pre-send a token for each new message for easier identification and detection of double sending
This commit is contained in:
Andreas Linde 2012-10-15 00:06:54 +02:00
parent c8c1ad8e10
commit d884936029
3 changed files with 78 additions and 17 deletions

View File

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

View File

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

View File

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