// // MessagesViewController.m // Signal // // Created by Dylan Bourgeois on 28/10/14. // Copyright (c) 2014 Open Whisper Systems. All rights reserved. // #import "AppDelegate.h" #import "MessagesViewController.h" #import "FullImageViewController.h" #import "FingerprintViewController.h" #import "NewGroupViewController.h" #import "JSQCallCollectionViewCell.h" #import "JSQCall.h" #import "JSQDisplayedMessageCollectionViewCell.h" #import "JSQInfoMessage.h" #import "JSQErrorMessage.h" #import "UIUtil.h" #import "DJWActionSheet.h" #import #import #import #import "TSContactThread.h" #import "TSGroupThread.h" #import "TSStorageManager.h" #import "TSDatabaseView.h" #import #import "TSMessageAdapter.h" #import "TSErrorMessage.h" #import "TSIncomingMessage.h" #import "TSInteraction.h" #import "TSAttachmentAdapter.h" #import "TSMessagesManager+sendMessages.h" #import "TSMessagesManager+attachments.h" #import "NSDate+millisecondTimeStamp.h" #import "PhoneNumber.h" #import "Environment.h" #import "PhoneManager.h" #import "ContactsManager.h" #import "PreferencesUtil.h" static NSTimeInterval const kTSMessageSentDateShowTimeInterval = 5 * 60; static NSString *const kUpdateGroupSegueIdentifier = @"updateGroupSegue"; static NSString *const kFingerprintSegueIdentifier = @"fingerprintSegue"; typedef enum : NSUInteger { kMediaTypePicture, kMediaTypeVideo, } kMediaTypes; @interface MessagesViewController () { UIImage* tappedImage; BOOL isGroupConversation; } @property (nonatomic, retain) TSThread *thread; @property (nonatomic, strong) YapDatabaseConnection *editingDatabaseConnection; @property (nonatomic, strong) YapDatabaseConnection *uiDatabaseConnection; @property (nonatomic, strong) YapDatabaseViewMappings *messageMappings; @property (nonatomic, retain) JSQMessagesBubbleImage *outgoingBubbleImageData; @property (nonatomic, retain) JSQMessagesBubbleImage *incomingBubbleImageData; @property (nonatomic, retain) JSQMessagesBubbleImage *outgoingMessageFailedImageData; @property (nonatomic, retain) NSTimer *readTimer; @property (nonatomic, retain) NSIndexPath *lastDeliveredMessageIndexPath; @end @implementation MessagesViewController - (void)setupWithTSIdentifier:(NSString *)identifier{ [self.editingDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { self.thread = [TSContactThread threadWithContactId:identifier transaction:transaction]; }]; } - (void)setupWithTSGroup:(GroupModel*)model { [self.editingDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { self.thread = [TSGroupThread threadWithGroupModel:model transaction:transaction]; TSOutgoingMessage *message = [[TSOutgoingMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] inThread:self.thread messageBody:@"" attachments:nil]; message.groupMetaMessage = TSGroupMessageNew; [[TSMessagesManager sharedManager] sendMessage:message inThread:self.thread]; isGroupConversation = YES; }]; } - (void)setupWithThread:(TSThread *)thread{ self.thread = thread; isGroupConversation = [self.thread isKindOfClass:[TSGroupThread class]]; } - (void)viewDidLoad { [super viewDidLoad]; [self markAllMessagesAsRead]; [self initializeBubbles]; self.messageMappings = [[YapDatabaseViewMappings alloc] initWithGroups:@[self.thread.uniqueId] view:TSMessageDatabaseViewExtensionName]; [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { [self.messageMappings updateWithTransaction:transaction]; }]; [self initializeNavigationBar]; [self initializeCollectionViewLayout]; self.senderId = ME_MESSAGE_IDENTIFIER self.senderDisplayName = ME_MESSAGE_IDENTIFIER [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(startReadTimer) name:UIApplicationWillEnterForegroundNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cancelReadTimer) name:UIApplicationDidEnterBackgroundNotification object:nil]; } -(void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.automaticallyScrollsToMostRecentMessage = YES; [self scrollToBottomAnimated:NO]; } - (void)startReadTimer{ self.readTimer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(markAllMessagesAsRead) userInfo:nil repeats:YES]; } - (void)cancelReadTimer{ [self.readTimer invalidate]; } - (void)viewDidAppear:(BOOL)animated{ [super viewDidAppear:animated]; [self startReadTimer]; } - (void)viewWillDisappear:(BOOL)animated{ [super viewDidDisappear:animated]; [self cancelReadTimer]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } #pragma mark - Initiliazers -(void)initializeNavigationBar { self.title = self.thread.name; if (!isGroupConversation && [self isRedPhoneReachable]) { UIBarButtonItem * lockButton = [[UIBarButtonItem alloc]initWithImage:[UIImage imageNamed:@"lock"] style:UIBarButtonItemStylePlain target:self action:@selector(showFingerprint)]; UIBarButtonItem * callButton = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"call_tab"] style:UIBarButtonItemStylePlain target:self action:@selector(callAction)]; [callButton setImageInsets:UIEdgeInsetsMake(0, -10, 0, -50)]; UIBarButtonItem *negativeSeparator = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil]; negativeSeparator.width = -8; self.navigationItem.rightBarButtonItems = @[negativeSeparator, lockButton, callButton]; } else { UIBarButtonItem *groupMenuButton = [[UIBarButtonItem alloc]initWithImage:[UIImage imageNamed:@"settings_tab"] style:UIBarButtonItemStylePlain target:self action:@selector(didPressGroupMenuButton:)]; self.navigationItem.rightBarButtonItem = groupMenuButton; } } -(void)initializeBubbles { JSQMessagesBubbleImageFactory *bubbleFactory = [[JSQMessagesBubbleImageFactory alloc] init]; self.outgoingBubbleImageData = [bubbleFactory outgoingMessagesBubbleImageWithColor:[UIColor ows_blueColor]]; self.incomingBubbleImageData = [bubbleFactory incomingMessagesBubbleImageWithColor:[UIColor jsq_messageBubbleLightGrayColor]]; self.outgoingMessageFailedImageData = [bubbleFactory outgoingMessageFailedBubbleImageWithColor:[UIColor ows_fadedBlueColor]]; } -(void)initializeCollectionViewLayout { if (self.collectionView){ [self.collectionView.collectionViewLayout setMessageBubbleFont:[UIFont ows_lightFontWithSize:16.0f]]; self.collectionView.showsVerticalScrollIndicator = NO; self.collectionView.showsHorizontalScrollIndicator = NO; self.automaticallyScrollsToMostRecentMessage = NO; self.collectionView.collectionViewLayout.incomingAvatarViewSize = CGSizeZero; self.collectionView.collectionViewLayout.outgoingAvatarViewSize = CGSizeZero; } } #pragma mark - Fingerprints -(void)showFingerprint { [self markAllMessagesAsRead]; [self performSegueWithIdentifier:kFingerprintSegueIdentifier sender:self]; } #pragma mark - Calls -(BOOL)isRedPhoneReachable { return [[Environment getCurrent].contactsManager isPhoneNumberRegisteredWithRedPhone:[self phoneNumberForThread]]; } -(PhoneNumber*)phoneNumberForThread { NSString * contactId = [(TSContactThread*)self.thread contactIdentifier]; PhoneNumber * phoneNumber = [PhoneNumber tryParsePhoneNumberFromUserSpecifiedText:contactId]; return phoneNumber; } -(void)callAction { if ([self isRedPhoneReachable]) { [Environment.phoneManager initiateOutgoingCallToRemoteNumber:[self phoneNumberForThread]]; } else { DDLogWarn(@"Tried to initiate a call but contact has no RedPhone identifier"); } } #pragma mark - JSQMessage custom methods -(void)updateMessageStatus:(JSQMessage*)message { if ([message.senderId isEqualToString:self.senderId]){ message.status = kMessageReceived; } } #pragma mark - JSQMessagesViewController method overrides - (void)didPressSendButton:(UIButton *)button withMessageText:(NSString *)text senderId:(NSString *)senderId senderDisplayName:(NSString *)senderDisplayName date:(NSDate *)date { if (text.length > 0) { [JSQSystemSoundPlayer jsq_playMessageSentSound]; TSOutgoingMessage *message = [[TSOutgoingMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] inThread:self.thread messageBody:text attachments:nil]; [[TSMessagesManager sharedManager] sendMessage:message inThread:self.thread]; [self finishSendingMessage]; } } #pragma mark - JSQMessages CollectionView DataSource - (id)collectionView:(JSQMessagesCollectionView *)collectionView messageDataForItemAtIndexPath:(NSIndexPath *)indexPath { return [self messageAtIndexPath:indexPath]; } - (id)collectionView:(JSQMessagesCollectionView *)collectionView messageBubbleImageDataForItemAtIndexPath:(NSIndexPath *)indexPath { id message = [self messageAtIndexPath:indexPath]; if ([message.senderId isEqualToString:self.senderId]) { if (message.messageState == TSOutgoingMessageStateUnsent || message.messageState == TSOutgoingMessageStateAttemptingOut) { return self.outgoingMessageFailedImageData; } return self.outgoingBubbleImageData; } return self.incomingBubbleImageData; } - (id)collectionView:(JSQMessagesCollectionView *)collectionView avatarImageDataForItemAtIndexPath:(NSIndexPath *)indexPath { return nil; } #pragma mark - UICollectionView DataSource - (UICollectionViewCell *)collectionView:(JSQMessagesCollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { TSMessageAdapter * msg = [self messageAtIndexPath:indexPath]; switch (msg.messageType) { case TSIncomingMessageAdapter: return [self loadIncomingMessageCellForMessage:msg atIndexPath:indexPath]; break; case TSOutgoingMessageAdapter: return [self loadOutgoingCellForMessage:msg atIndexPath:indexPath]; break; case TSCallAdapter: return [self loadCallCellForCall:msg atIndexPath:indexPath]; break; case TSInfoMessageAdapter: return [self loadInfoMessageCellForMessage:msg atIndexPath:indexPath]; break; case TSErrorMessageAdapter: return [self loadErrorMessageCellForMessage:msg atIndexPath:indexPath]; break; default: NSLog(@"Something went wrong"); return nil; break; } } #pragma mark - Loading message cells -(JSQMessagesCollectionViewCell*)loadIncomingMessageCellForMessage:(id)message atIndexPath:(NSIndexPath*)indexPath { JSQMessagesCollectionViewCell *cell = (JSQMessagesCollectionViewCell *)[super collectionView:self.collectionView cellForItemAtIndexPath:indexPath]; if (!message.isMediaMessage) { cell.textView.textColor = [UIColor blackColor]; cell.textView.linkTextAttributes = @{ NSForegroundColorAttributeName : cell.textView.textColor, NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle | NSUnderlinePatternSolid) }; } return cell; } -(JSQMessagesCollectionViewCell*)loadOutgoingCellForMessage:(id)message atIndexPath:(NSIndexPath*)indexPath { JSQMessagesCollectionViewCell *cell = (JSQMessagesCollectionViewCell *)[super collectionView:self.collectionView cellForItemAtIndexPath:indexPath]; if (!message.isMediaMessage) { cell.textView.textColor = [UIColor whiteColor]; cell.textView.linkTextAttributes = @{ NSForegroundColorAttributeName : cell.textView.textColor, NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle | NSUnderlinePatternSolid) }; } return cell; } -(JSQCallCollectionViewCell*)loadCallCellForCall:(id)call atIndexPath:(NSIndexPath*)indexPath { JSQCallCollectionViewCell *cell = (JSQCallCollectionViewCell *)[super collectionView:self.collectionView cellForItemAtIndexPath:indexPath]; return cell; } -(JSQDisplayedMessageCollectionViewCell *)loadInfoMessageCellForMessage:(id)message atIndexPath:(NSIndexPath*)indexPath { JSQDisplayedMessageCollectionViewCell * cell = (JSQDisplayedMessageCollectionViewCell *)[super collectionView:self.collectionView cellForItemAtIndexPath:indexPath]; return cell; } -(JSQDisplayedMessageCollectionViewCell *)loadErrorMessageCellForMessage:(id)message atIndexPath:(NSIndexPath*)indexPath { JSQDisplayedMessageCollectionViewCell * cell = (JSQDisplayedMessageCollectionViewCell *)[super collectionView:self.collectionView cellForItemAtIndexPath:indexPath]; return cell; } #pragma mark - Adjusting cell label heights - (CGFloat)collectionView:(JSQMessagesCollectionView *)collectionView layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout heightForCellTopLabelAtIndexPath:(NSIndexPath *)indexPath { if ([self showDateAtIndexPath:indexPath]) { return kJSQMessagesCollectionViewCellLabelHeightDefault; } return 0.0f; } - (BOOL)showDateAtIndexPath:(NSIndexPath *)indexPath { BOOL showDate = NO; if (indexPath.row == 0) { showDate = YES; } else { TSMessageAdapter *currentMessage = [self messageAtIndexPath:indexPath]; TSMessageAdapter *previousMessage = [self messageAtIndexPath:[NSIndexPath indexPathForItem:indexPath.row-1 inSection:indexPath.section]]; NSTimeInterval timeDifference = [currentMessage.date timeIntervalSinceDate:previousMessage.date]; if (timeDifference > kTSMessageSentDateShowTimeInterval) { showDate = YES; } } return showDate; } -(NSAttributedString*)collectionView:(JSQMessagesCollectionView *)collectionView attributedTextForCellTopLabelAtIndexPath:(NSIndexPath *)indexPath { if ([self showDateAtIndexPath:indexPath]) { TSMessageAdapter *currentMessage = [self messageAtIndexPath:indexPath]; return [[JSQMessagesTimestampFormatter sharedFormatter] attributedTimestampForDate:currentMessage.date]; } return nil; } -(BOOL)shouldShowMessageStatusAtIndexPath:(NSIndexPath*)indexPath { TSMessageAdapter *currentMessage = [self messageAtIndexPath:indexPath]; if([self.thread isKindOfClass:[TSGroupThread class]]) { return currentMessage.messageType == TSIncomingMessageAdapter; } else { if (indexPath.item == [self.collectionView numberOfItemsInSection:indexPath.section]-1) { return [self isMessageOutgoingAndDelivered:currentMessage]; } if (![self isMessageOutgoingAndDelivered:currentMessage]) { return NO; } TSMessageAdapter *nextMessage = [self nextOutgoingMessage:indexPath]; return ![self isMessageOutgoingAndDelivered:nextMessage]; } } -(TSMessageAdapter*)nextOutgoingMessage:(NSIndexPath*)indexPath { TSMessageAdapter * nextMessage = [self messageAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row+1 inSection:indexPath.section]]; int i = 1; while (indexPath.item+i < [self.collectionView numberOfItemsInSection:indexPath.section]-1 && ![self isMessageOutgoingAndDelivered:nextMessage]) { i++; nextMessage = [self messageAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row+i inSection:indexPath.section]]; } return nextMessage; } -(BOOL)isMessageOutgoingAndDelivered:(TSMessageAdapter*)message { return message.messageType == TSOutgoingMessageAdapter && message.messageState == TSOutgoingMessageStateDelivered; } -(NSAttributedString*)collectionView:(JSQMessagesCollectionView *)collectionView attributedTextForCellBottomLabelAtIndexPath:(NSIndexPath *)indexPath { TSMessageAdapter *msg = [self messageAtIndexPath:indexPath]; if ([self shouldShowMessageStatusAtIndexPath:indexPath]) { if([self.thread isKindOfClass:[TSGroupThread class]]) { NSTextAttachment *textAttachment = [[NSTextAttachment alloc] init]; textAttachment.bounds = CGRectMake(0, 0, 11.0f, 10.0f); NSString *name = [[Environment getCurrent].contactsManager nameStringForPhoneIdentifier:msg.senderId]; name = name ? name : msg.senderId; NSMutableAttributedString * attrStr = [[NSMutableAttributedString alloc]initWithString:name]; [attrStr appendAttributedString:[NSAttributedString attributedStringWithAttachment:textAttachment]]; return (NSAttributedString*)attrStr; } else { _lastDeliveredMessageIndexPath = indexPath; NSTextAttachment *textAttachment = [[NSTextAttachment alloc] init]; textAttachment.bounds = CGRectMake(0, 0, 11.0f, 10.0f); NSMutableAttributedString * attrStr = [[NSMutableAttributedString alloc]initWithString:@"Delivered"]; [attrStr appendAttributedString:[NSAttributedString attributedStringWithAttachment:textAttachment]]; return (NSAttributedString*)attrStr; } } return nil; } - (CGFloat)collectionView:(JSQMessagesCollectionView *)collectionView layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout heightForCellBottomLabelAtIndexPath:(NSIndexPath *)indexPath { TSMessageAdapter * msg = [self messageAtIndexPath:indexPath]; if([self.thread isKindOfClass:[TSGroupThread class]]) { if(msg.messageType == TSIncomingMessageAdapter) { return 16.0f; } } else if (msg.messageType == TSOutgoingMessageAdapter) { return 16.0f; } return 0.0f; } #pragma mark - Actions - (void)collectionView:(JSQMessagesCollectionView *)collectionView didTapMessageBubbleAtIndexPath:(NSIndexPath *)indexPath { TSMessageAdapter *messageItem = [collectionView.dataSource collectionView:collectionView messageDataForItemAtIndexPath:indexPath]; TSInteraction *interaction = [self interactionAtIndexPath:indexPath]; switch (messageItem.messageType) { case TSOutgoingMessageAdapter: if (messageItem.messageState == TSOutgoingMessageStateUnsent) { [self handleUnsentMessageTap:(TSOutgoingMessage*)interaction]; } case TSIncomingMessageAdapter:{ BOOL isMediaMessage = [messageItem isMediaMessage]; if (isMediaMessage) { TSAttachmentAdapter * messageMedia = (TSAttachmentAdapter*)[messageItem media]; if ([messageMedia isImage]) { //is a photo tappedImage = ((UIImageView*)[messageMedia mediaView]).image ; CGRect convertedRect = [self.collectionView convertRect:[collectionView cellForItemAtIndexPath:indexPath].frame toView:nil]; FullImageViewController * vc = [[FullImageViewController alloc]initWithImage:tappedImage fromRect:convertedRect]; [vc presentFromViewController:self]; } else { DDLogWarn(@"Currently unsupported"); } } break;} case TSErrorMessageAdapter: [self handleErrorMessageTap:(TSErrorMessage*)interaction]; break; case TSInfoMessageAdapter: break; default: break; } } #pragma mark Bubble User Actions - (void)handleUnsentMessageTap:(TSOutgoingMessage*)message{ [DJWActionSheet showInView:self.tabBarController.view withTitle:nil cancelButtonTitle:@"Cancel" destructiveButtonTitle:@"Delete" otherButtonTitles:@[@"Send again"] tapBlock:^(DJWActionSheet *actionSheet, NSInteger tappedButtonIndex) { if (tappedButtonIndex == actionSheet.cancelButtonIndex) { NSLog(@"User Cancelled"); } else if (tappedButtonIndex == actionSheet.destructiveButtonIndex) { [self.editingDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction){ [message removeWithTransaction:transaction]; }]; }else { [[TSMessagesManager sharedManager] sendMessage:message inThread:self.thread]; [self finishSendingMessage]; } }]; } - (void)handleErrorMessageTap:(TSErrorMessage*)message{ if (message.errorType == TSErrorMessageWrongTrustedIdentityKey) { NSString *newKeyFingerprint = [message newIdentityKey]; NSString *messageString = [NSString stringWithFormat:@"Do you want to accept %@'s new identity key: %@", _thread.name, newKeyFingerprint]; NSArray *actions = @[@"Accept new identity key", @"Copy new identity key to pasteboard"]; [self.inputToolbar.contentView resignFirstResponder]; [DJWActionSheet showInView:self.tabBarController.view withTitle:messageString cancelButtonTitle:@"Cancel" destructiveButtonTitle:@"Delete" otherButtonTitles:actions tapBlock:^(DJWActionSheet *actionSheet, NSInteger tappedButtonIndex) { if (tappedButtonIndex == actionSheet.cancelButtonIndex) { NSLog(@"User Cancelled"); } else if (tappedButtonIndex == actionSheet.destructiveButtonIndex) { [self.editingDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction){ [message removeWithTransaction:transaction]; }]; } else { switch (tappedButtonIndex) { case 0: [message acceptNewIdentityKey]; break; case 1: [[UIPasteboard generalPasteboard] setString:newKeyFingerprint]; break; default: break; } } }]; } } #pragma mark - Navigation - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([segue.identifier isEqualToString:kFingerprintSegueIdentifier]){ FingerprintViewController *vc = [segue destinationViewController]; [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { [vc configWithThread:self.thread]; }]; } else if ([segue.identifier isEqualToString:kUpdateGroupSegueIdentifier]) { NewGroupViewController *vc = [segue destinationViewController]; [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { [vc configWithThread:(TSGroupThread*)self.thread]; }]; } } #pragma mark - UIImagePickerController /* * Presenting UIImagePickerController */ - (void)takePictureOrVideo { UIImagePickerController *picker = [[UIImagePickerController alloc] init]; picker.delegate = self; picker.allowsEditing = NO; picker.sourceType = UIImagePickerControllerSourceTypeCamera; if ([UIImagePickerController isSourceTypeAvailable: UIImagePickerControllerSourceTypeCamera]) { picker.mediaTypes = [[NSArray alloc] initWithObjects: (NSString *)kUTTypeMovie, kUTTypeImage, kUTTypeVideo, nil]; [self presentViewController:picker animated:YES completion:NULL]; } } -(void)chooseFromLibrary:(kMediaTypes)mediaType { UIImagePickerController *picker = [[UIImagePickerController alloc] init]; picker.delegate = self; picker.sourceType = UIImagePickerControllerSourceTypeSavedPhotosAlbum; if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeSavedPhotosAlbum]) { NSArray* pictureTypeArray = [[NSArray alloc] initWithObjects:(NSString *)kUTTypeImage, nil]; NSArray* videoTypeArray = [[NSArray alloc] initWithObjects:(NSString *)kUTTypeMovie, (NSString*)kUTTypeVideo, nil]; picker.mediaTypes = (mediaType == kMediaTypePicture) ? pictureTypeArray : videoTypeArray; [self presentViewController:picker animated:YES completion:nil]; } } /* * Dismissing UIImagePickerController */ - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { [self dismissViewControllerAnimated:YES completion:nil]; } /* * Fetching data from UIImagePickerController */ -(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { UIImage *picture_camera = [info objectForKey:UIImagePickerControllerOriginalImage]; NSString *mediaType = [info objectForKey: UIImagePickerControllerMediaType]; if (CFStringCompare ((__bridge_retained CFStringRef)mediaType, kUTTypeMovie, 0) == kCFCompareEqualTo) { DDLogWarn(@"Video formats not supported, yet"); } else if (picture_camera) { DDLogVerbose(@"Sending picture attachement ..."); [[TSMessagesManager sharedManager] sendAttachment:[self qualityAdjustedAttachmentForImage:picture_camera] contentType:@"image/jpeg" thread:self.thread]; [self finishSendingMessage]; } [self dismissViewControllerAnimated:YES completion:nil]; } -(NSData*)qualityAdjustedAttachmentForImage:(UIImage*)image { return UIImageJPEGRepresentation([self adjustedImageSizedForSending:image], [self compressionRate]); } -(UIImage*)adjustedImageSizedForSending:(UIImage*)image { CGFloat correctedWidth; switch ([Environment.preferences imageUploadQuality]) { case TSImageQualityHigh: correctedWidth = 2048; break; case TSImageQualityMedium: correctedWidth = 1024; break; case TSImageQualityLow: correctedWidth = 512; break; default: break; } return [self imageScaled:image toMaxSize:correctedWidth]; } - (UIImage*)imageScaled:(UIImage *)image toMaxSize:(CGFloat)size { CGFloat scaleFactor; CGFloat aspectRatio = image.size.height / image.size.width; if( aspectRatio > 1 ) { scaleFactor = size / image.size.width; } else { scaleFactor = size / image.size.height; } CGSize newSize = CGSizeMake(image.size.width * scaleFactor, image.size.height * scaleFactor); UIGraphicsBeginImageContext(newSize); [image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)]; UIImage* updatedImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return updatedImage; } -(CGFloat)compressionRate { switch ([Environment.preferences imageUploadQuality]) { case TSImageQualityHigh: return 0.9f; break; case TSImageQualityMedium: return 0.5f; break; case TSImageQualityLow: return 0.3f; break; default: break; } } #pragma mark Storage access - (YapDatabaseConnection*)uiDatabaseConnection { NSAssert([NSThread isMainThread], @"Must access uiDatabaseConnection on main thread!"); if (!_uiDatabaseConnection) { _uiDatabaseConnection = [[TSStorageManager sharedManager] newDatabaseConnection]; [_uiDatabaseConnection beginLongLivedReadTransaction]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(yapDatabaseModified:) name:YapDatabaseModifiedNotification object:nil]; } return _uiDatabaseConnection; } - (YapDatabaseConnection*)editingDatabaseConnection { if (!_editingDatabaseConnection) { _editingDatabaseConnection = [[TSStorageManager sharedManager] newDatabaseConnection]; } return _editingDatabaseConnection; } - (void)yapDatabaseModified:(NSNotification *)notification { // Process the notification(s), // and get the change-set(s) as applies to my view and mappings configuration. NSArray *notifications = [self.uiDatabaseConnection beginLongLivedReadTransaction]; NSArray *messageRowChanges = nil; [[self.uiDatabaseConnection ext:TSMessageDatabaseViewExtensionName] getSectionChanges:nil rowChanges:&messageRowChanges forNotifications:notifications withMappings:self.messageMappings]; if (!messageRowChanges) { return; } [self.collectionView performBatchUpdates:^{ for (YapDatabaseViewRowChange *rowChange in messageRowChanges) { switch (rowChange.type) { case YapDatabaseViewChangeDelete : { [self.collectionView deleteItemsAtIndexPaths:@[ rowChange.indexPath ]]; break; } case YapDatabaseViewChangeInsert : { [self.collectionView insertItemsAtIndexPaths:@[ rowChange.newIndexPath ]]; break; } case YapDatabaseViewChangeMove : { [self.collectionView deleteItemsAtIndexPaths:@[ rowChange.indexPath]]; [self.collectionView insertItemsAtIndexPaths:@[ rowChange.newIndexPath]]; break; } case YapDatabaseViewChangeUpdate : { NSMutableArray *rowsToUpdate = [@[rowChange.indexPath] mutableCopy]; if (_lastDeliveredMessageIndexPath) { [rowsToUpdate addObject:_lastDeliveredMessageIndexPath]; } [self.collectionView reloadItemsAtIndexPaths:rowsToUpdate]; break; } } } } completion:^(BOOL finished) { [self finishReceivingMessage]; }]; } #pragma mark - UICollectionView DataSource - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { NSInteger numberOfMessages = [self.messageMappings numberOfItemsInSection:section]; return numberOfMessages; } - (TSInteraction*)interactionAtIndexPath:(NSIndexPath*)indexPath { __block TSInteraction *message = nil; [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { YapDatabaseViewTransaction *viewTransaction = [transaction ext:TSMessageDatabaseViewExtensionName]; NSParameterAssert(viewTransaction != nil); NSParameterAssert(self.messageMappings != nil); NSParameterAssert(indexPath != nil); NSUInteger row = indexPath.row; NSUInteger section = indexPath.section; NSUInteger numberOfItemsInSection = [self.messageMappings numberOfItemsInSection:section]; NSAssert(row < numberOfItemsInSection, @"Cannot fetch message because row %d is >= numberOfItemsInSection %d", (int)row, (int)numberOfItemsInSection); message = [viewTransaction objectAtRow:row inSection:section withMappings:self.messageMappings]; NSParameterAssert(message != nil); }]; return message; } - (TSMessageAdapter*)messageAtIndexPath:(NSIndexPath *)indexPath { TSInteraction *interaction = [self interactionAtIndexPath:indexPath]; return [TSMessageAdapter messageViewDataWithInteraction:interaction inThread:self.thread]; } #pragma mark group action view -(void)didPressGroupMenuButton:(UIButton *)sender { [self.inputToolbar.contentView.textView resignFirstResponder]; UIView *presenter = self.parentViewController.view; [DJWActionSheet showInView:presenter withTitle:nil cancelButtonTitle:@"Cancel" destructiveButtonTitle:nil otherButtonTitles:@[@"Update group", @"Leave group", @"Delete thread"] tapBlock:^(DJWActionSheet *actionSheet, NSInteger tappedButtonIndex) { if (tappedButtonIndex == actionSheet.cancelButtonIndex) { NSLog(@"User Cancelled"); } else if (tappedButtonIndex == actionSheet.destructiveButtonIndex) { NSLog(@"Destructive button tapped"); }else { switch (tappedButtonIndex) { case 0: DDLogDebug(@"update group picked"); [self performSegueWithIdentifier:kUpdateGroupSegueIdentifier sender:self]; break; case 1: DDLogDebug(@"leave group picket"); break; case 2: DDLogDebug(@"delete thread"); break; default: break; } } }]; } #pragma mark Accessory View -(void)didPressAccessoryButton:(UIButton *)sender { [self.inputToolbar.contentView.textView resignFirstResponder]; UIView *presenter = self.parentViewController.view; [DJWActionSheet showInView:presenter withTitle:nil cancelButtonTitle:@"Cancel" destructiveButtonTitle:nil otherButtonTitles:@[@"Take Photo or Video", @"Choose existing Photo", @"Choose existing Video", @"Send file"] tapBlock:^(DJWActionSheet *actionSheet, NSInteger tappedButtonIndex) { if (tappedButtonIndex == actionSheet.cancelButtonIndex) { NSLog(@"User Cancelled"); } else if (tappedButtonIndex == actionSheet.destructiveButtonIndex) { NSLog(@"Destructive button tapped"); }else { switch (tappedButtonIndex) { case 0: [self takePictureOrVideo]; break; case 1: [self chooseFromLibrary:kMediaTypePicture]; break; case 2: [self chooseFromLibrary:kMediaTypeVideo]; break; default: break; } } }]; } - (void)markAllMessagesAsRead { [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { YapDatabaseViewTransaction *viewTransaction = [transaction ext:TSUnreadDatabaseViewExtensionName]; NSUInteger numberOfItemsInSection = [viewTransaction numberOfItemsInGroup:self.thread.uniqueId]; [self.editingDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *writeTransaction) { for (NSUInteger i = 0; i < numberOfItemsInSection; i++) { TSIncomingMessage *message = [viewTransaction objectAtIndex:i inGroup:self.thread.uniqueId]; message.read = YES; [message saveWithTransaction:writeTransaction]; } }]; }]; } - (void)dealloc{ [[NSNotificationCenter defaultCenter] removeObserver:self]; } @end