2014-10-29 21:58:58 +01:00
//
// MessagesViewController . m
// Signal
//
// Created by Dylan Bourgeois on 28 / 10 / 14.
// Copyright ( c ) 2014 Open Whisper Systems . All rights reserved .
//
# import "AppDelegate.h"
2015-12-22 12:45:09 +01:00
# import "Environment.h"
2014-12-04 00:23:36 +01:00
# import "FingerprintViewController.h"
2015-12-22 12:45:09 +01:00
# import "FullImageViewController.h"
# import "MessagesViewController.h"
# import "NSDate+millisecondTimeStamp.h"
2014-12-17 06:44:36 +01:00
# import "NewGroupViewController.h"
2016-09-02 16:22:06 +02:00
# import "OWSCall.h"
# import "OWSCallCollectionViewCell.h"
# import "OWSContactsManager.h"
2016-09-21 14:37:51 +02:00
# import "OWSConversationSettingsTableViewController.h"
# import "OWSDisappearingMessagesJob.h"
2016-09-02 16:22:06 +02:00
# import "OWSDisplayedMessageCollectionViewCell.h"
2016-09-21 14:37:51 +02:00
# import "OWSExpirableMessageView.h"
# import "OWSIncomingMessageCollectionViewCell.h"
2016-09-02 16:22:06 +02:00
# import "OWSMessagesBubblesSizeCalculator.h"
2016-09-21 14:37:51 +02:00
# import "OWSOutgoingMessageCollectionViewCell.h"
2015-12-22 12:45:09 +01:00
# import "PhoneManager.h"
2016-10-10 22:02:09 +02:00
# import "PropertyListPreferences.h"
2016-11-01 20:02:15 +01:00
# import "Signal-Swift.h"
2014-12-24 02:25:10 +01:00
# import "SignalKeyingStorage.h"
2015-12-22 12:45:09 +01:00
# import "TSAttachmentPointer.h"
2016-09-02 16:22:06 +02:00
# import "TSCall.h"
2016-09-11 22:53:12 +02:00
# import "TSContactThread.h"
2015-12-22 12:45:09 +01:00
# import "TSContentAdapters.h"
2014-11-25 16:38:33 +01:00
# import "TSDatabaseView.h"
2014-12-11 00:05:41 +01:00
# import "TSErrorMessage.h"
2016-09-21 14:37:51 +02:00
# import "TSGroupThread.h"
2014-12-06 17:45:42 +01:00
# import "TSIncomingMessage.h"
2016-09-02 16:22:06 +02:00
# import "TSInfoMessage.h"
2015-12-22 12:45:09 +01:00
# import "TSInvalidIdentityKeyErrorMessage.h"
2015-12-26 17:27:27 +01:00
# import "UIFont+OWS.h"
2015-12-22 12:45:09 +01:00
# import "UIUtil.h"
2016-11-04 23:41:37 +01:00
# import "UIViewController+CameraPermissions.h"
2016-09-02 16:22:06 +02:00
# import < AddressBookUI / AddressBookUI . h >
# import < ContactsUI / CNContactViewController . h >
# import < JSQMessagesViewController / JSQMessagesBubbleImage . h >
# import < JSQMessagesViewController / JSQMessagesBubbleImageFactory . h >
# import < JSQMessagesViewController / JSQMessagesCollectionViewFlowLayoutInvalidationContext . h >
# import < JSQMessagesViewController / JSQMessagesTimestampFormatter . h >
# import < JSQMessagesViewController / JSQSystemSoundPlayer + JSQMessages . h >
# import < JSQMessagesViewController / UIColor + JSQMessages . h >
# import < JSQSystemSoundPlayer . h >
# import < MobileCoreServices / UTCoreTypes . h >
# import < SignalServiceKit / MimeTypeUtil . h >
2016-10-14 22:59:58 +02:00
# import < SignalServiceKit / OWSAttachmentsProcessor . h >
2016-09-21 14:37:51 +02:00
# import < SignalServiceKit / OWSDisappearingMessagesConfiguration . h >
2016-09-11 22:53:12 +02:00
# import < SignalServiceKit / OWSFingerprint . h >
2016-09-21 14:37:51 +02:00
# import < SignalServiceKit / OWSFingerprintBuilder . h >
2016-10-14 22:59:58 +02:00
# import < SignalServiceKit / OWSMessageSender . h >
2016-09-02 16:22:06 +02:00
# import < SignalServiceKit / SignalRecipient . h >
# import < SignalServiceKit / TSAccountManager . h >
2016-10-14 22:59:58 +02:00
# import < SignalServiceKit / TSInvalidIdentityKeySendingErrorMessage . h >
# import < SignalServiceKit / TSMessagesManager . h >
# import < SignalServiceKit / TSNetworkManager . h >
2016-09-02 16:22:06 +02:00
# import < YapDatabase / YapDatabaseView . h >
2014-12-31 13:22:40 +01:00
2016-11-04 23:41:37 +01:00
2016-07-22 02:15:34 +02:00
@ import Photos ;
2014-12-31 13:22:40 +01:00
# define kYapDatabaseRangeLength 50
# define kYapDatabaseRangeMaxLength 300
# define kYapDatabaseRangeMinLength 20
2015-01-27 21:17:49 +01:00
# define JSQ_TOOLBAR _ICON _HEIGHT 22
# define JSQ_TOOLBAR _ICON _WIDTH 22
# define JSQ_IMAGE _INSET 5
2014-12-31 13:22:40 +01:00
2014-11-25 16:38:33 +01:00
static NSTimeInterval const kTSMessageSentDateShowTimeInterval = 5 * 60 ;
2016-09-21 14:37:51 +02:00
static NSString * const OWSMessagesViewControllerSegueShowFingerprint = @ "fingerprintSegue" ;
static NSString * const OWSMessagesViewControllerSeguePushConversationSettings =
@ "OWSMessagesViewControllerSeguePushConversationSettings" ;
NSString * const OWSMessagesViewControllerDidAppearNotification = @ "OWSMessagesViewControllerDidAppear" ;
2014-12-17 06:44:36 +01:00
2014-10-29 21:58:58 +01:00
typedef enum : NSUInteger {
kMediaTypePicture ,
kMediaTypeVideo ,
} kMediaTypes ;
@ interface MessagesViewController ( ) {
2015-12-22 12:45:09 +01:00
UIImage * tappedImage ;
2014-10-29 21:58:58 +01:00
BOOL isGroupConversation ;
2015-12-22 12:45:09 +01:00
2015-05-23 15:54:50 +02:00
UIView * _unreadContainer ;
UIImageView * _unreadBackground ;
UILabel * _unreadLabel ;
NSUInteger _unreadCount ;
2014-10-29 21:58:58 +01:00
}
2016-08-23 20:24:11 +02:00
@ property TSThread * thread ;
2016-10-12 19:00:06 +02:00
@ property TSMessageAdapter * lastDeliveredMessage ;
2015-12-22 12:45:09 +01:00
@ property ( nonatomic , strong ) YapDatabaseConnection * editingDatabaseConnection ;
@ property ( nonatomic , strong ) YapDatabaseConnection * uiDatabaseConnection ;
2014-11-25 16:38:33 +01:00
@ property ( nonatomic , strong ) YapDatabaseViewMappings * messageMappings ;
2016-09-21 14:37:51 +02:00
2015-12-22 12:45:09 +01:00
@ property ( nonatomic , retain ) JSQMessagesBubbleImage * outgoingBubbleImageData ;
@ property ( nonatomic , retain ) JSQMessagesBubbleImage * incomingBubbleImageData ;
@ property ( nonatomic , retain ) JSQMessagesBubbleImage * currentlyOutgoingBubbleImageData ;
@ property ( nonatomic , retain ) JSQMessagesBubbleImage * outgoingMessageFailedImageData ;
2016-09-21 14:37:51 +02:00
2015-01-22 05:08:12 +01:00
@ property ( nonatomic , strong ) NSTimer * audioPlayerPoller ;
2015-01-25 00:48:40 +01:00
@ property ( nonatomic , strong ) TSVideoAttachmentAdapter * currentMediaAdapter ;
2014-12-06 17:45:42 +01:00
@ property ( nonatomic , retain ) NSTimer * readTimer ;
2016-09-21 14:37:51 +02:00
@ property ( nonatomic , strong ) UILabel * navbarTitleLabel ;
2015-01-27 02:20:11 +01:00
@ property ( nonatomic , retain ) UIButton * attachButton ;
2014-12-06 23:21:15 +01:00
2016-10-06 21:47:45 +02:00
@ property ( nonatomic ) CGFloat previousCollectionViewFrameWidth ;
2014-12-31 13:22:40 +01:00
@ property NSUInteger page ;
2015-12-22 12:45:09 +01:00
@ property ( nonatomic ) BOOL composeOnOpen ;
@ property ( nonatomic ) BOOL peek ;
2015-01-31 12:00:58 +01:00
2016-09-11 22:53:12 +02:00
@ property ( nonatomic , readonly ) OWSContactsManager * contactsManager ;
2016-10-14 22:59:58 +02:00
@ property ( nonatomic , readonly ) ContactsUpdater * contactsUpdater ;
2016-11-01 20:02:15 +01:00
@ property ( nonatomic , readonly ) OWSMessageSender * messageSender ;
@ property ( nonatomic , readonly ) TSStorageManager * storageManager ;
2016-09-21 14:37:51 +02:00
@ property ( nonatomic , readonly ) OWSDisappearingMessagesJob * disappearingMessagesJob ;
2016-10-14 22:59:58 +02:00
@ property ( nonatomic , readonly ) TSMessagesManager * messagesManager ;
@ property ( nonatomic , readonly ) TSNetworkManager * networkManager ;
2016-04-13 19:05:09 +02:00
2016-09-21 14:37:51 +02:00
@ property NSCache * messageAdapterCache ;
2014-10-29 21:58:58 +01:00
2015-05-23 15:54:50 +02:00
@ end
2014-10-29 21:58:58 +01:00
@ implementation MessagesViewController
2016-07-13 21:17:09 +02:00
- ( void ) dealloc
{
2016-06-17 19:45:48 +02:00
[ [ NSNotificationCenter defaultCenter ] removeObserver : self ] ;
}
2016-09-11 22:53:12 +02:00
- ( instancetype ) init
{
self = [ super init ] ;
if ( ! self ) {
return self ;
}
2016-11-01 20:02:15 +01:00
[ self commonInit ] ;
2016-09-11 22:53:12 +02:00
return self ;
}
- ( instancetype ) initWithCoder : ( NSCoder * ) aDecoder
{
self = [ super initWithCoder : aDecoder ] ;
if ( ! self ) {
return self ;
}
2016-11-01 20:02:15 +01:00
[ self commonInit ] ;
return self ;
}
- ( void ) commonInit
{
2016-10-14 22:59:58 +02:00
_contactsManager = [ Environment getCurrent ] . contactsManager ;
_contactsUpdater = [ Environment getCurrent ] . contactsUpdater ;
2016-11-01 20:02:15 +01:00
_messageSender = [ Environment getCurrent ] . messageSender ;
2016-09-11 22:53:12 +02:00
_storageManager = [ TSStorageManager sharedManager ] ;
2016-09-21 14:37:51 +02:00
_disappearingMessagesJob = [ [ OWSDisappearingMessagesJob alloc ] initWithStorageManager : _storageManager ] ;
2016-10-14 22:59:58 +02:00
_messagesManager = [ TSMessagesManager sharedManager ] ;
_networkManager = [ TSNetworkManager sharedManager ] ;
2016-09-11 22:53:12 +02:00
}
2015-10-31 16:53:32 +01:00
- ( void ) peekSetup {
_peek = YES ;
[ self setComposeOnOpen : NO ] ;
}
- ( void ) popped {
_peek = NO ;
[ self hideInputIfNeeded ] ;
}
2015-12-26 17:27:27 +01:00
- ( void ) configureForThread : ( TSThread * ) thread keyboardOnViewAppearing : ( BOOL ) keyboardAppearing {
_thread = thread ;
isGroupConversation = [ self . thread isKindOfClass : [ TSGroupThread class ] ] ;
_composeOnOpen = keyboardAppearing ;
2014-11-26 16:00:10 +01:00
2015-12-26 17:27:27 +01:00
[ self . uiDatabaseConnection beginLongLivedReadTransaction ] ;
self . messageMappings =
[ [ YapDatabaseViewMappings alloc ] initWithGroups : @ [ thread . uniqueId ] view : TSMessageDatabaseViewExtensionName ] ;
[ self . uiDatabaseConnection readWithBlock : ^ ( YapDatabaseReadTransaction * transaction ) {
[ self . messageMappings updateWithTransaction : transaction ] ;
self . page = 0 ;
[ self updateRangeOptionsForPage : self . page ] ;
[ self markAllMessagesAsRead ] ;
[ self . collectionView reloadData ] ;
} ] ;
2016-04-27 03:57:55 +02:00
[ self updateLoadEarlierVisible ] ;
2015-05-23 15:54:50 +02:00
}
2015-01-14 22:30:01 +01:00
2016-09-21 14:37:51 +02:00
- ( BOOL ) userLeftGroup
{
if ( ! [ _thread isKindOfClass : [ TSGroupThread class ] ] ) {
return NO ;
}
TSGroupThread * groupThread = ( TSGroupThread * ) self . thread ;
return ! [ groupThread . groupModel . groupMemberIds containsObject : [ TSAccountManager localNumber ] ] ;
}
2015-03-01 00:04:39 +01:00
- ( void ) hideInputIfNeeded {
2015-10-31 16:53:32 +01:00
if ( _peek ) {
[ self inputToolbar ] . hidden = YES ;
2016-07-21 17:50:41 +02:00
[ self . inputToolbar endEditing : TRUE ] ;
2015-10-31 16:53:32 +01:00
return ;
}
2015-12-22 12:45:09 +01:00
2016-09-21 14:37:51 +02:00
if ( self . userLeftGroup ) {
[ self inputToolbar ] . hidden = YES ; // user has requested they leave the group . further sends disallowed
[ self . inputToolbar endEditing : TRUE ] ;
2015-03-01 00:04:39 +01:00
} else {
2015-12-22 12:45:09 +01:00
[ self inputToolbar ] . hidden = NO ;
2015-03-01 00:04:39 +01:00
[ self loadDraftInCompose ] ;
2015-01-27 02:20:11 +01:00
}
2015-01-14 22:30:01 +01:00
}
2015-01-31 12:00:58 +01:00
2016-07-13 21:17:09 +02:00
- ( void ) viewDidLoad
{
2015-01-22 05:08:12 +01:00
[ super viewDidLoad ] ;
2016-07-22 09:05:24 +02:00
2015-01-14 22:30:01 +01:00
[ self . navigationController . navigationBar setTranslucent : NO ] ;
2015-12-22 12:45:09 +01:00
2016-04-13 19:05:09 +02:00
self . messageAdapterCache = [ [ NSCache alloc ] init ] ;
2015-01-27 02:20:11 +01:00
_attachButton = [ [ UIButton alloc ] init ] ;
2015-12-22 12:45:09 +01:00
[ _attachButton setFrame : CGRectMake ( 0 ,
0 ,
JSQ_TOOLBAR _ICON _WIDTH + JSQ_IMAGE _INSET * 2 ,
JSQ_TOOLBAR _ICON _HEIGHT + JSQ_IMAGE _INSET * 2 ) ] ;
_attachButton . imageEdgeInsets =
UIEdgeInsetsMake ( JSQ_IMAGE _INSET , JSQ_IMAGE _INSET , JSQ_IMAGE _INSET , JSQ_IMAGE _INSET ) ;
2015-01-27 21:17:49 +01:00
[ _attachButton setImage : [ UIImage imageNamed : @ "btnAttachments--blue" ] forState : UIControlStateNormal ] ;
2015-12-22 12:45:09 +01:00
2015-01-14 22:30:01 +01:00
[ self initializeTextView ] ;
2015-12-22 12:45:09 +01:00
2016-03-11 18:51:37 +01:00
[ JSQMessagesCollectionViewCell registerMenuAction : @ selector ( delete : ) ] ;
2016-03-11 19:34:39 +01:00
SEL saveSelector = NSSelectorFromString ( @ "save:" ) ;
[ JSQMessagesCollectionViewCell registerMenuAction : saveSelector ] ;
[ UIMenuController sharedMenuController ] . menuItems = @ [ [ [ UIMenuItem alloc ] initWithTitle : NSLocalizedString ( @ "EDIT_ITEM_SAVE_ACTION" , @ "Short name for edit menu item to save contents of media message." )
action : saveSelector ] ] ;
2016-03-11 18:51:37 +01:00
2014-11-29 19:54:33 +01:00
[ self initializeCollectionViewLayout ] ;
2016-07-09 00:25:28 +02:00
[ self registerCustomMessageNibs ] ;
2015-12-22 12:45:09 +01:00
2015-12-26 17:27:27 +01:00
self . senderId = ME_MESSAGE _IDENTIFIER ;
self . senderDisplayName = ME_MESSAGE _IDENTIFIER ;
2016-07-22 09:05:24 +02:00
[ self initializeToolbars ] ;
2016-07-09 00:25:28 +02:00
}
2015-12-22 12:45:09 +01:00
2016-10-06 21:47:45 +02:00
- ( void ) viewDidLayoutSubviews
{
[ super viewDidLayoutSubviews ] ;
// JSQMVC width is initially 375 px on iphone6 / ios9 ( as specified by the xib ) , which causes
// our initial bubble calculations to be off since they happen before the containing
// view is layed out . https : // github . com / jessesquires / JSQMessagesViewController / issues / 1257
if ( CGRectGetWidth ( self . collectionView . frame ) ! = self . previousCollectionViewFrameWidth ) {
// save frame value from next comparison
self . previousCollectionViewFrameWidth = CGRectGetWidth ( self . collectionView . frame ) ;
// invalidate layout
[ self . collectionView . collectionViewLayout
invalidateLayoutWithContext : [ JSQMessagesCollectionViewFlowLayoutInvalidationContext context ] ] ;
}
}
2016-09-21 14:37:51 +02:00
- ( void ) didMoveToParentViewController : ( UIViewController * ) parent
{
[ self setupTitleLabelGestureRecognizer ] ;
}
2016-07-09 00:25:28 +02:00
- ( void ) registerCustomMessageNibs
{
2016-07-13 20:03:50 +02:00
[ self . collectionView registerNib : [ OWSCallCollectionViewCell nib ]
forCellWithReuseIdentifier : [ OWSCallCollectionViewCell cellReuseIdentifier ] ] ;
2016-06-17 19:45:48 +02:00
2016-07-13 20:03:50 +02:00
[ self . collectionView registerNib : [ OWSDisplayedMessageCollectionViewCell nib ]
forCellWithReuseIdentifier : [ OWSDisplayedMessageCollectionViewCell cellReuseIdentifier ] ] ;
2016-09-21 14:37:51 +02:00
self . outgoingCellIdentifier = [ OWSOutgoingMessageCollectionViewCell cellReuseIdentifier ] ;
[ self . collectionView registerNib : [ OWSOutgoingMessageCollectionViewCell nib ]
forCellWithReuseIdentifier : [ OWSOutgoingMessageCollectionViewCell cellReuseIdentifier ] ] ;
self . outgoingMediaCellIdentifier = [ OWSOutgoingMessageCollectionViewCell mediaCellReuseIdentifier ] ;
[ self . collectionView registerNib : [ OWSOutgoingMessageCollectionViewCell nib ]
forCellWithReuseIdentifier : [ OWSOutgoingMessageCollectionViewCell mediaCellReuseIdentifier ] ] ;
self . incomingCellIdentifier = [ OWSIncomingMessageCollectionViewCell cellReuseIdentifier ] ;
[ self . collectionView registerNib : [ OWSIncomingMessageCollectionViewCell nib ]
forCellWithReuseIdentifier : [ OWSIncomingMessageCollectionViewCell cellReuseIdentifier ] ] ;
self . incomingMediaCellIdentifier = [ OWSIncomingMessageCollectionViewCell mediaCellReuseIdentifier ] ;
[ self . collectionView registerNib : [ OWSIncomingMessageCollectionViewCell nib ]
forCellWithReuseIdentifier : [ OWSIncomingMessageCollectionViewCell mediaCellReuseIdentifier ] ] ;
2016-06-17 19:45:48 +02:00
}
2016-07-13 21:17:09 +02:00
- ( void ) toggleObservers : ( BOOL ) shouldObserve
{
2016-06-17 19:45:48 +02:00
if ( shouldObserve ) {
2016-11-17 23:07:18 +01:00
[ [ NSNotificationCenter defaultCenter ] addObserver : self
selector : @ selector ( didChangePreferredContentSize : )
name : UIContentSizeCategoryDidChangeNotification
object : nil ] ;
2016-06-17 19:45:48 +02:00
[ [ NSNotificationCenter defaultCenter ] addObserver : self
selector : @ selector ( yapDatabaseModified : )
name : YapDatabaseModifiedNotification
object : nil ] ;
[ [ NSNotificationCenter defaultCenter ] addObserver : self
selector : @ selector ( startReadTimer )
name : UIApplicationWillEnterForegroundNotification
object : nil ] ;
2016-10-12 21:18:27 +02:00
[ [ NSNotificationCenter defaultCenter ] addObserver : self
selector : @ selector ( startExpirationTimerAnimations )
name : UIApplicationWillEnterForegroundNotification
object : nil ] ;
2016-06-17 19:45:48 +02:00
[ [ NSNotificationCenter defaultCenter ] addObserver : self
selector : @ selector ( cancelReadTimer )
name : UIApplicationDidEnterBackgroundNotification
object : nil ] ;
} else {
2016-11-17 23:07:18 +01:00
[ [ NSNotificationCenter defaultCenter ] removeObserver : self
name : UIContentSizeCategoryDidChangeNotification
object : nil ] ;
2016-06-17 19:45:48 +02:00
[ [ NSNotificationCenter defaultCenter ] removeObserver : self
name : YapDatabaseModifiedNotification
object : nil ] ;
[ [ NSNotificationCenter defaultCenter ] removeObserver : self
name : UIApplicationWillEnterForegroundNotification
object : nil ] ;
[ [ NSNotificationCenter defaultCenter ] removeObserver : self
name : UIApplicationDidEnterBackgroundNotification
object : nil ] ;
}
2014-12-09 20:11:14 +01:00
}
2015-03-01 00:04:39 +01:00
- ( void ) initializeTextView {
2015-12-26 17:27:27 +01:00
[ self . inputToolbar . contentView . textView setFont : [ UIFont ows_dynamicTypeBodyFont ] ] ;
2016-07-14 18:24:59 +02:00
self . inputToolbar . contentView . leftBarButtonItem = self . attachButton ;
UILabel * sendLabel = self . inputToolbar . contentView . rightBarButtonItem . titleLabel ;
// override superclass translations since we support more translations than upstream .
sendLabel . text = NSLocalizedString ( @ "SEND_BUTTON_TITLE" , nil ) ;
sendLabel . font = [ UIFont ows_regularFontWithSize : 17.0 f ] ;
sendLabel . textColor = [ UIColor ows_materialBlueColor ] ;
sendLabel . textAlignment = NSTextAlignmentCenter ;
2015-01-14 22:30:01 +01:00
}
2016-07-22 09:05:24 +02:00
- ( void ) viewWillAppear : ( BOOL ) animated
{
2014-12-24 11:50:07 +01:00
[ super viewWillAppear : animated ] ;
2016-06-17 19:45:48 +02:00
2016-09-21 14:37:51 +02:00
// We need to recheck on every appearance , since the user may have left the group in the settings VC ,
// or on another device .
[ self hideInputIfNeeded ] ;
2016-06-17 19:45:48 +02:00
[ self toggleObservers : YES ] ;
2015-12-22 12:45:09 +01:00
2016-10-08 17:49:52 +02:00
// Triggering modified notification renders "call notification" when leaving full screen call view
[ self . thread touch ] ;
2016-09-21 14:37:51 +02:00
// restart any animations that were stopped e . g . while inspecting the contact info screens .
2016-10-12 21:18:27 +02:00
[ self startExpirationTimerAnimations ] ;
2016-09-21 14:37:51 +02:00
OWSDisappearingMessagesConfiguration * configuration =
[ OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID : self . thread . uniqueId ] ;
[ self setBarButtonItemsForDisappearingMessagesConfiguration : configuration ] ;
[ self setNavigationTitle ] ;
2015-01-05 03:15:18 +01:00
NSInteger numberOfMessages = ( NSInteger ) [ self . messageMappings numberOfItemsInGroup : self . thread . uniqueId ] ;
if ( numberOfMessages > 0 ) {
2015-12-22 12:45:09 +01:00
NSIndexPath * lastCellIndexPath = [ NSIndexPath indexPathForRow : numberOfMessages - 1 inSection : 0 ] ;
[ self . collectionView scrollToItemAtIndexPath : lastCellIndexPath
atScrollPosition : UICollectionViewScrollPositionBottom
animated : NO ] ;
2015-01-05 03:15:18 +01:00
}
2014-12-24 11:50:07 +01:00
}
2015-03-19 01:59:44 +01:00
- ( void ) startReadTimer {
2015-12-22 12:45:09 +01:00
self . readTimer = [ NSTimer scheduledTimerWithTimeInterval : 1
target : self
selector : @ selector ( markAllMessagesAsRead )
userInfo : nil
repeats : YES ] ;
2014-12-09 20:11:14 +01:00
}
2015-03-19 01:59:44 +01:00
- ( void ) cancelReadTimer {
2014-12-09 20:11:14 +01:00
[ self . readTimer invalidate ] ;
}
2015-03-19 01:59:44 +01:00
- ( void ) viewDidAppear : ( BOOL ) animated {
2014-12-09 20:11:14 +01:00
[ super viewDidAppear : animated ] ;
2015-12-26 17:27:27 +01:00
[ self dismissKeyBoard ] ;
2014-12-09 20:11:14 +01:00
[ self startReadTimer ] ;
2015-12-26 17:27:27 +01:00
2016-09-02 21:50:36 +02:00
// TODO prep this sync one time before view loads so we don ' t have to repaint .
2015-10-31 16:53:32 +01:00
[ self updateBackButtonAsync ] ;
2015-12-22 12:45:09 +01:00
2015-12-26 17:27:27 +01:00
[ self . inputToolbar . contentView . textView endEditing : YES ] ;
self . inputToolbar . contentView . textView . editable = YES ;
2016-09-21 14:37:51 +02:00
if ( _composeOnOpen && ! self . inputToolbar . hidden ) {
2015-04-14 21:49:00 +02:00
[ self popKeyBoard ] ;
}
2014-12-09 20:11:14 +01:00
}
2015-10-31 16:53:32 +01:00
- ( void ) updateBackButtonAsync {
dispatch_async ( dispatch_get _global _queue ( DISPATCH_QUEUE _PRIORITY _DEFAULT , 0 ) , ^ {
2016-10-14 22:59:58 +02:00
NSUInteger count = [ self . messagesManager unreadMessagesCountExcept : self . thread ] ;
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
if ( self ) {
[ self setUnreadCount : count ] ;
}
} ) ;
2015-10-31 16:53:32 +01:00
} ) ;
2015-05-23 15:54:50 +02:00
}
2015-03-19 01:59:44 +01:00
- ( void ) viewWillDisappear : ( BOOL ) animated {
2015-12-26 17:27:27 +01:00
[ super viewWillDisappear : animated ] ;
2016-06-17 19:45:48 +02:00
[ self toggleObservers : NO ] ;
2015-12-26 17:27:27 +01:00
2015-05-23 15:54:50 +02:00
[ _unreadContainer removeFromSuperview ] ;
_unreadContainer = nil ;
2015-12-22 12:45:09 +01:00
2015-01-14 22:30:01 +01:00
[ _audioPlayerPoller invalidate ] ;
[ _audioPlayer stop ] ;
2015-12-22 12:45:09 +01:00
2015-01-14 22:30:01 +01:00
// reset all audio bars to 0
JSQMessagesCollectionView * collectionView = self . collectionView ;
2015-12-22 12:45:09 +01:00
NSInteger num_bubbles = [ self collectionView : collectionView numberOfItemsInSection : 0 ] ;
for ( NSInteger i = 0 ; i < num_bubbles ; i + + ) {
2016-09-21 14:37:51 +02:00
NSIndexPath * indexPath = [ NSIndexPath indexPathForRow : i inSection : 0 ] ;
id < OWSMessageData > message = [ self messageAtIndexPath : indexPath ] ;
if ( message . messageType = = TSIncomingMessageAdapter && message . isMediaMessage &&
[ message isKindOfClass : [ TSVideoAttachmentAdapter class ] ] ) {
TSVideoAttachmentAdapter * msgMedia = ( TSVideoAttachmentAdapter * ) message . media ;
2015-01-14 22:30:01 +01:00
if ( [ msgMedia isAudio ] ) {
2015-12-22 12:45:09 +01:00
msgMedia . isPaused = NO ;
2015-01-14 22:30:01 +01:00
msgMedia . isAudioPlaying = NO ;
[ msgMedia setAudioProgressFromFloat : 0 ] ;
[ msgMedia setAudioIconToPlay ] ;
}
}
}
2015-12-22 12:45:09 +01:00
2014-12-09 20:11:14 +01:00
[ self cancelReadTimer ] ;
2015-03-01 00:04:39 +01:00
[ self saveDraft ] ;
2014-10-29 21:58:58 +01:00
}
2016-10-12 21:18:27 +02:00
- ( void ) startExpirationTimerAnimations
{
[ [ NSNotificationCenter defaultCenter ] postNotificationName : OWSMessagesViewControllerDidAppearNotification
object : nil ] ;
}
2015-03-19 01:59:44 +01:00
- ( void ) viewDidDisappear : ( BOOL ) animated {
2015-12-26 17:27:27 +01:00
[ super viewDidDisappear : animated ] ;
self . inputToolbar . contentView . textView . editable = NO ;
2015-01-31 12:00:58 +01:00
}
2014-11-29 19:54:33 +01:00
# pragma mark - Initiliazers
2016-09-21 14:37:51 +02:00
- ( void ) setNavigationTitle
{
NSString * navTitle = self . thread . name ;
if ( isGroupConversation && [ navTitle length ] = = 0 ) {
navTitle = NSLocalizedString ( @ "NEW_GROUP_DEFAULT_TITLE" , @ "" ) ;
}
self . title = navTitle ;
}
2015-01-14 22:30:01 +01:00
2016-09-21 14:37:51 +02:00
- ( void ) setBarButtonItemsForDisappearingMessagesConfiguration :
( OWSDisappearingMessagesConfiguration * ) disappearingMessagesConfiguration
2015-12-22 12:45:09 +01:00
2016-09-21 14:37:51 +02:00
{
if ( self . userLeftGroup ) {
self . navigationItem . rightBarButtonItems = @ [ ] ;
return ;
}
2015-12-22 12:45:09 +01:00
2016-09-21 14:37:51 +02:00
NSMutableArray < UIBarButtonItem * > * barButtons = [ NSMutableArray new ] ;
if ( [ self canCall ] ) {
UIBarButtonItem * callButton = [ [ UIBarButtonItem alloc ] initWithImage : [ UIImage imageNamed : @ "btnPhone--white" ]
style : UIBarButtonItemStylePlain
target : self
action : @ selector ( callAction ) ] ;
callButton . imageInsets = UIEdgeInsetsMake ( 0 , -10 , 0 , 10 ) ;
[ barButtons addObject : callButton ] ;
} else if ( [ self . thread isGroupThread ] ) {
UIBarButtonItem * manageGroupButton =
[ [ UIBarButtonItem alloc ] initWithImage : [ UIImage imageNamed : @ "contact-options-action" ]
style : UIBarButtonItemStylePlain
target : self
action : @ selector ( didTapManageGroupButton : ) ] ;
// Hack to shrink button image
manageGroupButton . imageInsets = UIEdgeInsetsMake ( 10 , 20 , 10 , 0 ) ;
[ barButtons addObject : manageGroupButton ] ;
2015-01-14 22:30:01 +01:00
}
2016-09-21 14:37:51 +02:00
if ( disappearingMessagesConfiguration . isEnabled ) {
[ barButtons addObject : [ [ UIBarButtonItem alloc ] initWithImage : [ UIImage imageNamed : @ "ic_timer" ]
style : UIBarButtonItemStylePlain
target : self
action : @ selector ( didTapTimerInNavbar ) ] ] ;
2015-01-27 02:20:11 +01:00
}
2016-09-21 14:37:51 +02:00
self . navigationItem . rightBarButtonItems = [ barButtons copy ] ;
2015-01-14 22:30:01 +01:00
}
2016-07-22 09:05:24 +02:00
- ( void ) initializeToolbars
{
// HACK JSQMessagesViewController doesn ' t yet support dynamic type in the inputToolbar .
// See : https : // github . com / jessesquires / JSQMessagesViewController / pull / 1169 / files
[ self . inputToolbar . contentView . textView sizeToFit ] ;
self . inputToolbar . preferredDefaultHeight = self . inputToolbar . contentView . textView . frame . size . height + 16 ;
// prevent draft from obscuring message history in case user wants to scroll back to refer to something
// while composing a long message .
self . inputToolbar . maximumHeight = 300 ;
2016-09-21 14:37:51 +02:00
}
2015-12-22 12:45:09 +01:00
2016-09-21 14:37:51 +02:00
- ( void ) setupTitleLabelGestureRecognizer
{
// Called on load / unload , but we only want to init once .
if ( self . navbarTitleLabel ) {
return ;
2015-01-27 02:20:11 +01:00
}
2015-12-22 12:45:09 +01:00
2016-09-21 14:37:51 +02:00
UILabel * navbarTitleLabel = [ self findNavbarTitleLabel ] ;
if ( ! navbarTitleLabel ) {
DDLogError ( @ "%@ Unable to find navbar title label. Skipping gesture recognition" , self . tag ) ;
2015-02-10 12:02:58 +01:00
return ;
}
2015-12-22 12:45:09 +01:00
2016-09-21 14:37:51 +02:00
self . navbarTitleLabel = navbarTitleLabel ;
navbarTitleLabel . userInteractionEnabled = YES ;
navbarTitleLabel . superview . userInteractionEnabled = YES ;
UITapGestureRecognizer * titleTapRecognizer =
[ [ UITapGestureRecognizer alloc ] initWithTarget : self action : @ selector ( didTapTitle ) ] ;
[ navbarTitleLabel addGestureRecognizer : titleTapRecognizer ] ;
}
- ( nullable UILabel * ) findNavbarTitleLabel
{
2015-02-10 12:02:58 +01:00
for ( UIView * view in self . navigationController . navigationBar . subviews ) {
if ( [ view isKindOfClass : NSClassFromString ( @ "UINavigationItemView" ) ] ) {
2016-09-21 14:37:51 +02:00
UIView * navItemView = view ;
for ( UIView * aView in navItemView . subviews ) {
2015-02-10 12:02:58 +01:00
if ( [ aView isKindOfClass : [ UILabel class ] ] ) {
2015-12-22 12:45:09 +01:00
UILabel * label = ( UILabel * ) aView ;
2015-02-17 00:14:50 +01:00
if ( [ label . text isEqualToString : self . title ] ) {
2016-09-21 14:37:51 +02:00
return label ;
2015-02-17 00:14:50 +01:00
}
2015-02-10 12:02:58 +01:00
}
2015-02-17 00:14:50 +01:00
}
}
}
2016-09-21 14:37:51 +02:00
return nil ;
2014-11-29 19:54:33 +01:00
}
2016-07-20 22:21:20 +02:00
// Overiding JSQMVC layout defaults
- ( void ) initializeCollectionViewLayout
2016-07-13 21:17:09 +02:00
{
2016-07-20 22:21:20 +02:00
[ self . collectionView . collectionViewLayout setMessageBubbleFont : [ UIFont ows_dynamicTypeBodyFont ] ] ;
self . collectionView . showsVerticalScrollIndicator = NO ;
self . collectionView . showsHorizontalScrollIndicator = NO ;
[ self updateLoadEarlierVisible ] ;
self . collectionView . collectionViewLayout . incomingAvatarViewSize = CGSizeZero ;
self . collectionView . collectionViewLayout . outgoingAvatarViewSize = CGSizeZero ;
if ( [ UIDevice currentDevice ] . userInterfaceIdiom ! = UIUserInterfaceIdiomPad ) {
// Narrow the bubbles a bit to create more white space in the messages view
// Since we ' re not using avatars it gets a bit crowded otherwise .
self . collectionView . collectionViewLayout . messageBubbleLeftRightMargin = 80.0 f ;
}
// Bubbles
2016-03-11 19:34:39 +01:00
self . collectionView . collectionViewLayout . bubbleSizeCalculator = [ [ OWSMessagesBubblesSizeCalculator alloc ] init ] ;
2014-11-29 19:54:33 +01:00
JSQMessagesBubbleImageFactory * bubbleFactory = [ [ JSQMessagesBubbleImageFactory alloc ] init ] ;
2016-07-09 00:25:28 +02:00
self . incomingBubbleImageData = [ bubbleFactory incomingMessagesBubbleImageWithColor : [ UIColor jsq_messageBubbleLightGrayColor ] ] ;
2015-12-22 12:45:09 +01:00
self . outgoingBubbleImageData = [ bubbleFactory outgoingMessagesBubbleImageWithColor : [ UIColor ows_materialBlueColor ] ] ;
2016-07-09 00:25:28 +02:00
self . currentlyOutgoingBubbleImageData = [ bubbleFactory outgoingMessagesBubbleImageWithColor : [ UIColor ows_fadedBlueColor ] ] ;
self . outgoingMessageFailedImageData = [ bubbleFactory outgoingMessagesBubbleImageWithColor : [ UIColor grayColor ] ] ;
2015-12-22 12:45:09 +01:00
2014-11-29 19:54:33 +01:00
}
2014-10-29 21:58:58 +01:00
# pragma mark - Fingerprints
2016-09-11 22:53:12 +02:00
- ( void ) showFingerprintWithTheirIdentityKey : ( NSData * ) theirIdentityKey theirSignalId : ( NSString * ) theirSignalId
{
2016-11-12 19:37:57 +01:00
OWSFingerprintBuilder * builder =
[ [ OWSFingerprintBuilder alloc ] initWithStorageManager : self . storageManager contactsManager : self . contactsManager ] ;
2016-09-21 14:37:51 +02:00
OWSFingerprint * fingerprint =
[ builder fingerprintWithTheirSignalId : self . thread . contactIdentifier theirIdentityKey : theirIdentityKey ] ;
2014-12-06 17:45:42 +01:00
[ self markAllMessagesAsRead ] ;
2016-09-21 14:37:51 +02:00
[ self performSegueWithIdentifier : OWSMessagesViewControllerSegueShowFingerprint sender : fingerprint ] ;
2014-12-24 02:25:10 +01:00
}
2014-11-29 19:54:33 +01:00
# pragma mark - Calls
2015-12-22 12:45:09 +01:00
- ( SignalRecipient * ) signalRecipient {
2015-11-29 01:14:49 +01:00
__block SignalRecipient * recipient ;
[ self . editingDatabaseConnection readWithBlock : ^ ( YapDatabaseReadTransaction * transaction ) {
2015-12-22 12:45:09 +01:00
recipient = [ SignalRecipient recipientWithTextSecureIdentifier : [ self phoneNumberForThread ] . toE164
withTransaction : transaction ] ;
2015-11-29 01:14:49 +01:00
} ] ;
return recipient ;
}
2015-12-22 12:45:09 +01:00
- ( BOOL ) isTextSecureReachable {
2016-06-28 04:51:57 +02:00
return isGroupConversation || [ self signalRecipient ] ;
2015-01-27 02:20:11 +01:00
}
2015-12-22 12:45:09 +01:00
- ( PhoneNumber * ) phoneNumberForThread {
NSString * contactId = [ ( TSContactThread * ) self . thread contactIdentifier ] ;
2014-12-29 11:43:11 +01:00
return [ PhoneNumber tryParsePhoneNumberFromUserSpecifiedText : contactId ] ;
2014-11-29 19:54:33 +01:00
}
2015-12-22 12:45:09 +01:00
- ( void ) callAction {
2016-06-28 04:51:57 +02:00
if ( [ self canCall ] ) {
2014-12-29 11:43:11 +01:00
PhoneNumber * number = [ self phoneNumberForThread ] ;
2016-09-11 22:53:12 +02:00
Contact * contact = [ self . contactsManager latestContactForPhoneNumber : number ] ;
2014-12-29 11:43:11 +01:00
[ Environment . phoneManager initiateOutgoingCallToContact : contact atRemoteNumber : number ] ;
2014-11-29 19:54:33 +01:00
} else {
2016-06-28 04:51:57 +02:00
DDLogWarn ( @ "Tried to initiate a call but thread is not callable." ) ;
2014-11-29 19:54:33 +01:00
}
}
2015-12-22 12:45:09 +01:00
- ( BOOL ) canCall {
2016-06-28 04:51:57 +02:00
return ! ( isGroupConversation || [ ( ( TSContactThread * ) self . thread ) . contactIdentifier isEqualToString : [ TSAccountManager localNumber ] ] ) ;
2015-01-27 21:17:49 +01:00
}
2015-02-17 00:14:50 +01:00
2014-10-29 21:58:58 +01:00
# pragma mark - JSQMessagesViewController method overrides
- ( void ) didPressSendButton : ( UIButton * ) button
withMessageText : ( NSString * ) text
senderId : ( NSString * ) senderId
senderDisplayName : ( NSString * ) senderDisplayName
2016-08-01 00:25:07 +02:00
date : ( NSDate * ) date
{
2014-12-04 11:27:45 +01:00
if ( text . length > 0 ) {
2016-11-03 19:51:38 +01:00
if ( [ Environment . preferences soundInForeground ] ) {
[ JSQSystemSoundPlayer jsq_playMessageSentSound ] ;
}
2015-12-22 12:45:09 +01:00
2016-09-21 14:37:51 +02:00
TSOutgoingMessage * message ;
OWSDisappearingMessagesConfiguration * configuration =
[ OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID : self . thread . uniqueId ] ;
if ( configuration . isEnabled ) {
message = [ [ TSOutgoingMessage alloc ] initWithTimestamp : [ NSDate ows_millisecondTimeStamp ]
inThread : self . thread
messageBody : text
2016-10-06 21:47:45 +02:00
attachmentIds : [ NSMutableArray new ]
2016-09-21 14:37:51 +02:00
expiresInSeconds : configuration . durationSeconds ] ;
} else {
message = [ [ TSOutgoingMessage alloc ] initWithTimestamp : [ NSDate ows_millisecondTimeStamp ]
inThread : self . thread
messageBody : text ] ;
}
2015-12-22 12:45:09 +01:00
2016-10-14 22:59:58 +02:00
[ self . messageSender sendMessage : message
2016-09-21 14:37:51 +02:00
success : ^ {
2016-10-12 19:00:06 +02:00
DDLogInfo ( @ "%@ Successfully sent message." , self . tag ) ;
2016-09-21 14:37:51 +02:00
}
2016-10-14 22:59:58 +02:00
failure : ^ ( NSError * error ) {
DDLogWarn ( @ "%@ Failed to deliver message with error: %@" , self . tag , error ) ;
2016-09-21 14:37:51 +02:00
} ] ;
2016-11-14 21:33:24 +01:00
[ self toggleDefaultKeyboard ] ;
2014-10-29 21:58:58 +01:00
[ self finishSendingMessage ] ;
}
}
2016-11-14 21:33:24 +01:00
- ( void ) toggleDefaultKeyboard
{
2016-11-21 20:26:42 +01:00
// Primary language is nil for the emoji keyboard & we want to stay on it after sending
if ( ! [ self . inputToolbar . contentView . textView . textInputMode primaryLanguage ] ) {
return ;
2016-11-14 21:33:24 +01:00
}
2016-11-21 20:26:42 +01:00
[ self . keyboardController endListeningForKeyboard ] ;
[ self dismissKeyBoard ] ;
[ self popKeyBoard ] ;
[ self . keyboardController beginListeningForKeyboard ] ;
2016-11-14 21:33:24 +01:00
}
2016-10-15 22:41:40 +02:00
# pragma mark - UICollectionViewDelegate
// Override JSQMVC
2016-03-11 19:34:39 +01:00
- ( BOOL ) collectionView : ( JSQMessagesCollectionView * ) collectionView shouldShowMenuForItemAtIndexPath : ( NSIndexPath * ) indexPath
{
if ( indexPath = = nil ) {
DDLogError ( @ "Aborting shouldShowMenuForItemAtIndexPath because indexPath is nil" ) ;
// Not sure why this is nil , but occasionally it is , which crashes .
return NO ;
}
// JSQM does some setup in super method
[ super collectionView : collectionView shouldShowMenuForItemAtIndexPath : indexPath ] ;
// Super method returns false for media methods . We want menu for * all * items
return YES ;
}
2016-10-15 22:41:40 +02:00
- ( void ) collectionView : ( UICollectionView * ) collectionView
didEndDisplayingCell : ( nonnull UICollectionViewCell * ) cell
forItemAtIndexPath : ( nonnull NSIndexPath * ) indexPath
{
if ( [ cell conformsToProtocol : @ protocol ( OWSExpirableMessageView ) ] ) {
id < OWSExpirableMessageView > expirableView = ( id < OWSExpirableMessageView > ) cell ;
[ expirableView stopExpirationTimer ] ;
}
}
2014-10-29 21:58:58 +01:00
# pragma mark - JSQMessages CollectionView DataSource
2016-09-21 14:37:51 +02:00
- ( id < OWSMessageData > ) collectionView : ( JSQMessagesCollectionView * ) collectionView
2016-07-13 21:17:09 +02:00
messageDataForItemAtIndexPath : ( NSIndexPath * ) indexPath
{
2014-11-25 21:33:29 +01:00
return [ self messageAtIndexPath : indexPath ] ;
2014-10-29 21:58:58 +01:00
}
2015-12-22 12:45:09 +01:00
- ( id < JSQMessageBubbleImageDataSource > ) collectionView : ( JSQMessagesCollectionView * ) collectionView
2016-07-09 00:25:28 +02:00
messageBubbleImageDataForItemAtIndexPath : ( NSIndexPath * ) indexPath
{
TSInteraction * message = [ self interactionAtIndexPath : indexPath ] ;
2015-12-22 12:45:09 +01:00
2016-07-09 00:25:28 +02:00
if ( [ message isKindOfClass : [ TSOutgoingMessage class ] ] ) {
TSOutgoingMessage * outgoingMessage = ( TSOutgoingMessage * ) message ;
switch ( outgoingMessage . messageState ) {
2015-06-07 19:04:24 +02:00
case TSOutgoingMessageStateUnsent :
return self . outgoingMessageFailedImageData ;
case TSOutgoingMessageStateAttemptingOut :
return self . currentlyOutgoingBubbleImageData ;
default :
return self . outgoingBubbleImageData ;
2014-12-09 22:18:39 +01:00
}
2014-10-29 21:58:58 +01:00
}
2015-12-22 12:45:09 +01:00
2014-11-25 16:38:33 +01:00
return self . incomingBubbleImageData ;
2014-10-29 21:58:58 +01:00
}
2015-12-22 12:45:09 +01:00
- ( id < JSQMessageAvatarImageDataSource > ) collectionView : ( JSQMessagesCollectionView * ) collectionView
avatarImageDataForItemAtIndexPath : ( NSIndexPath * ) indexPath {
2014-10-29 21:58:58 +01:00
return nil ;
}
# pragma mark - UICollectionView DataSource
2015-12-22 12:45:09 +01:00
- ( UICollectionViewCell * ) collectionView : ( JSQMessagesCollectionView * ) collectionView
2016-07-09 00:25:28 +02:00
cellForItemAtIndexPath : ( NSIndexPath * ) indexPath
{
2016-09-21 14:37:51 +02:00
id < OWSMessageData > message = [ self messageAtIndexPath : indexPath ] ;
2016-07-09 00:25:28 +02:00
NSParameterAssert ( message ! = nil ) ;
JSQMessagesCollectionViewCell * cell ;
switch ( message . messageType ) {
case TSCallAdapter : {
2016-07-13 20:03:50 +02:00
OWSCall * call = ( OWSCall * ) message ;
2016-07-09 00:25:28 +02:00
cell = [ self loadCallCellForCall : call atIndexPath : indexPath ] ;
} break ;
case TSInfoMessageAdapter : {
2016-10-23 19:39:42 +02:00
cell = [ self loadInfoMessageCellForMessage : ( TSMessageAdapter * ) message atIndexPath : indexPath ] ;
2016-07-09 00:25:28 +02:00
} break ;
case TSErrorMessageAdapter : {
2016-10-23 19:39:42 +02:00
cell = [ self loadErrorMessageCellForMessage : ( TSMessageAdapter * ) message atIndexPath : indexPath ] ;
2016-07-09 00:25:28 +02:00
} break ;
case TSIncomingMessageAdapter : {
cell = [ self loadIncomingMessageCellForMessage : message atIndexPath : indexPath ] ;
} break ;
case TSOutgoingMessageAdapter : {
cell = [ self loadOutgoingCellForMessage : message atIndexPath : indexPath ] ;
} break ;
default : {
2016-07-13 21:17:09 +02:00
DDLogWarn ( @ "using default cell constructor for message: %@" , message ) ;
2016-07-09 00:25:28 +02:00
cell = ( JSQMessagesCollectionViewCell * ) [ super collectionView : collectionView cellForItemAtIndexPath : indexPath ] ;
} break ;
2014-11-29 19:54:33 +01:00
}
2016-07-09 00:25:28 +02:00
cell . delegate = collectionView ;
2016-10-08 03:34:39 +02:00
if ( message . shouldStartExpireTimer && [ cell conformsToProtocol : @ protocol ( OWSExpirableMessageView ) ] ) {
2016-09-21 14:37:51 +02:00
id < OWSExpirableMessageView > expirableView = ( id < OWSExpirableMessageView > ) cell ;
[ expirableView startExpirationTimerWithExpiresAtSeconds : message . expiresAtSeconds
initialDurationSeconds : message . expiresInSeconds ] ;
}
2016-07-09 00:25:28 +02:00
return cell ;
2014-11-29 19:54:33 +01:00
}
# pragma mark - Loading message cells
2016-09-21 14:37:51 +02:00
- ( JSQMessagesCollectionViewCell * ) loadIncomingMessageCellForMessage : ( id < OWSMessageData > ) message
2016-07-13 21:17:09 +02:00
atIndexPath : ( NSIndexPath * ) indexPath
{
2016-11-13 19:30:22 +01:00
OWSIncomingMessageCollectionViewCell * cell
= ( OWSIncomingMessageCollectionViewCell * ) [ super collectionView : self . collectionView
cellForItemAtIndexPath : indexPath ] ;
2015-12-22 12:45:09 +01:00
2016-11-13 19:30:22 +01:00
if ( ! [ cell isKindOfClass : [ OWSIncomingMessageCollectionViewCell class ] ] ) {
DDLogError ( @ "%@ Unexpected cell type: %@" , self . tag , cell ) ;
return cell ;
}
[ cell ows_didLoad ] ;
2014-11-25 16:38:33 +01:00
return cell ;
2014-11-29 19:54:33 +01:00
}
2016-09-21 14:37:51 +02:00
- ( JSQMessagesCollectionViewCell * ) loadOutgoingCellForMessage : ( id < OWSMessageData > ) message
2016-07-13 21:17:09 +02:00
atIndexPath : ( NSIndexPath * ) indexPath
{
2016-09-21 14:37:51 +02:00
OWSOutgoingMessageCollectionViewCell * cell
= ( OWSOutgoingMessageCollectionViewCell * ) [ super collectionView : self . collectionView
cellForItemAtIndexPath : indexPath ] ;
2016-10-07 01:33:34 +02:00
2016-11-13 19:30:22 +01:00
if ( ! [ cell isKindOfClass : [ OWSOutgoingMessageCollectionViewCell class ] ] ) {
DDLogError ( @ "%@ Unexpected cell type: %@" , self . tag , cell ) ;
return cell ;
}
[ cell ows_didLoad ] ;
2016-10-14 22:59:58 +02:00
if ( message . isMediaMessage ) {
if ( ! [ message isKindOfClass : [ TSMessageAdapter class ] ] ) {
DDLogError ( @ "%@ Unexpected media message:%@" , self . tag , message . class ) ;
}
TSMessageAdapter * messageAdapter = ( TSMessageAdapter * ) message ;
cell . mediaView . alpha = messageAdapter . mediaViewAlpha ;
2014-11-29 19:54:33 +01:00
}
2016-11-13 19:30:22 +01:00
2014-11-29 19:54:33 +01:00
return cell ;
}
2016-07-13 21:17:09 +02:00
- ( OWSCallCollectionViewCell * ) loadCallCellForCall : ( OWSCall * ) call atIndexPath : ( NSIndexPath * ) indexPath
2016-07-09 00:25:28 +02:00
{
2016-07-13 20:03:50 +02:00
OWSCallCollectionViewCell * callCell = [ self . collectionView dequeueReusableCellWithReuseIdentifier : [ OWSCallCollectionViewCell cellReuseIdentifier ]
2016-07-09 00:25:28 +02:00
forIndexPath : indexPath ] ;
NSString * text = call . date ! = nil ? [ call text ] : call . senderDisplayName ;
NSString * allText = call . date ! = nil ? [ text stringByAppendingString : [ call dateText ] ] : text ;
UIFont * boldFont = [ UIFont fontWithName : @ "HelveticaNeue-Medium" size : 12.0 f ] ;
NSMutableAttributedString * attributedText = [ [ NSMutableAttributedString alloc ] initWithString : allText
2016-07-13 21:40:12 +02:00
attributes : @ { NSFontAttributeName : boldFont } ] ;
2016-07-09 00:25:28 +02:00
if ( [ call date ] ! = nil ) {
// Not a group meta message
2016-07-13 21:40:12 +02:00
UIFont * regularFont = [ UIFont fontWithName : @ "HelveticaNeue-Light" size : 12.0 f ] ;
const NSRange range = NSMakeRange ( [ text length ] , [ [ call dateText ] length ] ) ;
[ attributedText setAttributes : @ { NSFontAttributeName : regularFont }
range : range ] ;
2016-07-09 00:25:28 +02:00
}
2016-10-08 17:48:50 +02:00
callCell . textView . text = nil ;
callCell . textView . attributedText = attributedText ;
2016-07-09 00:25:28 +02:00
2016-10-08 17:48:50 +02:00
callCell . textView . textAlignment = NSTextAlignmentCenter ;
callCell . textView . textColor = [ UIColor ows_materialBlueColor ] ;
2016-07-09 00:25:28 +02:00
callCell . layer . shouldRasterize = YES ;
callCell . layer . rasterizationScale = [ UIScreen mainScreen ] . scale ;
2016-10-08 17:48:50 +02:00
// Disable text selectability . Specifying this in prepareForReuse / awakeFromNib was not sufficient .
callCell . textView . userInteractionEnabled = NO ;
callCell . textView . selectable = NO ;
2016-07-09 00:25:28 +02:00
return callCell ;
2014-11-29 19:54:33 +01:00
}
2016-07-20 20:59:18 +02:00
- ( OWSDisplayedMessageCollectionViewCell * ) loadDisplayedMessageCollectionViewCellForIndexPath : ( NSIndexPath * ) indexPath
{
OWSDisplayedMessageCollectionViewCell * messageCell = [ self . collectionView dequeueReusableCellWithReuseIdentifier : [ OWSDisplayedMessageCollectionViewCell cellReuseIdentifier ]
forIndexPath : indexPath ] ;
messageCell . layer . shouldRasterize = YES ;
messageCell . layer . rasterizationScale = [ UIScreen mainScreen ] . scale ;
2016-10-06 21:47:45 +02:00
messageCell . textView . textColor = [ UIColor darkGrayColor ] ;
2016-07-20 20:59:18 +02:00
messageCell . cellTopLabel . attributedText = [ self . collectionView . dataSource collectionView : self . collectionView attributedTextForCellTopLabelAtIndexPath : indexPath ] ;
return messageCell ;
}
2016-10-23 19:39:42 +02:00
- ( OWSDisplayedMessageCollectionViewCell * ) loadInfoMessageCellForMessage : ( TSMessageAdapter * ) infoMessage
2016-07-09 00:25:28 +02:00
atIndexPath : ( NSIndexPath * ) indexPath
{
2016-07-20 20:59:18 +02:00
OWSDisplayedMessageCollectionViewCell * infoCell = [ self loadDisplayedMessageCollectionViewCellForIndexPath : indexPath ] ;
2016-09-21 14:37:51 +02:00
// HACK this will get called when we get a new info message , but there ' s gotta be a better spot for this .
OWSDisappearingMessagesConfiguration * configuration =
[ OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID : self . thread . uniqueId ] ;
[ self setBarButtonItemsForDisappearingMessagesConfiguration : configuration ] ;
2016-10-06 21:47:45 +02:00
infoCell . textView . text = [ infoMessage text ] ;
2016-10-08 16:10:25 +02:00
// Disable text selectability . Specifying this in prepareForReuse / awakeFromNib was not sufficient .
infoCell . textView . userInteractionEnabled = NO ;
infoCell . textView . selectable = NO ;
2016-10-08 17:48:50 +02:00
2016-09-21 14:37:51 +02:00
infoCell . messageBubbleContainerView . layer . borderColor = [ [ UIColor ows_infoMessageBorderColor ] CGColor ] ;
2016-10-08 18:48:15 +02:00
if ( infoMessage . infoMessageType = = TSInfoMessageTypeDisappearingMessagesUpdate ) {
infoCell . headerImageView . image = [ UIImage imageNamed : @ "ic_timer" ] ;
infoCell . headerImageView . backgroundColor = [ UIColor whiteColor ] ;
// Lighten up the broad stroke header icon to match the perceived color of the border .
infoCell . headerImageView . tintColor = [ UIColor ows_infoMessageBorderColor ] ;
} else {
infoCell . headerImageView . image = [ UIImage imageNamed : @ "warning_white" ] ;
}
2016-07-20 20:59:18 +02:00
2016-07-09 00:25:28 +02:00
return infoCell ;
2014-11-29 19:54:33 +01:00
}
2016-10-23 19:39:42 +02:00
- ( OWSDisplayedMessageCollectionViewCell * ) loadErrorMessageCellForMessage : ( TSMessageAdapter * ) errorMessage
2016-07-13 21:17:09 +02:00
atIndexPath : ( NSIndexPath * ) indexPath
{
2016-07-20 20:59:18 +02:00
OWSDisplayedMessageCollectionViewCell * errorCell = [ self loadDisplayedMessageCollectionViewCellForIndexPath : indexPath ] ;
2016-10-06 21:47:45 +02:00
errorCell . textView . text = [ errorMessage text ] ;
2016-10-08 17:48:50 +02:00
2016-10-08 16:10:25 +02:00
// Disable text selectability . Specifying this in prepareForReuse / awakeFromNib was not sufficient .
errorCell . textView . userInteractionEnabled = NO ;
errorCell . textView . selectable = NO ;
2016-10-08 17:48:50 +02:00
2016-09-21 14:37:51 +02:00
errorCell . messageBubbleContainerView . layer . borderColor = [ [ UIColor ows_errorMessageBorderColor ] CGColor ] ;
2016-07-09 00:25:28 +02:00
errorCell . headerImageView . image = [ UIImage imageNamed : @ "error_white" ] ;
2016-07-20 20:59:18 +02:00
2016-07-09 00:25:28 +02:00
return errorCell ;
2014-10-29 21:58:58 +01:00
}
# pragma mark - Adjusting cell label heights
2016-11-17 23:07:18 +01:00
/ * *
Due to the usage of JSQMessagesViewController , and it non - conformity to Dynamyc Type
we ' re left to our own devices to make this as usable as possible .
JSQMessagesVC also does not expose the constraint for the input toolbar height nor does it seem to
give us a method to tell it to re - adjust ( I think it should observe the preferredDefaultHeight property ) .
With that in mind , we use magical runtime to get that property , and if it doesn ' t exist , we just don ' t apply the dynamic
type change . If it does exist , than we apply the font changes and adjust the views to contain them properly .
This is not the prettiest code , but it ' s working code . We should tag this code for deletion as soon as JSQMessagesVC adops Dynamic type .
* /
- ( void ) reloadInputToolbarSizeIfNeeded {
NSLayoutConstraint * heightConstraint = ( ( NSLayoutConstraint * ) [ self valueForKeyPath : @ "toolbarHeightConstraint" ] ) ;
if ( heightConstraint = = nil ) {
return ;
}
[ self . inputToolbar . contentView . textView setFont : [ UIFont ows_dynamicTypeBodyFont ] ] ;
CGRect f = self . inputToolbar . contentView . textView . frame ;
f . size . height = [ self . inputToolbar . contentView . textView sizeThatFits : self . inputToolbar . contentView . textView . frame . size ] . height ;
self . inputToolbar . contentView . textView . frame = f ;
self . inputToolbar . preferredDefaultHeight = self . inputToolbar . contentView . textView . frame . size . height + 16 ;
heightConstraint . constant = self . inputToolbar . preferredDefaultHeight ;
[ self . inputToolbar setNeedsLayout ] ;
}
/ * *
Called whenever the user manually changes the dynamic type options inside Settings .
@ param notification NSNotification with the dynamic type change information .
* /
- ( void ) didChangePreferredContentSize : ( NSNotification * ) notification {
[ self . collectionView . collectionViewLayout setMessageBubbleFont : [ UIFont ows_dynamicTypeBodyFont ] ] ;
[ self . collectionView reloadData ] ;
[ self reloadInputToolbarSizeIfNeeded ] ;
}
2014-10-29 21:58:58 +01:00
- ( CGFloat ) collectionView : ( JSQMessagesCollectionView * ) collectionView
2015-12-22 12:45:09 +01:00
layout : ( JSQMessagesCollectionViewFlowLayout * ) collectionViewLayout
heightForCellTopLabelAtIndexPath : ( NSIndexPath * ) indexPath {
2014-11-25 16:38:33 +01:00
if ( [ self showDateAtIndexPath : indexPath ] ) {
2014-10-29 21:58:58 +01:00
return kJSQMessagesCollectionViewCellLabelHeightDefault ;
}
2015-12-22 12:45:09 +01:00
2014-10-29 21:58:58 +01:00
return 0.0 f ;
}
2015-12-22 12:45:09 +01:00
- ( BOOL ) showDateAtIndexPath : ( NSIndexPath * ) indexPath {
2014-11-25 16:38:33 +01:00
BOOL showDate = NO ;
if ( indexPath . row = = 0 ) {
showDate = YES ;
2015-12-22 12:45:09 +01:00
} else {
2016-10-14 22:59:58 +02:00
id < OWSMessageData > currentMessage = [ self messageAtIndexPath : indexPath ] ;
2015-12-22 12:45:09 +01:00
2016-10-14 22:59:58 +02:00
id < OWSMessageData > previousMessage =
2015-12-22 12:45:09 +01:00
[ self messageAtIndexPath : [ NSIndexPath indexPathForItem : indexPath . row - 1 inSection : indexPath . section ] ] ;
2014-11-25 16:38:33 +01:00
NSTimeInterval timeDifference = [ currentMessage . date timeIntervalSinceDate : previousMessage . date ] ;
if ( timeDifference > kTSMessageSentDateShowTimeInterval ) {
showDate = YES ;
2014-10-29 21:58:58 +01:00
}
}
2014-11-25 16:38:33 +01:00
return showDate ;
2014-10-29 21:58:58 +01:00
}
2015-12-22 12:45:09 +01:00
- ( NSAttributedString * ) collectionView : ( JSQMessagesCollectionView * ) collectionView
attributedTextForCellTopLabelAtIndexPath : ( NSIndexPath * ) indexPath {
2014-12-17 06:44:36 +01:00
if ( [ self showDateAtIndexPath : indexPath ] ) {
2016-10-14 22:59:58 +02:00
id < OWSMessageData > currentMessage = [ self messageAtIndexPath : indexPath ] ;
2015-12-22 12:45:09 +01:00
2014-12-17 06:44:36 +01:00
return [ [ JSQMessagesTimestampFormatter sharedFormatter ] attributedTimestampForDate : currentMessage . date ] ;
2014-11-29 19:54:33 +01:00
}
2015-12-22 12:45:09 +01:00
2014-11-29 19:54:33 +01:00
return nil ;
}
2016-09-21 14:37:51 +02:00
- ( BOOL ) shouldShowMessageStatusAtIndexPath : ( NSIndexPath * ) indexPath
{
2016-10-14 22:59:58 +02:00
id < OWSMessageData > currentMessage = [ self messageAtIndexPath : indexPath ] ;
2015-12-22 12:45:09 +01:00
2016-09-21 14:37:51 +02:00
if ( currentMessage . isExpiringMessage ) {
return YES ;
}
2016-10-12 19:00:06 +02:00
return ! ! [ self collectionView : self . collectionView attributedTextForCellBottomLabelAtIndexPath : indexPath ] ;
2014-12-04 15:01:05 +01:00
}
2016-10-14 22:59:58 +02:00
- ( id < OWSMessageData > ) nextOutgoingMessage : ( NSIndexPath * ) indexPath
{
id < OWSMessageData > nextMessage =
2015-12-22 12:45:09 +01:00
[ self messageAtIndexPath : [ NSIndexPath indexPathForRow : indexPath . row + 1 inSection : indexPath . section ] ] ;
2014-12-04 15:01:05 +01:00
int i = 1 ;
2015-12-22 12:45:09 +01:00
2016-10-14 22:59:58 +02:00
while ( indexPath . item + i < [ self . collectionView numberOfItemsInSection : indexPath . section ] - 1
&& ! nextMessage . isOutgoingAndDelivered ) {
2014-12-04 15:01:05 +01:00
i + + ;
2015-12-22 12:45:09 +01:00
nextMessage =
[ self messageAtIndexPath : [ NSIndexPath indexPathForRow : indexPath . row + i inSection : indexPath . section ] ] ;
2014-12-04 15:01:05 +01:00
}
2015-12-22 12:45:09 +01:00
2014-12-04 15:01:05 +01:00
return nextMessage ;
2014-11-29 19:54:33 +01:00
}
2015-12-22 12:45:09 +01:00
- ( NSAttributedString * ) collectionView : ( JSQMessagesCollectionView * ) collectionView
2016-09-21 14:37:51 +02:00
attributedTextForCellBottomLabelAtIndexPath : ( NSIndexPath * ) indexPath
{
id < OWSMessageData > messageData = [ self messageAtIndexPath : indexPath ] ;
if ( ! [ messageData isKindOfClass : [ TSMessageAdapter class ] ] ) {
return nil ;
}
2015-12-22 12:45:09 +01:00
2016-09-21 14:37:51 +02:00
TSMessageAdapter * message = ( TSMessageAdapter * ) messageData ;
if ( message . messageType = = TSOutgoingMessageAdapter ) {
TSOutgoingMessage * outgoingMessage = ( TSOutgoingMessage * ) message . interaction ;
if ( outgoingMessage . messageState = = TSOutgoingMessageStateUnsent ) {
2016-10-14 22:59:58 +02:00
return [ [ NSAttributedString alloc ] initWithString : NSLocalizedString ( @ "FAILED_SENDING_TEXT" , nil ) ] ;
} else if ( message . isOutgoingAndDelivered ) {
2016-09-21 14:37:51 +02:00
NSAttributedString * deliveredString =
[ [ NSAttributedString alloc ] initWithString : NSLocalizedString ( @ "DELIVERED_MESSAGE_TEXT" , @ "" ) ] ;
2016-10-12 19:00:06 +02:00
// Show when it ' s the last message in the thread
if ( indexPath . item = = [ self . collectionView numberOfItemsInSection : indexPath . section ] - 1 ) {
[ self updateLastDeliveredMessage : message ] ;
return deliveredString ;
}
// Or when the next message is * not * an outgoing delivered message .
TSMessageAdapter * nextMessage = [ self nextOutgoingMessage : indexPath ] ;
2016-10-14 22:59:58 +02:00
if ( ! nextMessage . isOutgoingAndDelivered ) {
2016-10-12 19:00:06 +02:00
[ self updateLastDeliveredMessage : message ] ;
return deliveredString ;
}
2016-10-14 22:59:58 +02:00
} else if ( message . isMediaBeingSent ) {
return [ [ NSAttributedString alloc ] initWithString : NSLocalizedString ( @ "UPLOADING_MESSAGE_TEXT" ,
@ "message footer while attachment is uploading" ) ] ;
2014-12-17 06:44:36 +01:00
}
2016-09-21 14:37:51 +02:00
} else if ( message . messageType = = TSIncomingMessageAdapter && [ self . thread isKindOfClass : [ TSGroupThread class ] ] ) {
TSIncomingMessage * incomingMessage = ( TSIncomingMessage * ) message . interaction ;
NSString * _Nonnull name = [ self . contactsManager nameStringForPhoneIdentifier : incomingMessage . authorId ] ;
NSAttributedString * senderNameString = [ [ NSAttributedString alloc ] initWithString : name ] ;
return senderNameString ;
2014-11-29 19:54:33 +01:00
}
2016-09-21 14:37:51 +02:00
2014-11-29 19:54:33 +01:00
return nil ;
}
2016-10-12 19:00:06 +02:00
- ( void ) updateLastDeliveredMessage : ( TSMessageAdapter * ) newLastDeliveredMessage
{
if ( newLastDeliveredMessage . interaction . timestamp > self . lastDeliveredMessage . interaction . timestamp ) {
TSMessageAdapter * penultimateDeliveredMessage = self . lastDeliveredMessage ;
self . lastDeliveredMessage = newLastDeliveredMessage ;
[ penultimateDeliveredMessage . interaction touch ] ;
}
}
2014-10-29 21:58:58 +01:00
- ( CGFloat ) collectionView : ( JSQMessagesCollectionView * ) collectionView
2015-12-22 12:45:09 +01:00
layout : ( JSQMessagesCollectionViewFlowLayout * ) collectionViewLayout
2016-10-12 19:00:06 +02:00
heightForCellBottomLabelAtIndexPath : ( NSIndexPath * ) indexPath
{
2015-03-19 01:59:44 +01:00
if ( [ self shouldShowMessageStatusAtIndexPath : indexPath ] ) {
2014-11-29 19:54:33 +01:00
return 16.0 f ;
}
2015-12-22 12:45:09 +01:00
2014-11-29 19:54:33 +01:00
return 0.0 f ;
2014-10-29 21:58:58 +01:00
}
# pragma mark - Actions
2016-09-21 14:37:51 +02:00
- ( void ) showConversationSettings
{
if ( self . userLeftGroup ) {
DDLogDebug ( @ "%@ Ignoring request to show conversation settings, since user left group" , self . tag ) ;
return ;
}
[ self performSegueWithIdentifier : OWSMessagesViewControllerSeguePushConversationSettings sender : self ] ;
}
- ( void ) didTapTitle
{
DDLogDebug ( @ "%@ Tapped title in navbar" , self . tag ) ;
[ self showConversationSettings ] ;
}
- ( void ) didTapManageGroupButton : ( id ) sender
{
DDLogDebug ( @ "%@ Tapped options menu in navbar" , self . tag ) ;
[ self showConversationSettings ] ;
}
- ( void ) didTapTimerInNavbar
{
DDLogDebug ( @ "%@ Tapped timer in navbar" , self . tag ) ;
[ self showConversationSettings ] ;
}
2015-12-22 12:45:09 +01:00
- ( void ) collectionView : ( JSQMessagesCollectionView * ) collectionView
2016-07-13 21:17:09 +02:00
didTapMessageBubbleAtIndexPath : ( NSIndexPath * ) indexPath
{
2016-09-21 14:37:51 +02:00
id < OWSMessageData > messageItem = [ self messageAtIndexPath : indexPath ] ;
2015-12-22 12:45:09 +01:00
TSInteraction * interaction = [ self interactionAtIndexPath : indexPath ] ;
2014-12-11 00:05:41 +01:00
switch ( messageItem . messageType ) {
2016-07-09 00:25:28 +02:00
case TSOutgoingMessageAdapter : {
2016-10-14 22:59:58 +02:00
TSOutgoingMessage * outgoingMessage = ( TSOutgoingMessage * ) interaction ;
2016-07-09 00:25:28 +02:00
if ( outgoingMessage . messageState = = TSOutgoingMessageStateUnsent ) {
2016-10-14 22:59:58 +02:00
[ self handleUnsentMessageTap : outgoingMessage ] ;
// This ` break` is intentionally within the if .
// We want to activate fullscreen media view for sent items
// but not those which failed - to - send
break ;
2014-12-11 00:05:41 +01:00
}
2016-10-14 22:59:58 +02:00
// No ` break` as we want to fall through to capture tapping on Outgoing media items too
2016-07-09 00:25:28 +02:00
}
2015-12-22 12:45:09 +01:00
case TSIncomingMessageAdapter : {
2014-12-11 00:05:41 +01:00
BOOL isMediaMessage = [ messageItem isMediaMessage ] ;
2015-12-22 12:45:09 +01:00
2014-12-11 00:05:41 +01:00
if ( isMediaMessage ) {
2015-12-22 12:45:09 +01:00
if ( [ [ messageItem media ] isKindOfClass : [ TSPhotoAdapter class ] ] ) {
TSPhotoAdapter * messageMedia = ( TSPhotoAdapter * ) [ messageItem media ] ;
2016-04-13 20:38:42 +02:00
tappedImage = ( ( UIImageView * ) [ messageMedia mediaView ] ) . image ;
if ( tappedImage = = nil ) {
DDLogWarn ( @ "tapped TSPhotoAdapter with nil image" ) ;
} else {
2015-12-22 12:45:09 +01:00
CGRect convertedRect =
2016-04-13 20:38:42 +02:00
[ self . collectionView convertRect : [ collectionView cellForItemAtIndexPath : indexPath ] . frame
toView : nil ] ;
2015-01-14 22:30:01 +01:00
__block TSAttachment * attachment = nil ;
[ self . uiDatabaseConnection readWithBlock : ^ ( YapDatabaseReadTransaction * transaction ) {
2016-04-13 20:38:42 +02:00
attachment =
[ TSAttachment fetchObjectWithUniqueID : messageMedia . attachmentId transaction : transaction ] ;
2015-01-14 22:30:01 +01:00
} ] ;
2015-12-22 12:45:09 +01:00
2015-01-14 22:30:01 +01:00
if ( [ attachment isKindOfClass : [ TSAttachmentStream class ] ] ) {
2015-12-22 12:45:09 +01:00
TSAttachmentStream * attStream = ( TSAttachmentStream * ) attachment ;
FullImageViewController * vc = [ [ FullImageViewController alloc ]
2016-04-13 20:38:42 +02:00
initWithAttachment : attStream
fromRect : convertedRect
forInteraction : [ self interactionAtIndexPath : indexPath ]
isAnimated : NO ] ;
2015-12-22 12:45:09 +01:00
2015-01-25 02:40:51 +01:00
[ vc presentFromViewController : self . navigationController ] ;
2015-01-14 22:30:01 +01:00
}
}
2015-12-22 12:45:09 +01:00
} else if ( [ [ messageItem media ] isKindOfClass : [ TSAnimatedAdapter class ] ] ) {
2015-09-01 04:51:39 +02:00
// Show animated image full - screen
2015-12-22 12:45:09 +01:00
TSAnimatedAdapter * messageMedia = ( TSAnimatedAdapter * ) [ messageItem media ] ;
tappedImage = ( ( UIImageView * ) [ messageMedia mediaView ] ) . image ;
2016-04-13 20:38:42 +02:00
if ( tappedImage = = nil ) {
DDLogWarn ( @ "tapped TSAnimatedAdapter with nil image" ) ;
} else {
CGRect convertedRect =
2015-12-22 12:45:09 +01:00
[ self . collectionView convertRect : [ collectionView cellForItemAtIndexPath : indexPath ] . frame
toView : nil ] ;
2016-04-13 20:38:42 +02:00
__block TSAttachment * attachment = nil ;
[ self . uiDatabaseConnection readWithBlock : ^ ( YapDatabaseReadTransaction * transaction ) {
attachment =
[ TSAttachment fetchObjectWithUniqueID : messageMedia . attachmentId transaction : transaction ] ;
} ] ;
if ( [ attachment isKindOfClass : [ TSAttachmentStream class ] ] ) {
TSAttachmentStream * attStream = ( TSAttachmentStream * ) attachment ;
FullImageViewController * vc =
2015-12-22 12:45:09 +01:00
[ [ FullImageViewController alloc ] initWithAttachment : attStream
fromRect : convertedRect
forInteraction : [ self interactionAtIndexPath : indexPath ]
isAnimated : YES ] ;
2016-04-13 20:38:42 +02:00
[ vc presentFromViewController : self . navigationController ] ;
}
2015-09-01 04:51:39 +02:00
}
2015-12-22 12:45:09 +01:00
} else if ( [ [ messageItem media ] isKindOfClass : [ TSVideoAttachmentAdapter class ] ] ) {
2015-01-14 22:30:01 +01:00
// fileurl disappeared should look up in db as before . will do refactor
// full screen , check this setup with a . mov
2015-12-22 12:45:09 +01:00
TSVideoAttachmentAdapter * messageMedia = ( TSVideoAttachmentAdapter * ) [ messageItem media ] ;
_currentMediaAdapter = messageMedia ;
__block TSAttachment * attachment = nil ;
2014-12-26 23:18:54 +01:00
[ self . uiDatabaseConnection readWithBlock : ^ ( YapDatabaseReadTransaction * transaction ) {
2015-12-22 12:45:09 +01:00
attachment =
[ TSAttachment fetchObjectWithUniqueID : messageMedia . attachmentId transaction : transaction ] ;
2014-12-26 23:18:54 +01:00
} ] ;
2015-12-22 12:45:09 +01:00
2014-12-26 23:18:54 +01:00
if ( [ attachment isKindOfClass : [ TSAttachmentStream class ] ] ) {
2015-12-22 12:45:09 +01:00
TSAttachmentStream * attStream = ( TSAttachmentStream * ) attachment ;
NSFileManager * fileManager = [ NSFileManager defaultManager ] ;
if ( [ messageMedia isVideo ] ) {
2015-01-22 05:08:12 +01:00
if ( [ fileManager fileExistsAtPath : [ attStream . mediaURL path ] ] ) {
2015-01-30 23:28:05 +01:00
[ self dismissKeyBoard ] ;
2015-01-22 05:08:12 +01:00
_videoPlayer = [ [ MPMoviePlayerController alloc ] initWithContentURL : attStream . mediaURL ] ;
2015-01-14 22:30:01 +01:00
[ _videoPlayer prepareToPlay ] ;
2015-12-22 12:45:09 +01:00
[ [ NSNotificationCenter defaultCenter ]
addObserver : self
selector : @ selector ( moviePlayBackDidFinish : )
name : MPMoviePlayerPlaybackDidFinishNotification
object : _videoPlayer ] ;
_videoPlayer . controlStyle = MPMovieControlStyleDefault ;
2015-02-17 00:14:50 +01:00
_videoPlayer . shouldAutoplay = YES ;
2015-12-22 12:45:09 +01:00
[ self . view addSubview : _videoPlayer . view ] ;
2015-01-14 22:30:01 +01:00
[ _videoPlayer setFullscreen : YES animated : YES ] ;
}
2015-12-22 12:45:09 +01:00
} else if ( [ messageMedia isAudio ] ) {
2015-01-22 05:08:12 +01:00
if ( messageMedia . isAudioPlaying ) {
// if you had started playing an audio msg and now you ' re tapping it to pause
messageMedia . isAudioPlaying = NO ;
[ _audioPlayer pause ] ;
messageMedia . isPaused = YES ;
[ _audioPlayerPoller invalidate ] ;
2015-12-22 12:45:09 +01:00
double current = [ _audioPlayer currentTime ] / [ _audioPlayer duration ] ;
2015-01-22 05:08:12 +01:00
[ messageMedia setAudioProgressFromFloat : ( float ) current ] ;
[ messageMedia setAudioIconToPlay ] ;
} else {
BOOL isResuming = NO ;
[ _audioPlayerPoller invalidate ] ;
2015-12-22 12:45:09 +01:00
2015-01-22 05:08:12 +01:00
// loop through all the other bubbles and set their isPlaying to false
NSInteger num_bubbles = [ self collectionView : collectionView numberOfItemsInSection : 0 ] ;
2015-12-22 12:45:09 +01:00
for ( NSInteger i = 0 ; i < num_bubbles ; i + + ) {
2016-09-21 14:37:51 +02:00
NSIndexPath * indexPathI = [ NSIndexPath indexPathForRow : i inSection : 0 ] ;
id < OWSMessageData > message = [ self messageAtIndexPath : indexPathI ] ;
if ( message . messageType = = TSIncomingMessageAdapter && message . isMediaMessage ) {
TSVideoAttachmentAdapter * msgMedia
= ( TSVideoAttachmentAdapter * ) [ message media ] ;
2015-01-22 05:08:12 +01:00
if ( [ msgMedia isAudio ] ) {
if ( msgMedia = = messageMedia && messageMedia . isPaused ) {
isResuming = YES ;
} else {
msgMedia . isAudioPlaying = NO ;
2015-12-22 12:45:09 +01:00
msgMedia . isPaused = NO ;
2015-01-22 05:08:12 +01:00
[ msgMedia setAudioIconToPlay ] ;
[ msgMedia setAudioProgressFromFloat : 0 ] ;
2015-01-25 00:48:40 +01:00
[ msgMedia resetAudioDuration ] ;
2015-01-22 05:08:12 +01:00
}
}
}
}
2015-12-22 12:45:09 +01:00
2015-01-22 05:08:12 +01:00
if ( isResuming ) {
// if you had paused an audio msg and now you ' re tapping to resume
[ _audioPlayer prepareToPlay ] ;
[ _audioPlayer play ] ;
[ messageMedia setAudioIconToPause ] ;
messageMedia . isAudioPlaying = YES ;
2015-12-22 12:45:09 +01:00
messageMedia . isPaused = NO ;
_audioPlayerPoller =
[ NSTimer scheduledTimerWithTimeInterval : .05
target : self
selector : @ selector ( audioPlayerUpdated : )
userInfo : @ {
@ "adapter" : messageMedia
}
repeats : YES ] ;
2015-01-22 05:08:12 +01:00
} else {
// if you are tapping an audio msg for the first time to play
messageMedia . isAudioPlaying = YES ;
NSError * error ;
2015-12-22 12:45:09 +01:00
_audioPlayer =
[ [ AVAudioPlayer alloc ] initWithContentsOfURL : attStream . mediaURL error : & error ] ;
2015-01-25 00:48:40 +01:00
if ( error ) {
2015-08-14 00:19:29 +02:00
DDLogError ( @ "error: %@" , error ) ;
2015-01-25 00:48:40 +01:00
}
2015-01-22 05:08:12 +01:00
[ _audioPlayer prepareToPlay ] ;
[ _audioPlayer play ] ;
[ messageMedia setAudioIconToPause ] ;
_audioPlayer . delegate = self ;
2015-12-22 12:45:09 +01:00
_audioPlayerPoller =
[ NSTimer scheduledTimerWithTimeInterval : .05
target : self
selector : @ selector ( audioPlayerUpdated : )
userInfo : @ {
@ "adapter" : messageMedia
}
repeats : YES ] ;
2015-01-22 05:08:12 +01:00
}
}
2015-01-14 22:30:01 +01:00
}
2014-12-26 23:18:54 +01:00
}
2014-12-11 00:05:41 +01:00
}
}
2015-12-22 12:45:09 +01:00
} break ;
2016-07-21 19:23:39 +02:00
case TSErrorMessageAdapter :
[ self handleErrorMessageTap : ( TSErrorMessage * ) interaction ] ;
break ;
case TSInfoMessageAdapter :
[ self handleWarningTap : interaction ] ;
break ;
2014-12-31 21:30:20 +01:00
case TSCallAdapter :
break ;
2014-12-11 00:05:41 +01:00
default :
2016-07-09 00:25:28 +02:00
DDLogDebug ( @ "Unhandled bubble touch for interaction: %@." , interaction ) ;
2014-12-11 00:05:41 +01:00
break ;
2014-11-29 19:54:33 +01:00
}
2014-12-11 00:05:41 +01:00
}
2016-07-13 21:17:09 +02:00
- ( void ) handleWarningTap : ( TSInteraction * ) interaction
{
2015-02-19 01:04:32 +01:00
if ( [ interaction isKindOfClass : [ TSIncomingMessage class ] ] ) {
2015-12-22 12:45:09 +01:00
TSIncomingMessage * message = ( TSIncomingMessage * ) interaction ;
2016-08-01 00:25:07 +02:00
for ( NSString * attachmentId in message . attachmentIds ) {
2015-02-19 01:04:32 +01:00
__block TSAttachment * attachment ;
2015-12-22 12:45:09 +01:00
2015-02-19 01:04:32 +01:00
[ self . editingDatabaseConnection readWithBlock : ^ ( YapDatabaseReadTransaction * transaction ) {
2015-12-22 12:45:09 +01:00
attachment = [ TSAttachment fetchObjectWithUniqueID : attachmentId transaction : transaction ] ;
2015-02-19 01:04:32 +01:00
} ] ;
2015-12-22 12:45:09 +01:00
2015-02-19 01:04:32 +01:00
if ( [ attachment isKindOfClass : [ TSAttachmentPointer class ] ] ) {
2015-12-22 12:45:09 +01:00
TSAttachmentPointer * pointer = ( TSAttachmentPointer * ) attachment ;
2016-07-09 00:25:28 +02:00
// FIXME possible for pointer to get stuck in isDownloading state if app is closed while downloading .
2016-07-13 21:17:09 +02:00
// see : https : // github . com / WhisperSystems / Signal - iOS / issues / 1254
2015-02-19 01:04:32 +01:00
if ( ! pointer . isDownloading ) {
2016-10-14 22:59:58 +02:00
OWSAttachmentsProcessor * processor =
[ [ OWSAttachmentsProcessor alloc ] initWithAttachmentPointer : pointer
networkManager : self . networkManager ] ;
[ processor fetchAttachmentsForMessage : message
success : ^ ( TSAttachmentStream * _Nonnull attachmentStream ) {
DDLogInfo (
@ "%@ Successfully redownloaded attachment in thread: %@" , self . tag , message . thread ) ;
}
failure : ^ ( NSError * _Nonnull error ) {
DDLogWarn ( @ "%@ Failed to redownload message with error: %@" , self . tag , error ) ;
} ] ;
2015-02-19 01:04:32 +01:00
}
}
}
}
}
2015-01-29 04:50:41 +01:00
2015-12-22 12:45:09 +01:00
- ( void ) moviePlayBackDidFinish : ( id ) sender {
2015-01-14 22:30:01 +01:00
DDLogDebug ( @ "playback finished" ) ;
}
2015-12-22 12:45:09 +01:00
- ( void ) collectionView : ( JSQMessagesCollectionView * ) collectionView
header : ( JSQMessagesLoadEarlierHeaderView * ) headerView
didTapLoadEarlierMessagesButton : ( UIButton * ) sender {
2014-12-31 13:22:40 +01:00
if ( [ self shouldShowLoadEarlierMessages ] ) {
self . page + + ;
}
2015-12-22 12:45:09 +01:00
2014-12-31 13:22:40 +01:00
NSInteger item = ( NSInteger ) [ self scrollToItem ] ;
2015-12-22 12:45:09 +01:00
2014-12-31 13:22:40 +01:00
[ self updateRangeOptionsForPage : self . page ] ;
2015-12-22 12:45:09 +01:00
2014-12-31 13:22:40 +01:00
[ self . uiDatabaseConnection readWithBlock : ^ ( YapDatabaseReadTransaction * transaction ) {
2015-12-22 12:45:09 +01:00
[ self . messageMappings updateWithTransaction : transaction ] ;
2014-12-31 13:22:40 +01:00
} ] ;
2015-12-22 12:45:09 +01:00
2014-12-31 13:22:40 +01:00
[ self updateLayoutForEarlierMessagesWithOffset : item ] ;
}
2015-12-22 12:45:09 +01:00
- ( BOOL ) shouldShowLoadEarlierMessages {
2014-12-31 13:22:40 +01:00
__block BOOL show = YES ;
2015-12-22 12:45:09 +01:00
[ self . uiDatabaseConnection readWithBlock : ^ ( YapDatabaseReadTransaction * transaction ) {
show = [ self . messageMappings numberOfItemsInGroup : self . thread . uniqueId ] <
[ [ transaction ext : TSMessageDatabaseViewExtensionName ] numberOfItemsInGroup : self . thread . uniqueId ] ;
2014-12-31 13:22:40 +01:00
} ] ;
2015-12-22 12:45:09 +01:00
2014-12-31 13:22:40 +01:00
return show ;
}
2015-12-22 12:45:09 +01:00
- ( NSUInteger ) scrollToItem {
__block NSUInteger item =
kYapDatabaseRangeLength * ( self . page + 1 ) - [ self . messageMappings numberOfItemsInGroup : self . thread . uniqueId ] ;
2014-12-31 13:22:40 +01:00
[ self . uiDatabaseConnection readWithBlock : ^ ( YapDatabaseReadTransaction * transaction ) {
2015-12-22 12:45:09 +01:00
NSUInteger numberOfVisibleMessages = [ self . messageMappings numberOfItemsInGroup : self . thread . uniqueId ] ;
NSUInteger numberOfTotalMessages =
[ [ transaction ext : TSMessageDatabaseViewExtensionName ] numberOfItemsInGroup : self . thread . uniqueId ] ;
NSUInteger numberOfMessagesToLoad = numberOfTotalMessages - numberOfVisibleMessages ;
BOOL canLoadFullRange = numberOfMessagesToLoad >= kYapDatabaseRangeLength ;
if ( ! canLoadFullRange ) {
item = numberOfMessagesToLoad ;
}
2014-12-31 13:22:40 +01:00
} ] ;
2015-12-22 12:45:09 +01:00
2014-12-31 13:22:40 +01:00
return item = = 0 ? item : item - 1 ;
}
2015-12-22 12:45:09 +01:00
- ( void ) updateLoadEarlierVisible {
2014-12-31 13:22:40 +01:00
[ self setShowLoadEarlierMessagesHeader : [ self shouldShowLoadEarlierMessages ] ] ;
}
2015-12-22 12:45:09 +01:00
- ( void ) updateLayoutForEarlierMessagesWithOffset : ( NSInteger ) offset {
[ self . collectionView . collectionViewLayout
invalidateLayoutWithContext : [ JSQMessagesCollectionViewFlowLayoutInvalidationContext context ] ] ;
2014-12-31 13:22:40 +01:00
[ self . collectionView reloadData ] ;
2015-12-22 12:45:09 +01:00
[ self . collectionView scrollToItemAtIndexPath : [ NSIndexPath indexPathForItem : offset inSection : 0 ]
atScrollPosition : UICollectionViewScrollPositionTop
animated : NO ] ;
2014-12-31 13:22:40 +01:00
[ self updateLoadEarlierVisible ] ;
}
2015-12-22 12:45:09 +01:00
- ( void ) updateRangeOptionsForPage : ( NSUInteger ) page {
YapDatabaseViewRangeOptions * rangeOptions =
[ YapDatabaseViewRangeOptions flexibleRangeWithLength : kYapDatabaseRangeLength * ( page + 1 )
offset : 0
from : YapDatabaseViewEnd ] ;
2014-12-31 13:22:40 +01:00
rangeOptions . maxLength = kYapDatabaseRangeMaxLength ;
rangeOptions . minLength = kYapDatabaseRangeMinLength ;
2015-12-22 12:45:09 +01:00
2014-12-31 13:22:40 +01:00
[ self . messageMappings setRangeOptions : rangeOptions forGroup : self . thread . uniqueId ] ;
}
2014-12-11 00:05:41 +01:00
# pragma mark Bubble User Actions
2015-12-22 12:45:09 +01:00
- ( void ) handleUnsentMessageTap : ( TSOutgoingMessage * ) message {
2016-11-26 00:12:00 +01:00
UIAlertController * actionSheetController = [ UIAlertController alertControllerWithTitle : message . mostRecentFailureText
message : nil
preferredStyle : UIAlertControllerStyleActionSheet ] ;
UIAlertAction * dismissAction = [ UIAlertAction actionWithTitle : NSLocalizedString ( @ "TXT_CANCEL_TITLE" , @ "" )
style : UIAlertActionStyleCancel
handler : nil ] ;
[ actionSheetController addAction : dismissAction ] ;
UIAlertAction * deleteMessageAction = [ UIAlertAction actionWithTitle : NSLocalizedString ( @ "TXT_DELETE_TITLE" , @ "" )
style : UIAlertActionStyleDestructive
handler : ^ ( UIAlertAction * _Nonnull action ) {
[ message remove ] ;
} ] ;
[ actionSheetController addAction : deleteMessageAction ] ;
UIAlertAction * resendMessageAction = [ UIAlertAction actionWithTitle : NSLocalizedString ( @ "SEND_AGAIN_BUTTON" , @ "" )
style : UIAlertActionStyleDefault
handler : ^ ( UIAlertAction * _Nonnull action ) {
[ self . messageSender sendMessage : message
success : ^ {
DDLogInfo ( @ "%@ Successfully resent failed message." , self . tag ) ;
}
failure : ^ ( NSError * _Nonnull error ) {
DDLogWarn ( @ "%@ Failed to send message with error: %@" , self . tag , error ) ;
} ] ;
} ] ;
[ actionSheetController addAction : resendMessageAction ] ;
[ self presentViewController : actionSheetController animated : YES completion : nil ] ;
2014-12-11 00:05:41 +01:00
}
2016-09-11 22:53:12 +02:00
- ( void ) handleErrorMessageTap : ( TSErrorMessage * ) message
{
2014-12-31 21:30:20 +01:00
if ( [ message isKindOfClass : [ TSInvalidIdentityKeyErrorMessage class ] ] ) {
2016-09-11 22:53:12 +02:00
[ self tappedInvalidIdentityKeyErrorMessage : ( TSInvalidIdentityKeyErrorMessage * ) message ] ;
2016-11-01 20:02:15 +01:00
} else if ( message . errorType = = TSErrorMessageInvalidMessage ) {
[ self tappedCorruptedMessage : message ] ;
} else {
DDLogWarn ( @ "%@ Unhandled tap for error message:%@" , self . tag , message ) ;
2016-09-11 22:53:12 +02:00
}
}
2015-12-22 12:45:09 +01:00
2016-11-01 20:02:15 +01:00
- ( void ) tappedCorruptedMessage : ( TSErrorMessage * ) message
{
2016-11-26 00:12:00 +01:00
NSString * alertMessage = [ NSString
2016-11-01 20:02:15 +01:00
stringWithFormat : NSLocalizedString ( @ "CORRUPTED_SESSION_DESCRIPTION" , @ "ActionSheet title" ) , self . thread . name ] ;
2016-11-26 00:12:00 +01:00
UIAlertController * alertController = [ UIAlertController alertControllerWithTitle : nil
message : alertMessage
preferredStyle : UIAlertControllerStyleAlert ] ;
UIAlertAction * dismissAction = [ UIAlertAction actionWithTitle : NSLocalizedString ( @ "TXT_CANCEL_TITLE" , @ "" )
style : UIAlertActionStyleCancel
handler : nil ] ;
[ alertController addAction : dismissAction ] ;
UIAlertAction * resetSessionAction = [ UIAlertAction actionWithTitle : NSLocalizedString ( @ "FINGERPRINT_SHRED_KEYMATERIAL_BUTTON" , @ "" )
style : UIAlertActionStyleDefault
handler : ^ ( UIAlertAction * _Nonnull action ) {
if ( ! [ self . thread isKindOfClass : [ TSContactThread class ] ] ) {
// Corrupt Message errors only appear in contact threads .
DDLogError ( @ "%@ Unexpected request to reset session in group thread. Refusing" , self . tag ) ;
return ;
}
TSContactThread * contactThread = ( TSContactThread * ) self . thread ;
[ OWSSessionResetJob runWithCorruptedMessage : message
contactThread : contactThread
messageSender : self . messageSender
storageManager : self . storageManager ] ;
} ] ;
[ alertController addAction : resetSessionAction ] ;
[ self presentViewController : alertController animated : YES completion : nil ] ;
2016-11-01 20:02:15 +01:00
}
2016-09-11 22:53:12 +02:00
- ( void ) tappedInvalidIdentityKeyErrorMessage : ( TSInvalidIdentityKeyErrorMessage * ) errorMessage
{
NSString * keyOwner = [ self . contactsManager nameStringForPhoneIdentifier : errorMessage . theirSignalId ] ;
NSString * titleFormat = NSLocalizedString ( @ "SAFETY_NUMBERS_ACTIONSHEET_TITLE" , @ "Action sheet heading" ) ;
NSString * titleText = [ NSString stringWithFormat : titleFormat , keyOwner ] ;
2016-11-26 00:12:00 +01:00
UIAlertController * actionSheetController = [ UIAlertController alertControllerWithTitle : titleText
message : nil
preferredStyle : UIAlertControllerStyleActionSheet ] ;
UIAlertAction * dismissAction = [ UIAlertAction actionWithTitle : NSLocalizedString ( @ "TXT_CANCEL_TITLE" , @ "" )
style : UIAlertActionStyleCancel
handler : nil ] ;
[ actionSheetController addAction : dismissAction ] ;
UIAlertAction * showSafteyNumberAction = [ UIAlertAction actionWithTitle : NSLocalizedString ( @ "SHOW_SAFETY_NUMBER_ACTION" , @ "Action sheet item" )
style : UIAlertActionStyleDefault
handler : ^ ( UIAlertAction * _Nonnull action ) {
DDLogInfo ( @ "%@ Remote Key Changed actions: Show fingerprint display" , self . tag ) ;
[ self showFingerprintWithTheirIdentityKey : errorMessage . newIdentityKey
theirSignalId : errorMessage . theirSignalId ] ;
} ] ;
[ actionSheetController addAction : showSafteyNumberAction ] ;
UIAlertAction * acceptSafetyNumberAction = [ UIAlertAction actionWithTitle : NSLocalizedString ( @ "ACCEPT_NEW_IDENTITY_ACTION" , @ "Action sheet item" )
style : UIAlertActionStyleDefault
handler : ^ ( UIAlertAction * _Nonnull action ) {
DDLogInfo ( @ "%@ Remote Key Changed actions: Accepted new identity key" , self . tag ) ;
[ errorMessage acceptNewIdentityKey ] ;
if ( [ errorMessage isKindOfClass : [ TSInvalidIdentityKeySendingErrorMessage class ] ] ) {
[ self . messageSender
resendMessageFromKeyError : ( TSInvalidIdentityKeySendingErrorMessage * )
errorMessage
success : ^ {
DDLogDebug ( @ "%@ Successfully resent key-error message." , self . tag ) ;
}
failure : ^ ( NSError * _Nonnull error ) {
DDLogError ( @ "%@ Failed to resend key-error message with error:%@" ,
self . tag ,
error ) ;
} ] ;
}
} ] ;
[ actionSheetController addAction : acceptSafetyNumberAction ] ;
[ self presentViewController : actionSheetController animated : YES completion : nil ] ;
2014-10-29 21:58:58 +01:00
}
# pragma mark - Navigation
- ( void ) prepareForSegue : ( UIStoryboardSegue * ) segue sender : ( id ) sender {
2016-09-21 14:37:51 +02:00
if ( [ segue . identifier isEqualToString : OWSMessagesViewControllerSegueShowFingerprint ] ) {
if ( ! [ segue . destinationViewController isKindOfClass : [ FingerprintViewController class ] ] ) {
DDLogError ( @ "%@ Expected Fingerprint VC but got: %@" , self . tag , segue . destinationViewController ) ;
return ;
2016-09-11 22:53:12 +02:00
}
2016-09-21 14:37:51 +02:00
FingerprintViewController * vc = ( FingerprintViewController * ) segue . destinationViewController ;
if ( ! [ sender isKindOfClass : [ OWSFingerprint class ] ] ) {
DDLogError ( @ "%@ Attempting to segue to fingerprint VC without a valid fingerprint: %@" , self . tag , sender ) ;
return ;
}
OWSFingerprint * fingerprint = ( OWSFingerprint * ) sender ;
NSString * contactName = [ self . contactsManager nameStringForPhoneIdentifier : fingerprint . theirStableId ] ;
[ vc configureWithThread : self . thread fingerprint : fingerprint contactName : contactName ] ;
} else if ( [ segue . destinationViewController isKindOfClass : [ OWSConversationSettingsTableViewController class ] ] ) {
OWSConversationSettingsTableViewController * controller
= ( OWSConversationSettingsTableViewController * ) segue . destinationViewController ;
[ controller configureWithThread : self . thread ] ;
2014-12-24 02:25:10 +01:00
}
2016-09-21 14:37:51 +02:00
2014-10-29 21:58:58 +01:00
}
# pragma mark - UIImagePickerController
/ *
* Presenting UIImagePickerController
* /
2015-03-19 01:59:44 +01:00
- ( void ) takePictureOrVideo {
2016-11-04 23:41:37 +01:00
[ self ows_askForCameraPermissions : ^ {
UIImagePickerController * picker = [ [ UIImagePickerController alloc ] init ] ;
picker . sourceType = UIImagePickerControllerSourceTypeCamera ;
picker . mediaTypes = @ [ ( __bridge NSString * ) kUTTypeImage , ( __bridge NSString * ) kUTTypeMovie ] ;
picker . allowsEditing = NO ;
picker . delegate = self ;
[ self presentViewController : picker animated : YES completion : [ UIUtil modalCompletionBlock ] ] ;
2016-11-08 22:28:00 +01:00
}
alertActionHandler : nil ] ;
2016-07-07 18:54:30 +02:00
}
- ( void ) chooseFromLibrary {
if ( ! [ UIImagePickerController isSourceTypeAvailable : UIImagePickerControllerSourceTypePhotoLibrary ] ) {
DDLogError ( @ "PhotoLibrary ImagePicker source not available" ) ;
return ;
2014-10-29 21:58:58 +01:00
}
2016-07-07 18:54:30 +02:00
UIImagePickerController * picker = [ [ UIImagePickerController alloc ] init ] ;
picker . sourceType = UIImagePickerControllerSourceTypePhotoLibrary ;
picker . delegate = self ;
picker . mediaTypes = @ [ ( __bridge NSString * ) kUTTypeImage , ( __bridge NSString * ) kUTTypeMovie ] ;
[ self presentViewController : picker animated : YES completion : [ UIUtil modalCompletionBlock ] ] ;
2014-10-29 21:58:58 +01:00
}
/ *
* Dismissing UIImagePickerController
* /
- ( void ) imagePickerControllerDidCancel : ( UIImagePickerController * ) picker {
2015-01-31 09:38:52 +01:00
[ UIUtil modalCompletionBlock ] ( ) ;
2014-10-29 21:58:58 +01:00
[ self dismissViewControllerAnimated : YES completion : nil ] ;
}
2015-03-19 01:59:44 +01:00
- ( void ) resetFrame {
2015-01-31 09:38:52 +01:00
// fixes bug on frame being off after this selection
2015-12-22 12:45:09 +01:00
CGRect frame = [ UIScreen mainScreen ] . applicationFrame ;
2015-01-31 09:38:52 +01:00
self . view . frame = frame ;
}
2014-10-29 21:58:58 +01:00
/ *
2014-11-29 19:54:33 +01:00
* Fetching data from UIImagePickerController
2014-10-29 21:58:58 +01:00
* /
2016-07-22 02:15:34 +02:00
- ( void ) imagePickerController : ( UIImagePickerController * ) picker
didFinishPickingMediaWithInfo : ( NSDictionary < NSString * , id > * ) info
{
2015-01-31 09:38:52 +01:00
[ UIUtil modalCompletionBlock ] ( ) ;
[ self resetFrame ] ;
2015-12-22 12:45:09 +01:00
2016-07-22 02:15:34 +02:00
void ( ^ failedToPickAttachment ) ( NSError * error ) = ^ void ( NSError * error ) {
DDLogError ( @ "failed to pick attachment with error: %@" , error ) ;
} ;
NSString * mediaType = info [ UIImagePickerControllerMediaType ] ;
if ( [ mediaType isEqualToString : ( __bridge NSString * ) kUTTypeMovie ] ) {
// Video picked from library or captured with camera
NSURL * videoURL = info [ UIImagePickerControllerMediaURL ] ;
2016-10-14 22:59:58 +02:00
[ self sendQualityAdjustedAttachmentForVideo : videoURL ] ;
2016-07-22 02:15:34 +02:00
} else if ( picker . sourceType = = UIImagePickerControllerSourceTypeCamera ) {
// Static Image captured from camera
UIImage * imageFromCamera = [ info [ UIImagePickerControllerOriginalImage ] normalizedImage ] ;
if ( imageFromCamera ) {
[ self sendMessageAttachment : [ self qualityAdjustedAttachmentForImage : imageFromCamera ] ofType : @ "image/jpeg" ] ;
2016-04-13 20:38:42 +02:00
} else {
2016-07-22 02:15:34 +02:00
failedToPickAttachment ( nil ) ;
}
} else {
// Non - Video image picked from library
NSURL * assetURL = info [ UIImagePickerControllerReferenceURL ] ;
PHAsset * asset = [ [ PHAsset fetchAssetsWithALAssetURLs : @ [ assetURL ] options : nil ] lastObject ] ;
if ( ! asset ) {
return failedToPickAttachment ( nil ) ;
2016-04-13 20:38:42 +02:00
}
2016-07-22 02:15:34 +02:00
PHImageRequestOptions * options = [ [ PHImageRequestOptions alloc ] init ] ;
options . synchronous = YES ; // We ' re only fetching one asset .
options . networkAccessAllowed = YES ; // iCloud OK
options . deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat ; // Don ' t need quick / dirty version
[ [ PHImageManager defaultManager ]
requestImageDataForAsset : asset
options : options
resultHandler : ^ ( NSData * _Nullable imageData ,
NSString * _Nullable dataUTI ,
UIImageOrientation orientation ,
NSDictionary * _Nullable assetInfo ) {
NSError * assetFetchingError = assetInfo [ PHImageErrorKey ] ;
if ( assetFetchingError || ! imageData ) {
return failedToPickAttachment ( assetFetchingError ) ;
}
2016-09-21 14:37:51 +02:00
DDLogVerbose (
@ "Size in bytes: %lu; detected filetype: %@" , ( unsigned long ) imageData . length , dataUTI ) ;
2016-07-22 02:15:34 +02:00
if ( [ dataUTI isEqualToString : ( __bridge NSString * ) kUTTypeGIF ]
&& imageData . length <= 5 * 1024 * 1024 ) {
DDLogVerbose ( @ "Sending raw image/gif to retain any animation" ) ;
/ * *
* Media Size constraints lifted from Signal - Android
* ( org / thoughtcrime / securesms / mms / PushMediaConstraints . java )
*
* GifMaxSize return 5 * MB ;
* For reference , other media size limits we ' re not explicitly enforcing :
* ImageMaxSize return 420 * KB ;
* VideoMaxSize return 100 * MB ;
* getAudioMaxSize 100 * MB ;
* /
[ self sendMessageAttachment : imageData ofType : @ "image/gif" ] ;
} else {
DDLogVerbose ( @ "Compressing attachment as image/jpeg" ) ;
UIImage * pickedImage = [ [ UIImage alloc ] initWithData : imageData ] ;
[ self sendMessageAttachment : [ self qualityAdjustedAttachmentForImage : pickedImage ]
ofType : @ "image/jpeg" ] ;
}
} ] ;
2014-10-29 21:58:58 +01:00
}
2015-01-14 22:30:01 +01:00
}
2016-08-01 00:25:07 +02:00
- ( void ) sendMessageAttachment : ( NSData * ) attachmentData ofType : ( NSString * ) attachmentType
{
2016-09-21 14:37:51 +02:00
TSOutgoingMessage * message ;
OWSDisappearingMessagesConfiguration * configuration =
[ OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID : self . thread . uniqueId ] ;
if ( configuration . isEnabled ) {
message = [ [ TSOutgoingMessage alloc ] initWithTimestamp : [ NSDate ows_millisecondTimeStamp ]
inThread : self . thread
messageBody : nil
attachmentIds : [ NSMutableArray new ]
expiresInSeconds : configuration . durationSeconds ] ;
} else {
message = [ [ TSOutgoingMessage alloc ] initWithTimestamp : [ NSDate ows_millisecondTimeStamp ]
inThread : self . thread
messageBody : nil
attachmentIds : [ NSMutableArray new ] ] ;
}
2015-12-22 12:45:09 +01:00
[ self dismissViewControllerAnimated : YES
completion : ^ {
2016-06-23 22:43:31 +02:00
DDLogVerbose ( @ "Sending attachment. Size in bytes: %lu, contentType: %@" ,
2016-07-19 18:02:55 +02:00
( unsigned long ) attachmentData . length ,
2016-06-23 22:43:31 +02:00
attachmentType ) ;
2016-10-14 22:59:58 +02:00
[ self . messageSender sendAttachmentData : attachmentData
contentType : attachmentType
inMessage : message
success : ^ {
DDLogDebug ( @ "%@ Successfully sent message attachment." , self . tag ) ;
}
failure : ^ ( NSError * error ) {
DDLogError (
@ "%@ Failed to send message attachment with error: %@" , self . tag , error ) ;
} ] ;
2015-12-22 12:45:09 +01:00
} ] ;
2015-01-14 22:30:01 +01:00
}
2015-12-22 12:45:09 +01:00
- ( NSURL * ) videoTempFolder {
NSArray * paths = NSSearchPathForDirectoriesInDomains ( NSCachesDirectory , NSUserDomainMask , YES ) ;
2015-01-14 22:30:01 +01:00
NSString * basePath = ( [ paths count ] > 0 ) ? [ paths objectAtIndex : 0 ] : nil ;
2015-12-22 12:45:09 +01:00
basePath = [ basePath stringByAppendingPathComponent : @ "videos" ] ;
2015-01-14 22:30:01 +01:00
if ( ! [ [ NSFileManager defaultManager ] fileExistsAtPath : basePath ] ) {
2015-12-22 12:45:09 +01:00
[ [ NSFileManager defaultManager ] createDirectoryAtPath : basePath
withIntermediateDirectories : YES
attributes : nil
error : nil ] ;
2015-01-14 22:30:01 +01:00
}
2015-04-25 16:59:32 +02:00
return [ NSURL fileURLWithPath : basePath ] ;
}
2016-10-14 22:59:58 +02:00
- ( void ) sendQualityAdjustedAttachmentForVideo : ( NSURL * ) movieURL {
2015-04-25 16:59:32 +02:00
AVAsset * video = [ AVAsset assetWithURL : movieURL ] ;
2015-12-22 12:45:09 +01:00
AVAssetExportSession * exportSession =
[ AVAssetExportSession exportSessionWithAsset : video presetName : AVAssetExportPresetMediumQuality ] ;
2015-04-25 16:59:32 +02:00
exportSession . shouldOptimizeForNetworkUse = YES ;
2015-12-22 12:45:09 +01:00
exportSession . outputFileType = AVFileTypeMPEG4 ;
double currentTime = [ [ NSDate date ] timeIntervalSince1970 ] ;
NSString * strImageName = [ NSString stringWithFormat : @ "%f" , currentTime ] ;
NSURL * compressedVideoUrl =
[ [ self videoTempFolder ] URLByAppendingPathComponent : [ NSString stringWithFormat : @ "%@.mp4" , strImageName ] ] ;
2015-01-14 22:30:01 +01:00
exportSession . outputURL = compressedVideoUrl ;
[ exportSession exportAsynchronouslyWithCompletionHandler : ^ {
2015-12-22 12:45:09 +01:00
NSError * error ;
[ self sendMessageAttachment : [ NSData dataWithContentsOfURL : compressedVideoUrl ] ofType : @ "video/mp4" ] ;
[ [ NSFileManager defaultManager ] removeItemAtURL : compressedVideoUrl error : & error ] ;
if ( error ) {
DDLogWarn ( @ "Failed to remove cached video file: %@" , error . debugDescription ) ;
}
2015-01-14 22:30:01 +01:00
} ] ;
2014-11-25 16:38:33 +01:00
}
2015-12-22 12:45:09 +01:00
- ( NSData * ) qualityAdjustedAttachmentForImage : ( UIImage * ) image {
2014-12-26 21:17:43 +01:00
return UIImageJPEGRepresentation ( [ self adjustedImageSizedForSending : image ] , [ self compressionRate ] ) ;
}
2015-12-22 12:45:09 +01:00
- ( UIImage * ) adjustedImageSizedForSending : ( UIImage * ) image {
2014-12-26 21:17:43 +01:00
CGFloat correctedWidth ;
switch ( [ Environment . preferences imageUploadQuality ] ) {
2014-12-26 23:18:54 +01:00
case TSImageQualityUncropped :
return image ;
2015-12-22 12:45:09 +01:00
2014-12-26 21:17:43 +01:00
case TSImageQualityHigh :
correctedWidth = 2048 ;
break ;
case TSImageQualityMedium :
correctedWidth = 1024 ;
break ;
case TSImageQualityLow :
correctedWidth = 512 ;
break ;
default :
break ;
}
2015-12-22 12:45:09 +01:00
2014-12-26 21:17:43 +01:00
return [ self imageScaled : image toMaxSize : correctedWidth ] ;
}
2015-12-22 12:45:09 +01:00
- ( UIImage * ) imageScaled : ( UIImage * ) image toMaxSize : ( CGFloat ) size {
2014-12-26 21:17:43 +01:00
CGFloat scaleFactor ;
CGFloat aspectRatio = image . size . height / image . size . width ;
2015-12-22 12:45:09 +01:00
if ( aspectRatio > 1 ) {
2014-12-26 21:17:43 +01:00
scaleFactor = size / image . size . width ;
2015-12-22 12:45:09 +01:00
} else {
2014-12-26 21:17:43 +01:00
scaleFactor = size / image . size . height ;
}
2015-12-22 12:45:09 +01:00
2014-12-26 21:17:43 +01:00
CGSize newSize = CGSizeMake ( image . size . width * scaleFactor , image . size . height * scaleFactor ) ;
2015-12-22 12:45:09 +01:00
2014-12-26 21:17:43 +01:00
UIGraphicsBeginImageContext ( newSize ) ;
[ image drawInRect : CGRectMake ( 0 , 0 , newSize . width , newSize . height ) ] ;
2015-12-22 12:45:09 +01:00
UIImage * updatedImage = UIGraphicsGetImageFromCurrentImageContext ( ) ;
2014-12-26 21:17:43 +01:00
UIGraphicsEndImageContext ( ) ;
2015-12-22 12:45:09 +01:00
2014-12-26 21:17:43 +01:00
return updatedImage ;
}
2015-03-19 01:59:44 +01:00
- ( CGFloat ) compressionRate {
2014-12-26 21:17:43 +01:00
switch ( [ Environment . preferences imageUploadQuality ] ) {
2014-12-26 23:18:54 +01:00
case TSImageQualityUncropped :
return 1 ;
2014-12-26 21:17:43 +01:00
case TSImageQualityHigh :
return 0.9 f ;
case TSImageQualityMedium :
return 0.5 f ;
case TSImageQualityLow :
return 0.3 f ;
default :
break ;
}
}
2014-11-25 16:38:33 +01:00
# pragma mark Storage access
2015-12-22 12:45:09 +01:00
- ( YapDatabaseConnection * ) uiDatabaseConnection {
2014-11-25 16:38:33 +01:00
NSAssert ( [ NSThread isMainThread ] , @ "Must access uiDatabaseConnection on main thread!" ) ;
if ( ! _uiDatabaseConnection ) {
2016-09-11 22:53:12 +02:00
_uiDatabaseConnection = [ self . storageManager newDatabaseConnection ] ;
2014-11-25 16:38:33 +01:00
[ _uiDatabaseConnection beginLongLivedReadTransaction ] ;
}
return _uiDatabaseConnection ;
}
2015-12-22 12:45:09 +01:00
- ( YapDatabaseConnection * ) editingDatabaseConnection {
2014-12-06 17:45:42 +01:00
if ( ! _editingDatabaseConnection ) {
2016-09-11 22:53:12 +02:00
_editingDatabaseConnection = [ self . storageManager newDatabaseConnection ] ;
2014-12-06 17:45:42 +01:00
}
return _editingDatabaseConnection ;
}
2014-12-24 02:25:10 +01:00
2015-03-19 01:59:44 +01:00
- ( void ) yapDatabaseModified : ( NSNotification * ) notification {
2015-10-31 16:53:32 +01:00
[ self updateBackButtonAsync ] ;
2015-12-22 12:45:09 +01:00
if ( isGroupConversation ) {
2014-12-24 02:25:10 +01:00
[ self . uiDatabaseConnection readWithBlock : ^ ( YapDatabaseReadTransaction * transaction ) {
2015-12-22 12:45:09 +01:00
TSGroupThread * gThread = ( TSGroupThread * ) self . thread ;
2015-12-26 17:27:27 +01:00
if ( gThread . groupModel ) {
self . thread = [ TSGroupThread threadWithGroupModel : gThread . groupModel transaction : transaction ] ;
}
2014-12-24 02:25:10 +01:00
} ] ;
}
2015-12-22 12:45:09 +01:00
2014-11-25 16:38:33 +01:00
NSArray * notifications = [ self . uiDatabaseConnection beginLongLivedReadTransaction ] ;
2015-12-22 12:45:09 +01:00
if ( ! [ [ self . uiDatabaseConnection ext : TSMessageDatabaseViewExtensionName ]
hasChangesForNotifications : notifications ] ) {
[ self . uiDatabaseConnection readWithBlock : ^ ( YapDatabaseReadTransaction * transaction ) {
[ self . messageMappings updateWithTransaction : transaction ] ;
2015-01-31 12:00:58 +01:00
} ] ;
return ;
}
2015-12-22 12:45:09 +01:00
2016-11-01 15:01:45 +01:00
// HACK to work around radar #28167779
// "UICollectionView performBatchUpdates can trigger a crash if the collection view is flagged for layout"
// more : https : // github . com / PSPDFKit - labs / radar . apple . com / tree / master / 28167779 % 20 - % 20 CollectionViewBatchingIssue
// This was our #2 crash , and much exacerbated by the refactoring somewhere between 2.6 .2 .0 -2.6 .3 .8
[ self . collectionView layoutIfNeeded ] ;
// ENDHACK to work around radar #28167779
2014-11-25 16:38:33 +01:00
NSArray * messageRowChanges = nil ;
2015-01-31 12:00:58 +01:00
NSArray * sectionChanges = nil ;
[ [ self . uiDatabaseConnection ext : TSMessageDatabaseViewExtensionName ] getSectionChanges : & sectionChanges
2014-11-25 16:38:33 +01:00
rowChanges : & messageRowChanges
forNotifications : notifications
withMappings : self . messageMappings ] ;
2015-12-22 12:45:09 +01:00
2015-01-05 16:31:00 +01:00
__block BOOL scrollToBottom = NO ;
2015-12-22 12:45:09 +01:00
2015-03-19 01:59:44 +01:00
if ( [ sectionChanges count ] = = 0 & [ messageRowChanges count ] = = 0 ) {
2014-12-08 23:12:22 +01:00
return ;
}
2015-12-22 12:45:09 +01:00
2014-12-12 22:41:28 +01:00
[ self . collectionView performBatchUpdates : ^ {
2015-12-22 12:45:09 +01:00
for ( YapDatabaseViewRowChange * rowChange in messageRowChanges ) {
switch ( rowChange . type ) {
case YapDatabaseViewChangeDelete : {
[ self . collectionView deleteItemsAtIndexPaths : @ [ rowChange . indexPath ] ] ;
2016-04-18 19:07:35 +02:00
YapCollectionKey * collectionKey = rowChange . collectionKey ;
if ( collectionKey . key ) {
[ self . messageAdapterCache removeObjectForKey : collectionKey . key ] ;
}
2015-12-22 12:45:09 +01:00
break ;
}
case YapDatabaseViewChangeInsert : {
[ self . collectionView insertItemsAtIndexPaths : @ [ rowChange . newIndexPath ] ] ;
scrollToBottom = YES ;
break ;
}
case YapDatabaseViewChangeMove : {
[ self . collectionView deleteItemsAtIndexPaths : @ [ rowChange . indexPath ] ] ;
[ self . collectionView insertItemsAtIndexPaths : @ [ rowChange . newIndexPath ] ] ;
break ;
}
case YapDatabaseViewChangeUpdate : {
2016-04-18 19:07:35 +02:00
YapCollectionKey * collectionKey = rowChange . collectionKey ;
if ( collectionKey . key ) {
[ self . messageAdapterCache removeObjectForKey : collectionKey . key ] ;
}
2016-10-05 21:34:24 +02:00
[ self . collectionView reloadItemsAtIndexPaths : @ [ rowChange . indexPath ] ] ;
2015-12-22 12:45:09 +01:00
break ;
}
}
}
}
completion : ^ ( BOOL success ) {
if ( ! success ) {
[ self . collectionView . collectionViewLayout
invalidateLayoutWithContext : [ JSQMessagesCollectionViewFlowLayoutInvalidationContext context ] ] ;
[ self . collectionView reloadData ] ;
}
if ( scrollToBottom ) {
2016-10-28 15:36:39 +02:00
[ self scrollToBottomAnimated : YES ] ;
2015-12-22 12:45:09 +01:00
}
} ] ;
2014-11-25 16:38:33 +01:00
}
# pragma mark - UICollectionView DataSource
2015-03-19 01:59:44 +01:00
- ( NSInteger ) collectionView : ( UICollectionView * ) collectionView numberOfItemsInSection : ( NSInteger ) section {
2014-12-31 21:30:20 +01:00
NSInteger numberOfMessages = ( NSInteger ) [ self . messageMappings numberOfItemsInSection : ( NSUInteger ) section ] ;
2014-11-25 16:38:33 +01:00
return numberOfMessages ;
2014-10-29 21:58:58 +01:00
}
2014-11-25 16:38:33 +01:00
2015-12-22 12:45:09 +01:00
- ( TSInteraction * ) interactionAtIndexPath : ( NSIndexPath * ) indexPath {
2014-11-25 16:38:33 +01:00
__block TSInteraction * message = nil ;
[ self . uiDatabaseConnection readWithBlock : ^ ( YapDatabaseReadTransaction * transaction ) {
2015-12-22 12:45:09 +01:00
YapDatabaseViewTransaction * viewTransaction = [ transaction ext : TSMessageDatabaseViewExtensionName ] ;
NSParameterAssert ( viewTransaction ! = nil ) ;
NSParameterAssert ( self . messageMappings ! = nil ) ;
NSParameterAssert ( indexPath ! = nil ) ;
NSUInteger row = ( NSUInteger ) indexPath . row ;
NSUInteger section = ( NSUInteger ) indexPath . section ;
2016-09-21 14:37:51 +02:00
NSUInteger numberOfItemsInSection __unused = [ self . messageMappings numberOfItemsInSection : section ] ;
2015-12-22 12:45:09 +01:00
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 ) ;
2014-11-25 16:38:33 +01:00
} ] ;
2015-12-22 12:45:09 +01:00
2014-12-11 00:05:41 +01:00
return message ;
}
2016-09-21 14:37:51 +02:00
- ( id < OWSMessageData > ) messageAtIndexPath : ( NSIndexPath * ) indexPath
{
2015-12-26 17:27:27 +01:00
TSInteraction * interaction = [ self interactionAtIndexPath : indexPath ] ;
2016-04-13 19:05:09 +02:00
2016-09-21 14:37:51 +02:00
id < OWSMessageData > messageAdapter = [ self . messageAdapterCache objectForKey : interaction . uniqueId ] ;
2016-04-13 19:05:09 +02:00
2016-09-21 14:37:51 +02:00
if ( ! messageAdapter ) {
2016-11-14 19:28:51 +01:00
messageAdapter = [ TSMessageAdapter messageViewDataWithInteraction : interaction inThread : self . thread contactsManager : self . contactsManager ] ;
2016-04-13 19:05:09 +02:00
[ self . messageAdapterCache setObject : messageAdapter forKey : interaction . uniqueId ] ;
}
return messageAdapter ;
2014-11-25 16:38:33 +01:00
}
2014-12-31 13:22:40 +01:00
2015-01-22 05:08:12 +01:00
# pragma mark - Audio
2015-12-22 12:45:09 +01:00
- ( void ) recordAudio {
2015-01-22 05:08:12 +01:00
// Define the recorder setting
2015-12-22 12:45:09 +01:00
NSArray * pathComponents = [ NSArray
arrayWithObjects : [ NSSearchPathForDirectoriesInDomains ( NSDocumentDirectory , NSUserDomainMask , YES ) lastObject ] ,
[ NSString stringWithFormat : @ "%lld.m4a" , [ NSDate ows_millisecondTimeStamp ] ] ,
nil ] ;
2015-01-22 05:08:12 +01:00
NSURL * outputFileURL = [ NSURL fileURLWithPathComponents : pathComponents ] ;
2015-12-22 12:45:09 +01:00
2015-01-22 05:08:12 +01:00
// Setup audio session
AVAudioSession * session = [ AVAudioSession sharedInstance ] ;
[ session setCategory : AVAudioSessionCategoryPlayAndRecord error : nil ] ;
2015-12-22 12:45:09 +01:00
2015-01-22 05:08:12 +01:00
NSMutableDictionary * recordSetting = [ [ NSMutableDictionary alloc ] init ] ;
[ recordSetting setValue : [ NSNumber numberWithInt : kAudioFormatMPEG4AAC ] forKey : AVFormatIDKey ] ;
[ recordSetting setValue : [ NSNumber numberWithFloat : 44100.0 ] forKey : AVSampleRateKey ] ;
2015-12-22 12:45:09 +01:00
[ recordSetting setValue : [ NSNumber numberWithInt : 2 ] forKey : AVNumberOfChannelsKey ] ;
2015-01-22 05:08:12 +01:00
// Initiate and prepare the recorder
2015-12-22 12:45:09 +01:00
_audioRecorder = [ [ AVAudioRecorder alloc ] initWithURL : outputFileURL settings : recordSetting error : NULL ] ;
2015-01-22 05:08:12 +01:00
_audioRecorder . delegate = self ;
_audioRecorder . meteringEnabled = YES ;
[ _audioRecorder prepareToRecord ] ;
}
2015-12-22 12:45:09 +01:00
- ( void ) audioPlayerUpdated : ( NSTimer * ) timer {
double current = [ _audioPlayer currentTime ] / [ _audioPlayer duration ] ;
2015-01-25 00:48:40 +01:00
double interval = [ _audioPlayer duration ] - [ _audioPlayer currentTime ] ;
[ _currentMediaAdapter setDurationOfAudio : interval ] ;
[ _currentMediaAdapter setAudioProgressFromFloat : ( float ) current ] ;
2015-01-22 05:08:12 +01:00
}
2015-12-22 12:45:09 +01:00
- ( void ) audioPlayerDidFinishPlaying : ( AVAudioPlayer * ) player successfully : ( BOOL ) flag {
2015-01-22 05:08:12 +01:00
[ _audioPlayerPoller invalidate ] ;
2015-01-25 00:48:40 +01:00
[ _currentMediaAdapter setAudioProgressFromFloat : 0 ] ;
[ _currentMediaAdapter setDurationOfAudio : _audioPlayer . duration ] ;
[ _currentMediaAdapter setAudioIconToPlay ] ;
2015-01-22 05:08:12 +01:00
}
2015-12-22 12:45:09 +01:00
- ( void ) audioRecorderDidFinishRecording : ( AVAudioRecorder * ) recorder successfully : ( BOOL ) flag {
if ( flag ) {
2015-01-22 05:08:12 +01:00
[ self sendMessageAttachment : [ NSData dataWithContentsOfURL : recorder . url ] ofType : @ "audio/m4a" ] ;
}
}
2014-11-25 16:38:33 +01:00
# pragma mark Accessory View
2015-03-19 01:59:44 +01:00
- ( void ) didPressAccessoryButton : ( UIButton * ) sender {
2016-11-26 00:12:00 +01:00
UIAlertController * actionSheetController = [ UIAlertController alertControllerWithTitle : nil
message : nil
preferredStyle : UIAlertControllerStyleActionSheet ] ;
UIAlertAction * cancelAction = [ UIAlertAction actionWithTitle : NSLocalizedString ( @ "TXT_CANCEL_TITLE" , @ "" )
style : UIAlertActionStyleCancel
handler : nil ] ;
[ actionSheetController addAction : cancelAction ] ;
2016-11-29 22:54:01 +01:00
UIAlertAction * takeMediaAction = [ UIAlertAction actionWithTitle : NSLocalizedString ( @ "MEDIA_FROM_CAMERA_BUTTON" , @ "media picker option to take photo or video" )
2016-11-26 00:12:00 +01:00
style : UIAlertActionStyleDefault
handler : ^ ( UIAlertAction * _Nonnull action ) {
[ self takePictureOrVideo ] ;
} ] ;
[ actionSheetController addAction : takeMediaAction ] ;
2016-11-29 22:54:01 +01:00
UIAlertAction * chooseMediaAction = [ UIAlertAction actionWithTitle : NSLocalizedString ( @ "MEDIA_FROM_LIBRARY_BUTTON" , @ "media picker option to choose from library" )
2016-11-26 00:12:00 +01:00
style : UIAlertActionStyleDefault
handler : ^ ( UIAlertAction * _Nonnull action ) {
[ self chooseFromLibrary ] ;
} ] ;
[ actionSheetController addAction : chooseMediaAction ] ;
[ self presentViewController : actionSheetController animated : true completion : nil ] ;
2014-11-25 16:38:33 +01:00
}
2016-09-21 14:37:51 +02:00
- ( void ) markAllMessagesAsRead
{
[ self . thread markAllAsRead ] ;
// In theory this should be unnecessary as read - status starts expiration
// but in practice I ' ve seen messages not have their timer started .
[ self . disappearingMessagesJob setExpirationsForThread : self . thread ] ;
2014-12-06 17:45:42 +01:00
}
2015-12-22 12:45:09 +01:00
- ( BOOL ) collectionView : ( UICollectionView * ) collectionView
canPerformAction : ( SEL ) action
forItemAtIndexPath : ( NSIndexPath * ) indexPath
2016-10-14 22:59:58 +02:00
withSender : ( id ) sender
{
id < OWSMessageData > messageData = [ self messageAtIndexPath : indexPath ] ;
return [ messageData canPerformEditingAction : action ] ;
2014-12-26 23:18:54 +01:00
}
2015-12-22 12:45:09 +01:00
- ( void ) collectionView : ( UICollectionView * ) collectionView
performAction : ( SEL ) action
forItemAtIndexPath : ( NSIndexPath * ) indexPath
2016-10-14 22:59:58 +02:00
withSender : ( id ) sender
{
id < OWSMessageData > messageData = [ self messageAtIndexPath : indexPath ] ;
[ messageData performEditingAction : action ] ;
2014-12-26 23:18:54 +01:00
}
2016-08-01 00:25:07 +02:00
- ( void ) updateGroupModelTo : ( TSGroupModel * ) newGroupModel
{
2015-12-22 12:45:09 +01:00
__block TSGroupThread * groupThread ;
2015-02-17 00:14:50 +01:00
__block TSOutgoingMessage * message ;
2015-12-22 12:45:09 +01:00
2014-12-24 02:25:10 +01:00
[ self . editingDatabaseConnection readWriteWithBlock : ^ ( YapDatabaseReadWriteTransaction * transaction ) {
2016-10-17 03:07:25 +02:00
groupThread = [ TSGroupThread getOrCreateThreadWithGroupModel : newGroupModel transaction : transaction ] ;
NSString * updateGroupInfo = [ groupThread . groupModel getInfoStringAboutUpdateTo : newGroupModel contactsManager : self . contactsManager ] ;
groupThread . groupModel = newGroupModel ;
[ groupThread saveWithTransaction : transaction ] ;
message = [ [ TSOutgoingMessage alloc ] initWithTimestamp : [ NSDate ows_millisecondTimeStamp ]
inThread : groupThread
messageBody : @ ""
attachmentIds : [ NSMutableArray new ] ] ;
message . groupMetaMessage = TSGroupMessageUpdate ;
message . customMessage = updateGroupInfo ;
2014-12-24 02:25:10 +01:00
} ] ;
2015-12-22 12:45:09 +01:00
2016-10-14 22:59:58 +02:00
if ( newGroupModel . groupImage ) {
[ self . messageSender sendAttachmentData : UIImagePNGRepresentation ( newGroupModel . groupImage )
contentType : OWSMimeTypeImagePng
inMessage : message
success : ^ {
DDLogDebug ( @ "%@ Successfully sent group update with avatar" , self . tag ) ;
}
failure : ^ ( NSError * _Nonnull error ) {
DDLogError ( @ "%@ Failed to send group avatar update with error: %@" , self . tag , error ) ;
} ] ;
2015-12-22 12:45:09 +01:00
} else {
2016-10-14 22:59:58 +02:00
[ self . messageSender sendMessage : message
success : ^ {
DDLogDebug ( @ "%@ Successfully sent group update" , self . tag ) ;
}
failure : ^ ( NSError * _Nonnull error ) {
DDLogError ( @ "%@ Failed to send group update with error: %@" , self . tag , error ) ;
} ] ;
2015-02-17 00:14:50 +01:00
}
2015-12-22 12:45:09 +01:00
2015-02-17 00:14:50 +01:00
self . thread = groupThread ;
2014-12-24 02:25:10 +01:00
}
2015-03-19 01:59:44 +01:00
- ( IBAction ) unwindGroupUpdated : ( UIStoryboardSegue * ) segue {
2015-12-22 12:45:09 +01:00
NewGroupViewController * ngc = [ segue sourceViewController ] ;
TSGroupModel * newGroupModel = [ ngc groupModel ] ;
NSMutableSet * groupMemberIds = [ NSMutableSet setWithArray : newGroupModel . groupMemberIds ] ;
[ groupMemberIds addObject : [ TSAccountManager localNumber ] ] ;
2015-02-28 17:49:46 +01:00
newGroupModel . groupMemberIds = [ NSMutableArray arrayWithArray : [ groupMemberIds allObjects ] ] ;
2014-12-24 02:25:10 +01:00
[ self updateGroupModelTo : newGroupModel ] ;
2015-12-22 12:45:09 +01:00
[ self . collectionView . collectionViewLayout
invalidateLayoutWithContext : [ JSQMessagesCollectionViewFlowLayoutInvalidationContext context ] ] ;
2015-02-28 17:49:46 +01:00
[ self . collectionView reloadData ] ;
2014-12-24 02:25:10 +01:00
}
2015-04-14 21:49:00 +02:00
- ( void ) popKeyBoard {
[ self . inputToolbar . contentView . textView becomeFirstResponder ] ;
}
2015-01-30 23:28:05 +01:00
- ( void ) dismissKeyBoard {
[ self . inputToolbar . contentView . textView resignFirstResponder ] ;
}
2015-03-01 00:04:39 +01:00
# pragma mark Drafts
2015-03-19 01:59:44 +01:00
- ( void ) loadDraftInCompose {
2015-03-01 00:04:39 +01:00
__block NSString * placeholder ;
[ self . editingDatabaseConnection asyncReadWithBlock : ^ ( YapDatabaseReadTransaction * transaction ) {
2015-12-22 12:45:09 +01:00
placeholder = [ _thread currentDraftWithTransaction : transaction ] ;
}
completionBlock : ^ {
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
2015-03-01 00:04:39 +01:00
[ self . inputToolbar . contentView . textView setText : placeholder ] ;
[ self textViewDidChange : self . inputToolbar . contentView . textView ] ;
2015-12-22 12:45:09 +01:00
} ) ;
} ] ;
2015-03-01 00:04:39 +01:00
}
2015-03-19 01:59:44 +01:00
- ( void ) saveDraft {
2015-03-01 00:04:39 +01:00
if ( self . inputToolbar . hidden = = NO ) {
2015-03-21 19:15:43 +01:00
__block TSThread * thread = _thread ;
__block NSString * currentDraft = self . inputToolbar . contentView . textView . text ;
2015-12-22 12:45:09 +01:00
2015-03-21 19:15:43 +01:00
[ self . editingDatabaseConnection asyncReadWriteWithBlock : ^ ( YapDatabaseReadWriteTransaction * transaction ) {
2015-12-22 12:45:09 +01:00
[ thread setDraft : currentDraft transaction : transaction ] ;
2015-03-01 00:04:39 +01:00
} ] ;
}
}
2015-05-23 15:54:50 +02:00
# pragma mark Unread Badge
- ( void ) setUnreadCount : ( NSUInteger ) unreadCount {
if ( _unreadCount ! = unreadCount ) {
_unreadCount = unreadCount ;
2015-12-22 12:45:09 +01:00
2015-05-23 15:54:50 +02:00
if ( _unreadCount > 0 ) {
if ( _unreadContainer = = nil ) {
static UIImage * backgroundImage = nil ;
static dispatch_once _t onceToken ;
2015-12-22 12:45:09 +01:00
dispatch_once ( & onceToken , ^ {
UIGraphicsBeginImageContextWithOptions ( CGSizeMake ( 17.0 f , 17.0 f ) , false , 0.0 f ) ;
CGContextRef context = UIGraphicsGetCurrentContext ( ) ;
CGContextSetFillColorWithColor ( context , [ UIColor redColor ] . CGColor ) ;
CGContextFillEllipseInRect ( context , CGRectMake ( 0.0 f , 0.0 f , 17.0 f , 17.0 f ) ) ;
backgroundImage =
[ UIGraphicsGetImageFromCurrentImageContext ( ) stretchableImageWithLeftCapWidth : 8 topCapHeight : 8 ] ;
UIGraphicsEndImageContext ( ) ;
} ) ;
2015-05-23 15:54:50 +02:00
_unreadContainer = [ [ UIImageView alloc ] initWithFrame : CGRectMake ( 0.0 f , 0.0 f , 10.0 f , 10.0 f ) ] ;
_unreadContainer . userInteractionEnabled = NO ;
_unreadContainer . layer . zPosition = 2000 ;
[ self . navigationController . navigationBar addSubview : _unreadContainer ] ;
2015-12-22 12:45:09 +01:00
2015-05-23 15:54:50 +02:00
_unreadBackground = [ [ UIImageView alloc ] initWithImage : backgroundImage ] ;
[ _unreadContainer addSubview : _unreadBackground ] ;
2015-12-22 12:45:09 +01:00
2015-05-23 15:54:50 +02:00
_unreadLabel = [ [ UILabel alloc ] init ] ;
_unreadLabel . backgroundColor = [ UIColor clearColor ] ;
_unreadLabel . textColor = [ UIColor whiteColor ] ;
_unreadLabel . font = [ UIFont systemFontOfSize : 12 ] ;
[ _unreadContainer addSubview : _unreadLabel ] ;
}
_unreadContainer . hidden = false ;
2015-12-22 12:45:09 +01:00
2015-05-23 15:54:50 +02:00
_unreadLabel . text = [ NSString stringWithFormat : @ "%lu" , ( unsigned long ) unreadCount ] ;
[ _unreadLabel sizeToFit ] ;
2015-12-22 12:45:09 +01:00
2015-05-23 15:54:50 +02:00
CGPoint offset = CGPointMake ( 17.0 f , 2.0 f ) ;
2015-12-22 12:45:09 +01:00
_unreadBackground . frame =
CGRectMake ( offset . x , offset . y , MAX ( _unreadLabel . frame . size . width + 8.0 f , 17.0 f ) , 17.0 f ) ;
2016-09-21 14:37:51 +02:00
_unreadLabel . frame = CGRectMake ( offset . x
+ ( CGFloat ) floor (
( 2.0 * ( _unreadBackground . frame . size . width - _unreadLabel . frame . size . width ) / 2.0 f ) / 2.0 f ) ,
2015-12-22 12:45:09 +01:00
offset . y + 1.0 f ,
_unreadLabel . frame . size . width ,
_unreadLabel . frame . size . height ) ;
2015-05-23 15:54:50 +02:00
} else if ( _unreadContainer ! = nil ) {
_unreadContainer . hidden = true ;
}
}
}
2015-12-26 17:27:27 +01:00
# pragma mark 3 D Touch Preview Actions
- ( NSArray < id < UIPreviewActionItem > > * ) previewActionItems {
return @ [ ] ;
}
2016-09-21 14:37:51 +02:00
# pragma mark - Logging
2016-09-11 22:53:12 +02:00
+ ( NSString * ) tag
{
return [ NSString stringWithFormat : @ "[%@]" , self . class ] ;
}
- ( NSString * ) tag
{
return self . class . tag ;
}
2014-10-29 21:58:58 +01:00
@ end