2014-10-29 21:58:58 +01:00
//
2017-02-01 23:49:32 +01:00
// Copyright ( c ) 2017 Open Whisper Systems . All rights reserved .
2014-10-29 21:58:58 +01:00
//
2017-04-04 18:35:52 +02:00
# import "MessagesViewController.h"
2014-10-29 21:58:58 +01:00
# import "AppDelegate.h"
2017-03-29 23:54:11 +02:00
# import "AttachmentSharing.h"
2017-04-04 18:35:52 +02:00
# import "BlockListUIUtils.h"
2017-04-04 21:38:00 +02:00
# import "BlockListViewController.h"
2017-04-10 03:39:04 +02:00
# import "DebugUITableViewController.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 "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"
2017-04-10 03:39:04 +02:00
# import "OWSUnknownContactBlockOfferMessage.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"
2017-03-29 23:54:11 +02:00
# import "TSGenericAttachmentAdapter.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"
2017-04-04 18:35:52 +02:00
# import "ThreadUtil.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"
2017-02-17 23:30:49 +01:00
# import "UIViewController+OWS.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 >
2017-01-26 19:39:13 +01:00
# import < SignalServiceKit / ContactsUpdater . h >
2016-09-02 16:22:06 +02:00
# import < SignalServiceKit / MimeTypeUtil . h >
2016-10-14 22:59:58 +02:00
# import < SignalServiceKit / OWSAttachmentsProcessor . h >
2017-04-04 18:35:52 +02:00
# import < SignalServiceKit / OWSBlockingManager . 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 >
2017-04-04 18:35:52 +02:00
# import < SignalServiceKit / Threading . h >
2016-09-02 16:22:06 +02:00
# import < YapDatabase / YapDatabaseView . h >
2014-12-31 13:22:40 +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-11-12 18:22:29 +01:00
2016-09-21 14:37:51 +02:00
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 ;
2017-03-10 14:56:12 +01:00
@ protocol OWSTextViewPasteDelegate < NSObject >
- ( void ) didPasteAttachment : ( SignalAttachment * _Nullable ) attachment ;
@ end
# pragma mark -
@ interface OWSMessagesComposerTextView ( )
@ property ( weak , nonatomic ) id < OWSTextViewPasteDelegate > textViewPasteDelegate ;
@ end
# pragma mark -
@ implementation OWSMessagesComposerTextView
- ( BOOL ) canBecomeFirstResponder {
return YES ;
}
- ( BOOL ) pasteBoardHasPossibleAttachment {
2017-04-12 22:37:37 +02:00
// We don ' t want to load / convert images more than once so we
// only do a cursory validation pass at this time .
return [ SignalAttachment pasteboardHasPossibleAttachment ] ;
2017-03-10 14:56:12 +01:00
}
- ( BOOL ) canPerformAction : ( SEL ) action withSender : ( id ) sender {
if ( action = = @ selector ( paste : ) ) {
if ( [ self pasteBoardHasPossibleAttachment ] ) {
return YES ;
}
}
return [ super canPerformAction : action withSender : sender ] ;
}
- ( void ) paste : ( id ) sender {
if ( [ self pasteBoardHasPossibleAttachment ] ) {
SignalAttachment * attachment = [ SignalAttachment attachmentFromPasteboard ] ;
// Note : attachment might be nil or have an error at this point ; that ' s fine .
[ self . textViewPasteDelegate didPasteAttachment : attachment ] ;
return ;
}
[ super paste : sender ] ;
}
@ end
# pragma mark -
@ implementation OWSMessagesToolbarContentView
# pragma mark - Class methods
+ ( UINib * ) nib
{
return [ UINib nibWithNibName : NSStringFromClass ( [ OWSMessagesToolbarContentView class ] )
bundle : [ NSBundle bundleForClass : [ OWSMessagesToolbarContentView class ] ] ] ;
}
@ end
# pragma mark -
@ implementation OWSMessagesInputToolbar
- ( JSQMessagesToolbarContentView * ) loadToolbarContentView {
NSArray * views = [ [ OWSMessagesToolbarContentView nib ] instantiateWithOwner : nil
options : nil ] ;
OWSAssert ( views . count = = 1 ) ;
OWSMessagesToolbarContentView * view = views [ 0 ] ;
OWSAssert ( [ view isKindOfClass : [ OWSMessagesToolbarContentView class ] ] ) ;
return view ;
}
@ end
# pragma mark -
@ interface MessagesViewController ( ) < JSQMessagesComposerTextViewPasteDelegate , OWSTextViewPasteDelegate > {
2015-12-22 12:45:09 +01:00
UIImage * tappedImage ;
2014-10-29 21:58:58 +01:00
BOOL isGroupConversation ;
}
2017-03-15 14:23:21 +01:00
@ property ( nonatomic ) TSThread * thread ;
@ property ( nonatomic ) TSMessageAdapter * lastDeliveredMessage ;
@ property ( nonatomic ) YapDatabaseConnection * editingDatabaseConnection ;
@ property ( nonatomic ) YapDatabaseConnection * uiDatabaseConnection ;
@ property ( nonatomic ) YapDatabaseViewMappings * messageMappings ;
@ property ( nonatomic ) JSQMessagesBubbleImage * outgoingBubbleImageData ;
@ property ( nonatomic ) JSQMessagesBubbleImage * incomingBubbleImageData ;
@ property ( nonatomic ) JSQMessagesBubbleImage * currentlyOutgoingBubbleImageData ;
@ property ( nonatomic ) JSQMessagesBubbleImage * outgoingMessageFailedImageData ;
@ property ( nonatomic ) NSTimer * audioPlayerPoller ;
@ property ( nonatomic ) TSVideoAttachmentAdapter * currentMediaAdapter ;
@ property ( nonatomic ) NSTimer * readTimer ;
@ property ( nonatomic ) UIView * navigationBarTitleView ;
@ property ( nonatomic ) UILabel * navigationBarTitleLabel ;
@ property ( nonatomic ) UILabel * navigationBarSubtitleLabel ;
@ property ( nonatomic ) UIButton * attachButton ;
2017-04-04 21:38:00 +02:00
@ property ( nonatomic ) UIView * blockStateIndicator ;
2014-12-06 23:21:15 +01:00
2017-04-09 21:31:31 +02:00
// Back Button Unread Count
@ property ( nonatomic , readonly ) UIView * backButtonUnreadCountView ;
@ property ( nonatomic , readonly ) UILabel * backButtonUnreadCountLabel ;
@ property ( nonatomic , readonly ) NSUInteger backButtonUnreadCount ;
2016-10-06 21:47:45 +02:00
@ property ( nonatomic ) CGFloat previousCollectionViewFrameWidth ;
2017-03-15 14:23:21 +01:00
@ property ( nonatomic ) 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 ;
2017-02-02 00:26:47 +01:00
@ property ( nonatomic , readonly ) OutboundCallInitiator * outboundCallInitiator ;
2017-04-04 18:35:52 +02:00
@ property ( nonatomic , readonly ) OWSBlockingManager * blockingManager ;
2016-04-13 19:05:09 +02:00
2017-03-15 14:23:21 +01:00
@ property ( nonatomic ) NSCache * messageAdapterCache ;
2017-04-05 00:08:51 +02:00
@ property ( nonatomic ) BOOL userHasScrolled ;
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 ] ;
2017-03-10 14:56:12 +01:00
return self ;
}
2016-11-01 20:02:15 +01:00
2017-03-10 14:56:12 +01:00
- ( instancetype ) initWithNibName : ( nullable NSString * ) nibNameOrNil bundle : ( nullable NSBundle * ) nibBundleOrNil {
self = [ super initWithNibName : nibNameOrNil bundle : nibBundleOrNil ] ;
if ( ! self ) {
return self ;
}
[ self commonInit ] ;
2016-11-01 20:02:15 +01:00
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 ;
2017-02-02 00:26:47 +01:00
_outboundCallInitiator = [ Environment getCurrent ] . outboundCallInitiator ;
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 ] ;
2017-04-04 18:35:52 +02:00
_blockingManager = [ OWSBlockingManager sharedManager ] ;
[ self addNotificationListeners ] ;
}
- ( void ) addNotificationListeners
{
[ [ NSNotificationCenter defaultCenter ] addObserver : self
selector : @ selector ( blockedPhoneNumbersDidChange : )
name : kNSNotificationName_BlockedPhoneNumbersDidChange
object : nil ] ;
}
- ( void ) blockedPhoneNumbersDidChange : ( id ) notification
{
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
[ self ensureBlockStateIndicator ] ;
} ) ;
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
2017-02-07 21:11:26 +01:00
[ self markAllMessagesAsRead ] ;
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 . 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 ] ;
2016-12-07 03:27:07 +01:00
_attachButton . accessibilityLabel = NSLocalizedString ( @ "ATTACHMENT_LABEL" , @ "Accessibility label for attaching photos" ) ;
_attachButton . accessibilityHint = NSLocalizedString ( @ "ATTACHMENT_HINT" , @ "Accessibility hint describing what you can do with the attachment button" ) ;
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 ] ;
2017-02-16 23:59:40 +01:00
SEL shareSelector = NSSelectorFromString ( @ "share:" ) ;
[ JSQMessagesCollectionViewCell registerMenuAction : shareSelector ] ;
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 ] ;
2017-04-10 03:39:04 +02:00
if ( [ self . thread isKindOfClass : [ TSContactThread class ] ] ) {
TSContactThread * contactThread = ( TSContactThread * ) self . thread ;
[ ThreadUtil createBlockOfferIfNecessary : contactThread
storageManager : self . storageManager
contactsManager : self . contactsManager
blockingManager : self . blockingManager ] ;
}
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-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 ] ;
2017-04-10 18:48:29 +02:00
[ [ NSNotificationCenter defaultCenter ] addObserver : self
selector : @ selector ( resetContentAndLayout )
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
2017-03-19 20:11:57 +01:00
// Since we ' re using a custom back button , we have to do some extra work to manage the interactivePopGestureRecognizer
self . navigationController . interactivePopGestureRecognizer . delegate = self ;
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
}
2017-02-11 05:21:10 +01:00
// Other views might change these custom menu items , so we
// need to set them every time we enter this view .
SEL saveSelector = NSSelectorFromString ( @ "save:" ) ;
2017-02-16 23:59:40 +01:00
SEL shareSelector = NSSelectorFromString ( @ "share:" ) ;
2017-02-11 05:21:10 +01:00
[ UIMenuController sharedMenuController ] . menuItems = @ [ [ [ UIMenuItem alloc ] initWithTitle : NSLocalizedString ( @ "EDIT_ITEM_SAVE_ACTION" ,
@ "Short name for edit menu item to save contents of media message." )
2017-02-16 23:59:40 +01:00
action : saveSelector ] ,
[ [ UIMenuItem alloc ] initWithTitle : NSLocalizedString ( @ "EDIT_ITEM_SHARE_ACTION" ,
@ "Short name for edit menu item to share contents of media message." )
action : shareSelector ] ,
] ;
2017-04-04 18:35:52 +02:00
[ self ensureBlockStateIndicator ] ;
2017-04-10 18:44:03 +02:00
2017-04-10 18:48:29 +02:00
[ self resetContentAndLayout ] ;
}
- ( void ) resetContentAndLayout
{
2017-04-10 18:44:03 +02:00
// Avoid layout corrupt issues and out - of - date message subtitles .
2017-04-04 16:51:32 +02:00
[ self . collectionView . collectionViewLayout invalidateLayout ] ;
[ self . collectionView reloadData ] ;
2017-04-04 18:35:52 +02:00
}
2017-04-05 00:08:51 +02:00
- ( void ) setUserHasScrolled : ( BOOL ) userHasScrolled {
_userHasScrolled = userHasScrolled ;
[ self ensureBlockStateIndicator ] ;
}
2017-04-04 18:35:52 +02:00
- ( void ) ensureBlockStateIndicator
{
2017-04-04 21:38:00 +02:00
// This method should be called rarely , so it ' s simplest to discard and
// rebuild the indicator view every time .
2017-04-04 18:35:52 +02:00
[ self . blockStateIndicator removeFromSuperview ] ;
self . blockStateIndicator = nil ;
2017-04-04 21:38:00 +02:00
2017-04-05 00:08:51 +02:00
if ( self . userHasScrolled ) {
return ;
}
2017-04-04 21:38:00 +02:00
NSString * blockStateMessage = nil ;
if ( [ self isBlockedContactConversation ] ) {
blockStateMessage = NSLocalizedString ( @ "MESSAGES_VIEW_CONTACT_BLOCKED" ,
@ "Indicates that this 1:1 conversation has been blocked." ) ;
2017-04-05 18:16:54 +02:00
} else if ( isGroupConversation ) {
2017-04-04 21:38:00 +02:00
int blockedGroupMemberCount = [ self blockedGroupMemberCount ] ;
if ( blockedGroupMemberCount = = 1 ) {
2017-04-05 18:16:54 +02:00
blockStateMessage = NSLocalizedString ( @ "MESSAGES_VIEW_GROUP_1_MEMBER_BLOCKED" ,
@ "Indicates that a single member of this group has been blocked." ) ;
2017-04-04 21:38:00 +02:00
} else if ( blockedGroupMemberCount > 1 ) {
blockStateMessage = [ NSString stringWithFormat : NSLocalizedString ( @ "MESSAGES_VIEW_GROUP_N_MEMBERS_BLOCKED_FORMAT" ,
@ "Indicates that some members of this group has been blocked. Embeds "
2017-04-05 18:16:54 +02:00
@ "{{the number of blocked users in this group}}." ) ,
2017-04-04 21:38:00 +02:00
blockedGroupMemberCount ] ;
}
}
if ( blockStateMessage ) {
UILabel * label = [ UILabel new ] ;
label . font = [ UIFont ows_mediumFontWithSize : 14. f ] ;
label . text = blockStateMessage ;
label . textColor = [ UIColor whiteColor ] ;
UIView * blockStateIndicator = [ UIView new ] ;
2017-04-05 18:16:54 +02:00
blockStateIndicator . backgroundColor = [ UIColor ows_redColor ] ;
2017-04-04 21:38:00 +02:00
blockStateIndicator . layer . cornerRadius = 2.5 f ;
// Use a shadow to "pop" the indicator above the other views .
blockStateIndicator . layer . shadowColor = [ UIColor blackColor ] . CGColor ;
blockStateIndicator . layer . shadowOffset = CGSizeMake ( 2 , 3 ) ;
blockStateIndicator . layer . shadowRadius = 2. f ;
blockStateIndicator . layer . shadowOpacity = 0.35 f ;
[ blockStateIndicator addSubview : label ] ;
[ label autoPinEdgeToSuperviewEdge : ALEdgeTop withInset : 5 ] ;
[ label autoPinEdgeToSuperviewEdge : ALEdgeBottom withInset : 5 ] ;
[ label autoPinEdgeToSuperviewEdge : ALEdgeLeft withInset : 15 ] ;
[ label autoPinEdgeToSuperviewEdge : ALEdgeRight withInset : 15 ] ;
[ blockStateIndicator addGestureRecognizer : [ [ UITapGestureRecognizer alloc ] initWithTarget : self
action : @ selector ( blockStateIndicatorWasTapped : ) ] ] ;
[ self . view addSubview : blockStateIndicator ] ;
[ blockStateIndicator autoHCenterInSuperview ] ;
[ blockStateIndicator autoPinToTopLayoutGuideOfViewController : self withInset : 10 ] ;
[ self . view layoutSubviews ] ;
self . blockStateIndicator = blockStateIndicator ;
}
2017-04-04 18:35:52 +02:00
}
2017-04-04 21:38:00 +02:00
- ( void ) blockStateIndicatorWasTapped : ( UIGestureRecognizer * ) sender {
if ( sender . state ! = UIGestureRecognizerStateRecognized ) {
return ;
}
2017-04-04 18:35:52 +02:00
2017-04-04 21:38:00 +02:00
if ( [ self isBlockedContactConversation ] ) {
// If this a blocked 1 : 1 conversation , offer to unblock the user .
2017-04-04 21:54:11 +02:00
[ self showUnblockContactUI : nil ] ;
2017-04-05 18:16:54 +02:00
} else if ( isGroupConversation ) {
2017-04-04 21:38:00 +02:00
// If this a group conversation with at least one blocked member ,
// Show the block list view .
int blockedGroupMemberCount = [ self blockedGroupMemberCount ] ;
if ( blockedGroupMemberCount > 0 ) {
BlockListViewController * vc = [ [ BlockListViewController alloc ] init ] ;
[ self . navigationController pushViewController : vc animated : YES ] ;
}
}
}
2017-04-04 18:35:52 +02:00
2017-04-04 21:54:11 +02:00
- ( void ) showUnblockContactUI : ( BlockActionCompletionBlock ) completionBlock
{
OWSAssert ( [ self . thread isKindOfClass : [ TSContactThread class ] ] ) ;
2017-04-05 18:16:54 +02:00
self . userHasScrolled = NO ;
// To avoid "noisy" animations ( hiding the keyboard before showing
// the action sheet , re - showing it after ) , hide the keyboard before
// showing the "unblock" action sheet .
//
// Unblocking is a rare interaction , so it ' s okay to leave the keyboard
// hidden .
[ self dismissKeyBoard ] ;
2017-04-04 21:54:11 +02:00
NSString * contactIdentifier = ( ( TSContactThread * ) self . thread ) . contactIdentifier ;
[ BlockListUIUtils showUnblockPhoneNumberActionSheet : contactIdentifier
fromViewController : self
blockingManager : _blockingManager
contactsManager : _contactsManager
completionBlock : completionBlock ] ;
}
2017-04-04 18:35:52 +02:00
- ( BOOL ) isBlockedContactConversation
{
if ( ! [ self . thread isKindOfClass : [ TSContactThread class ] ] ) {
return NO ;
}
NSString * contactIdentifier = ( ( TSContactThread * ) self . thread ) . contactIdentifier ;
return [ [ _blockingManager blockedPhoneNumbers ] containsObject : contactIdentifier ] ;
2014-12-24 11:50:07 +01:00
}
2017-04-04 21:38:00 +02:00
- ( int ) blockedGroupMemberCount
{
2017-04-05 18:16:54 +02:00
OWSAssert ( isGroupConversation ) ;
OWSAssert ( [ self . thread isKindOfClass : [ TSGroupThread class ] ] ) ;
2017-04-04 21:38:00 +02:00
TSGroupThread * groupThread = ( TSGroupThread * ) self . thread ;
int blockedMemberCount = 0 ;
NSArray < NSString * > * blockedPhoneNumbers = [ _blockingManager blockedPhoneNumbers ] ;
for ( NSString * contactIdentifier in groupThread . groupModel . groupMemberIds ) {
if ( [ blockedPhoneNumbers containsObject : contactIdentifier ] ) {
blockedMemberCount + + ;
}
}
return blockedMemberCount ;
}
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
2017-04-09 21:31:31 +02:00
[ self updateBackButtonUnreadCount ] ;
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-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
2017-03-19 20:11:57 +01:00
// Since we ' re using a custom back button , we have to do some extra work to manage the interactivePopGestureRecognizer
self . navigationController . interactivePopGestureRecognizer . delegate = nil ;
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 ;
2017-04-05 00:08:51 +02:00
self . userHasScrolled = 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" , @ "" ) ;
}
2017-02-15 22:09:57 +01:00
self . title = nil ;
if ( [ navTitle isEqualToString : self . navigationBarTitleLabel . text ] ) {
return ;
}
self . navigationBarTitleLabel . text = navTitle ;
// Changing the title requires relayout of the nav bar contents .
OWSDisappearingMessagesConfiguration * configuration =
[ OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID : self . thread . uniqueId ] ;
[ self setBarButtonItemsForDisappearingMessagesConfiguration : configuration ] ;
2016-09-21 14:37:51 +02:00
}
2015-01-14 22:30:01 +01:00
2016-09-21 14:37:51 +02:00
- ( void ) setBarButtonItemsForDisappearingMessagesConfiguration :
( OWSDisappearingMessagesConfiguration * ) disappearingMessagesConfiguration
{
2017-02-17 23:30:49 +01:00
UIBarButtonItem * backItem = [ self createOWSBackButton ] ;
2017-04-09 21:31:31 +02:00
const CGFloat unreadCountViewDiameter = 16 ;
if ( _backButtonUnreadCountView = = nil ) {
_backButtonUnreadCountView = [ UIView new ] ;
_backButtonUnreadCountView . layer . cornerRadius = unreadCountViewDiameter / 2 ;
_backButtonUnreadCountView . backgroundColor = [ UIColor redColor ] ;
_backButtonUnreadCountView . hidden = YES ;
_backButtonUnreadCountView . userInteractionEnabled = NO ;
_backButtonUnreadCountLabel = [ UILabel new ] ;
_backButtonUnreadCountLabel . backgroundColor = [ UIColor clearColor ] ;
_backButtonUnreadCountLabel . textColor = [ UIColor whiteColor ] ;
_backButtonUnreadCountLabel . font = [ UIFont systemFontOfSize : 11 ] ;
2017-04-11 01:28:18 +02:00
_backButtonUnreadCountLabel . textAlignment = NSTextAlignmentCenter ;
2017-04-09 21:31:31 +02:00
}
// This method gets called multiple times , so it ' s important we re - layout the unread badge
// with respect to the new backItem .
[ backItem . customView addSubview : _backButtonUnreadCountView ] ;
[ _backButtonUnreadCountView autoPinEdgeToSuperviewEdge : ALEdgeTop withInset : -6 ] ;
2017-04-11 01:28:18 +02:00
[ _backButtonUnreadCountView autoPinEdgeToSuperviewEdge : ALEdgeLeft withInset : 1 ] ;
2017-04-09 21:31:31 +02:00
[ _backButtonUnreadCountView autoSetDimension : ALDimensionHeight toSize : unreadCountViewDiameter ] ;
2017-04-11 01:28:18 +02:00
// We set a min width , but we will also pin to our subview label , so we can grow to accommodate multiple digits .
[ _backButtonUnreadCountView autoSetDimension : ALDimensionWidth
toSize : unreadCountViewDiameter
relation : NSLayoutRelationGreaterThanOrEqual ] ;
2017-04-09 21:31:31 +02:00
[ _backButtonUnreadCountView addSubview : _backButtonUnreadCountLabel ] ;
2017-04-11 01:28:18 +02:00
[ _backButtonUnreadCountLabel autoPinWidthToSuperviewWithMargin : 4 ] ;
[ _backButtonUnreadCountLabel autoPinHeightToSuperview ] ;
2017-04-09 21:31:31 +02:00
// Initialize newly created unread count badge to accurately reflect the current unread count .
[ self updateBackButtonUnreadCount ] ;
2017-02-17 23:30:49 +01:00
2017-02-15 22:09:57 +01:00
const CGFloat kTitleVSpacing = 0. f ;
if ( ! self . navigationBarTitleView ) {
self . navigationBarTitleView = [ UIView new ] ;
[ self . navigationBarTitleView addGestureRecognizer : [ [ UITapGestureRecognizer alloc ] initWithTarget : self
action : @ selector ( navigationTitleTapped : ) ] ] ;
2017-03-27 23:03:36 +02:00
# ifdef DEBUG
[ self . navigationBarTitleView addGestureRecognizer : [ [ UILongPressGestureRecognizer alloc ] initWithTarget : self
action : @ selector ( navigationTitleLongPressed : ) ] ] ;
# endif
2017-02-15 22:09:57 +01:00
self . navigationBarTitleLabel = [ UILabel new ] ;
self . navigationBarTitleLabel . textColor = [ UIColor whiteColor ] ;
self . navigationBarTitleLabel . font = [ UIFont ows_boldFontWithSize : 18. f ] ;
self . navigationBarTitleLabel . lineBreakMode = NSLineBreakByTruncatingTail ;
[ self . navigationBarTitleView addSubview : self . navigationBarTitleLabel ] ;
self . navigationBarSubtitleLabel = [ UILabel new ] ;
2017-03-06 01:33:43 +01:00
self . navigationBarSubtitleLabel . textColor = [ UIColor colorWithWhite : 0.9 f alpha : 1. f ] ;
self . navigationBarSubtitleLabel . font = [ UIFont ows_regularFontWithSize : 9. f ] ;
2017-02-15 22:09:57 +01:00
self . navigationBarSubtitleLabel . text = NSLocalizedString ( @ "MESSAGES_VIEW_TITLE_SUBTITLE" ,
@ "The subtitle for the messages view title indicates that the title can be tapped to access settings for this conversation." ) ;
[ self . navigationBarTitleView addSubview : self . navigationBarSubtitleLabel ] ;
}
// We need to manually resize and position the title views ;
// iOS AutoLayout doesn ' t work inside navigation bar items .
[ self . navigationBarTitleLabel sizeToFit ] ;
[ self . navigationBarSubtitleLabel sizeToFit ] ;
const CGFloat kShortScreenDimension = MIN ( [ UIScreen mainScreen ] . bounds . size . width ,
[ UIScreen mainScreen ] . bounds . size . height ) ;
2017-02-17 23:30:49 +01:00
// We want to leave space for the "back" button , the "timer" button , and the "call"
// button , and all of the whitespace around these views . There
2017-02-15 22:09:57 +01:00
// isn ' t a convenient way to calculate these in a navigation bar , so we just leave
// a constant amount of space which will be safe unless Apple makes radical changes
// to the appearance of the navigation bar .
2017-02-17 17:45:26 +01:00
int rightBarButtonItemCount = 0 ;
if ( [ self canCall ] ) {
rightBarButtonItemCount + + ;
}
if ( disappearingMessagesConfiguration . isEnabled ) {
rightBarButtonItemCount + + ;
}
2017-03-06 01:33:43 +01:00
CGFloat barButtonSize = 0 ;
2017-02-17 17:45:26 +01:00
switch ( rightBarButtonItemCount ) {
case 0 :
2017-03-06 01:33:43 +01:00
barButtonSize = 70 ;
2017-02-17 17:45:26 +01:00
break ;
case 1 :
2017-03-06 01:33:43 +01:00
barButtonSize = 105 ;
2017-02-17 17:45:26 +01:00
break ;
default :
OWSAssert ( 0 ) ;
2017-02-17 23:30:49 +01:00
// In production , fall through to the largest defined case .
2017-02-17 17:45:26 +01:00
case 2 :
2017-03-06 01:33:43 +01:00
barButtonSize = 150 ;
2017-02-17 17:45:26 +01:00
break ;
}
2017-03-06 01:33:43 +01:00
CGFloat maxTitleViewWidth = kShortScreenDimension - barButtonSize ;
2017-02-17 17:45:26 +01:00
const CGFloat titleViewWidth = MIN ( maxTitleViewWidth ,
2017-02-15 22:09:57 +01:00
MAX ( self . navigationBarTitleLabel . frame . size . width ,
self . navigationBarSubtitleLabel . frame . size . width ) ) ;
self . navigationBarTitleView . frame = CGRectMake ( 0 , 0 ,
titleViewWidth ,
self . navigationBarTitleLabel . frame . size . height +
self . navigationBarSubtitleLabel . frame . size . height +
kTitleVSpacing ) ;
self . navigationBarTitleLabel . frame = CGRectMake ( 0 ,
0 ,
titleViewWidth ,
self . navigationBarTitleLabel . frame . size . height ) ;
self . navigationBarSubtitleLabel . frame = CGRectMake ( 0 ,
self . navigationBarTitleView . frame . size . height - self . navigationBarSubtitleLabel . frame . size . height ,
titleViewWidth ,
self . navigationBarSubtitleLabel . frame . size . height ) ;
self . navigationItem . leftBarButtonItems = @ [
backItem ,
[ [ UIBarButtonItem alloc ] initWithCustomView : self . navigationBarTitleView ] ,
] ;
2017-04-04 18:35:52 +02:00
2016-09-21 14:37:51 +02:00
if ( self . userLeftGroup ) {
self . navigationItem . rightBarButtonItems = @ [ ] ;
return ;
}
2015-12-22 12:45:09 +01:00
2017-02-17 17:45:26 +01:00
const CGFloat kBarButtonSize = 44 ;
2016-09-21 14:37:51 +02:00
NSMutableArray < UIBarButtonItem * > * barButtons = [ NSMutableArray new ] ;
if ( [ self canCall ] ) {
2017-02-15 22:09:57 +01:00
// We use UIButtons with [ UIBarButtonItem initWithCustomView : . . . ] instead of
// UIBarButtonItem in order to ensure that these buttons are spaced tightly .
// The contents of the navigation bar are cramped in this view .
UIButton * callButton = [ UIButton buttonWithType : UIButtonTypeCustom ] ;
2017-02-17 17:45:26 +01:00
UIImage * image = [ UIImage imageNamed : @ "button_phone_white" ] ;
[ callButton setImage : image
2017-02-15 22:09:57 +01:00
forState : UIControlStateNormal ] ;
2017-02-17 17:45:26 +01:00
UIEdgeInsets imageEdgeInsets = UIEdgeInsetsZero ;
// We normally would want to use left and right insets that ensure the button
// is square and the icon is centered . However UINavigationBar doesn ' t offer us
// control over the margins and spacing of its content , and the buttons end up
// too far apart and too far from the edge of the screen . So we use a smaller
// right inset tighten up the layout .
imageEdgeInsets . left = round ( ( kBarButtonSize - image . size . width ) * 0.5 f ) ;
2017-02-17 23:30:49 +01:00
imageEdgeInsets . right = round ( ( kBarButtonSize - ( image . size . width + imageEdgeInsets . left ) ) * 0.5 f ) ;
2017-02-17 17:45:26 +01:00
imageEdgeInsets . top = round ( ( kBarButtonSize - image . size . height ) * 0.5 f ) ;
imageEdgeInsets . bottom = round ( kBarButtonSize - ( image . size . height + imageEdgeInsets . top ) ) ;
callButton . imageEdgeInsets = imageEdgeInsets ;
2016-12-07 03:27:07 +01:00
callButton . accessibilityLabel = NSLocalizedString ( @ "CALL_LABEL" , "Accessibilty label for placing call button" ) ;
2017-02-15 22:09:57 +01:00
[ callButton addTarget : self
action : @ selector ( callAction : )
forControlEvents : UIControlEventTouchUpInside ] ;
2017-02-17 17:45:26 +01:00
callButton . frame = CGRectMake ( 0 , 0 ,
round ( image . size . width + imageEdgeInsets . left + imageEdgeInsets . right ) ,
round ( image . size . height + imageEdgeInsets . top + imageEdgeInsets . bottom ) ) ;
2017-02-15 22:09:57 +01:00
[ barButtons addObject : [ [ UIBarButtonItem alloc ] initWithCustomView : callButton ] ] ;
2015-01-14 22:30:01 +01:00
}
2016-09-21 14:37:51 +02:00
if ( disappearingMessagesConfiguration . isEnabled ) {
2017-02-15 22:09:57 +01:00
UIButton * timerButton = [ UIButton buttonWithType : UIButtonTypeCustom ] ;
2017-02-17 17:45:26 +01:00
UIImage * image = [ UIImage imageNamed : @ "button_timer_white" ] ;
[ timerButton setImage : image
2017-02-15 22:09:57 +01:00
forState : UIControlStateNormal ] ;
2017-02-17 17:45:26 +01:00
UIEdgeInsets imageEdgeInsets = UIEdgeInsetsZero ;
// We normally would want to use left and right insets that ensure the button
// is square and the icon is centered . However UINavigationBar doesn ' t offer us
// control over the margins and spacing of its content , and the buttons end up
// too far apart and too far from the edge of the screen . So we use a smaller
// right inset tighten up the layout .
imageEdgeInsets . left = round ( ( kBarButtonSize - image . size . width ) * 0.5 f ) ;
2017-02-17 23:30:49 +01:00
imageEdgeInsets . right = round ( ( kBarButtonSize - ( image . size . width + imageEdgeInsets . left ) ) * 0.5 f ) ;
2017-02-17 17:45:26 +01:00
imageEdgeInsets . top = round ( ( kBarButtonSize - image . size . height ) * 0.5 f ) ;
imageEdgeInsets . bottom = round ( kBarButtonSize - ( image . size . height + imageEdgeInsets . top ) ) ;
timerButton . imageEdgeInsets = imageEdgeInsets ;
2016-12-07 03:27:07 +01:00
timerButton . accessibilityLabel = NSLocalizedString ( @ "DISAPPEARING_MESSAGES_LABEL" , @ "Accessibility label for disappearing messages" ) ;
NSString * formatString = NSLocalizedString ( @ "DISAPPEARING_MESSAGES_HINT" , @ "Accessibility hint that contains current timeout information" ) ;
timerButton . accessibilityHint = [ NSString stringWithFormat : formatString , [ disappearingMessagesConfiguration durationString ] ] ;
2017-02-15 22:09:57 +01:00
[ timerButton addTarget : self
action : @ selector ( didTapTimerInNavbar : )
forControlEvents : UIControlEventTouchUpInside ] ;
2017-02-17 17:45:26 +01:00
timerButton . frame = CGRectMake ( 0 , 0 ,
round ( image . size . width + imageEdgeInsets . left + imageEdgeInsets . right ) ,
round ( image . size . height + imageEdgeInsets . top + imageEdgeInsets . bottom ) ) ;
2017-02-15 22:09:57 +01:00
[ barButtons addObject : [ [ UIBarButtonItem alloc ] initWithCustomView : timerButton ] ] ;
2015-01-27 02:20:11 +01:00
}
2017-02-15 22:09:57 +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 ;
2017-03-10 14:56:12 +01:00
OWSAssert ( self . inputToolbar . contentView ) ;
OWSAssert ( self . inputToolbar . contentView . textView ) ;
self . inputToolbar . contentView . textView . pasteDelegate = self ;
( ( OWSMessagesComposerTextView * ) self . inputToolbar . contentView . textView ) . textViewPasteDelegate = self ;
2016-09-21 14:37:51 +02:00
}
2015-12-22 12:45:09 +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
{
2017-04-07 03:41:35 +02:00
// Ensure keyboard isn ' t hiding the "safety numbers changed" interaction when we
// return from FingerprintViewController .
[ self dismissKeyBoard ] ;
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 =
2016-11-30 21:04:26 +01:00
[ builder fingerprintWithTheirSignalId : theirSignalId theirIdentityKey : theirIdentityKey ] ;
2014-12-06 17:45:42 +01:00
[ self markAllMessagesAsRead ] ;
2017-03-31 19:44:43 +02:00
NSString * contactName = [ self . contactsManager displayNameForPhoneIdentifier : theirSignalId ] ;
UIViewController * viewController =
[ [ UIStoryboard main ] instantiateViewControllerWithIdentifier : @ "FingerprintViewController" ] ;
if ( ! [ viewController isKindOfClass : [ FingerprintViewController class ] ] ) {
OWSAssert ( NO ) ;
DDLogError ( @ "%@ expecting fingerprint view controller, but got: %@" , self . tag , viewController ) ;
return ;
}
FingerprintViewController * fingerprintViewController = ( FingerprintViewController * ) viewController ;
[ fingerprintViewController configureWithThread : self . thread fingerprint : fingerprint contactName : contactName ] ;
[ self presentViewController : fingerprintViewController animated : YES completion : nil ] ;
2014-12-24 02:25:10 +01:00
}
2014-11-29 19:54:33 +01:00
# pragma mark - Calls
2017-02-15 22:09:57 +01:00
- ( void ) callAction : ( id ) sender {
2017-01-25 17:41:30 +01:00
OWSAssert ( [ self . thread isKindOfClass : [ TSContactThread class ] ] ) ;
2017-01-26 19:39:13 +01:00
if ( ! [ self canCall ] ) {
2016-06-28 04:51:57 +02:00
DDLogWarn ( @ "Tried to initiate a call but thread is not callable." ) ;
2017-01-26 19:39:13 +01:00
return ;
2014-11-29 19:54:33 +01:00
}
2017-01-26 19:39:13 +01:00
2017-04-04 18:35:52 +02:00
if ( [ self isBlockedContactConversation ] ) {
__weak MessagesViewController * weakSelf = self ;
2017-04-04 21:54:11 +02:00
[ self showUnblockContactUI : ^ ( BOOL isBlocked ) {
if ( ! isBlocked ) {
[ weakSelf callAction : nil ] ;
}
} ] ;
2017-04-04 18:35:52 +02:00
return ;
}
2017-02-02 00:26:47 +01:00
[ self . outboundCallInitiator initiateCallWithRecipientId : self . thread . contactIdentifier ] ;
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
2017-04-05 18:16:54 +02:00
{
[ self didPressSendButton : button
withMessageText : text
senderId : senderId
senderDisplayName : senderDisplayName
date : date
updateKeyboardState : YES ] ;
}
- ( void ) didPressSendButton : ( UIButton * ) button
withMessageText : ( NSString * ) text
senderId : ( NSString * ) senderId
senderDisplayName : ( NSString * ) senderDisplayName
date : ( NSDate * ) date
updateKeyboardState : ( BOOL ) updateKeyboardState
2016-08-01 00:25:07 +02:00
{
2017-04-04 18:35:52 +02:00
if ( [ self isBlockedContactConversation ] ) {
__weak MessagesViewController * weakSelf = self ;
2017-04-04 21:54:11 +02:00
[ self showUnblockContactUI : ^ ( BOOL isBlocked ) {
if ( ! isBlocked ) {
[ weakSelf didPressSendButton : button
withMessageText : text
senderId : senderId
senderDisplayName : senderDisplayName
2017-04-05 18:16:54 +02:00
date : date
updateKeyboardState : NO ] ;
2017-04-04 21:54:11 +02:00
}
} ] ;
2017-04-04 18:35:52 +02:00
return ;
}
2017-03-14 16:17:54 +01:00
text = [ text stringByTrimmingCharactersInSet : [ NSCharacterSet whitespaceCharacterSet ] ] ;
2017-04-13 20:42:05 +02:00
if ( text . length > 0 ) {
2016-11-03 19:51:38 +01:00
if ( [ Environment . preferences soundInForeground ] ) {
[ JSQSystemSoundPlayer jsq_playMessageSentSound ] ;
}
2017-04-13 20:42:05 +02:00
// Limit outgoing text messages to 16 kb .
//
// We convert large text messages to attachments
// which are presented as normal text messages .
const NSUInteger kOversizeTextMessageSizeThreshold = 16 * 1024 ;
if ( [ text lengthOfBytesUsingEncoding : NSUTF8StringEncoding ] >= kOversizeTextMessageSizeThreshold ) {
SignalAttachment * attachment = [ SignalAttachment oversizeTextAttachmentWithText : text ] ;
[ ThreadUtil sendMessageWithAttachment : attachment inThread : self . thread messageSender : self . messageSender ] ;
} else {
[ ThreadUtil sendMessageWithText : text inThread : self . thread messageSender : self . messageSender ] ;
}
2017-04-05 18:16:54 +02:00
if ( updateKeyboardState )
{
[ 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
}
2017-03-14 21:27:35 +01:00
- ( TSOutgoingMessage * ) nextOutgoingMessage : ( NSIndexPath * ) indexPath
2016-10-14 22:59:58 +02:00
{
2017-03-14 21:27:35 +01:00
NSInteger rowCount = [ self . collectionView numberOfItemsInSection : indexPath . section ] ;
for ( NSInteger row = indexPath . row + 1 ; row < rowCount ; row + + ) {
id < OWSMessageData > nextMessage = [ self messageAtIndexPath : [ NSIndexPath indexPathForRow : row
inSection : indexPath . section ] ] ;
if ( [ nextMessage isKindOfClass : [ TSOutgoingMessage class ] ] ) {
return ( TSOutgoingMessage * ) nextMessage ;
}
2014-12-04 15:01:05 +01:00
}
2017-03-14 21:27:35 +01:00
return nil ;
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 ) {
2017-03-15 15:39:47 +01:00
return [ [ NSAttributedString alloc ] initWithString : NSLocalizedString ( @ "MESSAGE_STATUS_FAILED" ,
@ "message footer for failed messages" ) ] ;
2017-03-14 21:27:35 +01:00
} else if ( outgoingMessage . messageState = = TSOutgoingMessageStateSent ||
outgoingMessage . messageState = = TSOutgoingMessageStateDelivered ) {
2017-03-15 15:39:47 +01:00
NSString * text = ( outgoingMessage . messageState = = TSOutgoingMessageStateSent
? NSLocalizedString ( @ "MESSAGE_STATUS_SENT" ,
@ "message footer for sent messages" )
: NSLocalizedString ( @ "MESSAGE_STATUS_DELIVERED" ,
@ "message footer for delivered messages" ) ) ;
NSAttributedString * result = [ [ NSAttributedString alloc ] initWithString : 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 ] ;
2017-03-14 21:01:12 +01:00
return result ;
2016-10-12 19:00:06 +02:00
}
2017-03-14 21:27:35 +01:00
// Or when the next message is * not * an outgoing sent / delivered message .
TSOutgoingMessage * nextMessage = [ self nextOutgoingMessage : indexPath ] ;
if ( nextMessage &&
nextMessage . messageState ! = TSOutgoingMessageStateSent &&
nextMessage . messageState ! = TSOutgoingMessageStateDelivered ) {
2016-10-12 19:00:06 +02:00
[ self updateLastDeliveredMessage : message ] ;
2017-03-14 21:01:12 +01:00
return result ;
2016-10-12 19:00:06 +02:00
}
2016-10-14 22:59:58 +02:00
} else if ( message . isMediaBeingSent ) {
2017-03-15 15:39:47 +01:00
return [ [ NSAttributedString alloc ] initWithString : NSLocalizedString ( @ "MESSAGE_STATUS_UPLOADING" ,
2017-03-14 21:27:35 +01:00
@ "message footer while attachment is uploading" ) ] ;
} else {
OWSAssert ( outgoingMessage . messageState = = TSOutgoingMessageStateAttemptingOut ) ;
// Show an "..." ellisis icon .
//
// TODO : It ' d be nice to animate this , but JSQMessageViewController doesn ' t give us a great way to do so .
// We already have problems with unstable cell layout ; we don ' t want to exacerbate them .
2017-03-14 21:01:12 +01:00
NSAttributedString * result =
[ [ NSAttributedString alloc ] initWithString : @ "/"
attributes : @ {
NSFontAttributeName : [ UIFont ows_dripIconsFont : 14. f ] ,
} ] ;
return result ;
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 ;
2016-12-01 22:17:57 +01:00
NSString * _Nonnull name = [ self . contactsManager displayNameForPhoneIdentifier : incomingMessage . authorId ] ;
2016-09-21 14:37:51 +02:00
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 ;
}
2017-03-31 19:44:43 +02:00
OWSConversationSettingsTableViewController * settingsVC =
[ [ UIStoryboard main ] instantiateViewControllerWithIdentifier : @ "OWSConversationSettingsTableViewController" ] ;
2017-03-10 14:56:12 +01:00
[ settingsVC configureWithThread : self . thread ] ;
[ self . navigationController pushViewController : settingsVC animated : YES ] ;
2016-09-21 14:37:51 +02:00
}
2017-02-15 22:09:57 +01:00
- ( void ) didTapTimerInNavbar : ( id ) sender
2016-09-21 14:37:51 +02:00
{
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 ] ;
2017-03-29 18:20:28 +02:00
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 {
2017-02-11 05:21:40 +01:00
UIWindow * window = [ UIApplication sharedApplication ] . keyWindow ;
JSQMessagesCollectionViewCell * cell = ( JSQMessagesCollectionViewCell * ) [ collectionView cellForItemAtIndexPath : indexPath ] ;
OWSAssert ( [ cell isKindOfClass : [ JSQMessagesCollectionViewCell class ] ] ) ;
CGRect convertedRect = [ cell . mediaView convertRect : cell . mediaView . bounds
toView : window ] ;
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
2017-02-11 05:21:10 +01:00
forInteraction : interaction
messageItem : messageItem
2016-04-13 20:38:42 +02:00
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 {
2017-02-11 05:21:40 +01:00
UIWindow * window = [ UIApplication sharedApplication ] . keyWindow ;
JSQMessagesCollectionViewCell * cell = ( JSQMessagesCollectionViewCell * ) [ collectionView cellForItemAtIndexPath : indexPath ] ;
OWSAssert ( [ cell isKindOfClass : [ JSQMessagesCollectionViewCell class ] ] ) ;
CGRect convertedRect = [ cell . mediaView convertRect : cell . mediaView . bounds
toView : window ] ;
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
2017-02-11 05:21:10 +01:00
forInteraction : interaction
messageItem : messageItem
2015-12-22 12:45:09 +01:00
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
2017-02-11 05:26:43 +01:00
[ [ NSNotificationCenter defaultCenter ] addObserver : self
selector : @ selector ( moviePlayerWillExitFullscreen : )
name : MPMoviePlayerWillExitFullscreenNotification
object : _videoPlayer ] ;
[ [ NSNotificationCenter defaultCenter ] addObserver : self
selector : @ selector ( moviePlayerDidExitFullscreen : )
name : MPMoviePlayerDidExitFullscreenNotification
object : _videoPlayer ] ;
2015-12-22 12:45:09 +01:00
2017-02-11 05:22:09 +01:00
_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 ] ;
2017-02-11 05:22:09 +01:00
// We can ' t animate from the cell media frame ;
// MPMoviePlayerController will animate a crop of its
// contents rather than scaling them .
_videoPlayer . view . frame = self . view . bounds ;
[ _videoPlayer setFullscreen : YES animated : NO ] ;
2015-01-14 22:30:01 +01:00
}
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
}
2017-03-29 23:54:11 +02:00
if ( messageItem . messageType = = TSOutgoingMessageAdapter ||
messageItem . messageType = = TSIncomingMessageAdapter ) {
TSMessage * message = ( TSMessage * ) interaction ;
if ( [ message hasAttachments ] ) {
NSString * attachmentID = message . attachmentIds [ 0 ] ;
TSAttachment * attachment = [ TSAttachment fetchObjectWithUniqueID : attachmentID ] ;
if ( [ attachment isKindOfClass : [ TSAttachmentStream class ] ] ) {
TSAttachmentStream * stream = ( TSAttachmentStream * ) attachment ;
2017-03-29 18:20:28 +02:00
// Tapping on incoming and outgoing unknown extensions should show the
// sharing UI .
2017-03-29 23:54:11 +02:00
if ( [ [ messageItem media ] isKindOfClass : [ TSGenericAttachmentAdapter class ] ] ) {
[ AttachmentSharing showShareUIForAttachment : stream ] ;
}
2017-03-29 18:20:28 +02:00
// Tapping on incoming and outgoing "oversize text messages" should show the
// "oversize text message" view .
if ( [ attachment . contentType isEqualToString : OWSMimeTypeOversizeTextMessage ] ) {
OversizeTextMessageViewController * messageVC = [ [ OversizeTextMessageViewController alloc ] initWithMessage : message ] ;
[ self . navigationController pushViewController : messageVC animated : YES ] ;
}
2017-03-29 23:54:11 +02: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
2017-03-28 23:48:24 +02:00
if ( pointer . state ! = TSAttachmentPointerStateDownloading ) {
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
}
}
}
}
}
2017-02-13 22:30:54 +01:00
// There ' s more than one way to exit the fullscreen video playback .
// There ' s a done button , a "toggle fullscreen" button and I think
// there ' s some gestures too . These fire slightly different notifications .
// We want to hide & clean up the video player immediately in all of
// these cases .
2017-02-11 05:22:09 +01:00
- ( void ) moviePlayerWillExitFullscreen : ( id ) sender {
DDLogDebug ( @ "%@ %s" , self . tag , __PRETTY _FUNCTION __ ) ;
[ self clearVideoPlayer ] ;
}
2017-02-13 22:30:54 +01:00
// See comment on moviePlayerWillExitFullscreen :
2017-02-11 05:22:09 +01:00
- ( void ) moviePlayerDidExitFullscreen : ( id ) sender {
DDLogDebug ( @ "%@ %s" , self . tag , __PRETTY _FUNCTION __ ) ;
[ self clearVideoPlayer ] ;
}
- ( void ) clearVideoPlayer {
[ _videoPlayer stop ] ;
[ _videoPlayer . view removeFromSuperview ] ;
_videoPlayer = nil ;
2015-01-14 22:30:01 +01:00
}
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 ] ;
2017-04-10 03:39:04 +02:00
} else if ( [ message isKindOfClass : [ OWSUnknownContactBlockOfferMessage class ] ] ) {
[ self tappedUnknownContactBlockOfferMessage : ( OWSUnknownContactBlockOfferMessage * ) 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 ;
2017-04-07 02:13:38 +02:00
[ OWSSessionResetJob
runWithContactThread : contactThread
messageSender : self . messageSender
storageManager : self . storageManager ] ;
2016-11-26 00:12:00 +01:00
} ] ;
[ 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
{
2016-12-01 22:17:57 +01:00
NSString * keyOwner = [ self . contactsManager displayNameForPhoneIdentifier : errorMessage . theirSignalId ] ;
2016-09-11 22:53:12 +02:00
NSString * titleFormat = NSLocalizedString ( @ "SAFETY_NUMBERS_ACTIONSHEET_TITLE" , @ "Action sheet heading" ) ;
NSString * titleText = [ NSString stringWithFormat : titleFormat , keyOwner ] ;
2016-11-26 00:12:00 +01:00
2017-04-10 03:39:04 +02:00
UIAlertController * actionSheetController =
[ UIAlertController alertControllerWithTitle : titleText
message : nil
preferredStyle : UIAlertControllerStyleActionSheet ] ;
2016-11-26 00:12:00 +01:00
UIAlertAction * dismissAction = [ UIAlertAction actionWithTitle : NSLocalizedString ( @ "TXT_CANCEL_TITLE" , @ "" )
style : UIAlertActionStyleCancel
handler : nil ] ;
[ actionSheetController addAction : dismissAction ] ;
2017-04-10 03:39:04 +02:00
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 ] ;
} ] ;
2016-11-26 00:12:00 +01:00
[ actionSheetController addAction : showSafteyNumberAction ] ;
2017-04-10 03:39:04 +02:00
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 ) ;
} ] ;
}
} ] ;
2016-11-26 00:12:00 +01:00
[ actionSheetController addAction : acceptSafetyNumberAction ] ;
[ self presentViewController : actionSheetController animated : YES completion : nil ] ;
2014-10-29 21:58:58 +01:00
}
2017-04-10 03:39:04 +02:00
- ( void ) tappedUnknownContactBlockOfferMessage : ( OWSUnknownContactBlockOfferMessage * ) errorMessage
{
NSString * displayName = [ self . contactsManager displayNameForPhoneIdentifier : errorMessage . contactId ] ;
NSString * title =
[ NSString stringWithFormat : NSLocalizedString ( @ "BLOCK_OFFER_ACTIONSHEET_TITLE_FORMAT" ,
@ "Title format for action sheet that offers to block an unknown user."
@ "Embeds {{the unknown user's name or phone number}}." ) ,
[ BlockListUIUtils formatDisplayNameForAlertTitle : displayName ] ] ;
UIAlertController * actionSheetController =
[ UIAlertController alertControllerWithTitle : title message : nil preferredStyle : UIAlertControllerStyleActionSheet ] ;
UIAlertAction * dismissAction = [ UIAlertAction actionWithTitle : NSLocalizedString ( @ "TXT_CANCEL_TITLE" , @ "" )
style : UIAlertActionStyleCancel
handler : nil ] ;
[ actionSheetController addAction : dismissAction ] ;
UIAlertAction * blockAction =
[ UIAlertAction actionWithTitle : NSLocalizedString ( @ "BLOCK_OFFER_ACTIONSHEET_BLOCK_ACTION" ,
@ "Action sheet that will block an unknown user." )
style : UIAlertActionStyleDestructive
handler : ^ ( UIAlertAction * _Nonnull action ) {
DDLogInfo ( @ "%@ Blocking an unknown user." , self . tag ) ;
[ self . blockingManager addBlockedPhoneNumber : errorMessage . contactId ] ;
// Delete the block offer .
[ self . storageManager . dbConnection
readWriteWithBlock : ^ ( YapDatabaseReadWriteTransaction * transaction ) {
[ errorMessage removeWithTransaction : transaction ] ;
} ] ;
} ] ;
[ actionSheetController addAction : blockAction ] ;
[ self presentViewController : actionSheetController animated : YES completion : nil ] ;
}
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 ;
2016-12-08 17:52:12 +01:00
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
[ 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 ] ;
2016-12-08 17:52:12 +01:00
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
[ 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
{
2017-03-16 19:29:30 +01:00
OWSAssert ( [ NSThread isMainThread ] ) ;
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 ] ;
2017-03-16 19:29:30 +01:00
[ self dismissViewControllerAnimated : YES
completion : ^ {
[ 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 ] ;
2017-03-16 19:29:30 +01:00
[ self dismissViewControllerAnimated : YES
completion : ^ {
OWSAssert ( [ NSThread isMainThread ] ) ;
if ( imageFromCamera ) {
SignalAttachment * attachment = [ SignalAttachment imageAttachmentWithImage : imageFromCamera
dataUTI : ( NSString * ) kUTTypeJPEG ] ;
if ( ! attachment ||
[ attachment hasError ] ) {
DDLogWarn ( @ "%@ %s Invalid attachment: %@." ,
self . tag ,
__PRETTY _FUNCTION __ ,
2017-03-27 17:02:28 +02:00
attachment ? [ attachment errorName ] : @ "Missing data" ) ;
2017-03-24 03:32:42 +01:00
[ self showErrorAlertForAttachment : attachment ] ;
2017-03-16 19:29:30 +01:00
failedToPickAttachment ( nil ) ;
} else {
2017-04-07 04:04:10 +02:00
[ self tryToSendAttachmentIfApproved : attachment ] ;
2017-03-16 19:29:30 +01:00
}
} else {
failedToPickAttachment ( nil ) ;
}
} ] ;
2016-07-22 02:15:34 +02:00
} 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 ]
2017-03-10 14:56:12 +01:00
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 ) ;
}
OWSAssert ( [ NSThread isMainThread ] ) ;
SignalAttachment * attachment = [ SignalAttachment imageAttachmentWithData : imageData
dataUTI : dataUTI ] ;
2017-03-24 03:32:42 +01:00
[ self dismissViewControllerAnimated : YES
completion : ^ {
OWSAssert ( [ NSThread isMainThread ] ) ;
if ( ! attachment ||
[ attachment hasError ] ) {
DDLogWarn ( @ "%@ %s Invalid attachment: %@." ,
self . tag ,
__PRETTY _FUNCTION __ ,
2017-03-27 17:02:28 +02:00
attachment ? [ attachment errorName ] : @ "Missing data" ) ;
2017-03-24 03:32:42 +01:00
[ self showErrorAlertForAttachment : attachment ] ;
failedToPickAttachment ( nil ) ;
} else {
2017-04-07 04:04:10 +02:00
[ self tryToSendAttachmentIfApproved : attachment ] ;
2017-03-24 03:32:42 +01:00
}
} ] ;
2017-03-10 14:56:12 +01:00
} ] ;
}
}
- ( void ) sendMessageAttachment : ( SignalAttachment * ) attachment
2016-08-01 00:25:07 +02:00
{
2017-04-07 04:04:10 +02:00
OWSAssert ( [ NSThread isMainThread ] ) ;
2017-03-10 14:56:12 +01:00
// TODO : Should we assume non - nil or should we check for non - nil ?
OWSAssert ( attachment ! = nil ) ;
OWSAssert ( ! [ attachment hasError ] ) ;
OWSAssert ( [ attachment mimeType ] . length > 0 ) ;
2017-04-07 04:04:10 +02:00
DDLogVerbose ( @ "Sending attachment. Size in bytes: %lu, contentType: %@" ,
( unsigned long ) attachment . data . length ,
[ attachment mimeType ] ) ;
[ ThreadUtil sendMessageWithAttachment : attachment inThread : self . thread messageSender : self . messageSender ] ;
TSOutgoingMessage * message ;
OWSDisappearingMessagesConfiguration * configuration =
2016-09-21 14:37:51 +02:00
[ OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID : self . thread . uniqueId ] ;
2017-04-07 04:04:10 +02:00
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-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 : ^ {
2017-03-10 14:56:12 +01:00
NSData * videoData = [ NSData dataWithContentsOfURL : compressedVideoUrl ] ;
SignalAttachment * attachment = [ SignalAttachment videoAttachmentWithData : videoData
dataUTI : ( NSString * ) kUTTypeMPEG4 ] ;
if ( ! attachment ||
[ attachment hasError ] ) {
2017-03-12 02:34:38 +01:00
DDLogWarn ( @ "%@ %s Invalid attachment: %@." ,
self . tag ,
__PRETTY _FUNCTION __ ,
2017-03-27 17:02:28 +02:00
attachment ? [ attachment errorName ] : @ "Missing data" ) ;
2017-03-24 03:32:42 +01:00
[ self showErrorAlertForAttachment : attachment ] ;
2017-03-10 14:56:12 +01:00
} else {
2017-04-07 04:04:10 +02:00
[ self tryToSendAttachmentIfApproved : attachment ] ;
2017-03-10 14:56:12 +01:00
}
NSError * error ;
[ [ 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
}
2014-12-26 21:17:43 +01:00
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 ;
}
2015-03-19 01:59:44 +01:00
- ( void ) yapDatabaseModified : ( NSNotification * ) notification {
2017-03-11 02:43:33 +01:00
// Currently , we update thread and message state every time
// the database is modified . That doesn ' t seem optimal , but
// in practice it ' s efficient enough .
2017-03-11 14:01:45 +01:00
// We need to ` beginLongLivedReadTransaction` before we update our
// models in order to jump to the most recent commit .
NSArray * notifications = [ self . uiDatabaseConnection beginLongLivedReadTransaction ] ;
2017-04-09 21:31:31 +02:00
[ self updateBackButtonUnreadCount ] ;
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
} ] ;
2017-03-11 02:43:33 +01:00
[ self setNavigationTitle ] ;
2014-12-24 02:25:10 +01:00
}
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-03-19 01:59:44 +01:00
if ( [ sectionChanges count ] = = 0 & [ messageRowChanges count ] = = 0 ) {
2014-12-08 23:12:22 +01:00
return ;
}
2017-03-15 18:46:03 +01:00
const CGFloat kIsAtBottomTolerancePts = 5 ;
BOOL wasAtBottom = ( self . collectionView . contentOffset . y +
self . collectionView . bounds . size . height +
kIsAtBottomTolerancePts >=
self . collectionView . contentSize . height ) ;
// We want sending messages to feel snappy . So , if the only
// update is a new outgoing message AND we ' re already scrolled to
// the bottom of the conversation , skip the scroll animation .
__block BOOL shouldAnimateScrollToBottom = ! wasAtBottom ;
2017-04-10 22:25:12 +02:00
// We want to scroll to the bottom if the user :
//
// a ) already was at the bottom of the conversation .
// b ) is inserting new interactions .
__block BOOL scrollToBottom = wasAtBottom ;
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 ] ;
}
2017-03-15 18:46:03 +01:00
2015-12-22 12:45:09 +01:00
break ;
}
case YapDatabaseViewChangeInsert : {
[ self . collectionView insertItemsAtIndexPaths : @ [ rowChange . newIndexPath ] ] ;
scrollToBottom = YES ;
2017-03-15 18:46:03 +01:00
TSInteraction * interaction = [ self interactionAtIndexPath : rowChange . newIndexPath ] ;
if ( ! [ interaction isKindOfClass : [ TSOutgoingMessage class ] ] ) {
shouldAnimateScrollToBottom = YES ;
}
2015-12-22 12:45:09 +01:00
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 ) {
2017-03-15 18:46:03 +01:00
[ self scrollToBottomAnimated : shouldAnimateScrollToBottom ] ;
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 ) {
2017-03-10 14:56:12 +01:00
NSData * audioData = [ NSData dataWithContentsOfURL : recorder . url ] ;
SignalAttachment * attachment = [ SignalAttachment audioAttachmentWithData : audioData
dataUTI : ( NSString * ) kUTTypeMPEG4Audio ] ;
if ( ! attachment ||
[ attachment hasError ] ) {
2017-03-12 02:34:38 +01:00
DDLogWarn ( @ "%@ %s Invalid attachment: %@." ,
self . tag ,
__PRETTY _FUNCTION __ ,
2017-03-27 17:02:28 +02:00
attachment ? [ attachment errorName ] : @ "Missing data" ) ;
2017-03-24 03:32:42 +01:00
[ self showErrorAlertForAttachment : attachment ] ;
2017-03-10 14:56:12 +01:00
} else {
2017-04-07 04:04:10 +02:00
[ self tryToSendAttachmentIfApproved : attachment ] ;
2017-03-10 14:56:12 +01:00
}
2015-01-22 05:08:12 +01:00
}
}
2014-11-25 16:38:33 +01:00
# pragma mark Accessory View
2015-03-19 01:59:44 +01:00
- ( void ) didPressAccessoryButton : ( UIButton * ) sender {
2017-04-04 18:35:52 +02:00
if ( [ self isBlockedContactConversation ] ) {
__weak MessagesViewController * weakSelf = self ;
2017-04-04 21:54:11 +02:00
[ self showUnblockContactUI : ^ ( BOOL isBlocked ) {
if ( ! isBlocked ) {
[ weakSelf didPressAccessoryButton : nil ] ;
}
} ] ;
2017-04-04 18:35:52 +02:00
return ;
}
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
2017-04-09 21:31:31 +02:00
- ( void ) updateBackButtonUnreadCount
{
AssertIsOnMainThread ( ) ;
self . backButtonUnreadCount = [ self . messagesManager unreadMessagesCountExcept : self . thread ] ;
}
- ( void ) setBackButtonUnreadCount : ( NSUInteger ) unreadCount
{
AssertIsOnMainThread ( ) ;
if ( _backButtonUnreadCount = = unreadCount ) {
// No need to re - render same count .
return ;
2015-05-23 15:54:50 +02:00
}
2017-04-09 21:31:31 +02:00
_backButtonUnreadCount = unreadCount ;
OWSAssert ( _backButtonUnreadCountView ! = nil ) ;
_backButtonUnreadCountView . hidden = unreadCount <= 0 ;
OWSAssert ( _backButtonUnreadCountLabel ! = nil ) ;
_backButtonUnreadCountLabel . text = [ NSString stringWithFormat : @ "%lu" , ( unsigned long ) unreadCount ] ;
2015-05-23 15:54:50 +02:00
}
2015-12-26 17:27:27 +01:00
# pragma mark 3 D Touch Preview Actions
- ( NSArray < id < UIPreviewActionItem > > * ) previewActionItems {
return @ [ ] ;
}
2017-02-15 22:09:57 +01:00
# pragma mark - Event Handling
- ( void ) navigationTitleTapped : ( UIGestureRecognizer * ) gestureRecognizer {
if ( gestureRecognizer . state = = UIGestureRecognizerStateRecognized ) {
[ self showConversationSettings ] ;
}
}
2017-03-10 14:56:12 +01:00
2017-03-27 23:03:36 +02:00
# ifdef DEBUG
- ( void ) navigationTitleLongPressed : ( UIGestureRecognizer * ) gestureRecognizer {
if ( gestureRecognizer . state = = UIGestureRecognizerStateBegan ) {
[ DebugUITableViewController presentDebugUIForThread : self . thread
fromViewController : self ] ;
}
}
# endif
2017-03-10 14:56:12 +01:00
# pragma mark - JSQMessagesComposerTextViewPasteDelegate
- ( BOOL ) composerTextView : ( JSQMessagesComposerTextView * ) textView
shouldPasteWithSender : ( id ) sender {
return YES ;
}
# pragma mark - OWSTextViewPasteDelegate
- ( void ) didPasteAttachment : ( SignalAttachment * _Nullable ) attachment {
DDLogError ( @ "%@ %s" ,
self . tag ,
__PRETTY _FUNCTION __ ) ;
2017-04-04 18:35:52 +02:00
2017-04-07 04:04:10 +02:00
[ self tryToSendAttachmentIfApproved : attachment ] ;
}
2017-04-04 18:35:52 +02:00
2017-04-07 04:04:10 +02:00
- ( void ) tryToSendAttachmentIfApproved : ( SignalAttachment * _Nullable ) attachment
{
DDLogError ( @ "%@ %s" , self . tag , __PRETTY _FUNCTION __ ) ;
DispatchMainThreadSafe ( ^ {
if ( [ self isBlockedContactConversation ] ) {
__weak MessagesViewController * weakSelf = self ;
[ self showUnblockContactUI : ^ ( BOOL isBlocked ) {
if ( ! isBlocked ) {
2017-04-12 00:18:50 +02:00
[ weakSelf tryToSendAttachmentIfApproved : attachment ] ;
2017-04-07 04:04:10 +02:00
}
} ] ;
return ;
}
if ( attachment = = nil || [ attachment hasError ] ) {
DDLogWarn ( @ "%@ %s Invalid attachment: %@." ,
self . tag ,
__PRETTY _FUNCTION __ ,
attachment ? [ attachment errorName ] : @ "Missing data" ) ;
[ self showErrorAlertForAttachment : attachment ] ;
} else {
__weak MessagesViewController * weakSelf = self ;
UIViewController * viewController =
[ [ AttachmentApprovalViewController alloc ] initWithAttachment : attachment
successCompletion : ^ {
[ weakSelf sendMessageAttachment : attachment ] ;
} ] ;
UINavigationController * navigationController =
[ [ UINavigationController alloc ] initWithRootViewController : viewController ] ;
[ self . navigationController presentViewController : navigationController animated : YES completion : nil ] ;
}
} ) ;
2017-03-10 14:56:12 +01:00
}
2017-03-24 03:32:42 +01:00
- ( void ) showErrorAlertForAttachment : ( SignalAttachment * _Nullable ) attachment {
OWSAssert ( attachment = = nil || [ attachment hasError ] ) ;
NSString * errorMessage = ( attachment
2017-03-27 17:02:28 +02:00
? [ attachment localizedErrorDescription ]
2017-03-24 03:32:42 +01:00
: [ SignalAttachment missingDataErrorMessage ] ) ;
DDLogError ( @ "%@ %s: %@" ,
self . tag ,
__PRETTY _FUNCTION __ , errorMessage ) ;
UIAlertController * controller =
[ UIAlertController alertControllerWithTitle : NSLocalizedString ( @ "ATTACHMENT_ERROR_ALERT_TITLE" ,
@ "The title of the 'attachment error' alert." )
message : errorMessage
preferredStyle : UIAlertControllerStyleAlert ] ;
[ controller addAction : [ UIAlertAction actionWithTitle : NSLocalizedString ( @ "OK" , nil )
style : UIAlertActionStyleDefault
handler : nil ] ] ;
[ self presentViewController : controller
animated : YES
completion : nil ] ;
}
2017-04-05 00:08:51 +02:00
# pragma mark - UIScrollViewDelegate
- ( void ) scrollViewWillBeginDragging : ( UIScrollView * ) scrollView
{
self . userHasScrolled = YES ;
}
2017-03-10 14:56:12 +01:00
# pragma mark - Class methods
+ ( UINib * ) nib
{
return [ UINib nibWithNibName : NSStringFromClass ( [ MessagesViewController class ] )
bundle : [ NSBundle bundleForClass : [ MessagesViewController class ] ] ] ;
}
+ ( instancetype ) messagesViewController
{
return [ [ [ self class ] alloc ] initWithNibName : NSStringFromClass ( [ MessagesViewController class ] )
bundle : [ NSBundle bundleForClass : [ MessagesViewController class ] ] ] ;
}
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