diff --git a/Classes/BITActivityIndicatorButton.h b/Classes/BITActivityIndicatorButton.h new file mode 100644 index 0000000000..2c633222c9 --- /dev/null +++ b/Classes/BITActivityIndicatorButton.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#import + +@interface BITActivityIndicatorButton : UIButton + +- (void)setShowsActivityIndicator:(BOOL)showsIndicator; + +@end diff --git a/Classes/BITActivityIndicatorButton.m b/Classes/BITActivityIndicatorButton.m new file mode 100644 index 0000000000..00966186f6 --- /dev/null +++ b/Classes/BITActivityIndicatorButton.m @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#import "BITActivityIndicatorButton.h" + +@interface BITActivityIndicatorButton() + +@property (nonatomic, strong) UIActivityIndicatorView *indicator; +@property (nonatomic) BOOL indicatorVisible; + +@end + +@implementation BITActivityIndicatorButton + +- (void)setShowsActivityIndicator:(BOOL)showsIndicator { + if (self.indicatorVisible == showsIndicator){ + return; + } + + if (!self.indicator){ + self.indicator = [[UIActivityIndicatorView alloc] initWithFrame:self.bounds]; + [self addSubview:self.indicator]; + [self.indicator setColor:[UIColor blackColor]]; + } + + self.indicatorVisible = showsIndicator; + + if (showsIndicator){ + [self.indicator startAnimating]; + self.indicator.alpha = 1; + self.layer.borderWidth = 1; + self.layer.borderColor = [UIColor lightGrayColor].CGColor; + self.layer.cornerRadius = 5; + self.imageView.image = nil; + } else { + [self.indicator stopAnimating]; + self.layer.cornerRadius = 0; + self.indicator.alpha = 0; + self.layer.borderWidth = 0; + + } + +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + [self.indicator setFrame:self.bounds]; + +} + + +@end diff --git a/Classes/BITArrowImageAnnotation.h b/Classes/BITArrowImageAnnotation.h new file mode 100644 index 0000000000..feeca7613b --- /dev/null +++ b/Classes/BITArrowImageAnnotation.h @@ -0,0 +1,33 @@ +/* + * Author: Moritz Haarmann + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#import "BITImageAnnotation.h" + +@interface BITArrowImageAnnotation : BITImageAnnotation + +@end diff --git a/Classes/BITArrowImageAnnotation.m b/Classes/BITArrowImageAnnotation.m new file mode 100644 index 0000000000..2d739b4fdc --- /dev/null +++ b/Classes/BITArrowImageAnnotation.m @@ -0,0 +1,205 @@ +/* + * Author: Moritz Haarmann + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#import "BITArrowImageAnnotation.h" + +#define kArrowPointCount 7 + + +@interface BITArrowImageAnnotation() + +@property (nonatomic, strong) CAShapeLayer *shapeLayer; +@property (nonatomic, strong) CAShapeLayer *strokeLayer; + +@end + +@implementation BITArrowImageAnnotation + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + self.shapeLayer = [CAShapeLayer layer]; + self.shapeLayer.strokeColor = [UIColor whiteColor].CGColor; + self.shapeLayer.lineWidth = 5; + self.shapeLayer.fillColor = [UIColor redColor].CGColor; + + self.strokeLayer = [CAShapeLayer layer]; + self.strokeLayer.strokeColor = [UIColor redColor].CGColor; + self.strokeLayer.lineWidth = 10; + self.strokeLayer.fillColor = [UIColor clearColor].CGColor; + [self.layer addSublayer:self.strokeLayer]; + + [self.layer addSublayer:self.shapeLayer]; + + + } + return self; +} + +- (void)buildShape { + CGFloat baseWidth = MAX(self.frame.size.width, self.frame.size.height); + CGFloat topHeight = MAX(baseWidth / 3.0f,10); + + + CGFloat lineWidth = MAX(baseWidth / 10.0f,3); + CGFloat startX, startY, endX, endY; + + CGRect boundRect = CGRectInset(self.bounds, 0, 0); + CGFloat arrowLength= sqrt(pow(CGRectGetWidth(boundRect), 2) + pow(CGRectGetHeight(boundRect), 2)); + if (arrowLength < 30){ + + CGFloat factor = 30.f/arrowLength; + + boundRect = CGRectApplyAffineTransform(boundRect, CGAffineTransformMakeScale(factor,factor)); + } + + if ( self.movedDelta.width < 0){ + startX = CGRectGetMinX(boundRect); + endX = CGRectGetMaxX(boundRect); + } else { + startX = CGRectGetMaxX(boundRect); + endX = CGRectGetMinX(boundRect); + + } + + if ( self.movedDelta.height < 0){ + startY = CGRectGetMinY(boundRect); + endY = CGRectGetMaxY(boundRect); + } else { + startY = CGRectGetMaxY(boundRect); + endY = CGRectGetMinY(boundRect); + + } + + + if (abs(CGRectGetWidth(boundRect)) < 30 || abs(CGRectGetHeight(boundRect)) < 30){ + CGFloat smallerOne = MIN(abs(CGRectGetHeight(boundRect)), abs(CGRectGetWidth(boundRect))); + + CGFloat factor = smallerOne/30.f; + + CGRectApplyAffineTransform(boundRect, CGAffineTransformMakeScale(factor,factor)); + } + + UIBezierPath *path = [self bezierPathWithArrowFromPoint:CGPointMake(endX, endY) toPoint:CGPointMake(startX, startY) tailWidth:lineWidth headWidth:topHeight headLength:topHeight]; + + self.shapeLayer.path = path.CGPath; + self.strokeLayer.path = path.CGPath; + [CATransaction begin]; + [CATransaction setAnimationDuration:0]; + self.strokeLayer.lineWidth = lineWidth/1.5f; + self.shapeLayer.lineWidth = lineWidth / 3.0f; + + [CATransaction commit]; + +} + + +- (UIBezierPath *)bezierPathWithArrowFromPoint:(CGPoint)startPoint + toPoint:(CGPoint)endPoint + tailWidth:(CGFloat)tailWidth + headWidth:(CGFloat)headWidth + headLength:(CGFloat)headLength { + CGFloat length = hypotf(endPoint.x - startPoint.x, endPoint.y - startPoint.y); + + CGPoint points[kArrowPointCount]; + [self getAxisAlignedArrowPoints:points + forLength:length + tailWidth:tailWidth + headWidth:headWidth + headLength:headLength]; + + CGAffineTransform transform = [self transformForStartPoint:startPoint + endPoint:endPoint + length:length]; + + CGMutablePathRef cgPath = CGPathCreateMutable(); + CGPathAddLines(cgPath, &transform, points, sizeof points / sizeof *points); + CGPathCloseSubpath(cgPath); + + UIBezierPath *uiPath = [UIBezierPath bezierPathWithCGPath:cgPath]; + CGPathRelease(cgPath); + return uiPath; +} + +- (void)getAxisAlignedArrowPoints:(CGPoint[kArrowPointCount])points + forLength:(CGFloat)length + tailWidth:(CGFloat)tailWidth + headWidth:(CGFloat)headWidth + headLength:(CGFloat)headLength { + CGFloat tailLength = length - headLength; + points[0] = CGPointMake(0, tailWidth / 2); + points[1] = CGPointMake(tailLength, tailWidth / 2); + points[2] = CGPointMake(tailLength, headWidth / 2); + points[3] = CGPointMake(length, 0); + points[4] = CGPointMake(tailLength, -headWidth / 2); + points[5] = CGPointMake(tailLength, -tailWidth / 2); + points[6] = CGPointMake(0, -tailWidth / 2); +} + ++ (CGAffineTransform)dqd_transformForStartPoint:(CGPoint)startPoint + endPoint:(CGPoint)endPoint + length:(CGFloat)length { + CGFloat cosine = (endPoint.x - startPoint.x) / length; + CGFloat sine = (endPoint.y - startPoint.y) / length; + return (CGAffineTransform){ cosine, sine, -sine, cosine, startPoint.x, startPoint.y }; +} + +- (CGAffineTransform)transformForStartPoint:(CGPoint)startPoint + endPoint:(CGPoint)endPoint + length:(CGFloat)length { + CGFloat cosine = (endPoint.x - startPoint.x) / length; + CGFloat sine = (endPoint.y - startPoint.y) / length; + return (CGAffineTransform){ cosine, sine, -sine, cosine, startPoint.x, startPoint.y }; +} + +#pragma mark - UIView + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { + + CGPathRef strokePath = CGPathCreateCopyByStrokingPath(self.shapeLayer.path, NULL, fmaxf(90.0f, self.shapeLayer.lineWidth), kCGLineCapRound,kCGLineJoinMiter,0); + + BOOL containsPoint = CGPathContainsPoint(strokePath, NULL, point, NO); + + CGPathRelease(strokePath); + + if (containsPoint){ + return self; + } else { + return nil; + } + +} + +-(void)layoutSubviews{ + [super layoutSubviews]; + + [self buildShape]; +} + +@end diff --git a/Classes/BITBlurImageAnnotation.h b/Classes/BITBlurImageAnnotation.h new file mode 100644 index 0000000000..cb5a1f3ecc --- /dev/null +++ b/Classes/BITBlurImageAnnotation.h @@ -0,0 +1,33 @@ +/* + * Author: Moritz Haarmann + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#import "BITImageAnnotation.h" + +@interface BITBlurImageAnnotation : BITImageAnnotation + +@end diff --git a/Classes/BITBlurImageAnnotation.m b/Classes/BITBlurImageAnnotation.m new file mode 100644 index 0000000000..4760229d9f --- /dev/null +++ b/Classes/BITBlurImageAnnotation.m @@ -0,0 +1,102 @@ +/* + * Author: Moritz Haarmann + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#import "BITBlurImageAnnotation.h" + +@interface BITBlurImageAnnotation() + +@property (nonatomic, strong) CALayer* imageLayer; +@property (nonatomic, strong) UIImage* scaledImage; +@property (nonatomic, strong) CALayer* selectedLayer; + + +@end + +@implementation BITBlurImageAnnotation + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + self.clipsToBounds = YES; + self.imageLayer = [CALayer layer]; + [self.layer addSublayer:self.imageLayer]; + + self.selectedLayer = [CALayer layer]; + [self.layer insertSublayer:self.selectedLayer above:self.imageLayer]; + + self.selectedLayer.backgroundColor = [[UIColor redColor] colorWithAlphaComponent:0.5f].CGColor; + self.selectedLayer.opacity = 0.6f; + self.clipsToBounds = YES; + } + return self; +} + +-(void)setSourceImage:(UIImage *)sourceImage { + CGSize size = CGSizeMake(sourceImage.size.width/30, sourceImage.size.height/30); + + UIGraphicsBeginImageContext(size); + + [sourceImage drawInRect:CGRectMake(0, 0, size.width, size.height)]; + self.scaledImage = UIGraphicsGetImageFromCurrentImageContext(); + self.imageLayer.shouldRasterize = YES; + self.imageLayer.rasterizationScale = 1; + self.imageLayer.magnificationFilter = kCAFilterNearest; + self.imageLayer.contents = (id)self.scaledImage.CGImage; + + UIGraphicsEndImageContext(); +} + +- (void)setSelected:(BOOL)selected { + self->_selected = selected; + + if (selected){ + self.selectedLayer.opacity = 0.6f; + } else { + self.selectedLayer.opacity = 0.0f; + } +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + [CATransaction begin]; + [CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions]; + + self.imageLayer.frame = self.imageFrame; + self.imageLayer.masksToBounds = YES; + + self.selectedLayer.frame= self.bounds; + [CATransaction commit]; +} + +-(BOOL)resizable { + return YES; +} + +@end diff --git a/Classes/BITCrashManager.m b/Classes/BITCrashManager.m index 255e84ea56..bb7190f2d5 100644 --- a/Classes/BITCrashManager.m +++ b/Classes/BITCrashManager.m @@ -850,21 +850,23 @@ static PLCrashReporterCallbacks plCrashCallbacks = { if (_crashManagerStatus == BITCrashManagerStatusDisabled) return NO; if ([self.fileManager fileExistsAtPath:_crashesDir]) { - NSString *file = nil; NSError *error = NULL; - NSDirectoryEnumerator *dirEnum = [self.fileManager enumeratorAtPath: _crashesDir]; + NSArray *dirArray = [self.fileManager contentsOfDirectoryAtPath:_crashesDir error:&error]; - while ((file = [dirEnum nextObject])) { - NSDictionary *fileAttributes = [self.fileManager attributesOfItemAtPath:[_crashesDir stringByAppendingPathComponent:file] error:&error]; - if ([[fileAttributes objectForKey:NSFileSize] intValue] > 0 && + for (NSString *file in dirArray) { + NSString *filePath = [_crashesDir stringByAppendingPathComponent:file]; + + NSDictionary *fileAttributes = [self.fileManager attributesOfItemAtPath:filePath error:&error]; + if ([[fileAttributes objectForKey:NSFileType] isEqualToString:NSFileTypeRegular] && + [[fileAttributes objectForKey:NSFileSize] intValue] > 0 && ![file hasSuffix:@".DS_Store"] && ![file hasSuffix:@".analyzer"] && ![file hasSuffix:@".plist"] && ![file hasSuffix:@".data"] && ![file hasSuffix:@".meta"] && ![file hasSuffix:@".desc"]) { - [_crashFiles addObject:[_crashesDir stringByAppendingPathComponent: file]]; + [_crashFiles addObject:filePath]; } } } diff --git a/Classes/BITFeedbackComposeViewController.h b/Classes/BITFeedbackComposeViewController.h index 488e3004e6..2ef4f84664 100644 --- a/Classes/BITFeedbackComposeViewController.h +++ b/Classes/BITFeedbackComposeViewController.h @@ -70,8 +70,11 @@ The follwoing data object classes are currently supported: - NSString - NSURL + - UIImage + - NSData - These are automatically concatenated to one text string. + These are automatically concatenated to one text string, while any images and NSData + objects are added as attachments to the feedback. @param items Array of data objects to prefill the feedback text message. */ diff --git a/Classes/BITFeedbackComposeViewController.m b/Classes/BITFeedbackComposeViewController.m index c4eda0230c..1677536289 100644 --- a/Classes/BITFeedbackComposeViewController.m +++ b/Classes/BITFeedbackComposeViewController.m @@ -34,6 +34,7 @@ #import "HockeySDKPrivate.h" #import "BITFeedbackManagerPrivate.h" +#import "BITFeedbackMessageAttachment.h" #import "BITFeedbackComposeViewController.h" #import "BITFeedbackUserDataViewController.h" @@ -41,21 +42,35 @@ #import "BITHockeyHelper.h" +#import "BITImageAnnotationViewController.h" -@interface BITFeedbackComposeViewController () { + +@interface BITFeedbackComposeViewController () { UIStatusBarStyle _statusBarStyle; } @property (nonatomic, weak) BITFeedbackManager *manager; @property (nonatomic, strong) UITextView *textView; +@property (nonatomic, strong) UIView *contentViewContainer; +@property (nonatomic, strong) UIScrollView *attachmentScrollView; +@property (nonatomic, strong) NSMutableArray *attachmentScrollViewImageViews; @property (nonatomic, strong) NSString *text; +@property (nonatomic, strong) NSMutableArray *attachments; +@property (nonatomic, strong) NSMutableArray *imageAttachments; + +@property (nonatomic, strong) UIView *textAccessoryView; +@property (nonatomic) NSInteger selectedAttachmentIndex; +@property (nonatomic, strong) UITapGestureRecognizer *tapRecognizer; + @end @implementation BITFeedbackComposeViewController { - BOOL _blockUserDataScreen; + BOOL _blockUserDataScreen; + + BOOL _actionSheetVisible; } @@ -66,8 +81,14 @@ if (self) { self.title = BITHockeyLocalizedString(@"HockeyFeedbackComposeTitle"); _blockUserDataScreen = NO; + _actionSheetVisible = NO; _delegate = nil; _manager = [BITHockeyManager sharedHockeyManager].feedbackManager; + _attachments = [NSMutableArray new]; + _imageAttachments = [NSMutableArray new]; + _attachmentScrollViewImageViews = [NSMutableArray new]; + self.tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(scrollViewTapped:)]; + [self.attachmentScrollView addGestureRecognizer:self.tapRecognizer]; _text = nil; } @@ -84,6 +105,13 @@ self.text = [(self.text ? self.text : @"") stringByAppendingFormat:@"%@%@", (self.text ? @" " : @""), item]; } else if ([item isKindOfClass:[NSURL class]]) { self.text = [(self.text ? self.text : @"") stringByAppendingFormat:@"%@%@", (self.text ? @" " : @""), [(NSURL *)item absoluteString]]; + } else if ([item isKindOfClass:[UIImage class]]) { + UIImage *image = item; + BITFeedbackMessageAttachment *attachment = [BITFeedbackMessageAttachment attachmentWithData:UIImageJPEGRepresentation(image, 0.7f) contentType:@"image/jpeg"]; + [self.attachments addObject:attachment]; + [self.imageAttachments addObject:attachment]; + } else if ([item isKindOfClass:[NSData class]]) { + [self.attachments addObject:[BITFeedbackMessageAttachment attachmentWithData:item contentType:@"'application/octet-stream'"]]; } else { BITHockeyLog(@"Unknown item type %@", item); } @@ -122,12 +150,15 @@ frame.size.height = windowSize.width - navBarHeight - modalGap - kbSize.width; } } - [self.textView setFrame:frame]; + [self.contentViewContainer setFrame:frame]; + + [self performSelector:@selector(refreshAttachmentScrollview) withObject:nil afterDelay:0.0f]; + } - (void)keyboardWillBeHidden:(NSNotification*)aNotification { CGRect frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); - [self.textView setFrame:frame]; + [self.contentViewContainer setFrame:frame]; } @@ -142,20 +173,48 @@ self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(dismissAction:)]; - self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackComposeSend") style:UIBarButtonItemStyleDone target:self action:@selector(sendAction:)]; + + // Container that contains both the textfield and eventually the photo scroll view on the right side + self.contentViewContainer = [[UIView alloc] initWithFrame:self.view.bounds]; + self.contentViewContainer.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; + [self.view addSubview:self.contentViewContainer]; + // message input textfield - self.textView = [[UITextView alloc] initWithFrame:self.view.frame]; + self.textView = [[UITextView alloc] initWithFrame:self.view.bounds]; self.textView.font = [UIFont systemFontOfSize:17]; self.textView.delegate = self; self.textView.backgroundColor = [UIColor whiteColor]; self.textView.returnKeyType = UIReturnKeyDefault; - self.textView.autoresizingMask = UIViewAutoresizingFlexibleWidth; - [self.view addSubview:self.textView]; + self.textView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; + + [self.contentViewContainer addSubview:self.textView]; + + // Add Photo Button + Container that's displayed above the keyboard. + self.textAccessoryView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.view.frame), 44)]; + self.textAccessoryView.backgroundColor = [UIColor colorWithRed:0.9f green:0.9f blue:0.9f alpha:1.0f]; + UIButton *addPhotoButton = [UIButton buttonWithType:UIButtonTypeCustom]; + [addPhotoButton setTitle:BITHockeyLocalizedString(@"HockeyFeedbackComposeAttachmentAddImage") forState:UIControlStateNormal]; + [addPhotoButton setTitleColor:[UIColor darkGrayColor] forState:UIControlStateNormal]; + addPhotoButton.frame = CGRectMake(0, 0, CGRectGetWidth(self.view.frame), 44); + [addPhotoButton addTarget:self action:@selector(addPhotoAction:) forControlEvents:UIControlEventTouchUpInside]; + + [self.textAccessoryView addSubview:addPhotoButton]; + + self.textView.inputAccessoryView = self.textAccessoryView; + + // This could be a subclass, yet + self.attachmentScrollView = [[UIScrollView alloc] initWithFrame:CGRectZero]; + self.attachmentScrollView.scrollEnabled = YES; + self.attachmentScrollView.bounces = YES; + self.attachmentScrollView.autoresizesSubviews = NO; + self.attachmentScrollView.autoresizingMask = UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleRightMargin; + + [self.contentViewContainer addSubview:self.attachmentScrollView]; } - (void)viewWillAppear:(BOOL)animated { @@ -178,14 +237,13 @@ [[UIApplication sharedApplication] setStatusBarStyle:(self.navigationController.navigationBar.barStyle == UIBarStyleDefault) ? UIStatusBarStyleDefault : UIStatusBarStyleBlackOpaque]; #endif - [self.textView setFrame:self.view.frame]; + // [self.textView setFrame:self.view.frame]; if (_text) { self.textView.text = _text; - self.navigationItem.rightBarButtonItem.enabled = YES; - } else { - self.navigationItem.rightBarButtonItem.enabled = NO; } + + [self updateBarButtonState]; } - (void)viewDidAppear:(BOOL)animated { @@ -210,7 +268,7 @@ self.manager.currentFeedbackComposeViewController = nil; [super viewWillDisappear:animated]; - + [[UIApplication sharedApplication] setStatusBarStyle:_statusBarStyle]; } @@ -218,6 +276,89 @@ [super viewDidDisappear:animated]; } +-(void)refreshAttachmentScrollview { + CGFloat scrollViewWidth = 0; + + if (self.imageAttachments.count){ + scrollViewWidth = 100; + } + + CGRect textViewFrame = self.textView.frame; + + CGRect scrollViewFrame = self.attachmentScrollView.frame; + + BOOL alreadySetup = CGRectGetWidth(scrollViewFrame) > 0; + + if (alreadySetup && self.imageAttachments.count == 0) { + textViewFrame.size.width += 100; + self.textView.frame = textViewFrame; + scrollViewFrame.size.width = 0; + self.attachmentScrollView.frame = scrollViewFrame; + return; + } + + if (!alreadySetup) { + textViewFrame.size.width -= scrollViewWidth; + scrollViewFrame = CGRectMake(CGRectGetMaxX(textViewFrame), self.view.frame.origin.y, scrollViewWidth, CGRectGetHeight(self.view.bounds)); + self.textView.frame = textViewFrame; + self.attachmentScrollView.frame = scrollViewFrame; + self.attachmentScrollView.contentInset = self.textView.contentInset; + } + + if (self.imageAttachments.count > self.attachmentScrollViewImageViews.count){ + NSInteger numberOfViewsToCreate = self.imageAttachments.count - self.attachmentScrollViewImageViews.count; + for (int i = 0; i 0 ) { + self.navigationItem.rightBarButtonItem.enabled = YES; + } else { + self.navigationItem.rightBarButtonItem.enabled = NO; + } + + if (self.imageAttachments.count > 2){ + self.textView.inputAccessoryView = nil; + } else { + self.textView.inputAccessoryView = self.textAccessoryView; + + } +} + #pragma mark - UIViewController Rotation @@ -241,6 +382,10 @@ #pragma mark - Actions - (void)dismissAction:(id)sender { + for (BITFeedbackMessageAttachment *attachment in self.attachments){ + [attachment deleteContents]; + } + [self dismissWithResult:BITFeedbackComposeResultCancelled]; } @@ -250,7 +395,7 @@ NSString *text = self.textView.text; - [self.manager submitMessageWithText:text]; + [self.manager submitMessageWithText:text andAttachments:self.attachments]; [self dismissWithResult:BITFeedbackComposeResultSubmitted]; } @@ -268,6 +413,70 @@ } } +-(void)addPhotoAction:(id)sender { + if (_actionSheetVisible) return; + + // add photo. + UIImagePickerController *pickerController = [[UIImagePickerController alloc] init]; + pickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; + pickerController.delegate = self; + pickerController.editing = NO; + [self presentViewController:pickerController animated:YES completion:nil]; +} + +- (void)scrollViewTapped:(id)unused { + UIMenuController *menuController = [UIMenuController sharedMenuController]; + [menuController setTargetRect:CGRectMake([self.tapRecognizer locationInView:self.view].x, [self.tapRecognizer locationInView:self.view].x, 1, 1) inView:self.view]; + [menuController setMenuVisible:YES animated:YES]; +} + +- (void)paste:(id)sender { + +} + +#pragma mark - UIImagePickerControllerDelegate + +- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { + UIImage *pickedImage = info[UIImagePickerControllerOriginalImage]; + + if (pickedImage){ + NSData *imageData = UIImageJPEGRepresentation(pickedImage, 0.7f); + BITFeedbackMessageAttachment *newAttachment = [BITFeedbackMessageAttachment attachmentWithData:imageData contentType:@"image/jpeg"]; + NSURL *imagePath = [info objectForKey:@"UIImagePickerControllerReferenceURL"]; + NSString *imageName = [imagePath lastPathComponent]; + newAttachment.originalFilename = imageName; + [self.attachments addObject:newAttachment]; + [self.imageAttachments addObject:newAttachment]; + } + + [picker dismissViewControllerAnimated:YES completion:nil]; +} + +- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { + [picker dismissViewControllerAnimated:YES completion:nil]; +} + +- (void)imageButtonAction:(UIButton *)sender { + // determine the index of the feedback + NSInteger index = [self.attachmentScrollViewImageViews indexOfObject:sender]; + + self.selectedAttachmentIndex = index; + + UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle: nil + delegate: self + cancelButtonTitle: BITHockeyLocalizedString(@"HockeyFeedbackComposeAttachmentCancel") + destructiveButtonTitle: BITHockeyLocalizedString(@"HockeyFeedbackComposeAttachmentDelete") + otherButtonTitles: BITHockeyLocalizedString(@"HockeyFeedbackComposeAttachmentEdit"), nil]; + + [actionSheet showFromRect: sender.frame inView: self.attachmentScrollView animated: YES]; + + _actionSheetVisible = YES; + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + [self.textView resignFirstResponder]; + } +} + + #pragma mark - BITFeedbackUserDataDelegate - (void)userDataUpdateCancelled { @@ -298,15 +507,61 @@ #pragma mark - UITextViewDelegate - (void)textViewDidChange:(UITextView *)textView { - NSUInteger newLength = [textView.text length]; - if (newLength == 0) { - self.navigationItem.rightBarButtonItem.enabled = NO; - } else { - self.navigationItem.rightBarButtonItem.enabled = YES; - } + [self updateBarButtonState]; } +#pragma mark - UIActionSheet Delegate + +- (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSInteger)buttonIndex { + if (buttonIndex == [actionSheet destructiveButtonIndex]) { + + if (self.selectedAttachmentIndex != NSNotFound){ + BITFeedbackMessageAttachment *attachment = self.imageAttachments[self.selectedAttachmentIndex]; + [attachment deleteContents]; // mandatory call to delete the files associatd. + [self.imageAttachments removeObject:attachment]; + [self.attachments removeObject:attachment]; + } + self.selectedAttachmentIndex = NSNotFound; + + [self refreshAttachmentScrollview]; + + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + [self.textView becomeFirstResponder]; + } + } else if (buttonIndex != [actionSheet cancelButtonIndex]) { + if (self.selectedAttachmentIndex != NSNotFound){ + BITFeedbackMessageAttachment *attachment = self.imageAttachments[self.selectedAttachmentIndex]; + BITImageAnnotationViewController *annotationEditor = [[BITImageAnnotationViewController alloc ] init]; + annotationEditor.delegate = self; + UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:annotationEditor]; + annotationEditor.image = attachment.imageRepresentation; + [self presentViewController:navController animated:YES completion:nil]; + } + } else { + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + [self.textView becomeFirstResponder]; + } + } + _actionSheetVisible = NO; +} + + +#pragma mark - Image Annotation Delegate + +- (void)annotationController:(BITImageAnnotationViewController *)annotationController didFinishWithImage:(UIImage *)image { + if (self.selectedAttachmentIndex != NSNotFound){ + BITFeedbackMessageAttachment *attachment = self.imageAttachments[self.selectedAttachmentIndex]; + [attachment replaceData:UIImageJPEGRepresentation(image, 0.7f)]; + } + + self.selectedAttachmentIndex = NSNotFound; +} + +- (void)annotationControllerDidCancel:(BITImageAnnotationViewController *)annotationController { + self.selectedAttachmentIndex = NSNotFound; +} + @end #endif /* HOCKEYSDK_FEATURE_FEEDBACK */ diff --git a/Classes/BITFeedbackListViewCell.h b/Classes/BITFeedbackListViewCell.h index 1a71800af1..f146b8acf9 100644 --- a/Classes/BITFeedbackListViewCell.h +++ b/Classes/BITFeedbackListViewCell.h @@ -31,6 +31,15 @@ #import "BITFeedbackMessage.h" #import "BITAttributedLabel.h" +@class BITFeedbackMessageAttachment; + +@protocol BITFeedbackListViewCellDelegate + +- (void)listCell:(id)cell didSelectAttachment:(BITFeedbackMessageAttachment *)attachment; + +@end + + /** * Cell style depending on the iOS version */ @@ -59,6 +68,7 @@ typedef NS_ENUM(NSUInteger, BITFeedbackListViewCellBackgroundStyle) { BITFeedbackListViewCellBackgroundStyleAlternate = 1 }; + @interface BITFeedbackListViewCell : UITableViewCell @property (nonatomic, strong) BITFeedbackMessage *message; @@ -69,6 +79,10 @@ typedef NS_ENUM(NSUInteger, BITFeedbackListViewCellBackgroundStyle) { @property (nonatomic, strong) BITAttributedLabel *labelText; +@property (nonatomic, weak) id delegate; + + (CGFloat) heightForRowWithMessage:(BITFeedbackMessage *)message tableViewWidth:(CGFloat)width; +- (void)setAttachments:(NSArray *)attachments; + @end diff --git a/Classes/BITFeedbackListViewCell.m b/Classes/BITFeedbackListViewCell.m index 8c8e90bddb..367721ac39 100644 --- a/Classes/BITFeedbackListViewCell.m +++ b/Classes/BITFeedbackListViewCell.m @@ -26,9 +26,11 @@ * OTHER DEALINGS IN THE SOFTWARE. */ +#import "HockeySDKPrivate.h" #import "BITFeedbackListViewCell.h" -#import "HockeySDKPrivate.h" +#import "BITFeedbackMessageAttachment.h" +#import "BITActivityIndicatorButton.h" #define BACKGROUNDCOLOR_DEFAULT BIT_RGBCOLOR(245, 245, 245) #define BACKGROUNDCOLOR_ALTERNATE BIT_RGBCOLOR(235, 235, 235) @@ -54,6 +56,9 @@ #define LABEL_TEXT_Y 25 +#define ATTACHMENT_SIZE 45 + + @interface BITFeedbackListViewCell () @property (nonatomic, strong) NSDateFormatter *dateFormatter; @@ -61,6 +66,10 @@ @property (nonatomic, strong) UILabel *labelTitle; +@property (nonatomic, strong) NSMutableArray *attachmentViews; + +@property (nonatomic, strong) UIView *accessoryBackgroundView; + @end @@ -79,15 +88,15 @@ self.dateFormatter = [[NSDateFormatter alloc] init]; [self.dateFormatter setTimeStyle:NSDateFormatterNoStyle]; [self.dateFormatter setDateStyle:NSDateFormatterMediumStyle]; - [self.dateFormatter setLocale:[NSLocale currentLocale]]; + [self.dateFormatter setLocale:[NSLocale currentLocale]]; [self.dateFormatter setDoesRelativeDateFormatting:YES]; - + self.timeFormatter = [[NSDateFormatter alloc] init]; [self.timeFormatter setTimeStyle:NSDateFormatterShortStyle]; [self.timeFormatter setDateStyle:NSDateFormatterNoStyle]; [self.timeFormatter setLocale:[NSLocale currentLocale]]; [self.timeFormatter setDoesRelativeDateFormatting:YES]; - + self.labelTitle = [[UILabel alloc] init]; self.labelTitle.font = [UIFont systemFontOfSize:TITLE_FONTSIZE]; @@ -96,6 +105,8 @@ self.labelText.numberOfLines = 0; self.labelText.textAlignment = kBITTextLabelAlignmentLeft; self.labelText.dataDetectorTypes = UIDataDetectorTypeAll; + + self.attachmentViews = [NSMutableArray new]; } return self; } @@ -104,6 +115,7 @@ #pragma mark - Private - (UIColor *)backgroundColor { + if (self.backgroundStyle == BITFeedbackListViewCellBackgroundStyleNormal) { if (self.style == BITFeedbackListViewCellPresentatationStyleDefault) { return BACKGROUNDCOLOR_DEFAULT; @@ -135,6 +147,19 @@ #pragma mark - Layout + (CGFloat) heightForRowWithMessage:(BITFeedbackMessage *)message tableViewWidth:(CGFloat)width { + + CGFloat baseHeight = [self heightForTextInRowWithMessage:message tableViewWidth:width]; + + CGFloat attachmentsPerRow = floorf(width / (FRAME_SIDE_BORDER + ATTACHMENT_SIZE)); + + CGFloat calculatedHeight = baseHeight + (FRAME_TOP_BORDER + ATTACHMENT_SIZE) * ceil([message previewableAttachments].count / attachmentsPerRow); + + return ceil(calculatedHeight); +} + + + ++ (CGFloat) heightForTextInRowWithMessage:(BITFeedbackMessage *)message tableViewWidth:(CGFloat)width { CGFloat calculatedHeight; #if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_6_1 @@ -143,7 +168,11 @@ options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:TEXT_FONTSIZE]} context:nil]; - calculatedHeight = calculatedRect.size.height + FRAME_TOP_BORDER + LABEL_TEXT_Y + FRAME_BOTTOM_BORDER; + calculatedHeight = calculatedRect.size.height + FRAME_TOP_BORDER + LABEL_TEXT_Y + FRAME_BOTTOM_BORDER; + + // added to make space for the images. + + } else { #endif #pragma clang diagnostic push @@ -151,6 +180,7 @@ calculatedHeight = [message.text sizeWithFont:[UIFont systemFontOfSize:TEXT_FONTSIZE] constrainedToSize:CGSizeMake(width - (2 * FRAME_SIDE_BORDER), CGFLOAT_MAX) ].height + FRAME_TOP_BORDER + LABEL_TEXT_Y + FRAME_BOTTOM_BORDER; + #pragma clang diagnostic pop #if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_6_1 } @@ -159,13 +189,48 @@ return ceil(calculatedHeight); } -- (void)layoutSubviews { - UIView *accessoryViewBackground = [[UIView alloc] initWithFrame:CGRectMake(0, 2, self.frame.size.width * 2, self.frame.size.height - 2)]; - accessoryViewBackground.autoresizingMask = UIViewAutoresizingFlexibleHeight; - accessoryViewBackground.clipsToBounds = YES; +- (void)setAttachments:(NSArray *)attachments { + for (UIView *view in self.attachmentViews){ + [view removeFromSuperview]; + } - // colors - accessoryViewBackground.backgroundColor = [self backgroundColor]; + [self.attachmentViews removeAllObjects]; + + for (BITFeedbackMessageAttachment *attachment in attachments){ + if (attachment.localURL || attachment.sourceURL) { + BITActivityIndicatorButton *imageView = [BITActivityIndicatorButton buttonWithType:UIButtonTypeCustom]; + + if (attachment.localURL){ + [imageView setImage:[attachment thumbnailWithSize:CGSizeMake(ATTACHMENT_SIZE, ATTACHMENT_SIZE)] forState:UIControlStateNormal]; + [imageView setShowsActivityIndicator:NO]; + } else { + [imageView setImage:nil forState:UIControlStateNormal]; + [imageView setShowsActivityIndicator:YES]; + } + [imageView setContentMode:UIViewContentModeScaleAspectFit]; + [imageView addTarget:self action:@selector(imageButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; + + [self.attachmentViews addObject:imageView]; + } + } +} + + +- (void)layoutSubviews { + if (!self.accessoryBackgroundView){ + self.accessoryBackgroundView = [[UIView alloc] initWithFrame:CGRectMake(0, 2, self.frame.size.width * 2, self.frame.size.height - 2)]; + self.accessoryBackgroundView.autoresizingMask = UIViewAutoresizingFlexibleHeight; + self.accessoryBackgroundView.clipsToBounds = YES; + + // colors + self.accessoryBackgroundView.backgroundColor = [self backgroundColor]; + } + + if (self.style == BITFeedbackListViewCellPresentatationStyleDefault) { + [self addSubview:self.accessoryBackgroundView]; + } else if (self.accessoryBackgroundView.superview){ + [self.accessoryBackgroundView removeFromSuperview]; + } self.contentView.backgroundColor = [self backgroundColor]; self.labelTitle.backgroundColor = [self backgroundColor]; self.labelText.backgroundColor = [self backgroundColor]; @@ -176,11 +241,9 @@ } else { [self.labelText setTextColor:TEXTCOLOR_DEFAULT]; } - + // background for deletion accessory view - if (self.style == BITFeedbackListViewCellPresentatationStyleDefault) { - [self addSubview:accessoryViewBackground]; - } + // header NSString *dateString = @""; @@ -195,7 +258,7 @@ } [self.labelTitle setText:dateString]; [self.labelTitle setFrame:CGRectMake(FRAME_SIDE_BORDER, FRAME_TOP_BORDER + LABEL_TITLE_Y, self.frame.size.width - (2 * FRAME_SIDE_BORDER), LABEL_TITLE_HEIGHT)]; - + if (_message.userMessage) { self.labelTitle.textAlignment = kBITTextLabelAlignmentRight; self.labelText.textAlignment = kBITTextLabelAlignmentRight; @@ -203,20 +266,58 @@ self.labelTitle.textAlignment = kBITTextLabelAlignmentLeft; self.labelText.textAlignment = kBITTextLabelAlignmentLeft; } - + [self addSubview:self.labelTitle]; - + // text [self.labelText setText:_message.text]; - CGSize size = CGSizeMake(self.frame.size.width - (2 * FRAME_SIDE_BORDER), - [[self class] heightForRowWithMessage:_message tableViewWidth:self.frame.size.width] - LABEL_TEXT_Y - FRAME_BOTTOM_BORDER); + CGSize sizeForTextLabel = CGSizeMake(self.frame.size.width - (2 * FRAME_SIDE_BORDER), + [[self class] heightForTextInRowWithMessage:_message tableViewWidth:self.frame.size.width] - LABEL_TEXT_Y - FRAME_BOTTOM_BORDER); - [self.labelText setFrame:CGRectMake(FRAME_SIDE_BORDER, LABEL_TEXT_Y, size.width, size.height)]; + [self.labelText setFrame:CGRectMake(FRAME_SIDE_BORDER, LABEL_TEXT_Y, sizeForTextLabel.width, sizeForTextLabel.height)]; [self addSubview:self.labelText]; + CGFloat baseOffsetOfText = CGRectGetMaxY(self.labelText.frame); + + + int i = 0; + + CGFloat attachmentsPerRow = floorf(self.frame.size.width / (FRAME_SIDE_BORDER + ATTACHMENT_SIZE)); + + for (BITActivityIndicatorButton *imageButton in self.attachmentViews) { + imageButton.contentMode = UIViewContentModeScaleAspectFit; + imageButton.imageView.contentMode = UIViewContentModeScaleAspectFill; + + if (!_message.userMessage) { + imageButton.frame = CGRectMake(FRAME_SIDE_BORDER + (FRAME_SIDE_BORDER + ATTACHMENT_SIZE) * (i%(int)attachmentsPerRow) , floor(i/attachmentsPerRow)*(FRAME_SIDE_BORDER + ATTACHMENT_SIZE) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE); + } else { + imageButton.frame = CGRectMake(self.frame.size.width - FRAME_SIDE_BORDER - ATTACHMENT_SIZE - ((FRAME_SIDE_BORDER + ATTACHMENT_SIZE) * (i%(int)attachmentsPerRow) ), floor(i/attachmentsPerRow)*(FRAME_SIDE_BORDER + ATTACHMENT_SIZE) + baseOffsetOfText , ATTACHMENT_SIZE, ATTACHMENT_SIZE); + } + + if (!imageButton.superview) { + if (self.accessoryBackgroundView.superview) { + [self insertSubview:imageButton aboveSubview:self.accessoryBackgroundView]; + } else { + [self addSubview:imageButton]; + } + } + + i++; + } + [super layoutSubviews]; } +- (void)imageButtonPressed:(id)sender { + if ([self.delegate respondsToSelector:@selector(listCell:didSelectAttachment:)]) { + NSInteger index = [self.attachmentViews indexOfObject:sender]; + if (index != NSNotFound && [self.message previewableAttachments].count > index) { + BITFeedbackMessageAttachment *attachment = [self.message previewableAttachments][index]; + [self.delegate listCell:self didSelectAttachment:attachment]; + } + } +} + @end diff --git a/Classes/BITFeedbackListViewController.h b/Classes/BITFeedbackListViewController.h index 0a513904cf..8ee2b064d9 100644 --- a/Classes/BITFeedbackListViewController.h +++ b/Classes/BITFeedbackListViewController.h @@ -28,6 +28,7 @@ #import +#import #import "BITHockeyBaseViewController.h" @@ -54,7 +55,7 @@ This ensures that the presentation on iOS 6 and iOS 7 will use the corret design on each OS Version. */ -@interface BITFeedbackListViewController : BITHockeyBaseViewController { +@interface BITFeedbackListViewController : BITHockeyBaseViewController { } @end diff --git a/Classes/BITFeedbackListViewController.m b/Classes/BITFeedbackListViewController.m index 812634cac9..9397bf856a 100644 --- a/Classes/BITFeedbackListViewController.m +++ b/Classes/BITFeedbackListViewController.m @@ -34,17 +34,20 @@ #import "HockeySDKPrivate.h" #import "BITFeedbackManagerPrivate.h" +#import "BITFeedbackManager.h" #import "BITFeedbackListViewController.h" #import "BITFeedbackListViewCell.h" #import "BITFeedbackComposeViewController.h" #import "BITFeedbackUserDataViewController.h" #import "BITFeedbackMessage.h" +#import "BITFeedbackMessageAttachment.h" #import "BITAttributedLabel.h" #import "BITHockeyBaseManagerPrivate.h" #import "BITHockeyHelper.h" #import +#import #define DEFAULT_BACKGROUNDCOLOR BIT_RGBCOLOR(245, 245, 245) @@ -64,11 +67,13 @@ #define BORDER_COLOR BIT_RGBCOLOR(215, 215, 215) -@interface BITFeedbackListViewController () +@interface BITFeedbackListViewController () @property (nonatomic, weak) BITFeedbackManager *manager; @property (nonatomic, strong) NSDateFormatter *lastUpdateDateFormatter; @property (nonatomic) BOOL userDataComposeFlow; +@property (nonatomic, strong) NSArray *cachedPreviewItems; +@property (nonatomic, strong) NSOperationQueue *thumbnailQueue; @end @@ -90,6 +95,8 @@ [self.lastUpdateDateFormatter setDateStyle:NSDateFormatterShortStyle]; [self.lastUpdateDateFormatter setTimeStyle:NSDateFormatterShortStyle]; self.lastUpdateDateFormatter.locale = [NSLocale currentLocale]; + + _thumbnailQueue = [NSOperationQueue new]; } return self; } @@ -98,7 +105,7 @@ - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self name:BITHockeyFeedbackMessagesLoadingStarted object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:BITHockeyFeedbackMessagesLoadingFinished object:nil]; - + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(showDelayedUserDataViewController) object:nil]; } @@ -107,12 +114,12 @@ - (void)viewDidLoad { [super viewDidLoad]; - + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(startLoadingIndicator) name:BITHockeyFeedbackMessagesLoadingStarted object:nil]; - + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateList) name:BITHockeyFeedbackMessagesLoadingFinished @@ -128,13 +135,13 @@ [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]]; } else { -// [self.tableView setBackgroundColor:[UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1]]; + // [self.tableView setBackgroundColor:[UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1]]; } if ([self.manager isPreiOS7Environment]) { self.view.backgroundColor = DEFAULT_BACKGROUNDCOLOR; } else { -// self.view.backgroundColor = DEFAULT_BACKGROUNDCOLOR_OS7; + // self.view.backgroundColor = DEFAULT_BACKGROUNDCOLOR_OS7; } id refreshClass = NSClassFromString(@"UIRefreshControl"); @@ -143,9 +150,9 @@ [self.refreshControl addTarget:self action:@selector(reloadList) forControlEvents:UIControlEventValueChanged]; } else { self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh - target:self - action:@selector(reloadList)]; - } + target:self + action:@selector(reloadList)]; + } } - (void)startLoadingIndicator { @@ -184,7 +191,9 @@ CGSize contentSize = self.tableView.contentSize; CGPoint contentOffset = self.tableView.contentOffset; + [self refreshPreviewItems]; [self.tableView reloadData]; + if (contentSize.height > 0 && self.tableView.contentSize.height > self.tableView.frame.size.height && self.tableView.contentSize.height > contentSize.height && @@ -192,7 +201,7 @@ [self.tableView setContentOffset:CGPointMake(contentOffset.x, self.tableView.contentSize.height - contentSize.height + contentOffset.y) animated:NO]; [self stopLoadingIndicator]; - + [self.tableView flashScrollIndicators]; } @@ -224,7 +233,7 @@ } else { [self.tableView reloadData]; } - + [super viewDidAppear:animated]; } @@ -265,6 +274,8 @@ - (void)deleteAllMessages { [_manager deleteAllMessages]; + [self refreshPreviewItems]; + [self.tableView reloadData]; } @@ -282,9 +293,9 @@ } else { UIAlertView *deleteAction = [[UIAlertView alloc] initWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackListButonDeleteAllMessages") message:BITHockeyLocalizedString(@"HockeyFeedbackListDeleteAllTitle") - delegate:self - cancelButtonTitle:BITHockeyLocalizedString(@"HockeyFeedbackListDeleteAllCancel") - otherButtonTitles:BITHockeyLocalizedString(@"HockeyFeedbackListDeleteAllDelete"), nil]; + delegate:self + cancelButtonTitle:BITHockeyLocalizedString(@"HockeyFeedbackListDeleteAllCancel") + otherButtonTitles:BITHockeyLocalizedString(@"HockeyFeedbackListDeleteAllDelete"), nil]; [deleteAction setTag:0]; [deleteAction show]; @@ -329,6 +340,7 @@ -(void)userDataUpdateFinished { [self.manager saveMessages]; + [self refreshPreviewItems]; if (self.userDataComposeFlow) { if ([self.manager showFirstRequiredPresentationModal]) { @@ -487,7 +499,7 @@ if (!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier]; - + cell.textLabel.font = [UIFont systemFontOfSize:14]; cell.textLabel.numberOfLines = 0; cell.accessoryType = UITableViewCellAccessoryNone; @@ -628,7 +640,26 @@ cell.message = message; cell.labelText.delegate = self; cell.labelText.userInteractionEnabled = YES; - + cell.delegate = self; + [cell setAttachments:message.previewableAttachments]; + + for (BITFeedbackMessageAttachment *attachment in message.attachments){ + if (attachment.needsLoadingFromURL && !attachment.isLoading){ + attachment.isLoading = YES; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:attachment.sourceURL]]; + [NSURLConnection sendAsynchronousRequest:request queue:self.thumbnailQueue completionHandler:^(NSURLResponse *response, NSData *responseData, NSError *err) { + if (responseData.length) { + [attachment replaceData:responseData]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.tableView reloadData]; + }); + + [[BITHockeyManager sharedHockeyManager].feedbackManager saveMessages]; + } + }]; + } + } + if ( [self.manager isPreiOS7Environment] || (![self.manager isPreiOS7Environment] && indexPath.row != 0) @@ -653,12 +684,19 @@ - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { + BITFeedbackMessage *message = [self.manager messageAtIndex:indexPath.row]; + BOOL messageHasAttachments = ([message attachments].count > 0); + if ([_manager deleteMessageAtIndex:indexPath.row]) { if ([_manager numberOfMessages] > 0) { [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; } else { [tableView reloadData]; } + + if (messageHasAttachments) { + [self refreshPreviewItems]; + } } } } @@ -753,7 +791,7 @@ if (buttonIndex == actionSheet.cancelButtonIndex) { return; } - + if ([actionSheet tag] == 0) { if (buttonIndex == [actionSheet destructiveButtonIndex]) { [self deleteAllMessages]; @@ -768,6 +806,69 @@ } } + +#pragma mark - ListViewCellDelegate + +- (void)listCell:(id)cell didSelectAttachment:(BITFeedbackMessageAttachment *)attachment { + QLPreviewController *previewController = [[QLPreviewController alloc] init]; + previewController.dataSource = self; + + [self presentViewController:previewController animated:YES completion:nil]; + + if (self.cachedPreviewItems.count > [self.cachedPreviewItems indexOfObject:attachment]) { + [previewController setCurrentPreviewItemIndex:[self.cachedPreviewItems indexOfObject:attachment]]; + } +} + +- (void)refreshPreviewItems { + self.cachedPreviewItems = nil; + NSMutableArray *collectedAttachments = [NSMutableArray new]; + + for (int i = 0; i < self.manager.numberOfMessages; i++) { + BITFeedbackMessage *message = [self.manager messageAtIndex:i]; + [collectedAttachments addObjectsFromArray:message.previewableAttachments]; + } + + self.cachedPreviewItems = collectedAttachments; +} + +- (NSInteger)numberOfPreviewItemsInPreviewController:(QLPreviewController *)controller { + if (!self.cachedPreviewItems){ + [self refreshPreviewItems]; + } + + return self.cachedPreviewItems.count; +} + +- (id )previewController:(QLPreviewController *)controller previewItemAtIndex:(NSInteger)index { + if (index >= 0) { + __weak QLPreviewController* blockController = controller; + BITFeedbackMessageAttachment *attachment = self.cachedPreviewItems[index]; + + if (attachment.needsLoadingFromURL && !attachment.isLoading) { + attachment.isLoading = YES; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:attachment.sourceURL]]; + [NSURLConnection sendAsynchronousRequest:request queue:self.thumbnailQueue completionHandler:^(NSURLResponse *response, NSData *responseData, NSError *err) { + attachment.isLoading = NO; + if (responseData.length) { + [attachment replaceData:responseData]; + [blockController reloadData]; + + [[BITHockeyManager sharedHockeyManager].feedbackManager saveMessages]; + } else { + [blockController reloadData]; + } + }]; + + return attachment; + } else { + return self.cachedPreviewItems[index]; + } + } + + return nil; +} + @end #endif /* HOCKEYSDK_FEATURE_FEEDBACK */ diff --git a/Classes/BITFeedbackManager.h b/Classes/BITFeedbackManager.h index 1f1543df26..2056d1486d 100644 --- a/Classes/BITFeedbackManager.h +++ b/Classes/BITFeedbackManager.h @@ -32,6 +32,7 @@ #import "BITHockeyBaseManager.h" #import "BITFeedbackListViewController.h" #import "BITFeedbackComposeViewController.h" +#import "HockeySDKPrivate.h" // Notification message which tells that loading messages finished @@ -59,6 +60,24 @@ typedef NS_ENUM(NSInteger, BITFeedbackUserDataElement) { BITFeedbackUserDataElementRequired = 2 }; +/** + * Available modes for opening the feedback compose interface with a screenshot attached + */ +typedef NS_ENUM(NSInteger, BITFeedbackObservationMode) { + /** + * No SDK provided trigger is active. + */ + BITFeedbackObservationNone = 0, + /** + * Triggeres when the user takes a screenshot. This will grab the latest image from the camera roll. Requires iOS 7 or later! + */ + BITFeedbackObservationModeOnScreenshot = 1, + /** + * Triggers when the user tapps with three fingers for three seconds on the screen. + */ + BITFeedbackObservationModeThreeFingerTap = 2 +}; + @class BITFeedbackMessage; @protocol BITFeedbackManagerDelegate; @@ -192,6 +211,26 @@ typedef NS_ENUM(NSInteger, BITFeedbackUserDataElement) { @property (nonatomic, readwrite) BOOL showAlertOnIncomingMessages; +/** + Define the trigger that opens the feedback composer and attaches a screenshot + + The following modes are available: + + - `BITFeedbackObservationNone`: No SDK based trigger is active. You can implement your + own trigger and then call `[[BITHockeyManager sharedHockeyManager].feedbackManager showFeedbackComposeViewWithGeneratedScreenshot];` to handle your custom events + that should trigger this. + - `BITFeedbackObservationModeOnScreenshot`: Triggeres when the user takes a screenshot. + This will grab the latest image from the camera roll. Requires iOS 7 or later! + - `BITFeedbackObservationModeThreeFingerTap`: Triggers when the user tapps with three fingers + for three seconds on the screen. + + Default is `BITFeedbackObservationNone` + + @see showFeedbackComposeViewWithGeneratedScreenshot + */ +@property (nonatomic, readwrite) BITFeedbackObservationMode feedbackObservationMode; + + ///----------------------------------------------------------------------------- /// @name User Interface ///----------------------------------------------------------------------------- @@ -241,6 +280,24 @@ typedef NS_ENUM(NSInteger, BITFeedbackUserDataElement) { */ - (void)showFeedbackComposeView; +/** + Present the modal feedback compose message user interface with the items given. + All NSString-Content in the array will be concatenated and result in the message, + while all UIImage and NSData-instances will be turned into attachments. + */ +- (void)showFeedbackComposeViewWithPreparedItems:(NSArray *)items; + +/** + Presents a modal feedback compose interface with a screenshot attached which is taken at the time of calling this method. + + This should be used when your own trigger fires. The following code should be used: + + [[BITHockeyManager sharedHockeyManager].feedbackManager showFeedbackComposeViewWithGeneratedScreenshot]; + + @see feedbackObservationMode + */ +- (void)showFeedbackComposeViewWithGeneratedScreenshot; + /** Create an feedback compose view diff --git a/Classes/BITFeedbackManager.m b/Classes/BITFeedbackManager.m index 903183262f..056266fc23 100644 --- a/Classes/BITFeedbackManager.m +++ b/Classes/BITFeedbackManager.m @@ -29,11 +29,14 @@ #import "HockeySDK.h" +#import + #if HOCKEYSDK_FEATURE_FEEDBACK #import "HockeySDKPrivate.h" #import "BITFeedbackManager.h" +#import "BITFeedbackMessageAttachment.h" #import "BITFeedbackManagerPrivate.h" #import "BITHockeyBaseManagerPrivate.h" @@ -50,6 +53,12 @@ #define kBITFeedbackLastMessageID @"HockeyFeedbackLastMessageID" #define kBITFeedbackAppID @"HockeyFeedbackAppID" +@interface BITFeedbackManager() + +@property (nonatomic, strong) UITapGestureRecognizer *tapRecognizer; +@property (nonatomic) BOOL screenshotNotificationEnabled; + +@end @implementation BITFeedbackManager { NSFileManager *_fileManager; @@ -62,6 +71,8 @@ BOOL _incomingMessagesAlertShowing; BOOL _didEnterBackgroundState; BOOL _networkRequestInProgress; + + BITFeedbackObservationMode _observationMode; } #pragma mark - Initialization @@ -76,7 +87,7 @@ _requireUserEmail = BITFeedbackUserDataElementOptional; _showAlertOnIncomingMessages = YES; _showFirstRequiredPresentationModal = YES; - + _disableFeedbackManager = NO; _networkRequestInProgress = NO; _incomingMessagesAlertShowing = NO; @@ -85,9 +96,9 @@ _lastMessageID = nil; self.feedbackList = [NSMutableArray array]; - + _fileManager = [[NSFileManager alloc] init]; - + _settingsFile = [bit_settingsDir() stringByAppendingPathComponent:BITHOCKEY_FEEDBACK_SETTINGS]; _userID = nil; @@ -147,12 +158,12 @@ } if(nil == _networkDidBecomeReachableObserver) { _networkDidBecomeReachableObserver = [[NSNotificationCenter defaultCenter] addObserverForName:BITHockeyNetworkDidBecomeReachableNotification - object:nil - queue:NSOperationQueue.mainQueue - usingBlock:^(NSNotification *note) { - typeof(self) strongSelf = weakSelf; - [strongSelf didBecomeActiveActions]; - }]; + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification *note) { + typeof(self) strongSelf = weakSelf; + [strongSelf didBecomeActiveActions]; + }]; } } @@ -213,20 +224,31 @@ } - (void)showFeedbackComposeView { + [self showFeedbackComposeViewWithPreparedItems:nil]; +} + +- (void)showFeedbackComposeViewWithPreparedItems:(NSArray *)items{ if (_currentFeedbackComposeViewController) { BITHockeyLog(@"INFO: update view already visible, aborting"); return; } + BITFeedbackComposeViewController *composeView = [self feedbackComposeViewController]; + [composeView prepareWithItems:items]; + + [self showView:composeView]; - [self showView:[self feedbackComposeViewController]]; } +- (void)showFeedbackComposeViewWithGeneratedScreenshot { + UIImage *screenshot = bit_screenshot(); + [self showFeedbackComposeViewWithPreparedItems:@[screenshot]]; +} #pragma mark - Manager Control - (void)startManager { if ([self isFeedbackManagerDisabled]) return; - + [self registerObservers]; // we are already delayed, so the notification already came in and this won't invoked twice @@ -273,12 +295,12 @@ userIDForHockeyManager:[BITHockeyManager sharedHockeyManager] componentManager:self]; } - + if (userID) { availableViaDelegate = YES; self.userID = userID; } - + return availableViaDelegate; } @@ -290,16 +312,16 @@ if ([BITHockeyManager sharedHockeyManager].delegate && [[BITHockeyManager sharedHockeyManager].delegate respondsToSelector:@selector(userNameForHockeyManager:componentManager:)]) { userName = [[BITHockeyManager sharedHockeyManager].delegate - userNameForHockeyManager:[BITHockeyManager sharedHockeyManager] - componentManager:self]; + userNameForHockeyManager:[BITHockeyManager sharedHockeyManager] + componentManager:self]; } - + if (userName) { availableViaDelegate = YES; self.userName = userName; self.requireUserName = BITFeedbackUserDataElementDontShow; } - + return availableViaDelegate; } @@ -307,20 +329,20 @@ BOOL availableViaDelegate = NO; NSString *userEmail = [self stringValueFromKeychainForKey:kBITHockeyMetaUserEmail]; - + if ([BITHockeyManager sharedHockeyManager].delegate && [[BITHockeyManager sharedHockeyManager].delegate respondsToSelector:@selector(userEmailForHockeyManager:componentManager:)]) { userEmail = [[BITHockeyManager sharedHockeyManager].delegate userEmailForHockeyManager:[BITHockeyManager sharedHockeyManager] componentManager:self]; } - + if (userEmail) { availableViaDelegate = YES; self.userEmail = userEmail; self.requireUserEmail = BITFeedbackUserDataElementDontShow; } - + return availableViaDelegate; } @@ -328,7 +350,7 @@ [self updateUserIDUsingKeychainAndDelegate]; [self updateUserNameUsingKeychainAndDelegate]; [self updateUserEmailUsingKeychainAndDelegate]; - + // if both values are shown via the delegates, we never ever did ask and will never ever ask for user data if (self.requireUserName == BITFeedbackUserDataElementDontShow && self.requireUserEmail == BITFeedbackUserDataElementDontShow) { @@ -345,7 +367,7 @@ if (![_fileManager fileExistsAtPath:_settingsFile]) return; - + NSData *codedData = [[NSData alloc] initWithContentsOfFile:_settingsFile]; if (codedData == nil) return; @@ -357,7 +379,7 @@ @catch (NSException *exception) { return; } - + if (!userIDViaDelegate) { if ([unarchiver containsValueForKey:kBITFeedbackUserID]) { self.userID = [unarchiver decodeObjectForKey:kBITFeedbackUserID]; @@ -373,7 +395,7 @@ } self.userName = [self stringValueFromKeychainForKey:kBITFeedbackName]; } - + if (!userEmailViaDelegate) { if ([unarchiver containsValueForKey:kBITFeedbackEmail]) { self.userEmail = [unarchiver decodeObjectForKey:kBITFeedbackEmail]; @@ -393,7 +415,7 @@ if ([unarchiver containsValueForKey:kBITFeedbackAppID]) { NSString *appID = [unarchiver decodeObjectForKey:kBITFeedbackAppID]; - + // the stored thread is from another application identifier, so clear the token // which will cause the new posts to create a new thread on the server for the // current app identifier @@ -416,9 +438,9 @@ // inform the UI to update its data in case the list is already showing [[NSNotificationCenter defaultCenter] postNotificationName:BITHockeyFeedbackMessagesLoadingFinished object:nil]; } - + [unarchiver finishDecoding]; - + if (!self.lastCheck) { self.lastCheck = [NSDate distantPast]; } @@ -430,7 +452,7 @@ NSMutableData *data = [[NSMutableData alloc] init]; NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data]; - + if (_didAskUserData) [archiver encodeObject:[NSNumber numberWithBool:YES] forKey:kBITFeedbackUserDataAsked]; @@ -571,6 +593,8 @@ - (BOOL)deleteMessageAtIndex:(NSUInteger)index { if (_feedbackList && [_feedbackList count] > index && [_feedbackList objectAtIndex:index]) { + BITFeedbackMessage *message = _feedbackList[index]; + [message deleteContents]; [_feedbackList removeObjectAtIndex:index]; [self saveMessages]; @@ -613,7 +637,7 @@ - (BOOL)isManualUserDataAvailable { [self updateAppDefinedUserData]; - + if ((self.requireUserName != BITFeedbackUserDataElementDontShow && self.userName) || (self.requireUserEmail != BITFeedbackUserDataElementDontShow && self.userEmail)) return YES; @@ -627,23 +651,23 @@ - (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]; [_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 @@ -692,13 +716,21 @@ *stop2 = YES; } }]; - + if (matchingSendInProgressOrInConflictMessage) { matchingSendInProgressOrInConflictMessage.date = [self parseRFC3339Date:[(NSDictionary *)objMessage objectForKey:@"created_at"]]; matchingSendInProgressOrInConflictMessage.id = messageID; matchingSendInProgressOrInConflictMessage.status = BITFeedbackMessageStatusRead; + NSArray *feedbackAttachments =[(NSDictionary *)objMessage objectForKey:@"attachments"]; + if (matchingSendInProgressOrInConflictMessage.attachments.count == feedbackAttachments.count) { + int attachmentIndex = 0; + for (BITFeedbackMessageAttachment* attachment in matchingSendInProgressOrInConflictMessage.attachments){ + attachment.id =feedbackAttachments[attachmentIndex][@"id"]; + attachmentIndex++; + } + } } else { - if ([(NSDictionary *)objMessage objectForKey:@"clean_text"] || [(NSDictionary *)objMessage objectForKey:@"text"]) { + if ([(NSDictionary *)objMessage objectForKey:@"clean_text"] || [(NSDictionary *)objMessage objectForKey:@"text"] || [(NSDictionary *)objMessage objectForKey:@"attachments"]) { BITFeedbackMessage *message = [[BITFeedbackMessage alloc] init]; message.text = [(NSDictionary *)objMessage objectForKey:@"clean_text"] ?: [(NSDictionary *)objMessage objectForKey:@"text"] ?: @""; message.name = [(NSDictionary *)objMessage objectForKey:@"name"] ?: @""; @@ -708,6 +740,15 @@ message.id = [(NSDictionary *)objMessage objectForKey:@"id"]; message.status = BITFeedbackMessageStatusUnread; + for (NSDictionary *attachmentData in objMessage[@"attachments"]) { + BITFeedbackMessageAttachment *newAttachment = [BITFeedbackMessageAttachment new]; + newAttachment.originalFilename = attachmentData[@"file_name"]; + newAttachment.id = attachmentData[@"id"]; + newAttachment.sourceURL = attachmentData[@"url"]; + newAttachment.contentType = attachmentData[@"content_type"]; + [message addAttachmentsObject:newAttachment]; + } + [_feedbackList addObject:message]; newMessage = YES; @@ -724,7 +765,7 @@ [self sortFeedbackList]; [self updateLastMessageID]; - + // we got a new incoming message, trigger user notification system if (newMessage) { // check if the latest message is from the users own email address, then don't show an alert since he answered using his own email @@ -733,12 +774,12 @@ BITFeedbackMessage *latestMessage = [self lastMessageHavingID]; if (self.userEmail && latestMessage.email && [self.userEmail compare:latestMessage.email] == NSOrderedSame) latestMessageFromUser = YES; - + if (!latestMessageFromUser) { if([self.delegate respondsToSelector:@selector(feedbackManagerDidReceiveNewFeedback:)]) { [self.delegate feedbackManagerDidReceiveNewFeedback:self]; } - + if(self.showAlertOnIncomingMessages && !self.currentFeedbackListViewController && !self.currentFeedbackComposeViewController) { UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:BITHockeyLocalizedString(@"HockeyFeedbackNewMessageTitle") message:BITHockeyLocalizedString(@"HockeyFeedbackNewMessageText") @@ -754,7 +795,7 @@ } 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]; @@ -765,17 +806,18 @@ } [self saveMessages]; - + return; } + - (void)sendNetworkRequestWithHTTPMethod:(NSString *)httpMethod withMessage:(BITFeedbackMessage *)message completionHandler:(void (^)(NSError *err))completionHandler { NSString *boundary = @"----FOO"; _networkRequestInProgress = YES; // inform the UI to update its data in case the list is already showing [[NSNotificationCenter defaultCenter] postNotificationName:BITHockeyFeedbackMessagesLoadingStarted object:nil]; - + NSString *tokenParameter = @""; if ([self token]) { tokenParameter = [NSString stringWithFormat:@"/%@", [self token]]; @@ -835,8 +877,26 @@ [postBody appendData:[BITHockeyAppClient dataWithPostValue:self.userEmail forKey:@"email" boundary:boundary]]; } + + NSInteger photoIndex = 0; + + for (BITFeedbackMessageAttachment *attachment in message.attachments){ + NSString *key = [NSString stringWithFormat:@"attachment%ld", (long)photoIndex]; + + NSString *filename = attachment.originalFilename; + + if (!filename) { + filename = [NSString stringWithFormat:@"Attachment %ld", (long)photoIndex]; + } + + [postBody appendData:[BITHockeyAppClient dataWithPostValue:attachment.data forKey:key contentType:attachment.contentType boundary:boundary filename:filename]]; + + photoIndex++; + } + [postBody appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; + [request setHTTPBody:postBody]; } @@ -858,14 +918,14 @@ 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; } @@ -938,7 +998,7 @@ [self saveMessages]; NSArray *pendingMessages = [self messagesWithStatus:BITFeedbackMessageStatusSendPending]; - + if ([pendingMessages count] > 0) { // we send one message at a time BITFeedbackMessage *messageToSend = [pendingMessages objectAtIndex:0]; @@ -961,7 +1021,6 @@ completionHandler:^(NSError *err){ if (err) { [self markSendInProgressMessagesAsPending]; - [self saveMessages]; } @@ -971,11 +1030,12 @@ } } -- (void)submitMessageWithText:(NSString *)text { +- (void)submitMessageWithText:(NSString *)text andAttachments:(NSArray *)attachments { BITFeedbackMessage *message = [[BITFeedbackMessage alloc] init]; message.text = text; [message setStatus:BITFeedbackMessageStatusSendPending]; - [message setToken:[self uuidAsLowerCaseAndShortened]]; + [message setToken:[self uuidAsLowerCaseAndShortened]]; + [message setAttachments:attachments]; [message setUserMessage:YES]; [_feedbackList addObject:message]; @@ -996,6 +1056,88 @@ } } +#pragma mark - Observation Handling + +- (void)setFeedbackObservationMode:(BITFeedbackObservationMode)feedbackObservationMode { + if (feedbackObservationMode != _feedbackObservationMode) { + _feedbackObservationMode = feedbackObservationMode; + + if (feedbackObservationMode == BITFeedbackObservationModeOnScreenshot){ + if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1){ + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(screenshotNotificationReceived:) name:UIApplicationUserDidTakeScreenshotNotification object:nil]; + } else { + BITHockeyLog("WARNING: BITFeedbackObservationModeOnScreenshot requires iOS 7 or later."); + } + + self.screenshotNotificationEnabled = YES; + + if (self.tapRecognizer){ + [[[UIApplication sharedApplication] keyWindow] removeGestureRecognizer:self.tapRecognizer]; + self.tapRecognizer = nil; + } + } + + if (feedbackObservationMode == BITFeedbackObservationModeThreeFingerTap){ + if (!self.tapRecognizer){ + self.tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(screenshotTripleTap:)]; + self.tapRecognizer.numberOfTouchesRequired = 3; + self.tapRecognizer.delegate = self; + + dispatch_async(dispatch_get_main_queue(), ^{ + [[UIApplication sharedApplication].keyWindow addGestureRecognizer:self.tapRecognizer]; + }); + } + + if (self.screenshotNotificationEnabled){ + if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1){ + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationUserDidTakeScreenshotNotification object:nil]; + self.screenshotNotificationEnabled = NO; + } + } + } + } +} + +-(void)screenshotNotificationReceived:(NSNotification *)notification { + dispatch_async(dispatch_get_main_queue(), ^{ + [self extractLastPictureFromLibraryAndLaunchFeedback]; + }); +} + +-(void)extractLastPictureFromLibraryAndLaunchFeedback { + ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init]; + + [library enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos usingBlock:^(ALAssetsGroup *group, BOOL *stop) { + + [group setAssetsFilter:[ALAssetsFilter allPhotos]]; + + [group enumerateAssetsWithOptions:NSEnumerationReverse usingBlock:^(ALAsset *alAsset, NSUInteger index, BOOL *innerStop) { + + if (alAsset) { + ALAssetRepresentation *representation = [alAsset defaultRepresentation]; + UIImage *latestPhoto = [UIImage imageWithCGImage:[representation fullScreenImage]]; + + *stop = YES; + *innerStop = YES; + + [self showFeedbackComposeViewWithPreparedItems:@[latestPhoto]]; + } + }]; + } failureBlock: nil]; +} + +- (void)screenshotTripleTap:(UITapGestureRecognizer *)tapRecognizer { + if (tapRecognizer.state == UIGestureRecognizerStateRecognized){ + [self showFeedbackComposeViewWithGeneratedScreenshot]; + } +} + +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { + return YES; +} + @end + + #endif /* HOCKEYSDK_FEATURE_FEEDBACK */ diff --git a/Classes/BITFeedbackManagerPrivate.h b/Classes/BITFeedbackManagerPrivate.h index 0da30ca52e..c7ba433042 100644 --- a/Classes/BITFeedbackManagerPrivate.h +++ b/Classes/BITFeedbackManagerPrivate.h @@ -70,7 +70,7 @@ - (NSUInteger)numberOfMessages; - (BITFeedbackMessage *)messageAtIndex:(NSUInteger)index; -- (void)submitMessageWithText:(NSString *)text; +- (void)submitMessageWithText:(NSString *)text andAttachments:(NSArray *)photos; - (void)submitPendingMessages; // Returns YES if manual user data can be entered, required or optional diff --git a/Classes/BITFeedbackMessage.h b/Classes/BITFeedbackMessage.h index c938e21722..908e4fc8fd 100644 --- a/Classes/BITFeedbackMessage.h +++ b/Classes/BITFeedbackMessage.h @@ -29,6 +29,8 @@ #import +@class BITFeedbackMessageAttachment; + /** * Status for each feedback message */ @@ -69,7 +71,29 @@ typedef NS_ENUM(NSInteger, BITFeedbackMessageStatus) { @property (nonatomic, copy) NSDate *date; @property (nonatomic, copy) NSNumber *id; @property (nonatomic, copy) NSString *token; +@property (nonatomic, strong) NSArray *attachments; @property (nonatomic) BITFeedbackMessageStatus status; @property (nonatomic) BOOL userMessage; +/** + Delete local cached attachment data + + @warning This method must be called before a feedback message is deleted. + */ +- (void)deleteContents; + +/** + Add an attachment to a message + + @param object BITFeedbackMessageAttachment instance representing the attachment that should be added + */ +-(void)addAttachmentsObject:(BITFeedbackMessageAttachment *)object; + +/** + Return the attachments that can be viewed + + @return NSArray containing the attachment objects that can be previewed + */ +- (NSArray *)previewableAttachments; + @end diff --git a/Classes/BITFeedbackMessage.m b/Classes/BITFeedbackMessage.m index a5b0e0d8b2..7f8d60dea1 100644 --- a/Classes/BITFeedbackMessage.m +++ b/Classes/BITFeedbackMessage.m @@ -28,13 +28,14 @@ #import "BITFeedbackMessage.h" +#import "BITFeedbackMessageAttachment.h" @implementation BITFeedbackMessage #pragma mark - NSObject -- (id) init { +- (instancetype) init { if ((self = [super init])) { _text = nil; _userID = nil; @@ -42,6 +43,7 @@ _email = nil; _date = [[NSDate alloc] init]; _token = nil; + _attachments = nil; _id = [[NSNumber alloc] initWithInteger:0]; _status = BITFeedbackMessageStatusSendPending; _userMessage = NO; @@ -59,19 +61,21 @@ [encoder encodeObject:self.email forKey:@"email"]; [encoder encodeObject:self.date forKey:@"date"]; [encoder encodeObject:self.id forKey:@"id"]; + [encoder encodeObject:self.attachments forKey:@"attachments"]; [encoder encodeInteger:self.status forKey:@"status"]; [encoder encodeBool:self.userMessage forKey:@"userMessage"]; [encoder encodeObject:self.token forKey:@"token"]; } -- (id)initWithCoder:(NSCoder *)decoder { - if ((self = [super init])) { +- (instancetype)initWithCoder:(NSCoder *)decoder { + if ((self = [self init])) { self.text = [decoder decodeObjectForKey:@"text"]; self.userID = [decoder decodeObjectForKey:@"userID"]; self.name = [decoder decodeObjectForKey:@"name"]; self.email = [decoder decodeObjectForKey:@"email"]; self.date = [decoder decodeObjectForKey:@"date"]; self.id = [decoder decodeObjectForKey:@"id"]; + self.attachments = [decoder decodeObjectForKey:@"attachments"]; self.status = (BITFeedbackMessageStatus)[decoder decodeIntegerForKey:@"status"]; self.userMessage = [decoder decodeBoolForKey:@"userMessage"]; self.token = [decoder decodeObjectForKey:@"token"]; @@ -79,4 +83,32 @@ return self; } +#pragma mark - Deletion + +-(void)deleteContents { + for (BITFeedbackMessageAttachment *attachment in self.attachments){ + [attachment deleteContents]; + } +} + +- (NSArray *)previewableAttachments { + NSMutableArray *returnArray = [NSMutableArray new]; + + for (BITFeedbackMessageAttachment *attachment in self.attachments){ + if ([QLPreviewController canPreviewItem:attachment ]){ + [returnArray addObject:attachment]; + } + } + + return returnArray; +} + +-(void)addAttachmentsObject:(BITFeedbackMessageAttachment *)object{ + if (!self.attachments){ + self.attachments = [NSArray array]; + } + self.attachments = [self.attachments arrayByAddingObject:object]; +} + + @end diff --git a/Classes/BITFeedbackMessageAttachment.h b/Classes/BITFeedbackMessageAttachment.h new file mode 100644 index 0000000000..dbb398b1da --- /dev/null +++ b/Classes/BITFeedbackMessageAttachment.h @@ -0,0 +1,65 @@ +/* + * Author: Moritz Haarmann + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#import +#import +#import + +@interface BITFeedbackMessageAttachment : NSObject + +@property (nonatomic, copy) NSNumber *id; +@property (nonatomic, copy) NSString *originalFilename; +@property (nonatomic, copy) NSString *contentType; +@property (nonatomic, copy) NSString *sourceURL; +@property (nonatomic) BOOL isLoading; +@property (nonatomic, readonly) NSData *data; + + +@property (readonly) UIImage *imageRepresentation; + + ++ (BITFeedbackMessageAttachment *)attachmentWithData:(NSData *)data contentType:(NSString *)contentType; + +- (UIImage *)thumbnailWithSize:(CGSize)size; + +- (void)replaceData:(NSData *)data; + +- (void)deleteContents; + +- (BOOL)needsLoadingFromURL; + +- (BOOL)isImage; + +- (NSURL *)localURL; + +/** + Used to determine whether QuickLook can preview this file or not. If not, we don't download it. + */ +- (NSString*)possibleFilename; + +@end diff --git a/Classes/BITFeedbackMessageAttachment.m b/Classes/BITFeedbackMessageAttachment.m new file mode 100644 index 0000000000..1315e702f8 --- /dev/null +++ b/Classes/BITFeedbackMessageAttachment.m @@ -0,0 +1,258 @@ +/* + * Author: Moritz Haarmann + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + + +#import "BITFeedbackMessageAttachment.h" +#import "BITHockeyHelper.h" +#import "HockeySDKPrivate.h" +#import + +#define kCacheFolderName @"attachments" + +@interface BITFeedbackMessageAttachment() + +@property (nonatomic, strong) NSMutableDictionary *thumbnailRepresentations; +@property (nonatomic, strong) NSData *internalData; +@property (nonatomic, copy) NSString *filename; + + +@end + +@implementation BITFeedbackMessageAttachment { + NSString *_tempFilename; + + NSString *_cachePath; + + NSFileManager *_fm; +} + + ++ (BITFeedbackMessageAttachment *)attachmentWithData:(NSData *)data contentType:(NSString *)contentType { + + static NSDateFormatter *formatter; + + if(!formatter) { + formatter = [NSDateFormatter new]; + formatter.dateStyle = NSDateFormatterShortStyle; + formatter.timeStyle = NSDateFormatterShortStyle; + } + + BITFeedbackMessageAttachment *newAttachment = [BITFeedbackMessageAttachment new]; + newAttachment.contentType = contentType; + newAttachment.data = data; + newAttachment.originalFilename = [NSString stringWithFormat:@"Attachment: %@", [formatter stringFromDate:[NSDate date]]]; + + return newAttachment; +} + +- (instancetype)init { + if ((self = [super init])) { + self.isLoading = NO; + self.thumbnailRepresentations = [NSMutableDictionary new]; + + _fm = [[NSFileManager alloc] init]; + _cachePath = [bit_settingsDir() stringByAppendingPathComponent:kCacheFolderName]; + + BOOL isDirectory; + + if (![_fm fileExistsAtPath:_cachePath isDirectory:&isDirectory]){ + [_fm createDirectoryAtPath:_cachePath withIntermediateDirectories:YES attributes:nil error:nil]; + } + + } + return self; +} + +- (void)setData:(NSData *)data { + self->_internalData = data; + self.filename = [self possibleFilename]; + [self->_internalData writeToFile:self.filename atomically:NO]; +} + +- (NSData *)data { + if (!self->_internalData && self.filename) { + self.internalData = [NSData dataWithContentsOfFile:self.filename]; + } + + if (self.internalData) { + return self.internalData; + } + + return nil; +} + +- (void)replaceData:(NSData *)data { + self.data = data; + self.thumbnailRepresentations = [NSMutableDictionary new]; +} + +- (BOOL)needsLoadingFromURL { + return (self.sourceURL && ![_fm fileExistsAtPath:[self.localURL path]]); +} + +- (BOOL)isImage { + return ([self.contentType rangeOfString:@"image"].location != NSNotFound); +} + +- (NSURL *)localURL { + if (self.filename && [_fm fileExistsAtPath:self.filename]) { + return [NSURL fileURLWithPath:self.filename]; + } + + return nil; +} + + +#pragma mark NSCoding + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:self.contentType forKey:@"contentType"]; + [aCoder encodeObject:self.filename forKey:@"filename"]; + [aCoder encodeObject:self.originalFilename forKey:@"originalFilename"]; + [aCoder encodeObject:self.sourceURL forKey:@"url"]; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + if ((self = [self init])) { + self.contentType = [aDecoder decodeObjectForKey:@"contentType"]; + self.filename = [aDecoder decodeObjectForKey:@"filename"]; + self.thumbnailRepresentations = [NSMutableDictionary new]; + self.originalFilename = [aDecoder decodeObjectForKey:@"originalFilename"]; + self.sourceURL = [aDecoder decodeObjectForKey:@"url"]; + } + + return self; +} + + +#pragma mark - Thubmnails / Image Representation + +- (UIImage *)imageRepresentation { + if ([self.contentType rangeOfString:@"image"].location != NSNotFound && self.filename ) { + return [UIImage imageWithData:self.data]; + } else { + // Create a Icon .. + UIDocumentInteractionController* docController = [[UIDocumentInteractionController alloc] init]; + docController.name = self.originalFilename; + NSArray* icons = docController.icons; + if (icons.count){ + return icons[0]; + } else { + return nil; + } + } +} + +- (UIImage *)thumbnailWithSize:(CGSize)size { + id cacheKey = [NSValue valueWithCGSize:size]; + + if (!self.thumbnailRepresentations[cacheKey]) { + UIImage *image = self.imageRepresentation; + // consider the scale. + if (!image) { + return nil; + } + + CGFloat scale = [UIScreen mainScreen].scale; + + if (scale != image.scale) { + + CGSize scaledSize = CGSizeApplyAffineTransform(size, CGAffineTransformMakeScale(scale, scale)); + UIImage *thumbnail = bit_imageToFitSize(image, scaledSize, YES) ; + + UIImage *scaledTumbnail = [UIImage imageWithCGImage:thumbnail.CGImage scale:scale orientation:thumbnail.imageOrientation]; + if (thumbnail) { + [self.thumbnailRepresentations setObject:scaledTumbnail forKey:cacheKey]; + } + + } else { + UIImage *thumbnail = bit_imageToFitSize(image, size, YES) ; + + [self.thumbnailRepresentations setObject:thumbnail forKey:cacheKey]; + + } + + } + + return self.thumbnailRepresentations[cacheKey]; +} + + +#pragma mark - Persistence Helpers + +- (NSString *)possibleFilename { + if (_tempFilename) { + return _tempFilename; + } + + NSString *uniqueString = bit_UUID(); + _tempFilename = [_cachePath stringByAppendingPathComponent:uniqueString]; + + // File extension that suits the Content type. + + CFStringRef mimeType = (__bridge CFStringRef)self.contentType; + CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType, NULL); + CFStringRef extension = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassFilenameExtension); + if (extension) { + _tempFilename = [_tempFilename stringByAppendingPathExtension:(__bridge NSString *)(extension)]; + CFRelease(extension); + } + + CFRelease(uti); + + return _tempFilename; +} + +- (void)deleteContents { + if (self.filename) { + [_fm removeItemAtPath:self.filename error:nil]; + self.filename = nil; + } +} + + +#pragma mark - QLPreviewItem + +- (NSString *)previewItemTitle { + return self.originalFilename; +} + +- (NSURL *)previewItemURL { + if (self.localURL){ + return self.localURL; + } else if (self.sourceURL) { + NSString *filename = self.possibleFilename; + if (filename) { + return [NSURL fileURLWithPath:filename]; + } + } + + return nil; +} + +@end diff --git a/Classes/BITHockeyAppClient.h b/Classes/BITHockeyAppClient.h index 4de7fa24e7..b4abe93e0a 100644 --- a/Classes/BITHockeyAppClient.h +++ b/Classes/BITHockeyAppClient.h @@ -118,7 +118,7 @@ #pragma mark - Helpers /** - * create a post body from the given value, key and boundary. This is a convenience call to + * create a post body from the given value, key and boundary. This is a convenience call to * dataWithPostValue:forKey:contentType:boundary and aimed at NSString-content. * * @param value - diff --git a/Classes/BITHockeyAppClient.m b/Classes/BITHockeyAppClient.m index 37a22b6407..39944284d8 100644 --- a/Classes/BITHockeyAppClient.m +++ b/Classes/BITHockeyAppClient.m @@ -93,7 +93,7 @@ [postBody appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; - // There's certainly a better way to check if we are supposed to send binary data here. + // There's certainly a better way to check if we are supposed to send binary data here. if (filename){ [postBody appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", key, filename] dataUsingEncoding:NSUTF8StringEncoding]]; [postBody appendData:[[NSString stringWithFormat:@"Content-Type: %@\r\n", contentType] dataUsingEncoding:NSUTF8StringEncoding]]; diff --git a/Classes/BITHockeyHelper.m b/Classes/BITHockeyHelper.m index 4ea63f165d..0ab661e5f0 100644 --- a/Classes/BITHockeyHelper.m +++ b/Classes/BITHockeyHelper.m @@ -346,6 +346,11 @@ UIImage *bit_addGlossToImage(UIImage *inputImage) { #pragma mark UIImage helpers UIImage *bit_imageToFitSize(UIImage *inputImage, CGSize fitSize, BOOL honorScaleFactor) { + + if (!inputImage){ + return nil; + } + float imageScaleFactor = 1.0; if (honorScaleFactor) { if ([inputImage respondsToSelector:@selector(scale)]) { diff --git a/Classes/BITImageAnnotation.h b/Classes/BITImageAnnotation.h new file mode 100644 index 0000000000..8f1354a617 --- /dev/null +++ b/Classes/BITImageAnnotation.h @@ -0,0 +1,44 @@ +/* + * Author: Moritz Haarmann + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#import + +@interface BITImageAnnotation : UIView { + BOOL _selected; +} + +@property (nonatomic) CGSize movedDelta; +@property (nonatomic, weak) UIImage *sourceImage; +@property (nonatomic) CGRect imageFrame; + +-(BOOL)resizable; + +- (void)setSelected:(BOOL)selected; +- (BOOL)isSelected; + +@end diff --git a/Classes/BITImageAnnotation.m b/Classes/BITImageAnnotation.m new file mode 100644 index 0000000000..428e8b1163 --- /dev/null +++ b/Classes/BITImageAnnotation.m @@ -0,0 +1,56 @@ +/* + * Author: Moritz Haarmann + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#import "BITImageAnnotation.h" + +@implementation BITImageAnnotation + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + // Initialization code + //self.backgroundColor = [UIColor redColor]; + } + return self; +} + + +-(BOOL)resizable { + return NO; +} + +- (void)setSelected:(BOOL)selected { + self->_selected = selected; +} + +- (BOOL)isSelected { + return self->_selected; +} + +@end diff --git a/Classes/BITImageAnnotationViewController.h b/Classes/BITImageAnnotationViewController.h new file mode 100644 index 0000000000..026968ceb3 --- /dev/null +++ b/Classes/BITImageAnnotationViewController.h @@ -0,0 +1,45 @@ +/* + * Author: Moritz Haarmann + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#import + +@class BITImageAnnotationViewController; + +@protocol BITImageAnnotationDelegate + +- (void)annotationControllerDidCancel:(BITImageAnnotationViewController *)annotationController; +- (void)annotationController:(BITImageAnnotationViewController *)annotationController didFinishWithImage:(UIImage *)image; + +@end + +@interface BITImageAnnotationViewController : UIViewController + +@property (nonatomic, strong) UIImage *image; +@property (nonatomic, weak) id delegate; + +@end diff --git a/Classes/BITImageAnnotationViewController.m b/Classes/BITImageAnnotationViewController.m new file mode 100644 index 0000000000..e9420da0c4 --- /dev/null +++ b/Classes/BITImageAnnotationViewController.m @@ -0,0 +1,404 @@ +/* + * Author: Moritz Haarmann + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#import "BITImageAnnotationViewController.h" +#import "BITImageAnnotation.h" +#import "BITRectangleImageAnnotation.h" +#import "BITArrowImageAnnotation.h" +#import "BITBlurImageAnnotation.h" +#import "BITHockeyHelper.h" +#import "HockeySDKPrivate.h" + +typedef NS_ENUM(NSInteger, BITImageAnnotationViewControllerInteractionMode) { + BITImageAnnotationViewControllerInteractionModeNone, + BITImageAnnotationViewControllerInteractionModeDraw, + BITImageAnnotationViewControllerInteractionModeMove +}; + +@interface BITImageAnnotationViewController () + +@property (nonatomic, strong) UIImageView *imageView; +@property (nonatomic, strong) UISegmentedControl *editingControls; +@property (nonatomic, strong) NSMutableArray *objects; + +@property (nonatomic, strong) UITapGestureRecognizer *tapRecognizer; +@property (nonatomic, strong) UIPanGestureRecognizer *panRecognizer; +@property (nonatomic, strong) UIPinchGestureRecognizer *pinchRecognizer; + +@property (nonatomic) CGFloat scaleFactor; + +@property (nonatomic) CGPoint panStart; +@property (nonatomic,strong) BITImageAnnotation *currentAnnotation; + +@property (nonatomic) BITImageAnnotationViewControllerInteractionMode currentInteraction; + +@property (nonatomic) CGRect pinchStartingFrame; + +@end + +@implementation BITImageAnnotationViewController + +#pragma mark - UIViewController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + self.view.backgroundColor = [UIColor groupTableViewBackgroundColor]; + + NSArray *icons = @[@"Arrow.png",@"Rectangle.png", @"Blur.png"]; + + self.editingControls = [[UISegmentedControl alloc] initWithItems:@[@"Rectangle", @"Arrow", @"Blur"]]; + int i=0; + for (NSString *imageName in icons){ + [self.editingControls setImage:bit_imageNamed(imageName, BITHOCKEYSDK_BUNDLE) forSegmentAtIndex:i++]; + } + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [self.editingControls setSegmentedControlStyle:UISegmentedControlStyleBar]; +#pragma clang diagnostic pop + + self.navigationItem.titleView = self.editingControls; + + self.objects = [NSMutableArray new]; + + [self.editingControls addTarget:self action:@selector(editingAction:) forControlEvents:UIControlEventTouchUpInside]; + [self.editingControls setSelectedSegmentIndex:0]; + + self.imageView = [[UIImageView alloc] initWithFrame:self.view.bounds]; + + self.imageView.clipsToBounds = YES; + + self.imageView.image = self.image; + self.imageView.contentMode = UIViewContentModeScaleToFill; + + self.view.frame = UIScreen.mainScreen.applicationFrame; + + [self.view addSubview:self.imageView]; + // Erm. + self.imageView.frame = [UIScreen mainScreen].bounds; + + self.panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panned:)]; + self.pinchRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinched:)]; + self.tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)]; + + [self.imageView addGestureRecognizer:self.pinchRecognizer]; + [self.imageView addGestureRecognizer:self.panRecognizer]; + [self.view addGestureRecognizer:self.tapRecognizer]; + + self.imageView.userInteractionEnabled = YES; + + self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc ] initWithImage:bit_imageNamed(@"Cancel.png", BITHOCKEYSDK_BUNDLE) landscapeImagePhone:bit_imageNamed(@"Cancel.png", BITHOCKEYSDK_BUNDLE) style:UIBarButtonItemStyleBordered target:self action:@selector(discard:)]; + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc ] initWithImage:bit_imageNamed(@"Ok.png", BITHOCKEYSDK_BUNDLE) landscapeImagePhone:bit_imageNamed(@"Ok.png", BITHOCKEYSDK_BUNDLE) style:UIBarButtonItemStyleBordered target:self action:@selector(save:)]; + + self.view.autoresizesSubviews = NO; +} + + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(orientationDidChange:) name:UIDeviceOrientationDidChangeNotification object:nil]; + + [self fitImageViewFrame]; + +} + +- (void)viewWillDisappear:(BOOL)animated { + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil]; + +} + +- (BOOL)prefersStatusBarHidden { + return self.navigationController.navigationBarHidden || self.navigationController.navigationBar.alpha == 0.0f; +} + +- (void)orientationDidChange:(NSNotification *)notification { + [self fitImageViewFrame]; +} + + +- (void)fitImageViewFrame { + + CGSize size = [UIScreen mainScreen].bounds.size; + if (UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)){ + size = CGSizeMake(size.height, size.width); + } + + CGFloat heightScaleFactor = size.height / self.image.size.height; + CGFloat widthScaleFactor = size.width / self.image.size.width; + + CGFloat factor = MIN(heightScaleFactor, widthScaleFactor); + self.scaleFactor = factor; + CGSize scaledImageSize = CGSizeMake(self.image.size.width * factor, self.image.size.height * factor); + + CGRect baseFrame = CGRectMake(self.view.frame.size.width/2 - scaledImageSize.width/2, self.view.frame.size.height/2 - scaledImageSize.height/2, scaledImageSize.width, scaledImageSize.height); + + self.imageView.frame = baseFrame; +} + +-(void)editingAction:(id)sender { + +} + +- (BITImageAnnotation *)annotationForCurrentMode { + if (self.editingControls.selectedSegmentIndex == 0){ + return [[BITArrowImageAnnotation alloc] initWithFrame:CGRectZero]; + } else if(self.editingControls.selectedSegmentIndex==1){ + return [[BITRectangleImageAnnotation alloc] initWithFrame:CGRectZero]; + } else { + return [[BITBlurImageAnnotation alloc] initWithFrame:CGRectZero]; + } +} + +#pragma mark - Actions + +- (void)discard:(id)sender { + [self.delegate annotationControllerDidCancel:self]; + [self dismissViewControllerAnimated:YES completion:nil]; +} + +- (void)save:(id)sender { + UIImage *image = [self extractImage]; + [self.delegate annotationController:self didFinishWithImage:image]; + [self dismissViewControllerAnimated:YES completion:nil]; +} + +- (UIImage *)extractImage { + UIGraphicsBeginImageContextWithOptions(self.image.size, YES, 0.0); + CGContextRef ctx = UIGraphicsGetCurrentContext(); + [self.image drawInRect:CGRectMake(0, 0, self.image.size.width, self.image.size.height)]; + CGContextScaleCTM(ctx,1.0/self.scaleFactor,1.0f/self.scaleFactor); + + // Drawing all the annotations onto the final image. + for (BITImageAnnotation *annotation in self.objects){ + CGContextTranslateCTM(ctx, annotation.frame.origin.x, annotation.frame.origin.y); + [annotation.layer renderInContext:ctx]; + CGContextTranslateCTM(ctx,-1 * annotation.frame.origin.x,-1 * annotation.frame.origin.y); + } + + UIImage *renderedImageOfMyself = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return renderedImageOfMyself; +} + +#pragma mark - UIGestureRecognizers + +- (void)panned:(UIPanGestureRecognizer *)gestureRecognizer { + BITImageAnnotation *annotationAtLocation = (BITImageAnnotation *)[self.view hitTest:[gestureRecognizer locationInView:self.view] withEvent:nil]; + + if (![annotationAtLocation isKindOfClass:[BITImageAnnotation class]]){ + annotationAtLocation = nil; + } + + // determine the interaction mode if none is set so far. + + if (self.currentInteraction == BITImageAnnotationViewControllerInteractionModeNone){ + if (annotationAtLocation){ + self.currentInteraction = BITImageAnnotationViewControllerInteractionModeMove; + } else if ([self canDrawNewAnnotation]){ + self.currentInteraction = BITImageAnnotationViewControllerInteractionModeDraw; + } + } + + if (self.currentInteraction == BITImageAnnotationViewControllerInteractionModeNone){ + return; + } + + + if (self.currentInteraction == BITImageAnnotationViewControllerInteractionModeDraw){ + if (gestureRecognizer.state == UIGestureRecognizerStateBegan){ + self.currentAnnotation = [self annotationForCurrentMode]; + [self.objects addObject:self.currentAnnotation]; + self.currentAnnotation.sourceImage = self.image; + + if (self.imageView.subviews.count > 0 && [self.currentAnnotation isKindOfClass:[BITBlurImageAnnotation class]]){ + [self.imageView insertSubview:self.currentAnnotation belowSubview:[self firstAnnotationThatIsNotBlur]]; + } else { + [self.imageView addSubview:self.currentAnnotation]; + } + + self.panStart = [gestureRecognizer locationInView:self.imageView]; + + // [self.editingControls setSelectedSegmentIndex:UISegmentedControlNoSegment]; + + } else if (gestureRecognizer.state == UIGestureRecognizerStateChanged){ + CGPoint bla = [gestureRecognizer locationInView:self.imageView]; + self.currentAnnotation.frame = CGRectMake(self.panStart.x, self.panStart.y, bla.x - self.panStart.x, bla.y - self.panStart.y); + self.currentAnnotation.movedDelta = CGSizeMake(bla.x - self.panStart.x, bla.y - self.panStart.y); + self.currentAnnotation.imageFrame = [self.view convertRect:self.imageView.frame toView:self.currentAnnotation]; + [self.currentAnnotation setNeedsLayout]; + [self.currentAnnotation layoutIfNeeded]; + } else { + [self.currentAnnotation setSelected:NO]; + self.currentAnnotation = nil; + self.currentInteraction = BITImageAnnotationViewControllerInteractionModeNone; + } + } else if (self.currentInteraction == BITImageAnnotationViewControllerInteractionModeMove){ + if (gestureRecognizer.state == UIGestureRecognizerStateBegan){ + // find and possibly move an existing annotation. + + + if ([self.objects indexOfObject:annotationAtLocation] != NSNotFound){ + self.currentAnnotation = annotationAtLocation; + [annotationAtLocation setSelected:YES]; + } + + + } else if (gestureRecognizer.state == UIGestureRecognizerStateChanged && self.currentAnnotation){ + CGPoint delta = [gestureRecognizer translationInView:self.view]; + + CGRect annotationFrame = self.currentAnnotation.frame; + annotationFrame.origin.x += delta.x; + annotationFrame.origin.y += delta.y; + self.currentAnnotation.frame = annotationFrame; + self.currentAnnotation.imageFrame = [self.view convertRect:self.imageView.frame toView:self.currentAnnotation]; + + [self.currentAnnotation setNeedsLayout]; + [self.currentAnnotation layoutIfNeeded]; + + [gestureRecognizer setTranslation:CGPointZero inView:self.view]; + + } else { + [self.currentAnnotation setSelected:NO]; + self.currentAnnotation = nil; + self.currentInteraction = BITImageAnnotationViewControllerInteractionModeNone; + } + } +} + +-(void)pinched:(UIPinchGestureRecognizer *)gestureRecognizer { + if (gestureRecognizer.state == UIGestureRecognizerStateBegan){ + // try to figure out which view we are talking about. + BITImageAnnotation *candidate = nil; + BOOL validView = YES; + + for ( int i = 0; i1){ + CGRect newFrame= (self.pinchStartingFrame); + + // upper point? + CGPoint point1 = [gestureRecognizer locationOfTouch:0 inView:self.view]; + CGPoint point2 = [gestureRecognizer locationOfTouch:1 inView:self.view]; + + + newFrame.origin.x = point1.x; + newFrame.origin.y = point1.y; + + newFrame.origin.x = (point1.x > point2.x) ? point2.x : point1.x; + newFrame.origin.y = (point1.y > point2.y) ? point2.y : point1.y; + + newFrame.size.width = (point1.x > point2.x) ? point1.x - point2.x : point2.x - point1.x; + newFrame.size.height = (point1.y > point2.y) ? point1.y - point2.y : point2.y - point1.y; + + + self.currentAnnotation.frame = newFrame; + self.currentAnnotation.imageFrame = [self.view convertRect:self.imageView.frame toView:self.currentAnnotation]; + } else { + [self.currentAnnotation setSelected:NO]; + self.currentAnnotation = nil; + } +} + +-(void)tapped:(UIGestureRecognizer *)tapRecognizer { + // This toggles the nav and status bar. Since iOS7 and pre-iOS7 behave weirdly different, + // this might look rather hacky, but hiding the navbar under iOS6 leads to some ugly + // animation effect which is avoided by simply hiding the navbar setting it's alpha to 0. // moritzh + + if (self.navigationController.navigationBar.alpha == 0 || self.navigationController.navigationBarHidden ){ + + [UIView animateWithDuration:0.35f animations:^{ + + if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) { + [self.navigationController setNavigationBarHidden:NO animated:NO]; + } else { + self.navigationController.navigationBar.alpha = 1.0; + } + + [[UIApplication sharedApplication] setStatusBarHidden:NO]; + + } completion:^(BOOL finished) { + [self fitImageViewFrame]; + + }]; + } else { + [UIView animateWithDuration:0.35f animations:^{ + + if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) { + [self.navigationController setNavigationBarHidden:YES animated:NO]; + } else { + self.navigationController.navigationBar.alpha = 0.0; + } + + [[UIApplication sharedApplication] setStatusBarHidden:YES]; + + } completion:^(BOOL finished) { + [self fitImageViewFrame]; + + }]; + } + +} + +#pragma mark - Helpers + +-(UIView *)firstAnnotationThatIsNotBlur { + for (BITImageAnnotation *annotation in self.imageView.subviews){ + if (![annotation isKindOfClass:[BITBlurImageAnnotation class]]){ + return annotation; + } + } + + return self.imageView; +} + +- (BOOL)canDrawNewAnnotation { + return [self.editingControls selectedSegmentIndex] != UISegmentedControlNoSegment; +} +@end diff --git a/Classes/BITRectangleImageAnnotation.h b/Classes/BITRectangleImageAnnotation.h new file mode 100644 index 0000000000..024957998a --- /dev/null +++ b/Classes/BITRectangleImageAnnotation.h @@ -0,0 +1,33 @@ +/* + * Author: Moritz Haarmann + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#import "BITImageAnnotation.h" + +@interface BITRectangleImageAnnotation : BITImageAnnotation + +@end diff --git a/Classes/BITRectangleImageAnnotation.m b/Classes/BITRectangleImageAnnotation.m new file mode 100644 index 0000000000..b08f5d302e --- /dev/null +++ b/Classes/BITRectangleImageAnnotation.m @@ -0,0 +1,87 @@ +/* + * Author: Moritz Haarmann + * + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#import "BITRectangleImageAnnotation.h" + +@interface BITRectangleImageAnnotation() + +@property (nonatomic, strong) CAShapeLayer *shapeLayer; +@property (nonatomic, strong) CAShapeLayer *strokeLayer; + + +@end + +@implementation BITRectangleImageAnnotation + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + self.shapeLayer = [CAShapeLayer layer]; + self.shapeLayer.strokeColor = [UIColor redColor].CGColor; + self.shapeLayer.lineWidth = 5; + self.shapeLayer.fillColor = [UIColor clearColor].CGColor; + + self.strokeLayer = [CAShapeLayer layer]; + self.strokeLayer.strokeColor = [UIColor whiteColor].CGColor; + self.strokeLayer.lineWidth = 10; + self.strokeLayer.fillColor = [UIColor clearColor].CGColor; + [self.layer addSublayer:self.strokeLayer]; + + [self.layer addSublayer:self.shapeLayer]; + + } + return self; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + self.shapeLayer.frame = self.bounds; + self.shapeLayer.path = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:10].CGPath; + + + self.strokeLayer.frame = self.bounds; + self.strokeLayer.path = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:10].CGPath; + + CGFloat lineWidth = MAX(self.frame.size.width / 10.0f,10); + + [CATransaction begin]; + [CATransaction setAnimationDuration:0]; + self.strokeLayer.lineWidth = lineWidth/1.5f; + self.shapeLayer.lineWidth = lineWidth / 3.0f; + + [CATransaction commit]; +} + +-(BOOL)resizable { + return YES; +} + + +@end diff --git a/Resources/Arrow.png b/Resources/Arrow.png new file mode 100644 index 0000000000..6d8fac1039 Binary files /dev/null and b/Resources/Arrow.png differ diff --git a/Resources/Arrow@2x.png b/Resources/Arrow@2x.png new file mode 100644 index 0000000000..116a47ee03 Binary files /dev/null and b/Resources/Arrow@2x.png differ diff --git a/Resources/Blur.png b/Resources/Blur.png new file mode 100644 index 0000000000..7dd64b9fe3 Binary files /dev/null and b/Resources/Blur.png differ diff --git a/Resources/Blur@2x.png b/Resources/Blur@2x.png new file mode 100644 index 0000000000..8cf6ffe5bc Binary files /dev/null and b/Resources/Blur@2x.png differ diff --git a/Resources/Cancel.png b/Resources/Cancel.png new file mode 100644 index 0000000000..b8112db68f Binary files /dev/null and b/Resources/Cancel.png differ diff --git a/Resources/Cancel@2x.png b/Resources/Cancel@2x.png new file mode 100644 index 0000000000..3ddef8cb23 Binary files /dev/null and b/Resources/Cancel@2x.png differ diff --git a/Resources/Ok.png b/Resources/Ok.png new file mode 100644 index 0000000000..865372fa37 Binary files /dev/null and b/Resources/Ok.png differ diff --git a/Resources/Ok@2x.png b/Resources/Ok@2x.png new file mode 100644 index 0000000000..f1915b84f8 Binary files /dev/null and b/Resources/Ok@2x.png differ diff --git a/Resources/Rectangle.png b/Resources/Rectangle.png new file mode 100644 index 0000000000..49d81ea6b4 Binary files /dev/null and b/Resources/Rectangle.png differ diff --git a/Resources/Rectangle@2x.png b/Resources/Rectangle@2x.png new file mode 100644 index 0000000000..c7e413d1d4 Binary files /dev/null and b/Resources/Rectangle@2x.png differ diff --git a/Resources/de.lproj/HockeySDK.strings b/Resources/de.lproj/HockeySDK.strings index 8a4b45b734..67a9dbcd19 100644 --- a/Resources/de.lproj/HockeySDK.strings +++ b/Resources/de.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "Senden"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Bild Hinzufügen"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Anhang Bearbeiten"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Anhang Löschen"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Abbrechen"; + /* Set User Data */ diff --git a/Resources/en.lproj/HockeySDK.strings b/Resources/en.lproj/HockeySDK.strings index 25229c843d..97cfa87843 100644 --- a/Resources/en.lproj/HockeySDK.strings +++ b/Resources/en.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "Send"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; + /* Set User Data */ diff --git a/Resources/es.lproj/HockeySDK.strings b/Resources/es.lproj/HockeySDK.strings index 514fd282f8..935c961cda 100644 --- a/Resources/es.lproj/HockeySDK.strings +++ b/Resources/es.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "Enviar"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; + /* Set User Data */ diff --git a/Resources/fr.lproj/HockeySDK.strings b/Resources/fr.lproj/HockeySDK.strings index a231bf4f4d..4cae9d6442 100644 --- a/Resources/fr.lproj/HockeySDK.strings +++ b/Resources/fr.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "Envoyer"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; + /* Set User Data */ diff --git a/Resources/hr.lproj/HockeySDK.strings b/Resources/hr.lproj/HockeySDK.strings index 9f5a7520c8..142e4100a6 100644 --- a/Resources/hr.lproj/HockeySDK.strings +++ b/Resources/hr.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "Šalji"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; + /* Set User Data */ diff --git a/Resources/hu.lproj/HockeySDK.strings b/Resources/hu.lproj/HockeySDK.strings index 7ecaea350b..0b248eb55d 100644 --- a/Resources/hu.lproj/HockeySDK.strings +++ b/Resources/hu.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "Küldés"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; + /* Set User Data */ diff --git a/Resources/iconCamera.png b/Resources/iconCamera.png new file mode 100755 index 0000000000..bdace4d2e4 Binary files /dev/null and b/Resources/iconCamera.png differ diff --git a/Resources/iconCamera@2x.png b/Resources/iconCamera@2x.png new file mode 100755 index 0000000000..10afe448e5 Binary files /dev/null and b/Resources/iconCamera@2x.png differ diff --git a/Resources/it.lproj/HockeySDK.strings b/Resources/it.lproj/HockeySDK.strings index 75753bab84..e3c965dea5 100644 --- a/Resources/it.lproj/HockeySDK.strings +++ b/Resources/it.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "Invia"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; + /* Set User Data */ diff --git a/Resources/ja.lproj/HockeySDK.strings b/Resources/ja.lproj/HockeySDK.strings index 4532f8a583..8dbb471b05 100644 --- a/Resources/ja.lproj/HockeySDK.strings +++ b/Resources/ja.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "送信"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; + /* Set User Data */ diff --git a/Resources/nl.lproj/HockeySDK.strings b/Resources/nl.lproj/HockeySDK.strings index 0d37d47206..854d891641 100644 --- a/Resources/nl.lproj/HockeySDK.strings +++ b/Resources/nl.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "Verstuur"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; + /* Set User Data */ diff --git a/Resources/pt-PT.lproj/HockeySDK.strings b/Resources/pt-PT.lproj/HockeySDK.strings index 70928c4dcb..61df2895f3 100644 --- a/Resources/pt-PT.lproj/HockeySDK.strings +++ b/Resources/pt-PT.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "Enviar"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; + /* Set User Data */ diff --git a/Resources/pt.lproj/HockeySDK.strings b/Resources/pt.lproj/HockeySDK.strings index 1e09c89615..8f41112c85 100644 --- a/Resources/pt.lproj/HockeySDK.strings +++ b/Resources/pt.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "Enviar"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; + /* Set User Data */ diff --git a/Resources/ro.lproj/HockeySDK.strings b/Resources/ro.lproj/HockeySDK.strings index 6c0225cf8d..2cb78d86e5 100644 --- a/Resources/ro.lproj/HockeySDK.strings +++ b/Resources/ro.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "Trimite"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; + /* Set User Data */ diff --git a/Resources/ru.lproj/HockeySDK.strings b/Resources/ru.lproj/HockeySDK.strings index da77e3f87b..abaa8e140c 100644 --- a/Resources/ru.lproj/HockeySDK.strings +++ b/Resources/ru.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "Отправить"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; + /* Set User Data */ diff --git a/Resources/zh-Hans.lproj/HockeySDK.strings b/Resources/zh-Hans.lproj/HockeySDK.strings index fc8859be3c..fffe641d96 100644 --- a/Resources/zh-Hans.lproj/HockeySDK.strings +++ b/Resources/zh-Hans.lproj/HockeySDK.strings @@ -212,6 +212,18 @@ /* Send button */ "HockeyFeedbackComposeSend" = "发送"; +/* Add Image button for attachment actions */ +"HockeyFeedbackComposeAttachmentAddImage" = "+ Add Image"; + +/* Edit button for attachment actions */ +"HockeyFeedbackComposeAttachmentEdit" = "Edit Attachment"; + +/* Delete button for attachment actions */ +"HockeyFeedbackComposeAttachmentDelete" = "Delete Attachment"; + +/* Cancel button for attachment actions */ +"HockeyFeedbackComposeAttachmentCancel" = "Cancel"; + /* Set User Data */ diff --git a/Support/HockeySDK.xcconfig b/Support/HockeySDK.xcconfig index 45a57e9a0e..012479d8a8 100644 --- a/Support/HockeySDK.xcconfig +++ b/Support/HockeySDK.xcconfig @@ -1,3 +1,3 @@ -OTHER_LDFLAGS=$(inherited) -framework CoreText -framework CoreGraphics -framework Foundation -framework QuartzCore -framework SystemConfiguration -framework UIKit -framework Security +OTHER_LDFLAGS=$(inherited) -framework CoreText -framework CoreGraphics -framework Foundation -framework QuartzCore -framework SystemConfiguration -framework UIKit -framework Security -framework AssetsLibrary HOCKEYSDK_DOCSET_NAME=HockeySDK-iOS GCC_PREPROCESSOR_DEFINITIONS=$(inherited) CONFIGURATION_$(CONFIGURATION) diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index 73f560e05f..751e04ef51 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -138,6 +138,35 @@ 1EF95CA7162CB037000AE3AD /* BITFeedbackActivity.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EF95CA5162CB036000AE3AD /* BITFeedbackActivity.m */; }; 1EF95CAA162CB314000AE3AD /* BITFeedbackComposeViewControllerDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 1EF95CA9162CB313000AE3AD /* BITFeedbackComposeViewControllerDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1EFF03E517F2485500A5F13C /* BITCrashManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EFF03E417F2485500A5F13C /* BITCrashManagerTests.m */; }; + 973EC8B418BCA7BC00DBFFBB /* BITImageAnnotationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 97F0FA0018AE375E00EF50AA /* BITImageAnnotationViewController.m */; }; + 973EC8B718BCA8A200DBFFBB /* BITRectangleImageAnnotation.h in Headers */ = {isa = PBXBuildFile; fileRef = 973EC8B518BCA8A200DBFFBB /* BITRectangleImageAnnotation.h */; }; + 973EC8B818BCA8A200DBFFBB /* BITRectangleImageAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 973EC8B618BCA8A200DBFFBB /* BITRectangleImageAnnotation.m */; }; + 973EC8BB18BDE29800DBFFBB /* BITArrowImageAnnotation.h in Headers */ = {isa = PBXBuildFile; fileRef = 973EC8B918BDE29800DBFFBB /* BITArrowImageAnnotation.h */; }; + 973EC8BC18BDE29800DBFFBB /* BITArrowImageAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 973EC8BA18BDE29800DBFFBB /* BITArrowImageAnnotation.m */; }; + 973EC8BF18BE2B5B00DBFFBB /* BITBlurImageAnnotation.h in Headers */ = {isa = PBXBuildFile; fileRef = 973EC8BD18BE2B5B00DBFFBB /* BITBlurImageAnnotation.h */; }; + 973EC8C018BE2B5B00DBFFBB /* BITBlurImageAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 973EC8BE18BE2B5B00DBFFBB /* BITBlurImageAnnotation.m */; }; + 9760F6C418BB4D2D00959B93 /* AssetsLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9760F6C318BB4D2D00959B93 /* AssetsLibrary.framework */; }; + 9760F6CF18BB685600959B93 /* BITImageAnnotation.h in Headers */ = {isa = PBXBuildFile; fileRef = 9760F6CD18BB685600959B93 /* BITImageAnnotation.h */; }; + 9760F6D018BB685600959B93 /* BITImageAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 9760F6CE18BB685600959B93 /* BITImageAnnotation.m */; }; + 9774BCFF192CB20A00085EB5 /* BITActivityIndicatorButton.h in Headers */ = {isa = PBXBuildFile; fileRef = 9774BCFD192CB20A00085EB5 /* BITActivityIndicatorButton.h */; }; + 9774BD00192CB20A00085EB5 /* BITActivityIndicatorButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 9774BCFE192CB20A00085EB5 /* BITActivityIndicatorButton.m */; }; + 9782023218F81BFC00A98D8B /* Arrow.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022818F81BFC00A98D8B /* Arrow.png */; }; + 9782023318F81BFC00A98D8B /* Arrow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022918F81BFC00A98D8B /* Arrow@2x.png */; }; + 9782023418F81BFC00A98D8B /* Blur.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022A18F81BFC00A98D8B /* Blur.png */; }; + 9782023518F81BFC00A98D8B /* Blur@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022B18F81BFC00A98D8B /* Blur@2x.png */; }; + 9782023618F81BFC00A98D8B /* Cancel.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022C18F81BFC00A98D8B /* Cancel.png */; }; + 9782023718F81BFC00A98D8B /* Cancel@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022D18F81BFC00A98D8B /* Cancel@2x.png */; }; + 9782023818F81BFC00A98D8B /* Ok.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022E18F81BFC00A98D8B /* Ok.png */; }; + 9782023918F81BFC00A98D8B /* Ok@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782022F18F81BFC00A98D8B /* Ok@2x.png */; }; + 9782023A18F81BFC00A98D8B /* Rectangle.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782023018F81BFC00A98D8B /* Rectangle.png */; }; + 9782023B18F81BFC00A98D8B /* Rectangle@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9782023118F81BFC00A98D8B /* Rectangle@2x.png */; }; + 97BD9BD5191109730043FD59 /* QuickLook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97BD9BD4191109730043FD59 /* QuickLook.framework */; }; + 97CC11F71917AB7C0028768F /* QuickLook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97BD9BD4191109730043FD59 /* QuickLook.framework */; }; + 97CC11F91917C0310028768F /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97CC11F81917C0310028768F /* MobileCoreServices.framework */; }; + 97CC11FA1917C0390028768F /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97CC11F81917C0310028768F /* MobileCoreServices.framework */; }; + 97F0F9FD18ABAECD00EF50AA /* iconCamera.png in Resources */ = {isa = PBXBuildFile; fileRef = 97F0F9FB18ABAECD00EF50AA /* iconCamera.png */; }; + 97F0F9FE18ABAECD00EF50AA /* iconCamera@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 97F0F9FC18ABAECD00EF50AA /* iconCamera@2x.png */; }; + 97F0FA0518B2294D00EF50AA /* BITFeedbackMessageAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 97F0FA0318AE5AED00EF50AA /* BITFeedbackMessageAttachment.m */; }; E405266217A2AD300096359C /* BITFeedbackManagerDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E405266117A2AD300096359C /* BITFeedbackManagerDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; E40E0B0917DA19DC005E38C1 /* BITHockeyAppClientTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E40E0B0817DA19DC005E38C1 /* BITHockeyAppClientTests.m */; }; E40E0B0C17DA1AFF005E38C1 /* BITHockeyAppClient.h in Headers */ = {isa = PBXBuildFile; fileRef = E40E0B0A17DA1AFF005E38C1 /* BITHockeyAppClient.h */; }; @@ -303,6 +332,35 @@ 1EF95CA9162CB313000AE3AD /* BITFeedbackComposeViewControllerDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITFeedbackComposeViewControllerDelegate.h; sourceTree = ""; }; 1EFF03D717F20F8300A5F13C /* BITCrashManagerPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BITCrashManagerPrivate.h; sourceTree = ""; }; 1EFF03E417F2485500A5F13C /* BITCrashManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITCrashManagerTests.m; sourceTree = ""; }; + 973EC8B518BCA8A200DBFFBB /* BITRectangleImageAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITRectangleImageAnnotation.h; sourceTree = ""; }; + 973EC8B618BCA8A200DBFFBB /* BITRectangleImageAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITRectangleImageAnnotation.m; sourceTree = ""; }; + 973EC8B918BDE29800DBFFBB /* BITArrowImageAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITArrowImageAnnotation.h; sourceTree = ""; }; + 973EC8BA18BDE29800DBFFBB /* BITArrowImageAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITArrowImageAnnotation.m; sourceTree = ""; }; + 973EC8BD18BE2B5B00DBFFBB /* BITBlurImageAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITBlurImageAnnotation.h; sourceTree = ""; }; + 973EC8BE18BE2B5B00DBFFBB /* BITBlurImageAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITBlurImageAnnotation.m; sourceTree = ""; }; + 9760F6C318BB4D2D00959B93 /* AssetsLibrary.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AssetsLibrary.framework; path = System/Library/Frameworks/AssetsLibrary.framework; sourceTree = SDKROOT; }; + 9760F6CD18BB685600959B93 /* BITImageAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITImageAnnotation.h; sourceTree = ""; }; + 9760F6CE18BB685600959B93 /* BITImageAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITImageAnnotation.m; sourceTree = ""; }; + 9774BCFD192CB20A00085EB5 /* BITActivityIndicatorButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITActivityIndicatorButton.h; sourceTree = ""; }; + 9774BCFE192CB20A00085EB5 /* BITActivityIndicatorButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITActivityIndicatorButton.m; sourceTree = ""; }; + 9782022818F81BFC00A98D8B /* Arrow.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Arrow.png; sourceTree = ""; }; + 9782022918F81BFC00A98D8B /* Arrow@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Arrow@2x.png"; sourceTree = ""; }; + 9782022A18F81BFC00A98D8B /* Blur.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Blur.png; sourceTree = ""; }; + 9782022B18F81BFC00A98D8B /* Blur@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Blur@2x.png"; sourceTree = ""; }; + 9782022C18F81BFC00A98D8B /* Cancel.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Cancel.png; sourceTree = ""; }; + 9782022D18F81BFC00A98D8B /* Cancel@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Cancel@2x.png"; sourceTree = ""; }; + 9782022E18F81BFC00A98D8B /* Ok.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Ok.png; sourceTree = ""; }; + 9782022F18F81BFC00A98D8B /* Ok@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Ok@2x.png"; sourceTree = ""; }; + 9782023018F81BFC00A98D8B /* Rectangle.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Rectangle.png; sourceTree = ""; }; + 9782023118F81BFC00A98D8B /* Rectangle@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Rectangle@2x.png"; sourceTree = ""; }; + 97BD9BD4191109730043FD59 /* QuickLook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLook.framework; path = System/Library/Frameworks/QuickLook.framework; sourceTree = SDKROOT; }; + 97CC11F81917C0310028768F /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; + 97F0F9FB18ABAECD00EF50AA /* iconCamera.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = iconCamera.png; sourceTree = ""; }; + 97F0F9FC18ABAECD00EF50AA /* iconCamera@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "iconCamera@2x.png"; sourceTree = ""; }; + 97F0F9FF18AE375E00EF50AA /* BITImageAnnotationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITImageAnnotationViewController.h; sourceTree = ""; }; + 97F0FA0018AE375E00EF50AA /* BITImageAnnotationViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITImageAnnotationViewController.m; sourceTree = ""; }; + 97F0FA0218AE5AED00EF50AA /* BITFeedbackMessageAttachment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITFeedbackMessageAttachment.h; sourceTree = ""; }; + 97F0FA0318AE5AED00EF50AA /* BITFeedbackMessageAttachment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITFeedbackMessageAttachment.m; sourceTree = ""; }; BEE0207C16C5107E004426EA /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/HockeySDK.strings; sourceTree = ""; }; E400561D148D79B500EB22B9 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; E405266117A2AD300096359C /* BITFeedbackManagerDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITFeedbackManagerDelegate.h; sourceTree = ""; }; @@ -328,6 +386,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 97CC11F91917C0310028768F /* MobileCoreServices.framework in Frameworks */, + 97BD9BD5191109730043FD59 /* QuickLook.framework in Frameworks */, + 9760F6C418BB4D2D00959B93 /* AssetsLibrary.framework in Frameworks */, 1E5954DC15B6F24A00A03429 /* Foundation.framework in Frameworks */, 1E5954DD15B6F24A00A03429 /* CrashReporter.framework in Frameworks */, ); @@ -344,6 +405,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 97CC11FA1917C0390028768F /* MobileCoreServices.framework in Frameworks */, + 97CC11F71917AB7C0028768F /* QuickLook.framework in Frameworks */, 1EA1170016F4D32C001C015C /* libHockeySDK.a in Frameworks */, 1E5A459216F0DFC200B55C04 /* SenTestingKit.framework in Frameworks */, 1EA1170116F4D354001C015C /* CrashReporter.framework in Frameworks */, @@ -359,6 +422,18 @@ 1E5955A415B71BDC00A03429 /* Images */ = { isa = PBXGroup; children = ( + 9782022818F81BFC00A98D8B /* Arrow.png */, + 9782022918F81BFC00A98D8B /* Arrow@2x.png */, + 9782022A18F81BFC00A98D8B /* Blur.png */, + 9782022B18F81BFC00A98D8B /* Blur@2x.png */, + 9782022C18F81BFC00A98D8B /* Cancel.png */, + 9782022D18F81BFC00A98D8B /* Cancel@2x.png */, + 9782022E18F81BFC00A98D8B /* Ok.png */, + 9782022F18F81BFC00A98D8B /* Ok@2x.png */, + 9782023018F81BFC00A98D8B /* Rectangle.png */, + 9782023118F81BFC00A98D8B /* Rectangle@2x.png */, + 97F0F9FB18ABAECD00EF50AA /* iconCamera.png */, + 97F0F9FC18ABAECD00EF50AA /* iconCamera@2x.png */, 1E5955BB15B71C8600A03429 /* authorize_denied.png */, 1E5955BC15B71C8600A03429 /* authorize_denied@2x.png */, 1E5955BF15B71C8600A03429 /* bg.png */, @@ -450,8 +525,11 @@ 1E754E461621FA9A0070AB92 /* Feedback */ = { isa = PBXGroup; children = ( + 9760F6CC18BB684200959B93 /* Image Editor */, 1E49A4361612223B00463151 /* BITFeedbackMessage.h */, 1E49A4371612223B00463151 /* BITFeedbackMessage.m */, + 97F0FA0218AE5AED00EF50AA /* BITFeedbackMessageAttachment.h */, + 97F0FA0318AE5AED00EF50AA /* BITFeedbackMessageAttachment.m */, 1E49A42D1612223B00463151 /* BITFeedbackComposeViewController.h */, 1E49A42E1612223B00463151 /* BITFeedbackComposeViewController.m */, 1EF95CA9162CB313000AE3AD /* BITFeedbackComposeViewControllerDelegate.h */, @@ -467,6 +545,8 @@ 1E49A4341612223B00463151 /* BITFeedbackManager.m */, E405266117A2AD300096359C /* BITFeedbackManagerDelegate.h */, 1E49A4351612223B00463151 /* BITFeedbackManagerPrivate.h */, + 9774BCFD192CB20A00085EB5 /* BITActivityIndicatorButton.h */, + 9774BCFE192CB20A00085EB5 /* BITActivityIndicatorButton.m */, ); name = Feedback; sourceTree = ""; @@ -550,6 +630,23 @@ name = Private; sourceTree = ""; }; + 9760F6CC18BB684200959B93 /* Image Editor */ = { + isa = PBXGroup; + children = ( + 97F0F9FF18AE375E00EF50AA /* BITImageAnnotationViewController.h */, + 97F0FA0018AE375E00EF50AA /* BITImageAnnotationViewController.m */, + 9760F6CD18BB685600959B93 /* BITImageAnnotation.h */, + 9760F6CE18BB685600959B93 /* BITImageAnnotation.m */, + 973EC8B518BCA8A200DBFFBB /* BITRectangleImageAnnotation.h */, + 973EC8B618BCA8A200DBFFBB /* BITRectangleImageAnnotation.m */, + 973EC8B918BDE29800DBFFBB /* BITArrowImageAnnotation.h */, + 973EC8BA18BDE29800DBFFBB /* BITArrowImageAnnotation.m */, + 973EC8BD18BE2B5B00DBFFBB /* BITBlurImageAnnotation.h */, + 973EC8BE18BE2B5B00DBFFBB /* BITBlurImageAnnotation.m */, + ); + name = "Image Editor"; + sourceTree = ""; + }; E400560F148D79B500EB22B9 = { isa = PBXGroup; children = ( @@ -578,6 +675,9 @@ E400561C148D79B500EB22B9 /* Frameworks */ = { isa = PBXGroup; children = ( + 97CC11F81917C0310028768F /* MobileCoreServices.framework */, + 97BD9BD4191109730043FD59 /* QuickLook.framework */, + 9760F6C318BB4D2D00959B93 /* AssetsLibrary.framework */, E41EB48B148D7C4E0015DEDC /* CrashReporter.framework */, E400561D148D79B500EB22B9 /* Foundation.framework */, 1E5A459116F0DFC200B55C04 /* SenTestingKit.framework */, @@ -659,6 +759,7 @@ 1E49A4731612226D00463151 /* BITUpdateManager.h in Headers */, 1E49A4791612226D00463151 /* BITUpdateManagerDelegate.h in Headers */, 1E49A44E1612223B00463151 /* BITFeedbackManager.h in Headers */, + 973EC8BF18BE2B5B00DBFFBB /* BITBlurImageAnnotation.h in Headers */, E4B4DB7D17B435550099C67F /* BITAuthenticationViewController.h in Headers */, 1E49A4481612223B00463151 /* BITFeedbackListViewController.h in Headers */, 1ECA8F4D192B5BD8006B9416 /* BITCrashDetailsPrivate.h in Headers */, @@ -681,10 +782,13 @@ 1E49A4851612226D00463151 /* BITUpdateViewControllerPrivate.h in Headers */, 1ECA8F51192B6954006B9416 /* BITCrashMetaData.h in Headers */, 1E49A4B5161222B900463151 /* BITHockeyBaseManagerPrivate.h in Headers */, + 9774BCFF192CB20A00085EB5 /* BITActivityIndicatorButton.h in Headers */, E4933E8017B66CDA00B11ACC /* BITHTTPOperation.h in Headers */, 1E49A4BE161222B900463151 /* BITHockeyHelper.h in Headers */, + 973EC8BB18BDE29800DBFFBB /* BITArrowImageAnnotation.h in Headers */, 1E49A4C4161222B900463151 /* BITAppStoreHeader.h in Headers */, 1E49A4CA161222B900463151 /* BITStoreButton.h in Headers */, + 973EC8B718BCA8A200DBFFBB /* BITRectangleImageAnnotation.h in Headers */, E405266217A2AD300096359C /* BITFeedbackManagerDelegate.h in Headers */, 1E49A4D0161222B900463151 /* BITWebTableViewCell.h in Headers */, 1E49A4D8161222D400463151 /* HockeySDKPrivate.h in Headers */, @@ -694,6 +798,7 @@ 1EACC97B162F041E007578C5 /* BITAttributedLabel.h in Headers */, 1E0FEE28173BDB260061331F /* BITKeychainUtils.h in Headers */, 1E94F9E416E9136B006570AD /* BITStoreUpdateManagerPrivate.h in Headers */, + 9760F6CF18BB685600959B93 /* BITImageAnnotation.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -807,20 +912,32 @@ files = ( 1E5955C615B71C8600A03429 /* authorize_denied.png in Resources */, 1E5955C715B71C8600A03429 /* authorize_denied@2x.png in Resources */, + 9782023B18F81BFC00A98D8B /* Rectangle@2x.png in Resources */, 1E5955CA15B71C8600A03429 /* bg.png in Resources */, + 97F0F9FD18ABAECD00EF50AA /* iconCamera.png in Resources */, + 9782023618F81BFC00A98D8B /* Cancel.png in Resources */, + 9782023318F81BFC00A98D8B /* Arrow@2x.png in Resources */, + 9782023818F81BFC00A98D8B /* Ok.png in Resources */, 1E5955CB15B71C8600A03429 /* buttonHighlight.png in Resources */, 1E5955CC15B71C8600A03429 /* buttonHighlight@2x.png in Resources */, 1E5955CF15B71C8600A03429 /* IconGradient.png in Resources */, 1E5955D015B71C8600A03429 /* IconGradient@2x.png in Resources */, + 9782023218F81BFC00A98D8B /* Arrow.png in Resources */, + 9782023A18F81BFC00A98D8B /* Rectangle.png in Resources */, 1EAF20A8162DC0F600957B1D /* feedbackActivity@2x~ipad.png in Resources */, 1EAF20A9162DC0F600957B1D /* feedbackActivity~ipad.png in Resources */, + 9782023918F81BFC00A98D8B /* Ok@2x.png in Resources */, + 9782023418F81BFC00A98D8B /* Blur.png in Resources */, 1EAF20AA162DC0F600957B1D /* feedbackActiviy.png in Resources */, 1EAF20AB162DC0F600957B1D /* feedbackActiviy@2x.png in Resources */, + 9782023518F81BFC00A98D8B /* Blur@2x.png in Resources */, 1E1127C416580C87007067A2 /* buttonRoundedDelete.png in Resources */, 1E1127C516580C87007067A2 /* buttonRoundedDelete@2x.png in Resources */, 1E1127C616580C87007067A2 /* buttonRoundedDeleteHighlighted.png in Resources */, + 9782023718F81BFC00A98D8B /* Cancel@2x.png in Resources */, 1E1127C716580C87007067A2 /* buttonRoundedDeleteHighlighted@2x.png in Resources */, 1E1127C816580C87007067A2 /* buttonRoundedRegular.png in Resources */, + 97F0F9FE18ABAECD00EF50AA /* iconCamera@2x.png in Resources */, 1E1127C916580C87007067A2 /* buttonRoundedRegular@2x.png in Resources */, 1E1127CA16580C87007067A2 /* buttonRoundedRegularHighlighted.png in Resources */, 1E1127CB16580C87007067A2 /* buttonRoundedRegularHighlighted@2x.png in Resources */, @@ -896,6 +1013,7 @@ 1E49A43F1612223B00463151 /* BITFeedbackComposeViewController.m in Sources */, E40E0B0D17DA1AFF005E38C1 /* BITHockeyAppClient.m in Sources */, 1E49A4451612223B00463151 /* BITFeedbackListViewCell.m in Sources */, + 973EC8B818BCA8A200DBFFBB /* BITRectangleImageAnnotation.m in Sources */, 1E49A44B1612223B00463151 /* BITFeedbackListViewController.m in Sources */, 1E49A4511612223B00463151 /* BITFeedbackManager.m in Sources */, 1ECA8F52192B6954006B9416 /* BITCrashMetaData.m in Sources */, @@ -905,11 +1023,15 @@ 1E49A4601612223B00463151 /* BITFeedbackUserDataViewController.m in Sources */, 1E49A4701612226D00463151 /* BITAppVersionMetaInfo.m in Sources */, 1E49A4761612226D00463151 /* BITUpdateManager.m in Sources */, + 1E49A4C1161222B900463151 /* BITHockeyHelper.m in Sources */, + 9774BD00192CB20A00085EB5 /* BITActivityIndicatorButton.m in Sources */, 1E49A4821612226D00463151 /* BITUpdateViewController.m in Sources */, E4B4DB7E17B435550099C67F /* BITAuthenticationViewController.m in Sources */, 1E49A4B2161222B900463151 /* BITHockeyBaseManager.m in Sources */, + 973EC8C018BE2B5B00DBFFBB /* BITBlurImageAnnotation.m in Sources */, + 9760F6D018BB685600959B93 /* BITImageAnnotation.m in Sources */, 1E49A4BB161222B900463151 /* BITHockeyBaseViewController.m in Sources */, - 1E49A4C1161222B900463151 /* BITHockeyHelper.m in Sources */, + 97F0FA0518B2294D00EF50AA /* BITFeedbackMessageAttachment.m in Sources */, 1E49A4C7161222B900463151 /* BITAppStoreHeader.m in Sources */, 1E49A4CD161222B900463151 /* BITStoreButton.m in Sources */, 1E90FD7418EDB86400CF0417 /* BITCrashDetails.m in Sources */, @@ -920,6 +1042,8 @@ 1E754E611621FBB70070AB92 /* BITCrashReportTextFormatter.m in Sources */, 1EF95CA7162CB037000AE3AD /* BITFeedbackActivity.m in Sources */, 1EACC97C162F041E007578C5 /* BITAttributedLabel.m in Sources */, + 973EC8BC18BDE29800DBFFBB /* BITArrowImageAnnotation.m in Sources */, + 973EC8B418BCA7BC00DBFFBB /* BITImageAnnotationViewController.m in Sources */, 1E0FEE29173BDB260061331F /* BITKeychainUtils.m in Sources */, 1E94F9E216E91330006570AD /* BITStoreUpdateManager.m in Sources */, );