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-05-19 19:23:46 +02:00
|
|
|
|
#import "ContactsViewHelper.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"
|
2017-04-20 15:44:25 +02:00
|
|
|
|
#import "OWSAudioAttachmentPlayer.h"
|
2016-09-02 16:22:06 +02:00
|
|
|
|
#import "OWSCall.h"
|
|
|
|
|
#import "OWSContactsManager.h"
|
2016-09-21 14:37:51 +02:00
|
|
|
|
#import "OWSConversationSettingsTableViewController.h"
|
2017-04-28 18:18:42 +02:00
|
|
|
|
#import "OWSConversationSettingsViewDelegate.h"
|
2016-09-21 14:37:51 +02:00
|
|
|
|
#import "OWSDisappearingMessagesJob.h"
|
|
|
|
|
#import "OWSExpirableMessageView.h"
|
|
|
|
|
#import "OWSIncomingMessageCollectionViewCell.h"
|
2017-04-19 03:27:59 +02:00
|
|
|
|
#import "OWSMessageCollectionViewCell.h"
|
2016-09-02 16:22:06 +02:00
|
|
|
|
#import "OWSMessagesBubblesSizeCalculator.h"
|
2017-05-30 19:04:43 +02:00
|
|
|
|
#import "OWSMessagesComposerTextView.h"
|
|
|
|
|
#import "OWSMessagesInputToolbar.h"
|
|
|
|
|
#import "OWSMessagesToolbarContentView.h"
|
2016-09-21 14:37:51 +02:00
|
|
|
|
#import "OWSOutgoingMessageCollectionViewCell.h"
|
2017-06-02 21:49:34 +02:00
|
|
|
|
#import "OWSSystemMessageCell.h"
|
2017-05-16 17:26:01 +02:00
|
|
|
|
#import "OWSUnreadIndicatorCell.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-05-16 21:52:19 +02:00
|
|
|
|
#import "TSUnreadIndicatorInteraction.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"
|
2017-04-13 19:43:09 +02:00
|
|
|
|
#import "ViewControllerUtils.h"
|
2017-05-30 19:04:43 +02:00
|
|
|
|
#import <AVFoundation/AVFoundation.h>
|
2016-09-02 16:22:06 +02:00
|
|
|
|
#import <AddressBookUI/AddressBookUI.h>
|
2017-04-13 18:55:21 +02:00
|
|
|
|
#import <AssetsLibrary/AssetsLibrary.h>
|
2016-09-02 16:22:06 +02:00
|
|
|
|
#import <ContactsUI/CNContactViewController.h>
|
|
|
|
|
#import <JSQMessagesViewController/JSQMessagesBubbleImage.h>
|
|
|
|
|
#import <JSQMessagesViewController/JSQMessagesBubbleImageFactory.h>
|
|
|
|
|
#import <JSQMessagesViewController/JSQMessagesCollectionViewFlowLayoutInvalidationContext.h>
|
2017-06-01 03:12:27 +02:00
|
|
|
|
#import <JSQMessagesViewController/JSQMessagesCollectionViewLayoutAttributes.h>
|
2016-09-02 16:22:06 +02:00
|
|
|
|
#import <JSQMessagesViewController/JSQMessagesTimestampFormatter.h>
|
|
|
|
|
#import <JSQMessagesViewController/JSQSystemSoundPlayer+JSQMessages.h>
|
|
|
|
|
#import <JSQMessagesViewController/UIColor+JSQMessages.h>
|
|
|
|
|
#import <JSQSystemSoundPlayer.h>
|
2017-05-30 19:04:43 +02:00
|
|
|
|
#import <MediaPlayer/MediaPlayer.h>
|
2016-09-02 16:22:06 +02:00
|
|
|
|
#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>
|
2017-07-21 17:49:38 +02:00
|
|
|
|
#import <SignalServiceKit/NSDate+OWS.h>
|
2017-05-09 20:39:15 +02:00
|
|
|
|
#import <SignalServiceKit/NSTimer+OWS.h>
|
2017-05-19 19:23:46 +02:00
|
|
|
|
#import <SignalServiceKit/OWSAddToContactsOfferMessage.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>
|
2017-06-09 19:12:33 +02:00
|
|
|
|
#import <SignalServiceKit/OWSIdentityManager.h>
|
2016-10-14 22:59:58 +02:00
|
|
|
|
#import <SignalServiceKit/OWSMessageSender.h>
|
2017-05-19 19:23:46 +02:00
|
|
|
|
#import <SignalServiceKit/OWSUnknownContactBlockOfferMessage.h>
|
2017-06-07 22:51:22 +02:00
|
|
|
|
#import <SignalServiceKit/OWSVerificationStateChangeMessage.h>
|
2016-09-02 16:22:06 +02:00
|
|
|
|
#import <SignalServiceKit/SignalRecipient.h>
|
|
|
|
|
#import <SignalServiceKit/TSAccountManager.h>
|
2017-05-30 19:04:43 +02:00
|
|
|
|
#import <SignalServiceKit/TSGroupModel.h>
|
2017-06-08 05:30:51 +02:00
|
|
|
|
#import <SignalServiceKit/TSInvalidIdentityKeyReceivingErrorMessage.h>
|
2016-10-14 22:59:58 +02:00
|
|
|
|
#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;
|
|
|
|
|
|
2017-05-26 18:59:31 +02:00
|
|
|
|
// Always load up to 50 messages when user arrives.
|
2017-05-26 00:00:41 +02:00
|
|
|
|
static const int kYapDatabasePageSize = 50;
|
|
|
|
|
// Never show more than 50*50 = 2,500 messages in conversation view at a time.
|
|
|
|
|
static const int kYapDatabaseMaxPageCount = 50;
|
|
|
|
|
// Never show more than 6*50 = 300 messages in conversation view when user
|
|
|
|
|
// arrives.
|
|
|
|
|
static const int kYapDatabaseMaxInitialPageCount = 6;
|
|
|
|
|
static const int kYapDatabaseRangeMaxLength = kYapDatabasePageSize * kYapDatabaseMaxPageCount;
|
|
|
|
|
static const int kYapDatabaseRangeMinLength = 0;
|
|
|
|
|
static const int JSQ_TOOLBAR_ICON_HEIGHT = 22;
|
|
|
|
|
static const int JSQ_TOOLBAR_ICON_WIDTH = 22;
|
|
|
|
|
static const int JSQ_IMAGE_INSET = 5;
|
2014-12-31 13:22:40 +01:00
|
|
|
|
|
2017-07-21 17:49:38 +02:00
|
|
|
|
static NSTimeInterval const kTSMessageSentDateShowTimeInterval = 5 * kMinuteInterval;
|
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-06-01 03:12:27 +02:00
|
|
|
|
@protocol OWSMessagesCollectionViewFlowLayoutDelegate <NSObject>
|
|
|
|
|
|
2017-06-16 23:18:09 +02:00
|
|
|
|
// Returns YES for all but the unread indicator
|
|
|
|
|
- (BOOL)shouldShowCellDecorationsAtIndexPath:(NSIndexPath *)indexPath;
|
2017-06-01 03:12:27 +02:00
|
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
|
|
|
|
@interface OWSMessagesCollectionViewFlowLayout : JSQMessagesCollectionViewFlowLayout
|
|
|
|
|
|
|
|
|
|
@property (nonatomic, weak) id<OWSMessagesCollectionViewFlowLayoutDelegate> delegate;
|
|
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
|
|
|
|
@implementation OWSMessagesCollectionViewFlowLayout
|
|
|
|
|
|
|
|
|
|
- (CGSize)sizeForItemAtIndexPath:(NSIndexPath *)indexPath
|
|
|
|
|
{
|
|
|
|
|
// The unread indicator should be sized according to its desired size.
|
2017-06-16 23:18:09 +02:00
|
|
|
|
if ([self.delegate shouldShowCellDecorationsAtIndexPath:indexPath]) {
|
|
|
|
|
return [super sizeForItemAtIndexPath:indexPath];
|
|
|
|
|
} else {
|
2017-06-01 03:12:27 +02:00
|
|
|
|
CGSize messageBubbleSize = [self messageBubbleSizeForItemAtIndexPath:indexPath];
|
|
|
|
|
CGFloat finalHeight = messageBubbleSize.height;
|
|
|
|
|
return CGSizeMake(CGRectGetWidth(self.collectionView.frame), ceilf((float)finalHeight));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
2017-05-23 17:28:59 +02:00
|
|
|
|
#pragma mark -
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
@interface MessagesViewController () <AVAudioPlayerDelegate,
|
|
|
|
|
ContactsViewHelperDelegate,
|
|
|
|
|
ContactEditingDelegate,
|
|
|
|
|
CNContactViewControllerDelegate,
|
|
|
|
|
JSQMessagesComposerTextViewPasteDelegate,
|
|
|
|
|
OWSConversationSettingsViewDelegate,
|
2017-06-01 03:12:27 +02:00
|
|
|
|
OWSMessagesCollectionViewFlowLayoutDelegate,
|
2017-06-06 15:46:00 +02:00
|
|
|
|
OWSSystemMessageCellDelegate,
|
2017-04-20 23:51:45 +02:00
|
|
|
|
OWSTextViewPasteDelegate,
|
2017-05-30 17:04:58 +02:00
|
|
|
|
OWSVoiceMemoGestureDelegate,
|
2017-04-20 23:51:45 +02:00
|
|
|
|
UIDocumentMenuDelegate,
|
2017-05-19 19:23:46 +02:00
|
|
|
|
UIDocumentPickerDelegate,
|
2017-05-31 20:22:32 +02:00
|
|
|
|
UIGestureRecognizerDelegate,
|
|
|
|
|
UIImagePickerControllerDelegate,
|
|
|
|
|
UINavigationControllerDelegate,
|
|
|
|
|
UITextViewDelegate>
|
2014-10-29 21:58:58 +01:00
|
|
|
|
|
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;
|
|
|
|
|
|
2017-04-20 15:44:25 +02:00
|
|
|
|
@property (nonatomic) MPMoviePlayerController *videoPlayer;
|
|
|
|
|
@property (nonatomic) AVAudioRecorder *audioRecorder;
|
|
|
|
|
@property (nonatomic) OWSAudioAttachmentPlayer *audioAttachmentPlayer;
|
2017-05-17 23:17:37 +02:00
|
|
|
|
@property (nonatomic) NSUUID *voiceMessageUUID;
|
2017-03-15 14:23:21 +01:00
|
|
|
|
|
|
|
|
|
@property (nonatomic) NSTimer *readTimer;
|
|
|
|
|
@property (nonatomic) UIView *navigationBarTitleView;
|
|
|
|
|
@property (nonatomic) UILabel *navigationBarTitleLabel;
|
|
|
|
|
@property (nonatomic) UILabel *navigationBarSubtitleLabel;
|
|
|
|
|
@property (nonatomic) UIButton *attachButton;
|
2017-06-09 22:21:59 +02:00
|
|
|
|
@property (nonatomic) UIView *bannerView;
|
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;
|
|
|
|
|
|
2017-03-15 14:23:21 +01:00
|
|
|
|
@property (nonatomic) NSUInteger page;
|
2015-12-22 12:45:09 +01:00
|
|
|
|
@property (nonatomic) BOOL composeOnOpen;
|
2017-04-18 22:08:01 +02:00
|
|
|
|
@property (nonatomic) BOOL callOnOpen;
|
2015-12-22 12:45:09 +01:00
|
|
|
|
@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-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;
|
2017-05-17 23:17:37 +02:00
|
|
|
|
@property (nonatomic) NSDate *lastMessageSentDate;
|
2017-05-16 21:52:19 +02:00
|
|
|
|
@property (nonatomic) NSTimer *scrollLaterTimer;
|
2014-10-29 21:58:58 +01:00
|
|
|
|
|
2017-05-19 19:23:46 +02:00
|
|
|
|
@property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper;
|
2017-05-31 20:22:32 +02:00
|
|
|
|
@property (nonatomic, nullable) ThreadDynamicInteractions *dynamicInteractions;
|
2017-05-19 21:19:51 +02:00
|
|
|
|
@property (nonatomic) BOOL hasClearedUnreadMessagesIndicator;
|
2017-05-31 20:22:32 +02:00
|
|
|
|
@property (nonatomic) uint64_t lastVisibleTimestamp;
|
2017-05-19 21:19:51 +02:00
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
@property (nonatomic, readonly) BOOL isGroupConversation;
|
|
|
|
|
@property (nonatomic) BOOL isUserScrolling;
|
|
|
|
|
|
|
|
|
|
@property (nonatomic) UIView *scrollDownButton;
|
2017-05-19 19:23:46 +02:00
|
|
|
|
|
2017-07-25 18:52:30 +02:00
|
|
|
|
@property (nonatomic) BOOL isViewVisible;
|
|
|
|
|
@property (nonatomic) BOOL isAppInBackground;
|
|
|
|
|
@property (nonatomic) BOOL shouldObserveDBModifications;
|
|
|
|
|
|
2015-05-23 15:54:50 +02:00
|
|
|
|
@end
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
#pragma mark -
|
|
|
|
|
|
2014-10-29 21:58:58 +01:00
|
|
|
|
@implementation MessagesViewController
|
|
|
|
|
|
2016-07-13 21:17:09 +02:00
|
|
|
|
- (void)dealloc
|
|
|
|
|
{
|
2017-07-25 19:23:23 +02:00
|
|
|
|
// Surface memory leaks by logging the deallocation of view controllers.
|
|
|
|
|
DDLogVerbose(@"Dealloc: %@", self.class);
|
|
|
|
|
|
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-05-30 19:04:43 +02:00
|
|
|
|
|
2017-03-10 14:56:12 +01:00
|
|
|
|
return self;
|
|
|
|
|
}
|
2016-11-01 20:02:15 +01:00
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil
|
|
|
|
|
{
|
2017-03-10 14:56:12 +01:00
|
|
|
|
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
|
|
|
|
|
if (!self) {
|
|
|
|
|
return self;
|
|
|
|
|
}
|
2017-05-30 19:04:43 +02:00
|
|
|
|
|
2017-03-10 14:56:12 +01:00
|
|
|
|
[self commonInit];
|
2017-05-30 19:04:43 +02:00
|
|
|
|
|
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-10-14 22:59:58 +02:00
|
|
|
|
_messagesManager = [TSMessagesManager sharedManager];
|
|
|
|
|
_networkManager = [TSNetworkManager sharedManager];
|
2017-04-04 18:35:52 +02:00
|
|
|
|
_blockingManager = [OWSBlockingManager sharedManager];
|
2017-05-19 19:23:46 +02:00
|
|
|
|
_contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self];
|
2017-04-04 18:35:52 +02:00
|
|
|
|
|
|
|
|
|
[self addNotificationListeners];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)addNotificationListeners
|
|
|
|
|
{
|
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
|
|
|
selector:@selector(blockedPhoneNumbersDidChange:)
|
|
|
|
|
name:kNSNotificationName_BlockedPhoneNumbersDidChange
|
|
|
|
|
object:nil];
|
2017-06-09 19:12:33 +02:00
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
|
|
|
selector:@selector(identityStateDidChange:)
|
|
|
|
|
name:kNSNotificationName_IdentityStateDidChange
|
|
|
|
|
object:nil];
|
2017-07-25 18:52:30 +02:00
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
|
|
|
selector:@selector(didChangePreferredContentSize:)
|
|
|
|
|
name:UIContentSizeCategoryDidChangeNotification
|
|
|
|
|
object:nil];
|
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
|
|
|
selector:@selector(yapDatabaseModified:)
|
|
|
|
|
name:YapDatabaseModifiedNotification
|
|
|
|
|
object:nil];
|
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
|
|
|
selector:@selector(applicationWillEnterForeground:)
|
|
|
|
|
name:UIApplicationWillEnterForegroundNotification
|
|
|
|
|
object:nil];
|
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
|
|
|
selector:@selector(applicationDidEnterBackground:)
|
|
|
|
|
name:UIApplicationDidEnterBackgroundNotification
|
|
|
|
|
object:nil];
|
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
|
|
|
selector:@selector(applicationWillResignActive:)
|
|
|
|
|
name:UIApplicationWillResignActiveNotification
|
|
|
|
|
object:nil];
|
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
|
|
|
selector:@selector(cancelReadTimer)
|
|
|
|
|
name:UIApplicationDidEnterBackgroundNotification
|
|
|
|
|
object:nil];
|
2017-04-04 18:35:52 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)blockedPhoneNumbersDidChange:(id)notification
|
|
|
|
|
{
|
2017-06-09 22:21:59 +02:00
|
|
|
|
OWSAssert([NSThread isMainThread]);
|
|
|
|
|
|
|
|
|
|
[self ensureBannerState];
|
2016-09-11 22:53:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-06-09 19:12:33 +02:00
|
|
|
|
- (void)identityStateDidChange:(NSNotification *)notification
|
|
|
|
|
{
|
|
|
|
|
OWSAssert([NSThread isMainThread]);
|
|
|
|
|
|
|
|
|
|
[self updateNavigationBarSubtitleLabel];
|
2017-06-09 22:21:59 +02:00
|
|
|
|
[self ensureBannerState];
|
2017-06-09 19:12:33 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (void)peekSetup
|
|
|
|
|
{
|
2015-10-31 16:53:32 +01:00
|
|
|
|
_peek = YES;
|
|
|
|
|
[self setComposeOnOpen:NO];
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (void)popped
|
|
|
|
|
{
|
2015-10-31 16:53:32 +01:00
|
|
|
|
_peek = NO;
|
|
|
|
|
[self hideInputIfNeeded];
|
|
|
|
|
}
|
|
|
|
|
|
2017-04-18 22:08:01 +02:00
|
|
|
|
- (void)configureForThread:(TSThread *)thread
|
|
|
|
|
keyboardOnViewAppearing:(BOOL)keyboardOnViewAppearing
|
|
|
|
|
callOnViewAppearing:(BOOL)callOnViewAppearing
|
|
|
|
|
{
|
|
|
|
|
if (callOnViewAppearing) {
|
|
|
|
|
keyboardOnViewAppearing = NO;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_thread = thread;
|
2017-05-31 20:22:32 +02:00
|
|
|
|
_isGroupConversation = [self.thread isKindOfClass:[TSGroupThread class]];
|
2017-04-18 22:08:01 +02:00
|
|
|
|
_composeOnOpen = keyboardOnViewAppearing;
|
|
|
|
|
_callOnOpen = callOnViewAppearing;
|
2014-11-26 16:00:10 +01:00
|
|
|
|
|
2015-12-26 17:27:27 +01:00
|
|
|
|
[self.uiDatabaseConnection beginLongLivedReadTransaction];
|
|
|
|
|
self.messageMappings =
|
|
|
|
|
[[YapDatabaseViewMappings alloc] initWithGroups:@[ thread.uniqueId ] view:TSMessageDatabaseViewExtensionName];
|
2017-07-26 18:39:43 +02:00
|
|
|
|
// We need to impose the range restrictions on the mappings immediately to avoid
|
|
|
|
|
// doing a great deal of unnecessary work and causing a perf hotspot.
|
2017-07-25 18:52:30 +02:00
|
|
|
|
[self updateMessageMappingRangeOptions];
|
2015-12-26 17:27:27 +01:00
|
|
|
|
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
2017-05-31 20:22:32 +02:00
|
|
|
|
[self.messageMappings updateWithTransaction:transaction];
|
2015-12-26 17:27:27 +01:00
|
|
|
|
}];
|
2017-07-25 18:52:30 +02:00
|
|
|
|
[self updateShouldObserveDBModifications];
|
2017-05-23 00:12:01 +02:00
|
|
|
|
self.page = 0;
|
2017-05-26 00:00:41 +02:00
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
if (self.dynamicInteractions.unreadIndicatorPosition != nil) {
|
|
|
|
|
long unreadIndicatorPosition = [self.dynamicInteractions.unreadIndicatorPosition longValue];
|
2017-05-26 00:00:41 +02:00
|
|
|
|
// If there is an unread indicator, increase the initial load window
|
|
|
|
|
// to include it.
|
|
|
|
|
OWSAssert(unreadIndicatorPosition > 0);
|
|
|
|
|
OWSAssert(unreadIndicatorPosition <= kYapDatabaseRangeMaxLength);
|
|
|
|
|
|
|
|
|
|
// We'd like to include at least N seen messages, if possible,
|
|
|
|
|
// to give the user the context of where they left off the conversation.
|
|
|
|
|
const int kPreferredSeenMessageCount = 1;
|
|
|
|
|
self.page = (NSUInteger)MAX(0,
|
|
|
|
|
MIN(kYapDatabaseMaxInitialPageCount - 1,
|
|
|
|
|
(unreadIndicatorPosition + kPreferredSeenMessageCount) / kYapDatabasePageSize));
|
|
|
|
|
}
|
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]];
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-31 20:22:32 +02: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];
|
2017-05-31 20:22:32 +02: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,
|
2017-05-30 19:04:43 +02:00
|
|
|
|
0,
|
|
|
|
|
JSQ_TOOLBAR_ICON_WIDTH + JSQ_IMAGE_INSET * 2,
|
|
|
|
|
JSQ_TOOLBAR_ICON_HEIGHT + JSQ_IMAGE_INSET * 2)];
|
2017-05-31 20:22:32 +02:00
|
|
|
|
_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
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
self.senderId = ME_MESSAGE_IDENTIFIER;
|
2015-12-26 17:27:27 +01:00
|
|
|
|
self.senderDisplayName = ME_MESSAGE_IDENTIFIER;
|
2017-05-16 19:46:57 +02:00
|
|
|
|
self.automaticallyScrollsToMostRecentMessage = NO;
|
2016-07-22 09:05:24 +02:00
|
|
|
|
|
|
|
|
|
[self initializeToolbars];
|
2017-05-31 20:22:32 +02:00
|
|
|
|
[self createScrollDownButton];
|
2017-07-27 17:43:31 +02:00
|
|
|
|
[self createHeaderViews];
|
2016-07-09 00:25:28 +02:00
|
|
|
|
}
|
2015-12-22 12:45:09 +01:00
|
|
|
|
|
2016-07-09 00:25:28 +02:00
|
|
|
|
- (void)registerCustomMessageNibs
|
|
|
|
|
{
|
2017-06-02 21:49:34 +02:00
|
|
|
|
[self.collectionView registerClass:[OWSSystemMessageCell class]
|
|
|
|
|
forCellWithReuseIdentifier:[OWSSystemMessageCell cellReuseIdentifier]];
|
|
|
|
|
|
2017-05-16 17:26:01 +02:00
|
|
|
|
[self.collectionView registerClass:[OWSUnreadIndicatorCell class]
|
|
|
|
|
forCellWithReuseIdentifier:[OWSUnreadIndicatorCell 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
|
|
|
|
}
|
|
|
|
|
|
2017-05-26 18:59:31 +02:00
|
|
|
|
- (void)applicationWillEnterForeground:(NSNotification *)notification
|
|
|
|
|
{
|
|
|
|
|
[self startReadTimer];
|
|
|
|
|
[self startExpirationTimerAnimations];
|
2017-07-25 18:52:30 +02:00
|
|
|
|
self.isAppInBackground = NO;
|
2017-05-26 18:59:31 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-05-30 18:19:17 +02:00
|
|
|
|
- (void)applicationDidEnterBackground:(NSNotification *)notification
|
|
|
|
|
{
|
2017-07-25 18:52:30 +02:00
|
|
|
|
self.isAppInBackground = YES;
|
2017-05-30 18:19:17 +02:00
|
|
|
|
if (self.hasClearedUnreadMessagesIndicator) {
|
|
|
|
|
self.hasClearedUnreadMessagesIndicator = NO;
|
2017-05-31 20:22:32 +02:00
|
|
|
|
[self.dynamicInteractions clearUnreadIndicatorState];
|
2017-05-30 18:19:17 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-05 04:10:37 +02:00
|
|
|
|
- (void)applicationWillResignActive:(NSNotification *)notification
|
|
|
|
|
{
|
|
|
|
|
[self cancelVoiceMemo];
|
2017-05-31 20:22:32 +02:00
|
|
|
|
self.isUserScrolling = NO;
|
2017-05-05 04:10:37 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-05-31 20:22:32 +02: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.0f];
|
|
|
|
|
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
|
|
|
|
|
{
|
2017-06-19 23:10:34 +02:00
|
|
|
|
DDLogDebug(@"%@ viewWillAppear", self.tag);
|
|
|
|
|
|
2017-05-26 00:00:41 +02:00
|
|
|
|
// We need to update the dynamic interactions before we do any layout.
|
2017-05-31 20:22:32 +02:00
|
|
|
|
[self ensureDynamicInteractions];
|
2017-05-26 00:00:41 +02:00
|
|
|
|
|
2017-06-09 22:21:59 +02:00
|
|
|
|
[self ensureBannerState];
|
2017-05-26 00:00:41 +02:00
|
|
|
|
|
2014-12-24 11:50:07 +01:00
|
|
|
|
[super viewWillAppear:animated];
|
2016-06-17 19:45:48 +02:00
|
|
|
|
|
2017-05-19 19:23:46 +02:00
|
|
|
|
// In case we're dismissing a CNContactViewController which requires default system appearance
|
|
|
|
|
[UIUtil applySignalAppearence];
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
// Since we're using a custom back button, we have to do some extra work to manage the
|
|
|
|
|
// interactivePopGestureRecognizer
|
2017-03-19 20:11:57 +01:00
|
|
|
|
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];
|
|
|
|
|
|
2017-07-25 18:52:30 +02:00
|
|
|
|
self.isViewVisible = YES;
|
2015-12-22 12:45:09 +01:00
|
|
|
|
|
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
|
|
|
|
|
2017-05-01 20:28:37 +02:00
|
|
|
|
// We should have already requested contact access at this point, so this should be a no-op
|
2017-05-26 00:00:41 +02:00
|
|
|
|
// unless it ever becomes possible to load this VC without going via the SignalsViewController.
|
2017-05-01 20:28:37 +02:00
|
|
|
|
[self.contactsManager requestSystemContactsOnce];
|
|
|
|
|
|
2016-09-21 14:37:51 +02:00
|
|
|
|
OWSDisappearingMessagesConfiguration *configuration =
|
|
|
|
|
[OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:self.thread.uniqueId];
|
|
|
|
|
[self setBarButtonItemsForDisappearingMessagesConfiguration:configuration];
|
|
|
|
|
[self setNavigationTitle];
|
|
|
|
|
|
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-04-21 20:58:51 +02:00
|
|
|
|
[UIMenuController sharedMenuController].menuItems = @[
|
|
|
|
|
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_SAVE_ACTION",
|
|
|
|
|
@"Short name for edit menu item to save contents of media message.")
|
|
|
|
|
action:saveSelector],
|
|
|
|
|
[[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
|
|
|
|
|
2017-05-05 03:21:27 +02:00
|
|
|
|
|
2017-05-05 16:10:28 +02:00
|
|
|
|
[((OWSMessagesToolbarContentView *)self.inputToolbar.contentView)ensureSubviews];
|
2017-05-16 19:46:57 +02:00
|
|
|
|
|
2017-05-30 23:09:31 +02:00
|
|
|
|
[self.view layoutSubviews];
|
|
|
|
|
[self scrollToDefaultPosition];
|
|
|
|
|
|
2017-05-16 21:52:19 +02:00
|
|
|
|
[self.scrollLaterTimer invalidate];
|
2017-05-20 00:28:40 +02:00
|
|
|
|
// We want to scroll to the bottom _after_ the layout has been updated.
|
2017-05-16 21:52:19 +02:00
|
|
|
|
self.scrollLaterTimer = [NSTimer weakScheduledTimerWithTimeInterval:0.001f
|
|
|
|
|
target:self
|
|
|
|
|
selector:@selector(scrollToDefaultPosition)
|
|
|
|
|
userInfo:nil
|
|
|
|
|
repeats:NO];
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-16 19:46:57 +02:00
|
|
|
|
- (NSIndexPath *_Nullable)indexPathOfUnreadMessagesIndicator
|
|
|
|
|
{
|
|
|
|
|
int numberOfMessages = (int)[self.messageMappings numberOfItemsInGroup:self.thread.uniqueId];
|
|
|
|
|
for (int i = 0; i < numberOfMessages; i++) {
|
|
|
|
|
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
|
|
|
|
|
id<OWSMessageData> message = [self messageAtIndexPath:indexPath];
|
|
|
|
|
if (message.messageType == TSUnreadIndicatorAdapter) {
|
|
|
|
|
return indexPath;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)scrollToDefaultPosition
|
|
|
|
|
{
|
2017-05-16 21:52:19 +02:00
|
|
|
|
[self.scrollLaterTimer invalidate];
|
|
|
|
|
self.scrollLaterTimer = nil;
|
2017-05-16 20:04:44 +02:00
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
if (self.isUserScrolling) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-16 19:46:57 +02:00
|
|
|
|
NSIndexPath *_Nullable indexPath = [self indexPathOfUnreadMessagesIndicator];
|
|
|
|
|
if (indexPath) {
|
2017-05-26 00:00:41 +02:00
|
|
|
|
if (indexPath.section == 0 && indexPath.row == 0) {
|
|
|
|
|
[self.collectionView setContentOffset:CGPointZero animated:NO];
|
|
|
|
|
} else {
|
|
|
|
|
[self.collectionView scrollToItemAtIndexPath:indexPath
|
|
|
|
|
atScrollPosition:UICollectionViewScrollPositionTop
|
|
|
|
|
animated:NO];
|
|
|
|
|
}
|
2017-05-16 19:46:57 +02:00
|
|
|
|
} else {
|
|
|
|
|
[self scrollToBottomAnimated:NO];
|
|
|
|
|
}
|
2017-06-27 21:03:02 +02:00
|
|
|
|
|
|
|
|
|
[self ensureScrollDownButton];
|
2017-04-10 18:48:29 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-05-26 00:00:41 +02:00
|
|
|
|
- (void)scrollToUnreadIndicatorAnimated
|
|
|
|
|
{
|
|
|
|
|
[self.scrollLaterTimer invalidate];
|
|
|
|
|
self.scrollLaterTimer = nil;
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
if (self.isUserScrolling) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-26 00:00:41 +02:00
|
|
|
|
NSIndexPath *_Nullable indexPath = [self indexPathOfUnreadMessagesIndicator];
|
|
|
|
|
if (indexPath) {
|
|
|
|
|
if (indexPath.section == 0 && indexPath.row == 0) {
|
|
|
|
|
[self.collectionView setContentOffset:CGPointZero animated:YES];
|
|
|
|
|
} else {
|
|
|
|
|
[self.collectionView scrollToItemAtIndexPath:indexPath
|
|
|
|
|
atScrollPosition:UICollectionViewScrollPositionTop
|
|
|
|
|
animated:YES];
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-06-27 21:03:02 +02:00
|
|
|
|
|
|
|
|
|
[self ensureScrollDownButton];
|
2017-05-26 00:00:41 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-04-10 18:48:29 +02:00
|
|
|
|
- (void)resetContentAndLayout
|
|
|
|
|
{
|
2017-04-10 18:44:03 +02:00
|
|
|
|
// Avoid layout corrupt issues and out-of-date message subtitles.
|
2017-05-26 00:00:41 +02:00
|
|
|
|
[self.collectionView.collectionViewLayout
|
|
|
|
|
invalidateLayoutWithContext:[JSQMessagesCollectionViewFlowLayoutInvalidationContext context]];
|
2017-04-04 16:51:32 +02:00
|
|
|
|
[self.collectionView reloadData];
|
2017-04-04 18:35:52 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (void)setUserHasScrolled:(BOOL)userHasScrolled
|
|
|
|
|
{
|
2017-04-05 00:08:51 +02:00
|
|
|
|
_userHasScrolled = userHasScrolled;
|
2017-05-30 19:04:43 +02:00
|
|
|
|
|
2017-06-09 22:21:59 +02:00
|
|
|
|
[self ensureBannerState];
|
2017-04-05 00:08:51 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-06-12 21:50:54 +02:00
|
|
|
|
// Returns a collection of the group members who are "no longer verified".
|
|
|
|
|
- (NSArray<NSString *> *)noLongerVerifiedRecipientIds
|
|
|
|
|
{
|
|
|
|
|
NSMutableArray<NSString *> *result = [NSMutableArray new];
|
|
|
|
|
for (NSString *recipientId in self.thread.recipientIdentifiers) {
|
|
|
|
|
if ([[OWSIdentityManager sharedManager] verificationStateForRecipientId:recipientId]
|
|
|
|
|
== OWSVerificationStateNoLongerVerified) {
|
|
|
|
|
[result addObject:recipientId];
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-06-15 21:19:33 +02:00
|
|
|
|
return [result copy];
|
2017-06-12 21:50:54 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-06-09 22:21:59 +02:00
|
|
|
|
- (void)ensureBannerState
|
2017-04-04 18:35:52 +02:00
|
|
|
|
{
|
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-06-09 22:21:59 +02:00
|
|
|
|
[self.bannerView removeFromSuperview];
|
|
|
|
|
self.bannerView = nil;
|
2017-05-30 19:04:43 +02:00
|
|
|
|
|
2017-04-05 00:08:51 +02:00
|
|
|
|
if (self.userHasScrolled) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-12 21:50:54 +02:00
|
|
|
|
NSArray<NSString *> *noLongerVerifiedRecipientIds = [self noLongerVerifiedRecipientIds];
|
|
|
|
|
|
2017-06-09 22:21:59 +02:00
|
|
|
|
if (noLongerVerifiedRecipientIds.count > 0) {
|
|
|
|
|
NSString *message;
|
|
|
|
|
if (noLongerVerifiedRecipientIds.count > 1) {
|
|
|
|
|
message = NSLocalizedString(@"MESSAGES_VIEW_N_MEMBERS_NO_LONGER_VERIFIED",
|
|
|
|
|
@"Indicates that more than one member of this group conversation is no longer verified.");
|
|
|
|
|
} else {
|
|
|
|
|
NSString *recipientId = [noLongerVerifiedRecipientIds firstObject];
|
|
|
|
|
NSString *displayName = [self.contactsManager displayNameForPhoneIdentifier:recipientId];
|
|
|
|
|
NSString *format
|
|
|
|
|
= (self.isGroupConversation ? NSLocalizedString(@"MESSAGES_VIEW_1_MEMBER_NO_LONGER_VERIFIED_FORMAT",
|
|
|
|
|
@"Indicates that one member of this group conversation is no longer "
|
|
|
|
|
@"verified. Embeds {{user's name or phone number}}.")
|
|
|
|
|
: NSLocalizedString(@"MESSAGES_VIEW_CONTACT_NO_LONGER_VERIFIED_FORMAT",
|
|
|
|
|
@"Indicates that this 1:1 conversation is no longer verified. Embeds "
|
|
|
|
|
@"{{user's name or phone number}}."));
|
|
|
|
|
message = [NSString stringWithFormat:format, displayName];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[self createBannerWithTitle:message
|
|
|
|
|
bannerColor:[UIColor ows_destructiveRedColor]
|
|
|
|
|
tapSelector:@selector(noLongerVerifiedBannerViewWasTapped:)];
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2017-04-04 21:38:00 +02:00
|
|
|
|
NSString *blockStateMessage = nil;
|
|
|
|
|
if ([self isBlockedContactConversation]) {
|
2017-05-31 20:22:32 +02:00
|
|
|
|
blockStateMessage = NSLocalizedString(
|
|
|
|
|
@"MESSAGES_VIEW_CONTACT_BLOCKED", @"Indicates that this 1:1 conversation has been blocked.");
|
|
|
|
|
} else if (self.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",
|
2017-05-30 19:04:43 +02:00
|
|
|
|
@"Indicates that a single member of this group has been blocked.");
|
2017-04-04 21:38:00 +02:00
|
|
|
|
} else if (blockedGroupMemberCount > 1) {
|
2017-05-31 20:22:32 +02:00
|
|
|
|
blockStateMessage =
|
|
|
|
|
[NSString stringWithFormat:NSLocalizedString(@"MESSAGES_VIEW_GROUP_N_MEMBERS_BLOCKED_FORMAT",
|
|
|
|
|
@"Indicates that some members of this group has been blocked. Embeds "
|
|
|
|
|
@"{{the number of blocked users in this group}}."),
|
2017-07-12 16:46:54 +02:00
|
|
|
|
[ViewControllerUtils formatInt:blockedGroupMemberCount]];
|
2017-04-04 21:38:00 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2017-05-30 19:04:43 +02:00
|
|
|
|
|
2017-04-04 21:38:00 +02:00
|
|
|
|
if (blockStateMessage) {
|
2017-06-09 22:21:59 +02:00
|
|
|
|
[self createBannerWithTitle:blockStateMessage
|
|
|
|
|
bannerColor:[UIColor ows_destructiveRedColor]
|
|
|
|
|
tapSelector:@selector(blockBannerViewWasTapped:)];
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-05-31 20:22:32 +02:00
|
|
|
|
|
2017-06-09 22:21:59 +02:00
|
|
|
|
- (void)createBannerWithTitle:(NSString *)title bannerColor:(UIColor *)bannerColor tapSelector:(SEL)tapSelector
|
|
|
|
|
{
|
|
|
|
|
OWSAssert(title.length > 0);
|
|
|
|
|
OWSAssert(bannerColor);
|
|
|
|
|
|
2017-07-11 19:24:47 +02:00
|
|
|
|
UIView *bannerView = [UIView containerView];
|
2017-06-09 22:21:59 +02:00
|
|
|
|
bannerView.backgroundColor = bannerColor;
|
|
|
|
|
bannerView.layer.cornerRadius = 2.5f;
|
2017-05-30 19:04:43 +02:00
|
|
|
|
|
2017-06-09 22:21:59 +02:00
|
|
|
|
// Use a shadow to "pop" the indicator above the other views.
|
|
|
|
|
bannerView.layer.shadowColor = [UIColor blackColor].CGColor;
|
|
|
|
|
bannerView.layer.shadowOffset = CGSizeMake(2, 3);
|
|
|
|
|
bannerView.layer.shadowRadius = 2.f;
|
|
|
|
|
bannerView.layer.shadowOpacity = 0.35f;
|
2017-05-31 20:22:32 +02:00
|
|
|
|
|
2017-06-12 21:50:54 +02:00
|
|
|
|
UILabel *label = [UILabel new];
|
|
|
|
|
label.font = [UIFont ows_mediumFontWithSize:14.f];
|
|
|
|
|
label.text = title;
|
|
|
|
|
label.textColor = [UIColor whiteColor];
|
|
|
|
|
label.numberOfLines = 0;
|
|
|
|
|
label.lineBreakMode = NSLineBreakByWordWrapping;
|
|
|
|
|
label.textAlignment = NSTextAlignmentCenter;
|
|
|
|
|
|
|
|
|
|
UIImage *closeIcon = [UIImage imageNamed:@"banner_close"];
|
|
|
|
|
UIImageView *closeButton = [[UIImageView alloc] initWithImage:closeIcon];
|
|
|
|
|
[bannerView addSubview:closeButton];
|
|
|
|
|
const CGFloat kBannerCloseButtonPadding = 8.f;
|
|
|
|
|
[closeButton autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:kBannerCloseButtonPadding];
|
2017-07-11 19:24:47 +02:00
|
|
|
|
[closeButton autoPinTrailingToSuperViewWithMargin:kBannerCloseButtonPadding];
|
2017-06-12 21:50:54 +02:00
|
|
|
|
[closeButton autoSetDimension:ALDimensionWidth toSize:closeIcon.size.width];
|
|
|
|
|
[closeButton autoSetDimension:ALDimensionHeight toSize:closeIcon.size.height];
|
|
|
|
|
|
2017-06-09 22:21:59 +02:00
|
|
|
|
[bannerView addSubview:label];
|
|
|
|
|
[label autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:5];
|
|
|
|
|
[label autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:5];
|
|
|
|
|
const CGFloat kBannerHPadding = 15.f;
|
2017-07-11 19:24:47 +02:00
|
|
|
|
[label autoPinLeadingToSuperViewWithMargin:kBannerHPadding];
|
2017-06-12 21:50:54 +02:00
|
|
|
|
const CGFloat kBannerHSpacing = 10.f;
|
2017-07-11 19:24:47 +02:00
|
|
|
|
[closeButton autoPinLeadingToTrailingOfView:label margin:kBannerHSpacing];
|
2017-05-31 20:22:32 +02:00
|
|
|
|
|
2017-06-09 22:21:59 +02:00
|
|
|
|
[bannerView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:tapSelector]];
|
2017-05-30 19:04:43 +02:00
|
|
|
|
|
2017-06-09 22:21:59 +02:00
|
|
|
|
[self.view addSubview:bannerView];
|
|
|
|
|
[bannerView autoPinToTopLayoutGuideOfViewController:self withInset:10];
|
|
|
|
|
[bannerView autoHCenterInSuperview];
|
|
|
|
|
|
|
|
|
|
CGFloat labelDesiredWidth = [label sizeThatFits:CGSizeZero].width;
|
2017-06-12 21:50:54 +02:00
|
|
|
|
CGFloat bannerDesiredWidth
|
|
|
|
|
= (labelDesiredWidth + kBannerHPadding + kBannerHSpacing + closeIcon.size.width + kBannerCloseButtonPadding);
|
2017-06-09 22:21:59 +02:00
|
|
|
|
const CGFloat kMinBannerHMargin = 20.f;
|
|
|
|
|
if (bannerDesiredWidth + kMinBannerHMargin * 2.f >= self.view.width) {
|
|
|
|
|
[bannerView autoPinWidthToSuperviewWithMargin:kMinBannerHMargin];
|
2017-04-04 21:38:00 +02:00
|
|
|
|
}
|
2017-06-09 22:21:59 +02:00
|
|
|
|
|
|
|
|
|
[self.view layoutSubviews];
|
|
|
|
|
|
|
|
|
|
self.bannerView = bannerView;
|
2017-04-04 18:35:52 +02:00
|
|
|
|
}
|
2017-05-31 20:22:32 +02:00
|
|
|
|
|
2017-06-09 22:21:59 +02:00
|
|
|
|
- (void)blockBannerViewWasTapped:(UIGestureRecognizer *)sender
|
2017-05-31 20:22:32 +02:00
|
|
|
|
{
|
2017-04-04 21:38:00 +02:00
|
|
|
|
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-05-31 20:22:32 +02:00
|
|
|
|
} else if (self.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-06-09 22:21:59 +02:00
|
|
|
|
- (void)noLongerVerifiedBannerViewWasTapped:(UIGestureRecognizer *)sender
|
|
|
|
|
{
|
|
|
|
|
if (sender.state == UIGestureRecognizerStateRecognized) {
|
2017-06-20 16:24:29 +02:00
|
|
|
|
NSArray<NSString *> *noLongerVerifiedRecipientIds = [self noLongerVerifiedRecipientIds];
|
|
|
|
|
if (noLongerVerifiedRecipientIds.count < 1) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
BOOL hasMultiple = noLongerVerifiedRecipientIds.count > 1;
|
|
|
|
|
|
2017-06-12 21:50:54 +02:00
|
|
|
|
UIAlertController *actionSheetController =
|
|
|
|
|
[UIAlertController alertControllerWithTitle:nil
|
|
|
|
|
message:nil
|
|
|
|
|
preferredStyle:UIAlertControllerStyleActionSheet];
|
|
|
|
|
|
|
|
|
|
__weak MessagesViewController *weakSelf = self;
|
2017-06-15 21:19:33 +02:00
|
|
|
|
UIAlertAction *verifyAction = [UIAlertAction
|
2017-06-20 16:24:29 +02:00
|
|
|
|
actionWithTitle:(hasMultiple ? NSLocalizedString(@"VERIFY_PRIVACY_MULTIPLE",
|
|
|
|
|
@"Label for button or row which allows users to verify the safety "
|
|
|
|
|
@"numbers of multiple users.")
|
|
|
|
|
: NSLocalizedString(@"VERIFY_PRIVACY",
|
|
|
|
|
@"Label for button or row which allows users to verify the safety "
|
|
|
|
|
@"number of another user."))style:UIAlertActionStyleDefault
|
2017-06-12 21:50:54 +02:00
|
|
|
|
handler:^(UIAlertAction *_Nonnull action) {
|
2017-06-20 16:24:29 +02:00
|
|
|
|
[weakSelf showNoLongerVerifiedUI];
|
2017-06-12 21:50:54 +02:00
|
|
|
|
}];
|
2017-06-15 21:19:33 +02:00
|
|
|
|
[actionSheetController addAction:verifyAction];
|
2017-06-12 21:50:54 +02:00
|
|
|
|
|
2017-07-13 20:53:24 +02:00
|
|
|
|
UIAlertAction *dismissAction = [UIAlertAction actionWithTitle:CommonStrings.dismissButton
|
|
|
|
|
style:UIAlertActionStyleCancel
|
|
|
|
|
handler:^(UIAlertAction *_Nonnull action) {
|
|
|
|
|
[weakSelf resetVerificationStateToDefault];
|
|
|
|
|
}];
|
2017-06-12 21:50:54 +02:00
|
|
|
|
[actionSheetController addAction:dismissAction];
|
|
|
|
|
|
|
|
|
|
[self presentViewController:actionSheetController animated:YES completion:nil];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)resetVerificationStateToDefault
|
|
|
|
|
{
|
|
|
|
|
OWSAssert([NSThread isMainThread]);
|
|
|
|
|
|
|
|
|
|
NSArray<NSString *> *noLongerVerifiedRecipientIds = [self noLongerVerifiedRecipientIds];
|
|
|
|
|
for (NSString *recipientId in noLongerVerifiedRecipientIds) {
|
|
|
|
|
OWSAssert(recipientId.length > 0);
|
|
|
|
|
|
|
|
|
|
OWSRecipientIdentity *_Nullable recipientIdentity =
|
|
|
|
|
[[OWSIdentityManager sharedManager] recipientIdentityForRecipientId:recipientId];
|
|
|
|
|
OWSAssert(recipientIdentity);
|
|
|
|
|
|
|
|
|
|
NSData *identityKey = recipientIdentity.identityKey;
|
|
|
|
|
OWSAssert(identityKey.length > 0);
|
|
|
|
|
if (identityKey.length < 1) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[OWSIdentityManager.sharedManager setVerificationState:OWSVerificationStateDefault
|
|
|
|
|
identityKey:identityKey
|
|
|
|
|
recipientId:recipientId
|
2017-06-15 21:19:33 +02:00
|
|
|
|
isUserInitiatedChange:YES];
|
2017-06-09 22:21:59 +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;
|
2017-05-30 19:04:43 +02:00
|
|
|
|
|
2017-04-05 18:16:54 +02:00
|
|
|
|
// 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-05-31 20:22:32 +02:00
|
|
|
|
OWSAssert(self.isGroupConversation);
|
2017-04-05 18:16:54 +02:00
|
|
|
|
OWSAssert([self.thread isKindOfClass:[TSGroupThread class]]);
|
2017-05-30 19:04:43 +02:00
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (void)startReadTimer
|
|
|
|
|
{
|
2017-05-30 18:19:17 +02:00
|
|
|
|
[self.readTimer invalidate];
|
2017-05-31 20:27:46 +02:00
|
|
|
|
self.readTimer = [NSTimer weakScheduledTimerWithTimeInterval:3.f
|
2017-05-30 18:19:17 +02:00
|
|
|
|
target:self
|
|
|
|
|
selector:@selector(readTimerDidFire)
|
|
|
|
|
userInfo:nil
|
|
|
|
|
repeats:YES];
|
2014-12-09 20:11:14 +01:00
|
|
|
|
}
|
|
|
|
|
|
2017-05-23 17:28:12 +02:00
|
|
|
|
- (void)readTimerDidFire
|
|
|
|
|
{
|
2017-05-31 20:22:32 +02:00
|
|
|
|
[self markVisibleMessagesAsRead];
|
2017-05-23 17:28:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (void)cancelReadTimer
|
|
|
|
|
{
|
2014-12-09 20:11:14 +01:00
|
|
|
|
[self.readTimer invalidate];
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-31 20:22:32 +02: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];
|
2017-04-18 22:08:01 +02:00
|
|
|
|
_composeOnOpen = NO;
|
|
|
|
|
}
|
|
|
|
|
if (_callOnOpen) {
|
|
|
|
|
[self callAction:nil];
|
|
|
|
|
_callOnOpen = NO;
|
2015-04-14 21:49:00 +02:00
|
|
|
|
}
|
2017-04-17 21:59:04 +02:00
|
|
|
|
[self updateNavigationBarSubtitleLabel];
|
2017-05-24 22:42:29 +02:00
|
|
|
|
[ProfileFetcherJob runWithThread:self.thread networkManager:self.networkManager];
|
2017-05-31 22:37:08 +02:00
|
|
|
|
|
|
|
|
|
[self markVisibleMessagesAsRead];
|
2014-12-09 20:11:14 +01:00
|
|
|
|
}
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (void)viewWillDisappear:(BOOL)animated
|
|
|
|
|
{
|
2017-06-19 23:10:34 +02:00
|
|
|
|
DDLogDebug(@"%@ viewWillDisappear", self.tag);
|
|
|
|
|
|
2015-12-26 17:27:27 +01:00
|
|
|
|
[super viewWillDisappear:animated];
|
2017-07-25 18:52:30 +02:00
|
|
|
|
|
|
|
|
|
self.isViewVisible = NO;
|
2015-12-26 17:27:27 +01:00
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
// Since we're using a custom back button, we have to do some extra work to manage the
|
|
|
|
|
// interactivePopGestureRecognizer
|
2017-03-19 20:11:57 +01:00
|
|
|
|
self.navigationController.interactivePopGestureRecognizer.delegate = nil;
|
|
|
|
|
|
2017-04-20 15:44:25 +02:00
|
|
|
|
[self.audioAttachmentPlayer stop];
|
2017-04-20 19:43:33 +02:00
|
|
|
|
self.audioAttachmentPlayer = nil;
|
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];
|
2017-05-31 20:27:46 +02:00
|
|
|
|
[self markVisibleMessagesAsRead];
|
2017-05-05 03:21:27 +02:00
|
|
|
|
|
2017-05-05 04:10:37 +02:00
|
|
|
|
[self cancelVoiceMemo];
|
2017-05-31 20:22:32 +02:00
|
|
|
|
|
|
|
|
|
self.isUserScrolling = NO;
|
2014-10-29 21:58:58 +01:00
|
|
|
|
}
|
|
|
|
|
|
2016-10-12 21:18:27 +02:00
|
|
|
|
- (void)startExpirationTimerAnimations
|
|
|
|
|
{
|
|
|
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:OWSMessagesViewControllerDidAppearNotification
|
|
|
|
|
object:nil];
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-31 20:22:32 +02: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;
|
2017-05-31 20:22:32 +02:00
|
|
|
|
if (self.isGroupConversation && [navTitle length] == 0) {
|
2016-09-21 14:37:51 +02:00
|
|
|
|
navTitle = NSLocalizedString(@"NEW_GROUP_DEFAULT_TITLE", @"");
|
|
|
|
|
}
|
2017-02-15 22:09:57 +01:00
|
|
|
|
self.title = nil;
|
|
|
|
|
|
|
|
|
|
if ([navTitle isEqualToString:self.navigationBarTitleLabel.text]) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2017-05-30 19:04:43 +02:00
|
|
|
|
|
2017-02-15 22:09:57 +01:00
|
|
|
|
self.navigationBarTitleLabel.text = navTitle;
|
|
|
|
|
|
|
|
|
|
// Changing the title requires relayout of the nav bar contents.
|
|
|
|
|
OWSDisappearingMessagesConfiguration *configuration =
|
2017-05-30 19:04:43 +02:00
|
|
|
|
[OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:self.thread.uniqueId];
|
2017-02-15 22:09:57 +01:00
|
|
|
|
[self setBarButtonItemsForDisappearingMessagesConfiguration:configuration];
|
2016-09-21 14:37:51 +02:00
|
|
|
|
}
|
2015-01-14 22:30:01 +01:00
|
|
|
|
|
2017-07-27 17:43:31 +02:00
|
|
|
|
- (void)createHeaderViews
|
|
|
|
|
{
|
|
|
|
|
_backButtonUnreadCountView = [UIView new];
|
|
|
|
|
_backButtonUnreadCountView.layer.cornerRadius = self.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];
|
|
|
|
|
_backButtonUnreadCountLabel.textAlignment = NSTextAlignmentCenter;
|
|
|
|
|
|
|
|
|
|
self.navigationBarTitleView = [UIView containerView];
|
|
|
|
|
[self.navigationBarTitleView
|
|
|
|
|
addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self
|
|
|
|
|
action:@selector(navigationTitleTapped:)]];
|
|
|
|
|
#ifdef DEBUG
|
|
|
|
|
[self.navigationBarTitleView addGestureRecognizer:[[UILongPressGestureRecognizer alloc]
|
|
|
|
|
initWithTarget:self
|
|
|
|
|
action:@selector(navigationTitleLongPressed:)]];
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
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];
|
|
|
|
|
[self updateNavigationBarSubtitleLabel];
|
|
|
|
|
[self.navigationBarTitleView addSubview:self.navigationBarSubtitleLabel];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (CGFloat)unreadCountViewDiameter
|
|
|
|
|
{
|
|
|
|
|
return 16;
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
// 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];
|
2017-07-21 16:36:45 +02:00
|
|
|
|
// TODO: The back button assets are assymetrical. There are strong reasons
|
|
|
|
|
// to use spacing in the assets to manipulate the size and positioning of
|
|
|
|
|
// bar button items, but it means we'll probably need separate RTL and LTR
|
|
|
|
|
// flavors of these assets.
|
2017-04-09 21:31:31 +02:00
|
|
|
|
[_backButtonUnreadCountView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:-6];
|
2017-07-11 19:24:47 +02:00
|
|
|
|
[_backButtonUnreadCountView autoPinLeadingToSuperViewWithMargin:1];
|
2017-07-27 17:43:31 +02:00
|
|
|
|
[_backButtonUnreadCountView autoSetDimension:ALDimensionHeight toSize:self.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
|
2017-07-27 17:43:31 +02:00
|
|
|
|
toSize:self.unreadCountViewDiameter
|
2017-04-11 01:28:18 +02:00
|
|
|
|
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-15 22:09:57 +01:00
|
|
|
|
const CGFloat kTitleVSpacing = 0.f;
|
|
|
|
|
// 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];
|
2017-05-31 20:22:32 +02:00
|
|
|
|
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-05-30 19:04:43 +02: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-05-31 20:22:32 +02: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);
|
2017-07-11 18:17:28 +02:00
|
|
|
|
self.navigationBarSubtitleLabel.frame = CGRectMake((self.view.isRTL ? self.navigationBarTitleView.frame.size.width
|
|
|
|
|
- self.navigationBarSubtitleLabel.frame.size.width
|
|
|
|
|
: 0),
|
2017-05-30 19:04:43 +02:00
|
|
|
|
self.navigationBarTitleView.frame.size.height - self.navigationBarSubtitleLabel.frame.size.height,
|
|
|
|
|
titleViewWidth,
|
|
|
|
|
self.navigationBarSubtitleLabel.frame.size.height);
|
|
|
|
|
|
2017-02-15 22:09:57 +01:00
|
|
|
|
self.navigationItem.leftBarButtonItems = @[
|
2017-05-30 19:04:43 +02:00
|
|
|
|
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"];
|
2017-05-31 20:22:32 +02:00
|
|
|
|
[callButton setImage:image 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.5f);
|
2017-02-17 23:30:49 +01:00
|
|
|
|
imageEdgeInsets.right = round((kBarButtonSize - (image.size.width + imageEdgeInsets.left)) * 0.5f);
|
2017-02-17 17:45:26 +01:00
|
|
|
|
imageEdgeInsets.top = round((kBarButtonSize - image.size.height) * 0.5f);
|
|
|
|
|
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-05-31 20:22:32 +02:00
|
|
|
|
[callButton addTarget:self action:@selector(callAction:) forControlEvents:UIControlEventTouchUpInside];
|
|
|
|
|
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"];
|
2017-05-31 20:22:32 +02:00
|
|
|
|
[timerButton setImage:image 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.5f);
|
2017-02-17 23:30:49 +01:00
|
|
|
|
imageEdgeInsets.right = round((kBarButtonSize - (image.size.width + imageEdgeInsets.left)) * 0.5f);
|
2017-02-17 17:45:26 +01:00
|
|
|
|
imageEdgeInsets.top = round((kBarButtonSize - image.size.height) * 0.5f);
|
|
|
|
|
imageEdgeInsets.bottom = round(kBarButtonSize - (image.size.height + imageEdgeInsets.top));
|
|
|
|
|
timerButton.imageEdgeInsets = imageEdgeInsets;
|
2017-05-31 20:22:32 +02: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-05-31 20:22:32 +02: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-05-30 19:04:43 +02:00
|
|
|
|
|
2016-09-21 14:37:51 +02:00
|
|
|
|
self.navigationItem.rightBarButtonItems = [barButtons copy];
|
2015-01-14 22:30:01 +01:00
|
|
|
|
}
|
|
|
|
|
|
2017-04-17 21:50:43 +02:00
|
|
|
|
- (void)updateNavigationBarSubtitleLabel
|
|
|
|
|
{
|
|
|
|
|
NSMutableAttributedString *subtitleText = [NSMutableAttributedString new];
|
2017-06-09 19:12:33 +02:00
|
|
|
|
|
2017-04-17 21:50:43 +02:00
|
|
|
|
if (self.thread.isMuted) {
|
2017-04-17 22:06:39 +02:00
|
|
|
|
// Show a "mute" icon before the navigation bar subtitle if this thread is muted.
|
2017-04-17 21:50:43 +02:00
|
|
|
|
[subtitleText
|
|
|
|
|
appendAttributedString:[[NSAttributedString alloc]
|
|
|
|
|
initWithString:@"\ue067 "
|
|
|
|
|
attributes:@{
|
|
|
|
|
NSFontAttributeName : [UIFont ows_elegantIconsFont:7.f],
|
|
|
|
|
NSForegroundColorAttributeName : [UIColor colorWithWhite:0.9f alpha:1.f],
|
|
|
|
|
}]];
|
|
|
|
|
}
|
2017-06-09 19:12:33 +02:00
|
|
|
|
|
|
|
|
|
BOOL isVerified = YES;
|
|
|
|
|
for (NSString *recipientId in self.thread.recipientIdentifiers) {
|
|
|
|
|
if ([[OWSIdentityManager sharedManager] verificationStateForRecipientId:recipientId]
|
|
|
|
|
!= OWSVerificationStateVerified) {
|
|
|
|
|
isVerified = NO;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (isVerified) {
|
|
|
|
|
// Show a "checkmark" icon before the navigation bar subtitle if this thread is verified.
|
|
|
|
|
[subtitleText
|
|
|
|
|
appendAttributedString:[[NSAttributedString alloc]
|
|
|
|
|
initWithString:@"\uf00c "
|
|
|
|
|
attributes:@{
|
|
|
|
|
NSFontAttributeName : [UIFont ows_fontAwesomeFont:10.f],
|
|
|
|
|
NSForegroundColorAttributeName : [UIColor colorWithWhite:0.9f alpha:1.f],
|
|
|
|
|
}]];
|
|
|
|
|
}
|
|
|
|
|
|
2017-04-17 21:50:43 +02:00
|
|
|
|
[subtitleText
|
|
|
|
|
appendAttributedString:[[NSAttributedString alloc]
|
|
|
|
|
initWithString: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.")
|
|
|
|
|
attributes:@{
|
|
|
|
|
NSFontAttributeName : [UIFont ows_regularFontWithSize:9.f],
|
|
|
|
|
NSForegroundColorAttributeName : [UIColor colorWithWhite:0.9f alpha:1.f],
|
|
|
|
|
}]];
|
|
|
|
|
self.navigationBarSubtitleLabel.attributedText = subtitleText;
|
2017-04-24 17:58:07 +02:00
|
|
|
|
[self.navigationBarSubtitleLabel sizeToFit];
|
2017-04-17 21:50:43 +02: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-05-30 19:04:43 +02:00
|
|
|
|
|
2017-03-10 14:56:12 +01:00
|
|
|
|
OWSAssert(self.inputToolbar.contentView);
|
|
|
|
|
OWSAssert(self.inputToolbar.contentView.textView);
|
|
|
|
|
self.inputToolbar.contentView.textView.pasteDelegate = self;
|
2017-05-31 20:22:32 +02:00
|
|
|
|
((OWSMessagesComposerTextView *)self.inputToolbar.contentView.textView).textViewPasteDelegate = self;
|
2017-05-30 17:04:58 +02:00
|
|
|
|
((OWSMessagesToolbarContentView *)self.inputToolbar.contentView).voiceMemoGestureDelegate = 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
|
|
|
|
{
|
2017-05-30 23:09:31 +02:00
|
|
|
|
CGRect screenBounds = [UIScreen mainScreen].bounds;
|
|
|
|
|
CGRect viewFrame = CGRectMake(0, 0, screenBounds.size.width, screenBounds.size.height);
|
|
|
|
|
self.view.frame = viewFrame;
|
|
|
|
|
self.collectionView.frame = viewFrame;
|
|
|
|
|
|
2017-06-01 03:12:27 +02:00
|
|
|
|
OWSMessagesCollectionViewFlowLayout *layout = [OWSMessagesCollectionViewFlowLayout new];
|
|
|
|
|
layout.delegate = self;
|
|
|
|
|
self.collectionView.collectionViewLayout = layout;
|
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.0f;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Bubbles
|
2017-06-17 19:47:10 +02:00
|
|
|
|
self.collectionView.collectionViewLayout.bubbleSizeCalculator = [OWSMessagesBubblesSizeCalculator new];
|
2014-11-29 19:54:33 +01:00
|
|
|
|
JSQMessagesBubbleImageFactory *bubbleFactory = [[JSQMessagesBubbleImageFactory alloc] init];
|
2017-05-31 20:22:32 +02:00
|
|
|
|
self.incomingBubbleImageData =
|
|
|
|
|
[bubbleFactory incomingMessagesBubbleImageWithColor:[UIColor jsq_messageBubbleLightGrayColor]];
|
2015-12-22 12:45:09 +01:00
|
|
|
|
self.outgoingBubbleImageData = [bubbleFactory outgoingMessagesBubbleImageWithColor:[UIColor ows_materialBlueColor]];
|
2017-05-31 20:22:32 +02:00
|
|
|
|
self.currentlyOutgoingBubbleImageData =
|
|
|
|
|
[bubbleFactory outgoingMessagesBubbleImageWithColor:[UIColor ows_fadedBlueColor]];
|
2016-07-09 00:25:28 +02:00
|
|
|
|
self.outgoingMessageFailedImageData = [bubbleFactory outgoingMessagesBubbleImageWithColor:[UIColor grayColor]];
|
2014-11-29 19:54:33 +01:00
|
|
|
|
}
|
|
|
|
|
|
2017-05-25 19:45:42 +02:00
|
|
|
|
#pragma mark - Identity
|
|
|
|
|
|
|
|
|
|
/**
|
2017-05-27 03:19:46 +02:00
|
|
|
|
* Shows confirmation dialog if at least one of the recipient id's is not confirmed.
|
|
|
|
|
*
|
|
|
|
|
* returns YES if an alert was shown
|
|
|
|
|
* NO if there were no unconfirmed identities
|
2017-05-25 19:45:42 +02:00
|
|
|
|
*/
|
2017-05-27 03:19:46 +02:00
|
|
|
|
- (BOOL)showSafetyNumberConfirmationIfNecessaryWithConfirmationText:(NSString *)confirmationText
|
2017-06-07 20:27:24 +02:00
|
|
|
|
completion:(void (^)(BOOL didConfirmIdentity))completionHandler
|
2017-05-27 03:19:46 +02:00
|
|
|
|
{
|
2017-06-01 03:12:18 +02:00
|
|
|
|
return [SafetyNumberConfirmationAlert presentAlertIfNecessaryWithRecipientIds:self.thread.recipientIdentifiers
|
|
|
|
|
confirmationText:confirmationText
|
|
|
|
|
contactsManager:self.contactsManager
|
|
|
|
|
completion:completionHandler];
|
2017-05-25 19:45:42 +02:00
|
|
|
|
}
|
2014-10-29 21:58:58 +01:00
|
|
|
|
|
2017-06-07 22:51:22 +02:00
|
|
|
|
- (void)showFingerprintWithRecipientId:(NSString *)recipientId
|
2016-09-11 22:53:12 +02:00
|
|
|
|
{
|
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];
|
|
|
|
|
|
2017-06-15 22:20:33 +02:00
|
|
|
|
[FingerprintViewController presentFromViewController:self recipientId:recipientId];
|
2014-12-24 02:25:10 +01:00
|
|
|
|
}
|
|
|
|
|
|
2014-11-29 19:54:33 +01:00
|
|
|
|
#pragma mark - Calls
|
|
|
|
|
|
2017-05-31 20:22:32 +02: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-05-27 03:19:46 +02:00
|
|
|
|
__weak MessagesViewController *weakSelf = self;
|
2017-04-04 18:35:52 +02:00
|
|
|
|
if ([self isBlockedContactConversation]) {
|
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-06-01 03:12:18 +02:00
|
|
|
|
BOOL didShowSNAlert =
|
|
|
|
|
[self showSafetyNumberConfirmationIfNecessaryWithConfirmationText:[CallStrings confirmAndCallButtonTitle]
|
|
|
|
|
completion:^(BOOL didConfirmIdentity) {
|
|
|
|
|
if (didConfirmIdentity) {
|
|
|
|
|
[weakSelf callAction:sender];
|
|
|
|
|
}
|
|
|
|
|
}];
|
2017-05-27 03:19:46 +02:00
|
|
|
|
if (didShowSNAlert) {
|
2017-05-27 01:32:04 +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
|
|
|
|
}
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (BOOL)canCall
|
|
|
|
|
{
|
|
|
|
|
return !(self.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-05-27 03:19:46 +02:00
|
|
|
|
|
|
|
|
|
__weak MessagesViewController *weakSelf = self;
|
2017-04-04 18:35:52 +02:00
|
|
|
|
if ([self isBlockedContactConversation]) {
|
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-06-07 21:31:54 +02:00
|
|
|
|
BOOL didShowSNAlert =
|
|
|
|
|
[self showSafetyNumberConfirmationIfNecessaryWithConfirmationText:[SafetyNumberStrings confirmSendButton]
|
|
|
|
|
completion:^(BOOL didConfirmIdentity) {
|
|
|
|
|
if (didConfirmIdentity) {
|
|
|
|
|
[weakSelf didPressSendButton:button
|
|
|
|
|
withMessageText:text
|
|
|
|
|
senderId:senderId
|
|
|
|
|
senderDisplayName:senderDisplayName
|
|
|
|
|
date:date
|
|
|
|
|
updateKeyboardState:NO];
|
|
|
|
|
}
|
|
|
|
|
}];
|
|
|
|
|
if (didShowSNAlert) {
|
|
|
|
|
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 16kb.
|
|
|
|
|
//
|
|
|
|
|
// We convert large text messages to attachments
|
|
|
|
|
// which are presented as normal text messages.
|
|
|
|
|
const NSUInteger kOversizeTextMessageSizeThreshold = 16 * 1024;
|
|
|
|
|
if ([text lengthOfBytesUsingEncoding:NSUTF8StringEncoding] >= kOversizeTextMessageSizeThreshold) {
|
2017-04-25 04:30:46 +02:00
|
|
|
|
SignalAttachment *attachment =
|
|
|
|
|
[SignalAttachment attachmentWithData:[text dataUsingEncoding:NSUTF8StringEncoding]
|
|
|
|
|
dataUTI:SignalAttachment.kOversizeTextAttachmentUTI
|
|
|
|
|
filename:nil];
|
2017-06-07 21:31:54 +02:00
|
|
|
|
[self updateLastVisibleTimestamp:[ThreadUtil sendMessageWithAttachment:attachment
|
|
|
|
|
inThread:self.thread
|
|
|
|
|
messageSender:self.messageSender]
|
|
|
|
|
.timestampForSorting];
|
2017-04-13 20:42:05 +02:00
|
|
|
|
} else {
|
2017-06-07 21:31:54 +02:00
|
|
|
|
[self updateLastVisibleTimestamp:[ThreadUtil sendMessageWithText:text
|
|
|
|
|
inThread:self.thread
|
|
|
|
|
messageSender:self.messageSender]
|
|
|
|
|
.timestampForSorting];
|
2017-04-13 20:42:05 +02:00
|
|
|
|
}
|
2017-05-19 21:19:51 +02:00
|
|
|
|
|
2017-05-17 23:17:37 +02:00
|
|
|
|
self.lastMessageSentDate = [NSDate new];
|
2017-05-16 21:52:19 +02:00
|
|
|
|
[self clearUnreadMessagesIndicator];
|
|
|
|
|
|
2017-06-07 21:31:54 +02:00
|
|
|
|
if (updateKeyboardState) {
|
2017-04-05 18:16:54 +02:00
|
|
|
|
[self toggleDefaultKeyboard];
|
|
|
|
|
}
|
2017-05-09 16:33:35 +02:00
|
|
|
|
[self clearDraft];
|
2014-10-29 21:58:58 +01:00
|
|
|
|
[self finishSendingMessage];
|
2017-05-11 15:57:48 +02:00
|
|
|
|
[((OWSMessagesToolbarContentView *)self.inputToolbar.contentView)ensureSubviews];
|
2014-10-29 21:58:58 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
}
|
2017-06-20 19:43:05 +02:00
|
|
|
|
|
|
|
|
|
// The JSQ event listeners cause a bounce animation, so we temporarily disable them.
|
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
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (BOOL)collectionView:(JSQMessagesCollectionView *)collectionView
|
|
|
|
|
shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath
|
2016-03-11 19:34:39 +01:00
|
|
|
|
{
|
|
|
|
|
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];
|
|
|
|
|
|
2017-04-20 00:50:27 +02:00
|
|
|
|
|
|
|
|
|
// Don't show menu for in-progress downloads.
|
|
|
|
|
// We don't want to give the user the wrong idea that deleting would "cancel" the download.
|
|
|
|
|
id<OWSMessageData> message = [self messageAtIndexPath:indexPath];
|
2017-04-21 16:24:35 +02:00
|
|
|
|
if (message.isMediaMessage && [message.media isKindOfClass:[AttachmentPointerAdapter class]]) {
|
2017-04-20 00:50:27 +02:00
|
|
|
|
AttachmentPointerAdapter *attachmentPointerAdapter = (AttachmentPointerAdapter *)message.media;
|
|
|
|
|
return attachmentPointerAdapter.attachmentPointer.state == TSAttachmentPointerStateFailed;
|
|
|
|
|
}
|
|
|
|
|
|
2016-03-11 19:34:39 +01:00
|
|
|
|
// Super method returns false for media methods. We want menu for *all* items
|
|
|
|
|
return YES;
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-20 18:31:55 +02:00
|
|
|
|
- (BOOL)collectionView:(UICollectionView *)collectionView
|
|
|
|
|
canPerformAction:(SEL)action
|
|
|
|
|
forItemAtIndexPath:(NSIndexPath *)indexPath
|
|
|
|
|
withSender:(id)sender
|
|
|
|
|
{
|
|
|
|
|
id<OWSMessageData> messageData = [self messageAtIndexPath:indexPath];
|
|
|
|
|
return [messageData canPerformEditingAction:action];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)collectionView:(UICollectionView *)collectionView
|
|
|
|
|
performAction:(SEL)action
|
|
|
|
|
forItemAtIndexPath:(NSIndexPath *)indexPath
|
|
|
|
|
withSender:(id)sender
|
|
|
|
|
{
|
|
|
|
|
id<OWSMessageData> messageData = [self messageAtIndexPath:indexPath];
|
|
|
|
|
[messageData performEditingAction:action];
|
|
|
|
|
}
|
|
|
|
|
|
2017-04-19 03:27:59 +02:00
|
|
|
|
- (void)collectionView:(UICollectionView *)collectionView
|
|
|
|
|
willDisplayCell:(UICollectionViewCell *)cell
|
|
|
|
|
forItemAtIndexPath:(NSIndexPath *)indexPath
|
|
|
|
|
{
|
|
|
|
|
if ([cell conformsToProtocol:@protocol(OWSMessageCollectionViewCell)]) {
|
|
|
|
|
[((id<OWSMessageCollectionViewCell>)cell) setCellVisible: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];
|
|
|
|
|
}
|
2017-04-19 03:27:59 +02:00
|
|
|
|
|
|
|
|
|
if ([cell conformsToProtocol:@protocol(OWSMessageCollectionViewCell)]) {
|
|
|
|
|
[((id<OWSMessageCollectionViewCell>)cell) setCellVisible:NO];
|
|
|
|
|
}
|
2016-10-15 22:41:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
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;
|
2017-04-11 22:58:41 +02:00
|
|
|
|
case TSOutgoingMessageStateSent_OBSOLETE:
|
|
|
|
|
case TSOutgoingMessageStateDelivered_OBSOLETE:
|
|
|
|
|
OWSAssert(0);
|
|
|
|
|
return self.outgoingBubbleImageData;
|
|
|
|
|
case TSOutgoingMessageStateSentToService:
|
2015-06-07 19:04:24 +02:00
|
|
|
|
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
|
2017-05-31 20:22:32 +02:00
|
|
|
|
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: {
|
2017-06-02 23:17:59 +02:00
|
|
|
|
cell = [self loadSystemMessageCell:indexPath interaction:message.interaction];
|
2017-05-26 00:00:41 +02:00
|
|
|
|
break;
|
|
|
|
|
}
|
2016-07-09 00:25:28 +02:00
|
|
|
|
case TSInfoMessageAdapter: {
|
2017-06-05 23:25:28 +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];
|
|
|
|
|
|
2017-06-02 23:17:59 +02:00
|
|
|
|
cell = [self loadSystemMessageCell:indexPath interaction:message.interaction];
|
2017-05-26 00:00:41 +02:00
|
|
|
|
break;
|
|
|
|
|
}
|
2016-07-09 00:25:28 +02:00
|
|
|
|
case TSErrorMessageAdapter: {
|
2017-06-02 23:17:59 +02:00
|
|
|
|
cell = [self loadSystemMessageCell:indexPath interaction:message.interaction];
|
2017-05-26 00:00:41 +02:00
|
|
|
|
break;
|
|
|
|
|
}
|
2016-07-09 00:25:28 +02:00
|
|
|
|
case TSIncomingMessageAdapter: {
|
|
|
|
|
cell = [self loadIncomingMessageCellForMessage:message atIndexPath:indexPath];
|
2017-05-26 00:00:41 +02:00
|
|
|
|
break;
|
|
|
|
|
}
|
2016-07-09 00:25:28 +02:00
|
|
|
|
case TSOutgoingMessageAdapter: {
|
|
|
|
|
cell = [self loadOutgoingCellForMessage:message atIndexPath:indexPath];
|
2017-05-26 00:00:41 +02:00
|
|
|
|
break;
|
|
|
|
|
}
|
2017-05-16 17:26:01 +02:00
|
|
|
|
case TSUnreadIndicatorAdapter: {
|
2017-05-26 00:00:41 +02:00
|
|
|
|
cell = [self loadUnreadIndicatorCell:indexPath interaction:message.interaction];
|
|
|
|
|
break;
|
|
|
|
|
}
|
2016-07-09 00:25:28 +02:00
|
|
|
|
default: {
|
2016-07-13 21:17:09 +02:00
|
|
|
|
DDLogWarn(@"using default cell constructor for message: %@", message);
|
2017-05-31 20:22:32 +02:00
|
|
|
|
cell = (JSQMessagesCollectionViewCell *)[super collectionView:collectionView
|
|
|
|
|
cellForItemAtIndexPath:indexPath];
|
2017-05-26 00:00:41 +02:00
|
|
|
|
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);
|
2017-05-16 17:26:01 +02:00
|
|
|
|
OWSAssert(0);
|
2016-11-13 19:30:22 +01:00
|
|
|
|
return cell;
|
|
|
|
|
}
|
2017-04-19 03:27:59 +02:00
|
|
|
|
|
|
|
|
|
if ([message isMediaMessage] && [[message media] conformsToProtocol:@protocol(OWSMessageMediaAdapter)]) {
|
|
|
|
|
cell.mediaAdapter = (id<OWSMessageMediaAdapter>)[message media];
|
|
|
|
|
}
|
|
|
|
|
|
2016-11-13 19:30:22 +01:00
|
|
|
|
[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);
|
2017-05-16 17:26:01 +02:00
|
|
|
|
OWSAssert(0);
|
2016-11-13 19:30:22 +01:00
|
|
|
|
return cell;
|
|
|
|
|
}
|
2017-04-19 03:27:59 +02:00
|
|
|
|
|
|
|
|
|
if ([message isMediaMessage] && [[message media] conformsToProtocol:@protocol(OWSMessageMediaAdapter)]) {
|
|
|
|
|
cell.mediaAdapter = (id<OWSMessageMediaAdapter>)[message media];
|
|
|
|
|
}
|
|
|
|
|
|
2016-11-13 19:30:22 +01:00
|
|
|
|
[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;
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-16 17:26:01 +02:00
|
|
|
|
- (JSQMessagesCollectionViewCell *)loadUnreadIndicatorCell:(NSIndexPath *)indexPath
|
2017-05-26 00:00:41 +02:00
|
|
|
|
interaction:(TSInteraction *)interaction
|
2017-05-16 17:26:01 +02:00
|
|
|
|
{
|
|
|
|
|
OWSAssert(indexPath);
|
2017-05-26 00:00:41 +02:00
|
|
|
|
OWSAssert(interaction);
|
|
|
|
|
OWSAssert([interaction isKindOfClass:[TSUnreadIndicatorInteraction class]]);
|
|
|
|
|
|
|
|
|
|
TSUnreadIndicatorInteraction *unreadIndicator = (TSUnreadIndicatorInteraction *)interaction;
|
2017-05-16 17:26:01 +02:00
|
|
|
|
|
|
|
|
|
OWSUnreadIndicatorCell *cell =
|
|
|
|
|
[self.collectionView dequeueReusableCellWithReuseIdentifier:[OWSUnreadIndicatorCell cellReuseIdentifier]
|
|
|
|
|
forIndexPath:indexPath];
|
2017-06-06 16:15:17 +02:00
|
|
|
|
[cell configureWithInteraction:unreadIndicator];
|
2017-05-16 17:26:01 +02:00
|
|
|
|
|
|
|
|
|
return cell;
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-02 23:17:59 +02:00
|
|
|
|
- (OWSSystemMessageCell *)loadSystemMessageCell:(NSIndexPath *)indexPath interaction:(TSInteraction *)interaction
|
2016-07-13 21:17:09 +02:00
|
|
|
|
{
|
2017-06-02 21:49:34 +02:00
|
|
|
|
OWSAssert(indexPath);
|
|
|
|
|
OWSAssert(interaction);
|
|
|
|
|
|
|
|
|
|
OWSSystemMessageCell *cell =
|
|
|
|
|
[self.collectionView dequeueReusableCellWithReuseIdentifier:[OWSSystemMessageCell cellReuseIdentifier]
|
|
|
|
|
forIndexPath:indexPath];
|
2017-06-06 16:15:17 +02:00
|
|
|
|
[cell configureWithInteraction:interaction];
|
2017-06-16 22:06:39 +02:00
|
|
|
|
cell.cellTopLabel.attributedText =
|
|
|
|
|
[self collectionView:self.collectionView attributedTextForCellTopLabelAtIndexPath:indexPath];
|
|
|
|
|
|
2017-06-06 15:46:00 +02:00
|
|
|
|
cell.systemMessageCellDelegate = self;
|
2016-10-08 17:48:50 +02:00
|
|
|
|
|
2017-06-02 21:49:34 +02:00
|
|
|
|
return cell;
|
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).
|
2017-05-31 20:22:32 +02:00
|
|
|
|
|
|
|
|
|
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.
|
2016-11-17 23:07:18 +01:00
|
|
|
|
*/
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (void)reloadInputToolbarSizeIfNeeded
|
|
|
|
|
{
|
2016-11-17 23:07:18 +01:00
|
|
|
|
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;
|
2017-05-31 20:22:32 +02:00
|
|
|
|
f.size.height =
|
|
|
|
|
[self.inputToolbar.contentView.textView sizeThatFits:self.inputToolbar.contentView.textView.frame.size].height;
|
2016-11-17 23:07:18 +01:00
|
|
|
|
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.
|
|
|
|
|
*/
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (void)didChangePreferredContentSize:(NSNotification *)notification
|
|
|
|
|
{
|
2016-11-17 23:07:18 +01:00
|
|
|
|
[self.collectionView.collectionViewLayout setMessageBubbleFont:[UIFont ows_dynamicTypeBodyFont]];
|
2017-07-25 18:52:30 +02:00
|
|
|
|
[self resetContentAndLayout];
|
2016-11-17 23:07:18 +01:00
|
|
|
|
[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
|
2017-05-31 20:22:32 +02:00
|
|
|
|
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.0f;
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-31 20:22:32 +02: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]];
|
|
|
|
|
|
2017-07-01 23:06:02 +02:00
|
|
|
|
if ([previousMessage.interaction isKindOfClass:[TSUnreadIndicatorInteraction class]]) {
|
|
|
|
|
// Always show timestamp between unread indicator and the following interaction
|
|
|
|
|
return YES;
|
|
|
|
|
}
|
|
|
|
|
|
2017-07-01 22:55:07 +02:00
|
|
|
|
OWSAssert(currentMessage.date);
|
|
|
|
|
OWSAssert(previousMessage.date);
|
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
|
2017-05-31 20:22:32 +02:00
|
|
|
|
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++) {
|
2017-07-25 18:52:30 +02:00
|
|
|
|
NSIndexPath *nextIndexPath = [NSIndexPath indexPathForRow:row inSection:indexPath.section];
|
|
|
|
|
TSInteraction *nextMessage = [self interactionAtIndexPath:nextIndexPath];
|
2017-03-14 21:27:35 +01:00
|
|
|
|
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-05-31 20:22:32 +02:00
|
|
|
|
return [[NSAttributedString alloc]
|
|
|
|
|
initWithString:NSLocalizedString(@"MESSAGE_STATUS_FAILED", @"message footer for failed messages")];
|
2017-04-11 22:58:41 +02:00
|
|
|
|
} else if (outgoingMessage.messageState == TSOutgoingMessageStateSentToService) {
|
|
|
|
|
NSString *text = (outgoingMessage.wasDelivered
|
|
|
|
|
? NSLocalizedString(@"MESSAGE_STATUS_DELIVERED", @"message footer for delivered messages")
|
|
|
|
|
: NSLocalizedString(@"MESSAGE_STATUS_SENT", @"message footer for sent messages"));
|
2017-03-15 15:39:47 +01:00
|
|
|
|
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];
|
2017-04-11 22:58:41 +02:00
|
|
|
|
if (nextMessage && nextMessage.messageState != TSOutgoingMessageStateSentToService) {
|
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-05-30 19:04:43 +02:00
|
|
|
|
@"message footer while attachment is uploading")];
|
2017-03-14 21:27:35 +01:00
|
|
|
|
} 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 =
|
2017-05-31 20:22:32 +02:00
|
|
|
|
[[NSAttributedString alloc] initWithString:@"/"
|
|
|
|
|
attributes:@{
|
|
|
|
|
NSFontAttributeName : [UIFont ows_dripIconsFont:14.f],
|
|
|
|
|
}];
|
2017-03-14 21:01:12 +01:00
|
|
|
|
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.0f;
|
|
|
|
|
}
|
2015-12-22 12:45:09 +01:00
|
|
|
|
|
2014-11-29 19:54:33 +01:00
|
|
|
|
return 0.0f;
|
2014-10-29 21:58:58 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark - Actions
|
|
|
|
|
|
2017-06-20 16:24:29 +02:00
|
|
|
|
- (void)showNoLongerVerifiedUI
|
|
|
|
|
{
|
|
|
|
|
NSArray<NSString *> *noLongerVerifiedRecipientIds = [self noLongerVerifiedRecipientIds];
|
|
|
|
|
if (noLongerVerifiedRecipientIds.count > 1) {
|
|
|
|
|
[self showConversationSettingsAndShowVerification:YES];
|
|
|
|
|
} else if (noLongerVerifiedRecipientIds.count == 1) {
|
|
|
|
|
// Pick one in an arbitrary but deterministic manner.
|
|
|
|
|
NSString *recipientId = noLongerVerifiedRecipientIds.lastObject;
|
|
|
|
|
[self showFingerprintWithRecipientId:recipientId];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-21 14:37:51 +02:00
|
|
|
|
- (void)showConversationSettings
|
2017-06-09 22:21:59 +02:00
|
|
|
|
{
|
|
|
|
|
[self showConversationSettingsAndShowVerification:NO];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)showConversationSettingsAndShowVerification:(BOOL)showVerification
|
2016-09-21 14:37:51 +02:00
|
|
|
|
{
|
|
|
|
|
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
|
|
|
|
|
2017-05-24 15:33:17 +02:00
|
|
|
|
OWSConversationSettingsTableViewController *settingsVC = [OWSConversationSettingsTableViewController new];
|
2017-05-02 16:54:07 +02:00
|
|
|
|
settingsVC.conversationSettingsViewDelegate = self;
|
2017-03-10 14:56:12 +01:00
|
|
|
|
[settingsVC configureWithThread:self.thread];
|
2017-06-09 22:21:59 +02:00
|
|
|
|
settingsVC.showVerificationOnAppear = showVerification;
|
2017-03-10 14:56:12 +01:00
|
|
|
|
[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-05-30 19:04:43 +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;
|
2017-05-12 19:12:09 +02:00
|
|
|
|
} else if (outgoingMessage.messageState == TSOutgoingMessageStateAttemptingOut) {
|
|
|
|
|
// Ignore taps on outgoing messages being sent.
|
|
|
|
|
break;
|
2014-12-11 00:05:41 +01:00
|
|
|
|
}
|
2017-05-12 19:12:09 +02: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];
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
UIImage *tappedImage = ((UIImageView *)[messageMedia mediaView]).image;
|
|
|
|
|
if (tappedImage == nil) {
|
2016-04-13 20:38:42 +02:00
|
|
|
|
DDLogWarn(@"tapped TSPhotoAdapter with nil image");
|
|
|
|
|
} else {
|
2017-02-11 05:21:40 +01:00
|
|
|
|
UIWindow *window = [UIApplication sharedApplication].keyWindow;
|
2017-05-31 20:22:32 +02:00
|
|
|
|
JSQMessagesCollectionViewCell *cell
|
|
|
|
|
= (JSQMessagesCollectionViewCell *)[collectionView cellForItemAtIndexPath:indexPath];
|
2017-02-11 05:21:40 +01:00
|
|
|
|
OWSAssert([cell isKindOfClass:[JSQMessagesCollectionViewCell class]]);
|
2017-05-31 20:22:32 +02:00
|
|
|
|
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) {
|
2017-05-31 20:22:32 +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;
|
2017-05-31 20:22:32 +02:00
|
|
|
|
FullImageViewController *vc =
|
|
|
|
|
[[FullImageViewController alloc] initWithAttachment:attStream
|
|
|
|
|
fromRect:convertedRect
|
|
|
|
|
forInteraction:interaction
|
|
|
|
|
messageItem:messageItem
|
|
|
|
|
isAnimated:NO];
|
2015-12-22 12:45:09 +01:00
|
|
|
|
|
2017-04-21 20:58:51 +02:00
|
|
|
|
[vc presentFromViewController:self];
|
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];
|
2017-05-31 20:22:32 +02:00
|
|
|
|
UIImage *tappedImage = ((UIImageView *)[messageMedia mediaView]).image;
|
|
|
|
|
if (tappedImage == nil) {
|
2016-04-13 20:38:42 +02:00
|
|
|
|
DDLogWarn(@"tapped TSAnimatedAdapter with nil image");
|
|
|
|
|
} else {
|
2017-02-11 05:21:40 +01:00
|
|
|
|
UIWindow *window = [UIApplication sharedApplication].keyWindow;
|
2017-05-31 20:22:32 +02:00
|
|
|
|
JSQMessagesCollectionViewCell *cell
|
|
|
|
|
= (JSQMessagesCollectionViewCell *)[collectionView cellForItemAtIndexPath:indexPath];
|
2017-02-11 05:21:40 +01:00
|
|
|
|
OWSAssert([cell isKindOfClass:[JSQMessagesCollectionViewCell class]]);
|
2017-05-31 20:22:32 +02:00
|
|
|
|
CGRect convertedRect = [cell.mediaView convertRect:cell.mediaView.bounds toView:window];
|
2017-02-11 05:21:40 +01:00
|
|
|
|
|
2016-04-13 20:38:42 +02:00
|
|
|
|
__block TSAttachment *attachment = nil;
|
|
|
|
|
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
2017-05-31 20:22:32 +02:00
|
|
|
|
attachment = [TSAttachment fetchObjectWithUniqueID:messageMedia.attachmentId
|
|
|
|
|
transaction:transaction];
|
2016-04-13 20:38:42 +02:00
|
|
|
|
}];
|
|
|
|
|
if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
|
|
|
|
|
TSAttachmentStream *attStream = (TSAttachmentStream *)attachment;
|
|
|
|
|
FullImageViewController *vc =
|
2017-05-30 19:04:43 +02:00
|
|
|
|
[[FullImageViewController alloc] initWithAttachment:attStream
|
|
|
|
|
fromRect:convertedRect
|
|
|
|
|
forInteraction:interaction
|
|
|
|
|
messageItem:messageItem
|
|
|
|
|
isAnimated:YES];
|
2017-04-21 20:58:51 +02:00
|
|
|
|
[vc presentFromViewController:self];
|
2016-04-13 20:38:42 +02:00
|
|
|
|
}
|
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];
|
2017-05-31 20:22:32 +02:00
|
|
|
|
__block TSAttachment *attachment = nil;
|
2014-12-26 23:18:54 +01:00
|
|
|
|
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
2017-05-31 20:22:32 +02: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;
|
2017-05-31 20:22:32 +02:00
|
|
|
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
2015-12-22 12:45:09 +01:00
|
|
|
|
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];
|
2017-04-13 19:43:09 +02:00
|
|
|
|
self.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-05-31 20:22:32 +02: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]) {
|
2017-04-20 15:44:25 +02:00
|
|
|
|
if (self.audioAttachmentPlayer) {
|
2017-04-20 16:06:31 +02:00
|
|
|
|
// Is this player associated with this media adapter?
|
|
|
|
|
if (self.audioAttachmentPlayer.owner == messageMedia) {
|
2017-04-20 15:44:25 +02:00
|
|
|
|
// Tap to pause & unpause.
|
|
|
|
|
[self.audioAttachmentPlayer togglePlayState];
|
|
|
|
|
return;
|
2015-01-22 05:08:12 +01:00
|
|
|
|
}
|
2017-04-20 15:44:25 +02:00
|
|
|
|
[self.audioAttachmentPlayer stop];
|
|
|
|
|
self.audioAttachmentPlayer = nil;
|
2015-01-22 05:08:12 +01:00
|
|
|
|
}
|
2017-04-20 15:44:25 +02:00
|
|
|
|
self.audioAttachmentPlayer =
|
|
|
|
|
[[OWSAudioAttachmentPlayer alloc] initWithMediaAdapter:messageMedia
|
|
|
|
|
databaseConnection:self.uiDatabaseConnection];
|
2017-04-20 16:06:31 +02:00
|
|
|
|
// Associate the player with this media adapter.
|
|
|
|
|
self.audioAttachmentPlayer.owner = messageMedia;
|
2017-04-20 15:44:25 +02:00
|
|
|
|
[self.audioAttachmentPlayer play];
|
2015-01-14 22:30:01 +01:00
|
|
|
|
}
|
2014-12-26 23:18:54 +01:00
|
|
|
|
}
|
2017-04-20 00:50:27 +02:00
|
|
|
|
} else if ([messageItem.media isKindOfClass:[AttachmentPointerAdapter class]]) {
|
|
|
|
|
AttachmentPointerAdapter *attachmentPointerAdadpter = (AttachmentPointerAdapter *)messageItem.media;
|
|
|
|
|
TSAttachmentPointer *attachmentPointer = attachmentPointerAdadpter.attachmentPointer;
|
|
|
|
|
// Restart failed downloads
|
|
|
|
|
if (attachmentPointer.state == TSAttachmentPointerStateFailed) {
|
|
|
|
|
if (![interaction isKindOfClass:[TSMessage class]]) {
|
2017-05-31 20:22:32 +02:00
|
|
|
|
DDLogError(@"%@ Expected attachment downloads from an instance of message, but found: %@",
|
|
|
|
|
self.tag,
|
|
|
|
|
interaction);
|
2017-04-20 00:50:27 +02:00
|
|
|
|
OWSAssert(NO);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
TSMessage *message = (TSMessage *)interaction;
|
|
|
|
|
[self handleFailedDownloadTapForMessage:message attachmentPointer:attachmentPointer];
|
|
|
|
|
} else {
|
|
|
|
|
DDLogVerbose(@"%@ Ignoring tap for attachment pointer %@ with state %lu",
|
|
|
|
|
self.tag,
|
|
|
|
|
attachmentPointer,
|
|
|
|
|
(unsigned long)attachmentPointer.state);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
DDLogDebug(@"%@ Unhandled tap on 'media item' with media: %@", self.tag, messageItem.media);
|
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:
|
2017-05-19 19:23:46 +02:00
|
|
|
|
case TSInfoMessageAdapter:
|
2014-12-31 21:30:20 +01:00
|
|
|
|
case TSCallAdapter:
|
2017-05-16 17:26:01 +02:00
|
|
|
|
case TSUnreadIndicatorAdapter:
|
2017-06-06 15:46:00 +02:00
|
|
|
|
OWSFail(@"Unexpected tap for system message.");
|
2014-12-31 21:30:20 +01:00
|
|
|
|
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
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
if (messageItem.messageType == TSOutgoingMessageAdapter || messageItem.messageType == TSIncomingMessageAdapter) {
|
|
|
|
|
TSMessage *message = (TSMessage *)interaction;
|
2017-03-29 23:54:11 +02:00
|
|
|
|
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]) {
|
2017-05-31 20:22:32 +02:00
|
|
|
|
OversizeTextMessageViewController *messageVC =
|
|
|
|
|
[[OversizeTextMessageViewController alloc] initWithMessage:message];
|
2017-03-29 18:20:28 +02:00
|
|
|
|
[self.navigationController pushViewController:messageVC animated:YES];
|
|
|
|
|
}
|
2017-03-29 23:54:11 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2014-12-11 00:05:41 +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-05-31 20:22:32 +02:00
|
|
|
|
- (void)moviePlayerWillExitFullscreen:(id)sender
|
|
|
|
|
{
|
2017-02-11 05:22:09 +01:00
|
|
|
|
DDLogDebug(@"%@ %s", self.tag, __PRETTY_FUNCTION__);
|
|
|
|
|
|
|
|
|
|
[self clearVideoPlayer];
|
|
|
|
|
}
|
|
|
|
|
|
2017-02-13 22:30:54 +01:00
|
|
|
|
// See comment on moviePlayerWillExitFullscreen:
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (void)moviePlayerDidExitFullscreen:(id)sender
|
|
|
|
|
{
|
2017-02-11 05:22:09 +01:00
|
|
|
|
DDLogDebug(@"%@ %s", self.tag, __PRETTY_FUNCTION__);
|
2017-05-30 19:04:43 +02:00
|
|
|
|
|
2017-02-11 05:22:09 +01:00
|
|
|
|
[self clearVideoPlayer];
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (void)clearVideoPlayer
|
|
|
|
|
{
|
2017-02-11 05:22:09 +01:00
|
|
|
|
[_videoPlayer stop];
|
|
|
|
|
[_videoPlayer.view removeFromSuperview];
|
2017-04-13 19:43:09 +02:00
|
|
|
|
self.videoPlayer = nil;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)setVideoPlayer:(MPMoviePlayerController *)videoPlayer
|
|
|
|
|
{
|
|
|
|
|
_videoPlayer = videoPlayer;
|
|
|
|
|
|
|
|
|
|
[ViewControllerUtils setAudioIgnoresHardwareMuteSwitch: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
|
2017-05-26 00:00:41 +02:00
|
|
|
|
didTapLoadEarlierMessagesButton:(UIButton *)sender
|
|
|
|
|
{
|
2017-06-16 21:25:55 +02:00
|
|
|
|
OWSAssert(!self.isUserScrolling);
|
|
|
|
|
|
2017-06-17 20:03:44 +02:00
|
|
|
|
BOOL hasEarlierUnseenMessages = self.dynamicInteractions.hasMoreUnseenMessages;
|
2015-12-22 12:45:09 +01:00
|
|
|
|
|
2017-05-26 00:00:41 +02:00
|
|
|
|
// We want to restore the current scroll state after we update the range, update
|
|
|
|
|
// the dynamic interactions and re-layout. Here we take a "before" snapshot.
|
|
|
|
|
CGFloat scrollDistanceToBottom = self.collectionView.contentSize.height - self.collectionView.contentOffset.y;
|
|
|
|
|
|
|
|
|
|
self.page = MIN(self.page + 1, (NSUInteger)kYapDatabaseMaxPageCount - 1);
|
|
|
|
|
|
2017-07-26 18:39:43 +02:00
|
|
|
|
[self resetMappings];
|
2015-12-22 12:45:09 +01:00
|
|
|
|
|
2017-05-26 00:00:41 +02:00
|
|
|
|
[self.collectionView layoutSubviews];
|
|
|
|
|
|
|
|
|
|
self.collectionView.contentOffset = CGPointMake(0, self.collectionView.contentSize.height - scrollDistanceToBottom);
|
2017-06-16 21:25:55 +02:00
|
|
|
|
|
2017-05-26 00:00:41 +02:00
|
|
|
|
[self.scrollLaterTimer invalidate];
|
2017-06-17 20:03:44 +02:00
|
|
|
|
// Don’t auto-scroll after “loading more messages” unless we have “more unseen messages”.
|
|
|
|
|
//
|
|
|
|
|
// Otherwise, tapping on "load more messages" autoscrolls you downward which is completely wrong.
|
|
|
|
|
if (hasEarlierUnseenMessages) {
|
2017-06-16 21:25:55 +02:00
|
|
|
|
// We want to scroll to the bottom _after_ the layout has been updated.
|
|
|
|
|
self.scrollLaterTimer = [NSTimer weakScheduledTimerWithTimeInterval:0.001f
|
|
|
|
|
target:self
|
|
|
|
|
selector:@selector(scrollToUnreadIndicatorAnimated)
|
|
|
|
|
userInfo:nil
|
|
|
|
|
repeats:NO];
|
|
|
|
|
}
|
2017-05-26 00:00:41 +02:00
|
|
|
|
|
|
|
|
|
[self updateLoadEarlierVisible];
|
2014-12-31 13:22:40 +01:00
|
|
|
|
}
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (BOOL)shouldShowLoadEarlierMessages
|
|
|
|
|
{
|
2017-05-26 00:00:41 +02:00
|
|
|
|
if (self.page == kYapDatabaseMaxPageCount - 1) {
|
|
|
|
|
return NO;
|
|
|
|
|
}
|
|
|
|
|
|
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) {
|
2017-05-31 20:22:32 +02:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (void)updateLoadEarlierVisible
|
|
|
|
|
{
|
2014-12-31 13:22:40 +01:00
|
|
|
|
[self setShowLoadEarlierMessagesHeader:[self shouldShowLoadEarlierMessages]];
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-26 00:00:41 +02:00
|
|
|
|
- (void)updateMessageMappingRangeOptions
|
|
|
|
|
{
|
2017-07-28 23:06:17 +02:00
|
|
|
|
// The "page" length may have been increased by loading "prev" pages at the
|
|
|
|
|
// top of the window.
|
|
|
|
|
NSUInteger pageLength = kYapDatabasePageSize * (self.page + 1);
|
|
|
|
|
// The "old" length may have been increased by insertions of new messages
|
|
|
|
|
// at the bottom of the window.
|
|
|
|
|
NSUInteger oldLength = [self.messageMappings numberOfItemsInGroup:self.thread.uniqueId];
|
|
|
|
|
NSUInteger newLength = MAX(pageLength, oldLength);
|
2015-12-22 12:45:09 +01:00
|
|
|
|
YapDatabaseViewRangeOptions *rangeOptions =
|
2017-07-28 23:06:17 +02:00
|
|
|
|
[YapDatabaseViewRangeOptions flexibleRangeWithLength:newLength offset:0 from:YapDatabaseViewEnd];
|
2015-12-22 12:45:09 +01:00
|
|
|
|
|
2017-07-28 23:06:17 +02:00
|
|
|
|
rangeOptions.maxLength = MAX(newLength, kYapDatabaseRangeMaxLength);
|
2014-12-31 13:22:40 +01:00
|
|
|
|
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
|
|
|
|
|
|
2017-04-20 00:50:27 +02:00
|
|
|
|
- (void)handleFailedDownloadTapForMessage:(TSMessage *)message
|
|
|
|
|
attachmentPointer:(TSAttachmentPointer *)attachmentPointer
|
|
|
|
|
{
|
|
|
|
|
UIAlertController *actionSheetController = [UIAlertController
|
|
|
|
|
alertControllerWithTitle:NSLocalizedString(@"MESSAGES_VIEW_FAILED_DOWNLOAD_ACTIONSHEET_TITLE", comment
|
|
|
|
|
: "Action sheet title after tapping on failed download.")
|
|
|
|
|
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(@"MESSAGES_VIEW_FAILED_DOWNLOAD_RETRY_ACTION", @"Action sheet button text")
|
|
|
|
|
style:UIAlertActionStyleDefault
|
|
|
|
|
handler:^(UIAlertAction *_Nonnull action) {
|
|
|
|
|
OWSAttachmentsProcessor *processor =
|
|
|
|
|
[[OWSAttachmentsProcessor alloc] initWithAttachmentPointer:attachmentPointer
|
|
|
|
|
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);
|
|
|
|
|
}];
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
[actionSheetController addAction:resendMessageAction];
|
|
|
|
|
|
|
|
|
|
[self presentViewController:actionSheetController animated:YES completion:nil];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (void)handleUnsentMessageTap:(TSOutgoingMessage *)message
|
|
|
|
|
{
|
|
|
|
|
UIAlertController *actionSheetController =
|
|
|
|
|
[UIAlertController alertControllerWithTitle:message.mostRecentFailureText
|
|
|
|
|
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];
|
|
|
|
|
|
|
|
|
|
UIAlertAction *deleteMessageAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_DELETE_TITLE", @"")
|
|
|
|
|
style:UIAlertActionStyleDestructive
|
|
|
|
|
handler:^(UIAlertAction *_Nonnull action) {
|
|
|
|
|
[message remove];
|
|
|
|
|
}];
|
|
|
|
|
[actionSheetController addAction:deleteMessageAction];
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
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);
|
|
|
|
|
}];
|
|
|
|
|
}];
|
2016-11-26 00:12:00 +01:00
|
|
|
|
|
|
|
|
|
[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
|
|
|
|
|
{
|
2017-06-06 15:46:00 +02:00
|
|
|
|
OWSAssert(message);
|
|
|
|
|
|
|
|
|
|
switch (message.errorType) {
|
|
|
|
|
case TSErrorMessageInvalidKeyException:
|
|
|
|
|
break;
|
|
|
|
|
case TSErrorMessageNonBlockingIdentityChange:
|
|
|
|
|
[self tappedNonBlockingIdentityChangeForRecipientId:message.recipientId];
|
|
|
|
|
return;
|
|
|
|
|
case TSErrorMessageWrongTrustedIdentityKey:
|
|
|
|
|
OWSAssert([message isKindOfClass:[TSInvalidIdentityKeyErrorMessage class]]);
|
|
|
|
|
[self tappedInvalidIdentityKeyErrorMessage:(TSInvalidIdentityKeyErrorMessage *)message];
|
|
|
|
|
return;
|
|
|
|
|
case TSErrorMessageMissingKeyId:
|
|
|
|
|
// Unused.
|
|
|
|
|
break;
|
|
|
|
|
case TSErrorMessageNoSession:
|
|
|
|
|
break;
|
|
|
|
|
case TSErrorMessageInvalidMessage:
|
|
|
|
|
[self tappedCorruptedMessage:message];
|
|
|
|
|
return;
|
|
|
|
|
case TSErrorMessageDuplicateMessage:
|
|
|
|
|
// Unused.
|
|
|
|
|
break;
|
|
|
|
|
case TSErrorMessageInvalidVersion:
|
|
|
|
|
break;
|
|
|
|
|
case TSErrorMessageUnknownContactBlockOffer:
|
|
|
|
|
OWSAssert([message isKindOfClass:[OWSUnknownContactBlockOfferMessage class]]);
|
|
|
|
|
[self tappedUnknownContactBlockOfferMessage:(OWSUnknownContactBlockOfferMessage *)message];
|
|
|
|
|
return;
|
2017-06-13 21:09:47 +02:00
|
|
|
|
case TSErrorMessageGroupCreationFailed:
|
|
|
|
|
[self resendGroupUpdateForErrorMessage:message];
|
|
|
|
|
return;
|
2016-09-11 22:53:12 +02:00
|
|
|
|
}
|
2017-06-06 15:46:00 +02:00
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2017-06-16 17:59:59 +02:00
|
|
|
|
- (void)tappedNonBlockingIdentityChangeForRecipientId:(nullable NSString *)signalId
|
2017-05-23 17:28:12 +02:00
|
|
|
|
{
|
2017-06-16 17:59:59 +02:00
|
|
|
|
if (signalId == nil) {
|
2017-06-16 18:33:50 +02:00
|
|
|
|
if (self.thread.isGroupThread) {
|
|
|
|
|
// Before 2.13 we didn't track the recipient id in the identity change error.
|
|
|
|
|
DDLogWarn(@"%@ Ignoring tap on legacy nonblocking identity change since it has no signal id", self.tag);
|
|
|
|
|
} else {
|
|
|
|
|
DDLogInfo(
|
|
|
|
|
@"%@ Assuming tap on legacy nonblocking identity change corresponds to current contact thread: %@",
|
|
|
|
|
self.tag,
|
|
|
|
|
self.thread.contactIdentifier);
|
|
|
|
|
signalId = self.thread.contactIdentifier;
|
|
|
|
|
}
|
2017-06-16 17:59:59 +02:00
|
|
|
|
}
|
2017-05-23 17:28:12 +02:00
|
|
|
|
|
2017-06-07 22:51:22 +02:00
|
|
|
|
[self showFingerprintWithRecipientId:signalId];
|
2017-05-23 17:28:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-05-19 19:23:46 +02:00
|
|
|
|
- (void)handleInfoMessageTap:(TSInfoMessage *)message
|
|
|
|
|
{
|
2017-06-06 15:46:00 +02:00
|
|
|
|
OWSAssert(message);
|
|
|
|
|
|
|
|
|
|
switch (message.messageType) {
|
|
|
|
|
case TSInfoMessageUserNotRegistered:
|
|
|
|
|
break;
|
|
|
|
|
case TSInfoMessageTypeSessionDidEnd:
|
|
|
|
|
break;
|
|
|
|
|
case TSInfoMessageTypeUnsupportedMessage:
|
|
|
|
|
// Unused.
|
|
|
|
|
break;
|
|
|
|
|
case TSInfoMessageAddToContactsOffer:
|
|
|
|
|
OWSAssert([message isKindOfClass:[OWSAddToContactsOfferMessage class]]);
|
|
|
|
|
[self tappedAddToContactsOfferMessage:(OWSAddToContactsOfferMessage *)message];
|
|
|
|
|
return;
|
|
|
|
|
case TSInfoMessageTypeGroupUpdate:
|
|
|
|
|
[self showConversationSettings];
|
|
|
|
|
return;
|
|
|
|
|
case TSInfoMessageTypeGroupQuit:
|
|
|
|
|
break;
|
|
|
|
|
case TSInfoMessageTypeDisappearingMessagesUpdate:
|
|
|
|
|
[self showConversationSettings];
|
|
|
|
|
return;
|
2017-06-07 22:51:22 +02:00
|
|
|
|
case TSInfoMessageVerificationStateChange:
|
|
|
|
|
[self showFingerprintWithRecipientId:((OWSVerificationStateChangeMessage *)message).recipientId];
|
|
|
|
|
break;
|
2017-05-19 19:23:46 +02:00
|
|
|
|
}
|
2017-06-06 15:46:00 +02:00
|
|
|
|
|
|
|
|
|
DDLogInfo(@"%@ Unhandled tap for info message:%@", self.tag, message);
|
2017-05-19 19:23:46 +02: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];
|
2017-05-30 19:04:43 +02:00
|
|
|
|
|
2016-11-26 00:12:00 +01:00
|
|
|
|
UIAlertAction *dismissAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_CANCEL_TITLE", @"")
|
|
|
|
|
style:UIAlertActionStyleCancel
|
|
|
|
|
handler:nil];
|
|
|
|
|
[alertController addAction:dismissAction];
|
2017-05-31 20:22:32 +02:00
|
|
|
|
|
|
|
|
|
UIAlertAction *resetSessionAction = [UIAlertAction
|
|
|
|
|
actionWithTitle:NSLocalizedString(@"FINGERPRINT_SHRED_KEYMATERIAL_BUTTON", @"")
|
|
|
|
|
style:UIAlertActionStyleDefault
|
|
|
|
|
handler:^(UIAlertAction *_Nonnull action) {
|
|
|
|
|
if (![self.thread isKindOfClass:[TSContactThread class]]) {
|
|
|
|
|
// Corrupt Message errors only appear in contact threads.
|
|
|
|
|
DDLogError(@"%@ Unexpected request to reset session in group thread. Refusing", self.tag);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
TSContactThread *contactThread = (TSContactThread *)self.thread;
|
|
|
|
|
[OWSSessionResetJob runWithContactThread:contactThread
|
|
|
|
|
messageSender:self.messageSender
|
|
|
|
|
storageManager:self.storageManager];
|
|
|
|
|
}];
|
2016-11-26 00:12:00 +01:00
|
|
|
|
[alertController addAction:resetSessionAction];
|
2017-05-30 19:04:43 +02:00
|
|
|
|
|
2016-11-26 00:12:00 +01:00
|
|
|
|
[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);
|
2017-06-07 22:51:22 +02:00
|
|
|
|
[self showFingerprintWithRecipientId:errorMessage.theirSignalId];
|
2017-04-10 03:39:04 +02:00
|
|
|
|
}];
|
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);
|
2017-06-08 05:30:51 +02:00
|
|
|
|
|
|
|
|
|
// DEPRECATED: we're no longer creating these incoming SN error's per message,
|
|
|
|
|
// but there will be some legacy ones in the wild, behind which await as-of-yet-undecrypted
|
|
|
|
|
// messages
|
|
|
|
|
if ([errorMessage isKindOfClass:[TSInvalidIdentityKeyReceivingErrorMessage class]]) {
|
|
|
|
|
[errorMessage acceptNewIdentityKey];
|
2017-04-10 03:39:04 +02:00
|
|
|
|
}
|
|
|
|
|
}];
|
2016-11-26 00:12:00 +01:00
|
|
|
|
[actionSheetController addAction:acceptSafetyNumberAction];
|
2017-05-30 19:04:43 +02:00
|
|
|
|
|
2016-11-26 00:12:00 +01:00
|
|
|
|
[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.
|
2017-06-19 19:56:23 +02:00
|
|
|
|
[self.editingDatabaseConnection
|
2017-04-10 03:39:04 +02:00
|
|
|
|
readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
|
|
|
|
[errorMessage removeWithTransaction:transaction];
|
|
|
|
|
}];
|
|
|
|
|
}];
|
|
|
|
|
[actionSheetController addAction:blockAction];
|
|
|
|
|
|
|
|
|
|
[self presentViewController:actionSheetController animated:YES completion:nil];
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-19 19:23:46 +02:00
|
|
|
|
- (void)tappedAddToContactsOfferMessage:(OWSAddToContactsOfferMessage *)errorMessage
|
|
|
|
|
{
|
|
|
|
|
if (!self.contactsManager.supportsContactEditing) {
|
|
|
|
|
DDLogError(@"%@ Contact editing not supported", self.tag);
|
|
|
|
|
OWSAssert(NO);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (![self.thread isKindOfClass:[TSContactThread class]]) {
|
|
|
|
|
DDLogError(@"%@ unexpected thread: %@ in %s", self.tag, self.thread, __PRETTY_FUNCTION__);
|
|
|
|
|
OWSAssert(NO);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TSContactThread *contactThread = (TSContactThread *)self.thread;
|
|
|
|
|
[self.contactsViewHelper presentContactViewControllerForRecipientId:contactThread.contactIdentifier
|
|
|
|
|
fromViewController:self
|
|
|
|
|
editImmediately:YES];
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-06 15:46:00 +02:00
|
|
|
|
- (void)handleCallTap:(TSCall *)call
|
|
|
|
|
{
|
|
|
|
|
OWSAssert(call);
|
|
|
|
|
|
|
|
|
|
if (![self.thread isKindOfClass:[TSContactThread class]]) {
|
|
|
|
|
DDLogError(@"%@ unexpected thread: %@ in %s", self.tag, self.thread, __PRETTY_FUNCTION__);
|
|
|
|
|
OWSAssert(NO);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TSContactThread *contactThread = (TSContactThread *)self.thread;
|
|
|
|
|
NSString *displayName = [self.contactsManager displayNameForPhoneIdentifier:contactThread.contactIdentifier];
|
|
|
|
|
|
|
|
|
|
UIAlertController *alertController = [UIAlertController
|
|
|
|
|
alertControllerWithTitle:[CallStrings callBackAlertTitle]
|
|
|
|
|
message:[NSString stringWithFormat:[CallStrings callBackAlertMessageFormat], displayName]
|
|
|
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
|
|
|
|
|
|
|
|
__weak MessagesViewController *weakSelf = self;
|
|
|
|
|
UIAlertAction *callAction = [UIAlertAction actionWithTitle:[CallStrings callBackAlertCallButton]
|
|
|
|
|
style:UIAlertActionStyleDefault
|
|
|
|
|
handler:^(UIAlertAction *_Nonnull action) {
|
|
|
|
|
[weakSelf callAction:nil];
|
|
|
|
|
}];
|
|
|
|
|
[alertController addAction:callAction];
|
|
|
|
|
UIAlertAction *dismissAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_CANCEL_TITLE", nil)
|
|
|
|
|
style:UIAlertActionStyleCancel
|
|
|
|
|
handler:nil];
|
|
|
|
|
[alertController addAction:dismissAction];
|
|
|
|
|
|
|
|
|
|
[[UIApplication sharedApplication].frontmostViewController presentViewController:alertController
|
|
|
|
|
animated:YES
|
|
|
|
|
completion:nil];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark - OWSSystemMessageCellDelegate
|
|
|
|
|
|
|
|
|
|
- (void)didTapSystemMessageWithInteraction:(TSInteraction *)interaction
|
|
|
|
|
{
|
|
|
|
|
OWSAssert([NSThread isMainThread]);
|
|
|
|
|
OWSAssert(interaction);
|
|
|
|
|
|
|
|
|
|
if ([interaction isKindOfClass:[TSErrorMessage class]]) {
|
|
|
|
|
[self handleErrorMessageTap:(TSErrorMessage *)interaction];
|
|
|
|
|
} else if ([interaction isKindOfClass:[TSInfoMessage class]]) {
|
|
|
|
|
[self handleInfoMessageTap:(TSInfoMessage *)interaction];
|
|
|
|
|
} else if ([interaction isKindOfClass:[TSCall class]]) {
|
|
|
|
|
[self handleCallTap:(TSCall *)interaction];
|
|
|
|
|
} else {
|
|
|
|
|
OWSFail(@"Tap for system messages of unknown type: %@", [interaction class]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-20 18:33:23 +02:00
|
|
|
|
- (void)didLongPressSystemMessageCell:(OWSSystemMessageCell *)systemMessageCell;
|
|
|
|
|
{
|
|
|
|
|
OWSAssert([NSThread isMainThread]);
|
|
|
|
|
OWSAssert(systemMessageCell);
|
|
|
|
|
OWSAssert(systemMessageCell.interaction);
|
|
|
|
|
|
|
|
|
|
DDLogDebug(@"%@ long pressed system message cell: %@", self.tag, systemMessageCell);
|
|
|
|
|
|
|
|
|
|
[systemMessageCell becomeFirstResponder];
|
|
|
|
|
|
|
|
|
|
UIMenuController *theMenu = [UIMenuController sharedMenuController];
|
2017-06-20 23:31:26 +02:00
|
|
|
|
CGRect targetRect = [systemMessageCell.titleLabel.superview convertRect:systemMessageCell.titleLabel.frame
|
|
|
|
|
toView:systemMessageCell];
|
|
|
|
|
[theMenu setTargetRect:targetRect inView:systemMessageCell];
|
2017-06-20 18:33:23 +02:00
|
|
|
|
[theMenu setMenuVisible:YES animated:YES];
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-19 19:23:46 +02:00
|
|
|
|
#pragma mark - ContactEditingDelegate
|
|
|
|
|
|
|
|
|
|
- (void)didFinishEditingContact
|
|
|
|
|
{
|
|
|
|
|
DDLogDebug(@"%@ %s", self.tag, __PRETTY_FUNCTION__);
|
|
|
|
|
|
|
|
|
|
[self dismissViewControllerAnimated:NO completion:nil];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark - CNContactViewControllerDelegate
|
|
|
|
|
|
|
|
|
|
- (void)contactViewController:(CNContactViewController *)viewController
|
|
|
|
|
didCompleteWithContact:(nullable CNContact *)contact
|
|
|
|
|
{
|
|
|
|
|
if (contact) {
|
|
|
|
|
// Saving normally returns you to the "Show Contact" view
|
|
|
|
|
// which we're not interested in, so we skip it here. There is
|
|
|
|
|
// an unfortunate blip of the "Show Contact" view on slower devices.
|
|
|
|
|
DDLogDebug(@"%@ completed editing contact.", self.tag);
|
|
|
|
|
[self dismissViewControllerAnimated:NO completion:nil];
|
|
|
|
|
} else {
|
|
|
|
|
DDLogDebug(@"%@ canceled editing contact.", self.tag);
|
|
|
|
|
[self dismissViewControllerAnimated:YES completion:nil];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark - ContactsViewHelperDelegate
|
|
|
|
|
|
|
|
|
|
- (void)contactsViewHelperDidUpdateContacts
|
|
|
|
|
{
|
2017-05-31 20:22:32 +02:00
|
|
|
|
[self ensureDynamicInteractions];
|
2017-05-19 19:23:46 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (void)ensureDynamicInteractions
|
2017-05-19 19:23:46 +02:00
|
|
|
|
{
|
|
|
|
|
OWSAssert([NSThread isMainThread]);
|
|
|
|
|
|
2017-05-26 00:00:41 +02:00
|
|
|
|
const int initialMaxRangeSize = kYapDatabasePageSize * kYapDatabaseMaxInitialPageCount;
|
|
|
|
|
const int currentMaxRangeSize = (int)(self.page + 1) * kYapDatabasePageSize;
|
|
|
|
|
const int maxRangeSize = MAX(initialMaxRangeSize, currentMaxRangeSize);
|
|
|
|
|
|
2017-06-19 20:05:59 +02:00
|
|
|
|
// `ensureDynamicInteractionsForThread` should operate on the latest thread contents, so
|
|
|
|
|
// we should _read_ from uiDatabaseConnection and _write_ to `editingDatabaseConnection`.
|
2017-05-31 20:22:32 +02:00
|
|
|
|
self.dynamicInteractions =
|
|
|
|
|
[ThreadUtil ensureDynamicInteractionsForThread:self.thread
|
|
|
|
|
contactsManager:self.contactsManager
|
|
|
|
|
blockingManager:self.blockingManager
|
2017-06-19 23:10:34 +02:00
|
|
|
|
dbConnection:self.editingDatabaseConnection
|
2017-05-31 20:22:32 +02:00
|
|
|
|
hideUnreadMessagesIndicator:self.hasClearedUnreadMessagesIndicator
|
|
|
|
|
firstUnseenInteractionTimestamp:self.dynamicInteractions.firstUnseenInteractionTimestamp
|
|
|
|
|
maxRangeSize:maxRangeSize];
|
|
|
|
|
|
|
|
|
|
[self updateLastVisibleTimestamp];
|
2017-05-19 21:19:51 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)clearUnreadMessagesIndicator
|
|
|
|
|
{
|
|
|
|
|
OWSAssert([NSThread isMainThread]);
|
|
|
|
|
|
|
|
|
|
if (self.hasClearedUnreadMessagesIndicator) {
|
2017-05-31 20:22:32 +02:00
|
|
|
|
// ensureDynamicInteractionsForThread is somewhat expensive
|
2017-05-19 21:36:43 +02:00
|
|
|
|
// so we don't want to call it unnecessarily.
|
2017-05-19 21:19:51 +02:00
|
|
|
|
return;
|
2017-05-19 19:23:46 +02:00
|
|
|
|
}
|
2017-05-19 21:19:51 +02:00
|
|
|
|
|
|
|
|
|
// Once we've cleared the unread messages indicator,
|
|
|
|
|
// make sure we don't show it again.
|
|
|
|
|
self.hasClearedUnreadMessagesIndicator = YES;
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
if (self.dynamicInteractions.unreadIndicatorPosition) {
|
2017-05-30 18:19:17 +02:00
|
|
|
|
// If we've just cleared the "unread messages" indicator,
|
|
|
|
|
// update the dynamic interactions.
|
2017-05-31 20:22:32 +02:00
|
|
|
|
[self ensureDynamicInteractions];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)viewDidLayoutSubviews
|
|
|
|
|
{
|
|
|
|
|
[super viewDidLayoutSubviews];
|
|
|
|
|
|
|
|
|
|
[self updateLastVisibleTimestamp];
|
2017-06-22 16:55:26 +02:00
|
|
|
|
[self ensureScrollDownButton];
|
2017-05-31 20:22:32 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)createScrollDownButton
|
|
|
|
|
{
|
2017-06-09 13:48:12 +02:00
|
|
|
|
const CGFloat kScrollDownButtonSize = ScaleFromIPhone5To7Plus(35.f, 40.f);
|
2017-05-31 20:22:32 +02:00
|
|
|
|
UIButton *scrollDownButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
|
|
|
|
self.scrollDownButton = scrollDownButton;
|
|
|
|
|
scrollDownButton.backgroundColor = [UIColor colorWithWhite:0.95f alpha:1.f];
|
|
|
|
|
scrollDownButton.frame = CGRectMake(0, 0, kScrollDownButtonSize, kScrollDownButtonSize);
|
|
|
|
|
scrollDownButton.layer.cornerRadius = kScrollDownButtonSize * 0.5f;
|
|
|
|
|
scrollDownButton.layer.shadowColor = [UIColor colorWithWhite:0.5f alpha:1.f].CGColor;
|
|
|
|
|
scrollDownButton.layer.shadowOffset = CGSizeMake(+1.f, +2.f);
|
|
|
|
|
scrollDownButton.layer.shadowRadius = 1.5f;
|
|
|
|
|
scrollDownButton.layer.shadowOpacity = 0.35f;
|
|
|
|
|
[scrollDownButton addTarget:self
|
|
|
|
|
action:@selector(scrollDownButtonTapped)
|
|
|
|
|
forControlEvents:UIControlEventTouchUpInside];
|
|
|
|
|
[self.view addSubview:self.scrollDownButton];
|
|
|
|
|
|
|
|
|
|
NSAttributedString *labelString = [[NSAttributedString alloc]
|
|
|
|
|
initWithString:@"\uf103"
|
|
|
|
|
attributes:@{
|
|
|
|
|
NSFontAttributeName : [UIFont ows_fontAwesomeFont:kScrollDownButtonSize * 0.8f],
|
|
|
|
|
NSForegroundColorAttributeName : [UIColor ows_materialBlueColor],
|
|
|
|
|
NSBaselineOffsetAttributeName : @(-0.5f),
|
|
|
|
|
}];
|
|
|
|
|
[scrollDownButton setAttributedTitle:labelString forState:UIControlStateNormal];
|
|
|
|
|
[scrollDownButton setTitleColor:[UIColor ows_materialBlueColor] forState:UIControlStateNormal];
|
|
|
|
|
scrollDownButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
|
|
|
|
|
scrollDownButton.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter;
|
|
|
|
|
|
|
|
|
|
[self updateLastVisibleTimestamp];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)scrollDownButtonTapped
|
|
|
|
|
{
|
|
|
|
|
[self scrollToBottomAnimated:YES];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)ensureScrollDownButton
|
|
|
|
|
{
|
|
|
|
|
OWSAssert([NSThread isMainThread]);
|
|
|
|
|
|
|
|
|
|
BOOL shouldShowScrollDownButton = NO;
|
|
|
|
|
NSUInteger numberOfMessages = [self.messageMappings numberOfItemsInSection:0];
|
2017-06-22 16:55:26 +02:00
|
|
|
|
CGFloat scrollSpaceToBottom = (self.collectionView.contentSize.height + self.collectionView.contentInset.bottom
|
|
|
|
|
- (self.collectionView.contentOffset.y + self.collectionView.frame.size.height));
|
|
|
|
|
CGFloat pageHeight = (self.collectionView.frame.size.height
|
|
|
|
|
- (self.collectionView.contentInset.top + self.collectionView.contentInset.bottom));
|
|
|
|
|
// Show "scroll down" button if user is scrolled up at least
|
|
|
|
|
// one page.
|
|
|
|
|
BOOL isScrolledUp = scrollSpaceToBottom > pageHeight * 1.f;
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
if (numberOfMessages > 0) {
|
|
|
|
|
TSInteraction *lastInteraction =
|
|
|
|
|
[self interactionAtIndexPath:[NSIndexPath indexPathForRow:(NSInteger)numberOfMessages - 1 inSection:0]];
|
|
|
|
|
OWSAssert(lastInteraction);
|
|
|
|
|
|
|
|
|
|
if (lastInteraction.timestampForSorting > self.lastVisibleTimestamp) {
|
|
|
|
|
shouldShowScrollDownButton = YES;
|
2017-06-22 16:55:26 +02:00
|
|
|
|
} else if (isScrolledUp) {
|
|
|
|
|
shouldShowScrollDownButton = YES;
|
2017-05-31 20:22:32 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (shouldShowScrollDownButton) {
|
|
|
|
|
self.scrollDownButton.hidden = NO;
|
|
|
|
|
|
|
|
|
|
const CGFloat kHMargin = 15.f;
|
|
|
|
|
const CGFloat kVMargin = 15.f;
|
|
|
|
|
self.scrollDownButton.frame
|
|
|
|
|
= CGRectMake(self.scrollDownButton.superview.width - (self.scrollDownButton.width + kHMargin),
|
|
|
|
|
self.inputToolbar.top - (self.scrollDownButton.height + kVMargin),
|
|
|
|
|
self.scrollDownButton.width,
|
|
|
|
|
self.scrollDownButton.height);
|
|
|
|
|
} else {
|
|
|
|
|
self.scrollDownButton.hidden = YES;
|
2017-05-30 18:19:17 +02:00
|
|
|
|
}
|
2017-05-19 19:23:46 +02:00
|
|
|
|
}
|
2017-04-20 23:51:45 +02:00
|
|
|
|
|
|
|
|
|
#pragma mark - Attachment Picking: Documents
|
|
|
|
|
|
|
|
|
|
- (void)showAttachmentDocumentPicker
|
|
|
|
|
{
|
2017-04-26 23:22:27 +02:00
|
|
|
|
NSString *allItems = (__bridge NSString *)kUTTypeItem;
|
2017-04-20 23:51:45 +02:00
|
|
|
|
NSArray<NSString *> *documentTypes = @[ allItems ];
|
|
|
|
|
// UIDocumentPickerModeImport copies to a temp file within our container.
|
2017-04-21 23:08:35 +02:00
|
|
|
|
// It uses more memory than "open" but lets us avoid working with security scoped URLs.
|
2017-04-20 23:51:45 +02:00
|
|
|
|
UIDocumentPickerMode pickerMode = UIDocumentPickerModeImport;
|
|
|
|
|
UIDocumentMenuViewController *menuController =
|
|
|
|
|
[[UIDocumentMenuViewController alloc] initWithDocumentTypes:documentTypes inMode:pickerMode];
|
|
|
|
|
menuController.delegate = self;
|
|
|
|
|
[self presentViewController:menuController animated:YES completion:nil];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark UIDocumentMenuDelegate
|
|
|
|
|
|
|
|
|
|
- (void)documentMenu:(UIDocumentMenuViewController *)documentMenu
|
|
|
|
|
didPickDocumentPicker:(UIDocumentPickerViewController *)documentPicker
|
|
|
|
|
{
|
|
|
|
|
documentPicker.delegate = self;
|
|
|
|
|
[self presentViewController:documentPicker animated:YES completion:nil];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark UIDocumentPickerDelegate
|
2017-04-22 15:45:20 +02:00
|
|
|
|
|
2017-04-20 23:51:45 +02:00
|
|
|
|
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url
|
|
|
|
|
{
|
|
|
|
|
DDLogDebug(@"%@ Picked document at url: %@", self.tag, url);
|
|
|
|
|
NSData *attachmentData = [NSData dataWithContentsOfURL:url];
|
|
|
|
|
|
|
|
|
|
NSString *type;
|
2017-04-26 23:22:27 +02:00
|
|
|
|
NSError *typeError;
|
|
|
|
|
[url getResourceValue:&type forKey:NSURLTypeIdentifierKey error:&typeError];
|
|
|
|
|
if (typeError) {
|
|
|
|
|
DDLogError(
|
|
|
|
|
@"%@ Determining type of picked document at url: %@ failed with error: %@", self.tag, url, typeError);
|
2017-04-20 23:51:45 +02:00
|
|
|
|
OWSAssert(NO);
|
|
|
|
|
}
|
2017-04-27 15:46:51 +02:00
|
|
|
|
if (!type) {
|
|
|
|
|
DDLogDebug(@"%@ falling back to default filetype for picked document at url: %@", self.tag, url);
|
|
|
|
|
OWSAssert(NO);
|
|
|
|
|
type = (__bridge NSString *)kUTTypeData;
|
|
|
|
|
}
|
2017-04-20 23:51:45 +02:00
|
|
|
|
|
2017-04-26 23:22:27 +02:00
|
|
|
|
NSNumber *isDirectory;
|
|
|
|
|
NSError *isDirectoryError;
|
|
|
|
|
[url getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:&isDirectoryError];
|
|
|
|
|
if (isDirectoryError) {
|
|
|
|
|
DDLogError(@"%@ Determining if picked document at url: %@ was a directory failed with error: %@",
|
|
|
|
|
self.tag,
|
|
|
|
|
url,
|
|
|
|
|
isDirectoryError);
|
|
|
|
|
OWSAssert(NO);
|
|
|
|
|
} else if ([isDirectory boolValue]) {
|
|
|
|
|
DDLogInfo(@"%@ User picked directory at url: %@", self.tag, url);
|
|
|
|
|
UIAlertController *alertController = [UIAlertController
|
|
|
|
|
alertControllerWithTitle:
|
|
|
|
|
NSLocalizedString(@"ATTACHMENT_PICKER_DOCUMENTS_PICKED_DIRECTORY_FAILED_ALERT_TITLE",
|
|
|
|
|
@"Alert title when picking a document fails because user picked a directory/bundle")
|
|
|
|
|
message:
|
|
|
|
|
NSLocalizedString(@"ATTACHMENT_PICKER_DOCUMENTS_PICKED_DIRECTORY_FAILED_ALERT_BODY",
|
|
|
|
|
@"Alert body when picking a document fails because user picked a directory/bundle")
|
|
|
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
|
|
|
|
2017-07-13 20:53:24 +02:00
|
|
|
|
UIAlertAction *dismissAction =
|
|
|
|
|
[UIAlertAction actionWithTitle:CommonStrings.dismissButton style:UIAlertActionStyleCancel handler:nil];
|
2017-04-26 23:22:27 +02:00
|
|
|
|
[alertController addAction:dismissAction];
|
|
|
|
|
|
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
|
[self presentViewController:alertController animated:YES completion:nil];
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2017-04-20 23:51:45 +02:00
|
|
|
|
NSString *filename = url.lastPathComponent;
|
|
|
|
|
if (!filename) {
|
|
|
|
|
DDLogDebug(@"%@ Unable to determine filename from url: %@", self.tag, url);
|
|
|
|
|
OWSAssert(NO);
|
|
|
|
|
filename = NSLocalizedString(
|
|
|
|
|
@"ATTACHMENT_DEFAULT_FILENAME", @"Generic filename for an attachment with no known name");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!attachmentData || attachmentData.length == 0) {
|
|
|
|
|
DDLogError(@"%@ attachment data was unexpectedly empty for picked document url: %@", self.tag, url);
|
|
|
|
|
OWSAssert(NO);
|
|
|
|
|
UIAlertController *alertController = [UIAlertController
|
|
|
|
|
alertControllerWithTitle:NSLocalizedString(@"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE",
|
|
|
|
|
@"Alert title when picking a document fails for an unknown reason")
|
|
|
|
|
message:nil
|
|
|
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
|
|
|
|
2017-07-13 20:53:24 +02:00
|
|
|
|
UIAlertAction *dismissAction =
|
|
|
|
|
[UIAlertAction actionWithTitle:CommonStrings.dismissButton style:UIAlertActionStyleCancel handler:nil];
|
2017-04-20 23:51:45 +02:00
|
|
|
|
[alertController addAction:dismissAction];
|
|
|
|
|
|
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
|
[self presentViewController:alertController animated:YES completion:nil];
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
OWSAssert(attachmentData);
|
|
|
|
|
OWSAssert(type);
|
|
|
|
|
OWSAssert(filename);
|
2017-04-24 23:43:49 +02:00
|
|
|
|
SignalAttachment *attachment = [SignalAttachment attachmentWithData:attachmentData dataUTI:type filename:filename];
|
2017-04-21 23:08:35 +02:00
|
|
|
|
[self tryToSendAttachmentIfApproved:attachment];
|
2017-04-20 23:51:45 +02:00
|
|
|
|
}
|
|
|
|
|
|
2014-10-29 21:58:58 +01:00
|
|
|
|
#pragma mark - UIImagePickerController
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Presenting UIImagePickerController
|
|
|
|
|
*/
|
|
|
|
|
|
2017-05-31 20:22:32 +02: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]];
|
|
|
|
|
});
|
2017-06-09 00:10:02 +02:00
|
|
|
|
}];
|
2016-07-07 18:54:30 +02:00
|
|
|
|
}
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (void)chooseFromLibrary
|
|
|
|
|
{
|
2016-07-07 18:54:30 +02:00
|
|
|
|
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
|
|
|
|
|
*/
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (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];
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (void)resetFrame
|
|
|
|
|
{
|
2015-01-31 09:38:52 +01:00
|
|
|
|
// fixes bug on frame being off after this selection
|
2017-05-31 20:22:32 +02:00
|
|
|
|
CGRect frame = [UIScreen mainScreen].applicationFrame;
|
2015-01-31 09:38:52 +01:00
|
|
|
|
self.view.frame = frame;
|
|
|
|
|
}
|
|
|
|
|
|
2014-10-29 21:58:58 +01:00
|
|
|
|
/*
|
2014-11-29 19:54:33 +01:00
|
|
|
|
* Fetching data from UIImagePickerController
|
2014-10-29 21:58:58 +01:00
|
|
|
|
*/
|
2016-07-22 02:15:34 +02:00
|
|
|
|
- (void)imagePickerController:(UIImagePickerController *)picker
|
|
|
|
|
didFinishPickingMediaWithInfo:(NSDictionary<NSString *, id> *)info
|
|
|
|
|
{
|
2015-01-31 09:38:52 +01:00
|
|
|
|
[UIUtil modalCompletionBlock]();
|
|
|
|
|
[self resetFrame];
|
2015-12-22 12:45:09 +01:00
|
|
|
|
|
2017-04-13 18:55:21 +02:00
|
|
|
|
NSURL *referenceURL = [info valueForKey:UIImagePickerControllerReferenceURL];
|
|
|
|
|
if (!referenceURL) {
|
2017-04-13 22:06:23 +02:00
|
|
|
|
DDLogVerbose(@"Could not retrieve reference URL for picked asset");
|
|
|
|
|
[self imagePickerController:picker didFinishPickingMediaWithInfo:info filename:nil];
|
2017-04-13 18:55:21 +02:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ALAssetsLibraryAssetForURLResultBlock resultblock = ^(ALAsset *imageAsset) {
|
|
|
|
|
ALAssetRepresentation *imageRep = [imageAsset defaultRepresentation];
|
|
|
|
|
NSString *filename = [imageRep filename];
|
|
|
|
|
[self imagePickerController:picker didFinishPickingMediaWithInfo:info filename:filename];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ALAssetsLibrary *assetslibrary = [[ALAssetsLibrary alloc] init];
|
|
|
|
|
[assetslibrary assetForURL:referenceURL
|
|
|
|
|
resultBlock:resultblock
|
|
|
|
|
failureBlock:^(NSError *error) {
|
|
|
|
|
DDLogError(@"Error retrieving filename for asset: %@", error);
|
|
|
|
|
OWSAssert(0);
|
|
|
|
|
}];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)imagePickerController:(UIImagePickerController *)picker
|
|
|
|
|
didFinishPickingMediaWithInfo:(NSDictionary<NSString *, id> *)info
|
|
|
|
|
filename:(NSString *)filename
|
|
|
|
|
{
|
|
|
|
|
OWSAssert([NSThread isMainThread]);
|
|
|
|
|
|
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
|
|
|
|
|
|
2017-04-22 15:45:20 +02:00
|
|
|
|
BOOL isFromCamera = picker.sourceType == UIImagePickerControllerSourceTypeCamera;
|
2016-07-22 02:15:34 +02:00
|
|
|
|
NSURL *videoURL = info[UIImagePickerControllerMediaURL];
|
2017-03-16 19:29:30 +01:00
|
|
|
|
[self dismissViewControllerAnimated:YES
|
|
|
|
|
completion:^{
|
2017-04-22 15:45:20 +02:00
|
|
|
|
[self sendQualityAdjustedAttachmentForVideo:videoURL
|
|
|
|
|
filename:filename
|
|
|
|
|
skipApprovalDialog:isFromCamera];
|
2017-03-16 19:29:30 +01:00
|
|
|
|
}];
|
2016-07-22 02:15:34 +02:00
|
|
|
|
} else if (picker.sourceType == UIImagePickerControllerSourceTypeCamera) {
|
|
|
|
|
// Static Image captured from camera
|
|
|
|
|
|
|
|
|
|
UIImage *imageFromCamera = [info[UIImagePickerControllerOriginalImage] normalizedImage];
|
2017-04-13 18:55:21 +02:00
|
|
|
|
|
2017-03-16 19:29:30 +01:00
|
|
|
|
[self dismissViewControllerAnimated:YES
|
|
|
|
|
completion:^{
|
|
|
|
|
OWSAssert([NSThread isMainThread]);
|
2017-05-30 19:04:43 +02:00
|
|
|
|
|
2017-03-16 19:29:30 +01:00
|
|
|
|
if (imageFromCamera) {
|
2017-04-13 18:55:21 +02:00
|
|
|
|
SignalAttachment *attachment =
|
|
|
|
|
[SignalAttachment imageAttachmentWithImage:imageFromCamera
|
|
|
|
|
dataUTI:(NSString *)kUTTypeJPEG
|
|
|
|
|
filename:filename];
|
2017-05-31 20:22:32 +02:00
|
|
|
|
if (!attachment || [attachment hasError]) {
|
2017-03-16 19:29:30 +01:00
|
|
|
|
DDLogWarn(@"%@ %s Invalid attachment: %@.",
|
2017-05-30 19:04:43 +02:00
|
|
|
|
self.tag,
|
|
|
|
|
__PRETTY_FUNCTION__,
|
|
|
|
|
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-22 15:45:20 +02:00
|
|
|
|
[self tryToSendAttachmentIfApproved:attachment skipApprovalDialog:YES];
|
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-05-30 19:04:43 +02: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 attachmentWithData:imageData dataUTI:dataUTI filename:filename];
|
|
|
|
|
[self dismissViewControllerAnimated:YES
|
|
|
|
|
completion:^{
|
|
|
|
|
OWSAssert([NSThread isMainThread]);
|
|
|
|
|
if (!attachment || [attachment hasError]) {
|
|
|
|
|
DDLogWarn(@"%@ %s Invalid attachment: %@.",
|
|
|
|
|
self.tag,
|
|
|
|
|
__PRETTY_FUNCTION__,
|
|
|
|
|
attachment ? [attachment errorName] : @"Missing data");
|
|
|
|
|
[self showErrorAlertForAttachment:attachment];
|
|
|
|
|
failedToPickAttachment(nil);
|
|
|
|
|
} else {
|
|
|
|
|
[self tryToSendAttachmentIfApproved:attachment];
|
|
|
|
|
}
|
|
|
|
|
}];
|
|
|
|
|
}];
|
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]);
|
2017-05-31 20:22:32 +02:00
|
|
|
|
[self updateLastVisibleTimestamp:[ThreadUtil sendMessageWithAttachment:attachment
|
|
|
|
|
inThread:self.thread
|
|
|
|
|
messageSender:self.messageSender]
|
|
|
|
|
.timestampForSorting];
|
2017-05-17 23:17:37 +02:00
|
|
|
|
self.lastMessageSentDate = [NSDate new];
|
2017-05-16 21:52:19 +02:00
|
|
|
|
[self clearUnreadMessagesIndicator];
|
2015-01-14 22:30:01 +01:00
|
|
|
|
}
|
|
|
|
|
|
2017-05-31 20:22:32 +02: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;
|
2017-05-31 20:22:32 +02: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];
|
|
|
|
|
}
|
|
|
|
|
|
2017-04-22 15:45:20 +02:00
|
|
|
|
- (void)sendQualityAdjustedAttachmentForVideo:(NSURL *)movieURL
|
|
|
|
|
filename:(NSString *)filename
|
|
|
|
|
skipApprovalDialog:(BOOL)skipApprovalDialog
|
2017-04-13 18:55:21 +02:00
|
|
|
|
{
|
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;
|
2017-05-31 20:22:32 +02:00
|
|
|
|
exportSession.outputFileType = AVFileTypeMPEG4;
|
2015-12-22 12:45:09 +01:00
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
double currentTime = [[NSDate date] timeIntervalSince1970];
|
2015-12-22 12:45:09 +01:00
|
|
|
|
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];
|
2017-04-13 22:06:23 +02:00
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
|
SignalAttachment *attachment =
|
2017-04-25 04:30:46 +02:00
|
|
|
|
[SignalAttachment attachmentWithData:videoData dataUTI:(NSString *)kUTTypeMPEG4 filename:filename];
|
2017-04-13 22:06:23 +02:00
|
|
|
|
if (!attachment || [attachment hasError]) {
|
|
|
|
|
DDLogWarn(@"%@ %s Invalid attachment: %@.",
|
|
|
|
|
self.tag,
|
|
|
|
|
__PRETTY_FUNCTION__,
|
|
|
|
|
attachment ? [attachment errorName] : @"Missing data");
|
|
|
|
|
[self showErrorAlertForAttachment:attachment];
|
|
|
|
|
} else {
|
2017-04-22 15:45:20 +02:00
|
|
|
|
[self tryToSendAttachmentIfApproved:attachment skipApprovalDialog:skipApprovalDialog];
|
2017-04-13 22:06:23 +02: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
|
|
|
|
|
|
2017-05-31 20:22:32 +02: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;
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-31 20:22:32 +02: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;
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-31 20:22:32 +02: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-07-25 18:52:30 +02:00
|
|
|
|
if (!self.shouldObserveDBModifications) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
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];
|
2017-04-17 21:50:43 +02:00
|
|
|
|
[self updateNavigationBarSubtitleLabel];
|
2015-12-22 12:45:09 +01:00
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
if (self.isGroupConversation) {
|
2014-12-24 02:25:10 +01:00
|
|
|
|
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
2017-05-31 20:22:32 +02:00
|
|
|
|
TSGroupThread *gThread = (TSGroupThread *)self.thread;
|
2017-05-30 19:04:43 +02:00
|
|
|
|
|
2017-05-31 20:22:32 +02: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
|
|
|
|
|
2017-06-19 19:56:23 +02:00
|
|
|
|
if (![[self.uiDatabaseConnection ext:TSMessageDatabaseViewExtensionName] hasChangesForGroup:self.thread.uniqueId
|
|
|
|
|
inNotifications:notifications]) {
|
2015-12-22 12:45:09 +01:00
|
|
|
|
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
2017-05-31 20:22:32 +02:00
|
|
|
|
[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-%20CollectionViewBatchingIssue
|
|
|
|
|
// 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;
|
2017-05-31 20:22:32 +02:00
|
|
|
|
NSArray *sectionChanges = nil;
|
2017-06-19 19:56:23 +02:00
|
|
|
|
[[self.uiDatabaseConnection ext:TSMessageDatabaseViewExtensionName]
|
|
|
|
|
getSectionChanges:§ionChanges
|
|
|
|
|
rowChanges:&messageRowChanges
|
|
|
|
|
forNotifications:notifications
|
|
|
|
|
withMappings:self.messageMappings];
|
|
|
|
|
|
2017-07-25 18:52:30 +02:00
|
|
|
|
if ([sectionChanges count] == 0 && [messageRowChanges count] == 0) {
|
2017-07-25 18:58:30 +02:00
|
|
|
|
// YapDatabase will ignore insertions within the message mapping's
|
|
|
|
|
// range that are not within the current mapping's contents. We
|
|
|
|
|
// may need to extend the mapping's contents to reflect the current
|
|
|
|
|
// range.
|
|
|
|
|
[self updateMessageMappingRangeOptions];
|
|
|
|
|
[self resetContentAndLayout];
|
2014-12-08 23:12:22 +01:00
|
|
|
|
return;
|
|
|
|
|
}
|
2017-05-16 20:04:44 +02:00
|
|
|
|
|
2017-05-16 19:46:57 +02:00
|
|
|
|
BOOL wasAtBottom = [self isScrolledToBottom];
|
2017-03-15 18:46:03 +01:00
|
|
|
|
// 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:^{
|
2017-05-26 00:00:41 +02:00
|
|
|
|
for (YapDatabaseViewRowChange *rowChange in messageRowChanges) {
|
|
|
|
|
switch (rowChange.type) {
|
|
|
|
|
case YapDatabaseViewChangeDelete: {
|
|
|
|
|
[self.collectionView deleteItemsAtIndexPaths:@[ rowChange.indexPath ]];
|
|
|
|
|
|
|
|
|
|
YapCollectionKey *collectionKey = rowChange.collectionKey;
|
2017-06-20 19:00:38 +02:00
|
|
|
|
OWSAssert(collectionKey.key.length > 0);
|
2017-05-26 00:00:41 +02:00
|
|
|
|
if (collectionKey.key) {
|
|
|
|
|
[self.messageAdapterCache removeObjectForKey:collectionKey.key];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case YapDatabaseViewChangeInsert: {
|
|
|
|
|
[self.collectionView insertItemsAtIndexPaths:@[ rowChange.newIndexPath ]];
|
|
|
|
|
|
|
|
|
|
TSInteraction *interaction = [self interactionAtIndexPath:rowChange.newIndexPath];
|
|
|
|
|
if ([interaction isKindOfClass:[TSOutgoingMessage class]]) {
|
|
|
|
|
scrollToBottom = YES;
|
|
|
|
|
shouldAnimateScrollToBottom = NO;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case YapDatabaseViewChangeMove: {
|
|
|
|
|
[self.collectionView deleteItemsAtIndexPaths:@[ rowChange.indexPath ]];
|
|
|
|
|
[self.collectionView insertItemsAtIndexPaths:@[ rowChange.newIndexPath ]];
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case YapDatabaseViewChangeUpdate: {
|
|
|
|
|
YapCollectionKey *collectionKey = rowChange.collectionKey;
|
2017-06-20 19:00:38 +02:00
|
|
|
|
OWSAssert(collectionKey.key.length > 0);
|
2017-05-26 00:00:41 +02:00
|
|
|
|
if (collectionKey.key) {
|
|
|
|
|
[self.messageAdapterCache removeObjectForKey:collectionKey.key];
|
|
|
|
|
}
|
|
|
|
|
[self.collectionView reloadItemsAtIndexPaths:@[ rowChange.indexPath ]];
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2015-12-22 12:45:09 +01:00
|
|
|
|
}
|
|
|
|
|
completion:^(BOOL success) {
|
2017-05-26 00:00:41 +02:00
|
|
|
|
if (!success) {
|
2017-07-25 18:52:30 +02:00
|
|
|
|
[self resetContentAndLayout];
|
2017-05-26 00:00:41 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
[self updateLastVisibleTimestamp];
|
|
|
|
|
|
2017-05-26 00:00:41 +02:00
|
|
|
|
if (scrollToBottom) {
|
|
|
|
|
[self.scrollLaterTimer invalidate];
|
|
|
|
|
self.scrollLaterTimer = nil;
|
|
|
|
|
[self scrollToBottomAnimated:shouldAnimateScrollToBottom];
|
|
|
|
|
}
|
2015-12-22 12:45:09 +01:00
|
|
|
|
}];
|
2014-11-25 16:38:33 +01:00
|
|
|
|
}
|
|
|
|
|
|
2017-05-16 19:46:57 +02:00
|
|
|
|
- (BOOL)isScrolledToBottom
|
|
|
|
|
{
|
|
|
|
|
const CGFloat kIsAtBottomTolerancePts = 5;
|
|
|
|
|
return (self.collectionView.contentOffset.y + self.collectionView.bounds.size.height + kIsAtBottomTolerancePts
|
|
|
|
|
>= self.collectionView.contentSize.height);
|
|
|
|
|
}
|
|
|
|
|
|
2014-11-25 16:38:33 +01:00
|
|
|
|
#pragma mark - UICollectionView DataSource
|
2015-03-19 01:59:44 +01:00
|
|
|
|
|
2017-05-31 20:22:32 +02: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
|
|
|
|
|
2017-05-26 00:00:41 +02:00
|
|
|
|
- (TSInteraction *)interactionAtIndexPath:(NSIndexPath *)indexPath
|
|
|
|
|
{
|
|
|
|
|
OWSAssert(indexPath);
|
|
|
|
|
OWSAssert(indexPath.section == 0);
|
|
|
|
|
OWSAssert(self.messageMappings);
|
|
|
|
|
|
|
|
|
|
__block TSInteraction *interaction;
|
|
|
|
|
|
2014-11-25 16:38:33 +01:00
|
|
|
|
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
2017-05-26 00:00:41 +02:00
|
|
|
|
YapDatabaseViewTransaction *viewTransaction = [transaction ext:TSMessageDatabaseViewExtensionName];
|
|
|
|
|
OWSAssert(viewTransaction);
|
|
|
|
|
interaction = [viewTransaction objectAtRow:(NSUInteger)indexPath.row
|
|
|
|
|
inSection:(NSUInteger)indexPath.section
|
|
|
|
|
withMappings:self.messageMappings];
|
|
|
|
|
OWSAssert(interaction);
|
2014-11-25 16:38:33 +01:00
|
|
|
|
}];
|
2017-05-26 00:00:41 +02:00
|
|
|
|
return interaction;
|
2014-12-11 00:05:41 +01:00
|
|
|
|
}
|
|
|
|
|
|
2016-09-21 14:37:51 +02:00
|
|
|
|
- (id<OWSMessageData>)messageAtIndexPath:(NSIndexPath *)indexPath
|
|
|
|
|
{
|
2017-07-25 18:52:30 +02:00
|
|
|
|
OWSAssert(self.messageAdapterCache);
|
|
|
|
|
|
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) {
|
2017-05-31 20:22:32 +02:00
|
|
|
|
messageAdapter = [TSMessageAdapter messageViewDataWithInteraction:interaction
|
|
|
|
|
inThread:self.thread
|
|
|
|
|
contactsManager:self.contactsManager];
|
|
|
|
|
[self.messageAdapterCache setObject:messageAdapter forKey:interaction.uniqueId];
|
2016-04-13 19:05:09 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2017-05-15 22:51:12 +02:00
|
|
|
|
- (void)requestRecordingVoiceMemo
|
|
|
|
|
{
|
|
|
|
|
OWSAssert([NSThread isMainThread]);
|
2017-05-15 23:03:46 +02:00
|
|
|
|
|
2017-05-17 23:17:37 +02:00
|
|
|
|
NSUUID *voiceMessageUUID = [NSUUID UUID];
|
|
|
|
|
self.voiceMessageUUID = voiceMessageUUID;
|
|
|
|
|
|
2017-05-15 23:03:46 +02:00
|
|
|
|
__weak typeof(self) weakSelf = self;
|
2017-05-15 22:51:12 +02:00
|
|
|
|
[[AVAudioSession sharedInstance] requestRecordPermission:^(BOOL granted) {
|
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
2017-05-15 23:03:46 +02:00
|
|
|
|
__strong typeof(self) strongSelf = weakSelf;
|
|
|
|
|
if (!strongSelf) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2017-05-30 19:04:43 +02:00
|
|
|
|
|
2017-05-17 23:17:37 +02:00
|
|
|
|
if (strongSelf.voiceMessageUUID != voiceMessageUUID) {
|
|
|
|
|
// This voice message recording has been cancelled
|
|
|
|
|
// before recording could begin.
|
|
|
|
|
return;
|
|
|
|
|
}
|
2017-05-30 19:04:43 +02:00
|
|
|
|
|
2017-05-15 22:51:12 +02:00
|
|
|
|
if (granted) {
|
2017-05-15 23:03:46 +02:00
|
|
|
|
[strongSelf startRecordingVoiceMemo];
|
2017-05-15 22:51:12 +02:00
|
|
|
|
} else {
|
|
|
|
|
DDLogInfo(@"%@ we do not have recording permission.", self.tag);
|
2017-05-15 23:03:46 +02:00
|
|
|
|
[strongSelf cancelVoiceMemo];
|
2017-05-15 22:51:12 +02:00
|
|
|
|
[OWSAlerts showNoMicrophonePermissionAlert];
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}];
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-05 04:10:37 +02:00
|
|
|
|
- (void)startRecordingVoiceMemo
|
|
|
|
|
{
|
|
|
|
|
OWSAssert([NSThread isMainThread]);
|
|
|
|
|
|
|
|
|
|
DDLogInfo(@"startRecordingVoiceMemo");
|
|
|
|
|
|
2017-05-12 15:22:56 +02:00
|
|
|
|
// Cancel any ongoing audio playback.
|
|
|
|
|
[self.audioAttachmentPlayer stop];
|
|
|
|
|
self.audioAttachmentPlayer = nil;
|
|
|
|
|
|
2017-05-05 04:10:37 +02:00
|
|
|
|
NSString *temporaryDirectory = NSTemporaryDirectory();
|
|
|
|
|
NSString *filename = [NSString stringWithFormat:@"%lld.m4a", [NSDate ows_millisecondTimeStamp]];
|
|
|
|
|
NSString *filepath = [temporaryDirectory stringByAppendingPathComponent:filename];
|
|
|
|
|
NSURL *fileURL = [NSURL fileURLWithPath:filepath];
|
2015-12-22 12:45:09 +01:00
|
|
|
|
|
2015-01-22 05:08:12 +01:00
|
|
|
|
// Setup audio session
|
|
|
|
|
AVAudioSession *session = [AVAudioSession sharedInstance];
|
2017-05-15 22:51:12 +02:00
|
|
|
|
OWSAssert(session.recordPermission == AVAudioSessionRecordPermissionGranted);
|
|
|
|
|
|
2017-05-05 04:10:37 +02:00
|
|
|
|
NSError *error;
|
2017-05-16 00:14:28 +02:00
|
|
|
|
[session setCategory:AVAudioSessionCategoryRecord error:&error];
|
2017-05-05 04:10:37 +02:00
|
|
|
|
if (error) {
|
|
|
|
|
DDLogError(@"%@ Couldn't configure audio session: %@", self.tag, error);
|
2017-05-08 17:13:51 +02:00
|
|
|
|
[self cancelVoiceMemo];
|
2017-05-05 04:10:37 +02:00
|
|
|
|
OWSAssert(0);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2015-12-22 12:45:09 +01:00
|
|
|
|
|
2015-01-22 05:08:12 +01:00
|
|
|
|
// Initiate and prepare the recorder
|
2017-05-05 04:10:37 +02:00
|
|
|
|
self.audioRecorder = [[AVAudioRecorder alloc] initWithURL:fileURL
|
|
|
|
|
settings:@{
|
|
|
|
|
AVFormatIDKey : @(kAudioFormatMPEG4AAC),
|
|
|
|
|
AVSampleRateKey : @(44100),
|
|
|
|
|
AVNumberOfChannelsKey : @(2),
|
2017-05-31 20:22:32 +02:00
|
|
|
|
AVEncoderBitRateKey : @(128 * 1024),
|
2017-05-05 04:10:37 +02:00
|
|
|
|
}
|
|
|
|
|
error:&error];
|
|
|
|
|
if (error) {
|
|
|
|
|
DDLogError(@"%@ Couldn't create audioRecorder: %@", self.tag, error);
|
2017-05-08 17:13:51 +02:00
|
|
|
|
[self cancelVoiceMemo];
|
2017-05-05 04:10:37 +02:00
|
|
|
|
OWSAssert(0);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.audioRecorder.meteringEnabled = YES;
|
|
|
|
|
|
|
|
|
|
if (![self.audioRecorder prepareToRecord]) {
|
|
|
|
|
DDLogError(@"%@ audioRecorder couldn't prepareToRecord.", self.tag);
|
2017-05-08 17:13:51 +02:00
|
|
|
|
[self cancelVoiceMemo];
|
2017-05-05 04:10:37 +02:00
|
|
|
|
OWSAssert(0);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (![self.audioRecorder record]) {
|
|
|
|
|
DDLogError(@"%@ audioRecorder couldn't record.", self.tag);
|
2017-05-08 17:13:51 +02:00
|
|
|
|
[self cancelVoiceMemo];
|
2017-05-05 04:10:37 +02:00
|
|
|
|
OWSAssert(0);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)endRecordingVoiceMemo
|
|
|
|
|
{
|
|
|
|
|
OWSAssert([NSThread isMainThread]);
|
|
|
|
|
|
|
|
|
|
DDLogInfo(@"endRecordingVoiceMemo");
|
|
|
|
|
|
2017-05-17 23:17:37 +02:00
|
|
|
|
self.voiceMessageUUID = nil;
|
|
|
|
|
|
2017-05-05 04:10:37 +02:00
|
|
|
|
if (!self.audioRecorder) {
|
2017-05-17 23:17:37 +02:00
|
|
|
|
// No voice message recording is in progress.
|
|
|
|
|
// We may be cancelling before the recording could begin.
|
2017-05-05 04:10:37 +02:00
|
|
|
|
DDLogError(@"%@ Missing audioRecorder", self.tag);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-05 04:35:02 +02:00
|
|
|
|
NSTimeInterval currentTime = self.audioRecorder.currentTime;
|
|
|
|
|
|
2017-05-05 04:10:37 +02:00
|
|
|
|
[self.audioRecorder stop];
|
|
|
|
|
|
2017-05-05 04:35:02 +02:00
|
|
|
|
const NSTimeInterval kMinimumRecordingTimeSeconds = 1.f;
|
|
|
|
|
if (currentTime < kMinimumRecordingTimeSeconds) {
|
2017-05-08 19:29:10 +02:00
|
|
|
|
DDLogInfo(@"Discarding voice message; too short.");
|
2017-05-05 04:35:02 +02:00
|
|
|
|
self.audioRecorder = nil;
|
2017-05-08 19:29:10 +02:00
|
|
|
|
|
2017-05-17 22:30:30 +02:00
|
|
|
|
[self dismissKeyBoard];
|
|
|
|
|
|
2017-05-08 19:29:10 +02:00
|
|
|
|
[OWSAlerts
|
|
|
|
|
showAlertWithTitle:
|
|
|
|
|
NSLocalizedString(@"VOICE_MESSAGE_TOO_SHORT_ALERT_TITLE",
|
|
|
|
|
@"Title for the alert indicating the 'voice message' needs to be held to be held down to record.")
|
|
|
|
|
message:NSLocalizedString(@"VOICE_MESSAGE_TOO_SHORT_ALERT_MESSAGE",
|
|
|
|
|
@"Message for the alert indicating the 'voice message' needs to be held to be held "
|
|
|
|
|
@"down to record.")];
|
2017-05-05 04:35:02 +02:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-05 04:10:37 +02:00
|
|
|
|
NSData *audioData = [NSData dataWithContentsOfURL:self.audioRecorder.url];
|
|
|
|
|
|
|
|
|
|
if (!audioData) {
|
|
|
|
|
DDLogError(@"%@ Couldn't load audioRecorder data", self.tag);
|
|
|
|
|
OWSAssert(0);
|
2017-05-05 16:29:27 +02:00
|
|
|
|
self.audioRecorder = nil;
|
2017-05-05 04:10:37 +02:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-05 16:29:27 +02:00
|
|
|
|
self.audioRecorder = nil;
|
|
|
|
|
|
2017-05-08 19:29:10 +02:00
|
|
|
|
NSString *filename = [NSLocalizedString(@"VOICE_MESSAGE_FILE_NAME", @"Filename for voice messages.")
|
2017-05-11 16:04:42 +02:00
|
|
|
|
stringByAppendingPathExtension:@"m4a"];
|
2017-05-08 17:13:51 +02:00
|
|
|
|
|
2017-05-09 15:31:30 +02:00
|
|
|
|
SignalAttachment *attachment = [SignalAttachment voiceMessageAttachmentWithData:audioData
|
|
|
|
|
dataUTI:(NSString *)kUTTypeMPEG4Audio
|
|
|
|
|
filename:filename];
|
2017-05-05 04:10:37 +02:00
|
|
|
|
if (!attachment || [attachment hasError]) {
|
|
|
|
|
DDLogWarn(@"%@ %s Invalid attachment: %@.",
|
|
|
|
|
self.tag,
|
|
|
|
|
__PRETTY_FUNCTION__,
|
|
|
|
|
attachment ? [attachment errorName] : @"Missing data");
|
|
|
|
|
[self showErrorAlertForAttachment:attachment];
|
|
|
|
|
} else {
|
|
|
|
|
[self tryToSendAttachmentIfApproved:attachment skipApprovalDialog:YES];
|
2015-01-22 05:08:12 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-05 04:10:37 +02:00
|
|
|
|
- (void)cancelRecordingVoiceMemo
|
|
|
|
|
{
|
|
|
|
|
OWSAssert([NSThread isMainThread]);
|
|
|
|
|
|
2017-06-22 16:10:56 +02:00
|
|
|
|
DDLogDebug(@"cancelRecordingVoiceMemo");
|
2017-05-05 04:10:37 +02:00
|
|
|
|
|
|
|
|
|
[self resetRecordingVoiceMemo];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)resetRecordingVoiceMemo
|
|
|
|
|
{
|
|
|
|
|
OWSAssert([NSThread isMainThread]);
|
|
|
|
|
|
|
|
|
|
[self.audioRecorder stop];
|
|
|
|
|
self.audioRecorder = nil;
|
2017-05-17 23:17:37 +02:00
|
|
|
|
self.voiceMessageUUID = nil;
|
2017-05-05 04:10:37 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-07-05 16:49:27 +02:00
|
|
|
|
- (void)setAudioRecorder:(AVAudioRecorder *)audioRecorder
|
|
|
|
|
{
|
|
|
|
|
// Prevent device from sleeping while recording a voice message.
|
|
|
|
|
if (audioRecorder) {
|
|
|
|
|
[DeviceSleepManager.sharedInstance addBlockWithBlockObject:audioRecorder];
|
|
|
|
|
} else if (_audioRecorder) {
|
|
|
|
|
[DeviceSleepManager.sharedInstance removeBlockWithBlockObject:_audioRecorder];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_audioRecorder = audioRecorder;
|
|
|
|
|
}
|
|
|
|
|
|
2014-11-25 16:38:33 +01:00
|
|
|
|
#pragma mark Accessory View
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (void)didPressAccessoryButton:(UIButton *)sender
|
|
|
|
|
{
|
2017-04-04 18:35:52 +02:00
|
|
|
|
|
2017-05-27 03:19:46 +02:00
|
|
|
|
__weak MessagesViewController *weakSelf = self;
|
2017-04-04 18:35:52 +02:00
|
|
|
|
if ([self isBlockedContactConversation]) {
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-27 03:19:46 +02:00
|
|
|
|
BOOL didShowSNAlert =
|
|
|
|
|
[self showSafetyNumberConfirmationIfNecessaryWithConfirmationText:
|
|
|
|
|
NSLocalizedString(@"CONFIRMATION_TITLE", @"Generic button text to proceed with an action")
|
|
|
|
|
completion:^(BOOL didConfirmIdentity) {
|
|
|
|
|
if (didConfirmIdentity) {
|
|
|
|
|
[weakSelf didPressAccessoryButton:nil];
|
|
|
|
|
}
|
|
|
|
|
}];
|
|
|
|
|
if (didShowSNAlert) {
|
2017-05-27 01:56:24 +02:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-27 03:19:46 +02:00
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
UIAlertController *actionSheetController =
|
|
|
|
|
[UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
|
|
|
|
|
|
2016-11-26 00:12:00 +01:00
|
|
|
|
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_CANCEL_TITLE", @"")
|
|
|
|
|
style:UIAlertActionStyleCancel
|
|
|
|
|
handler:nil];
|
|
|
|
|
[actionSheetController addAction:cancelAction];
|
2017-05-31 20:22:32 +02:00
|
|
|
|
|
|
|
|
|
UIAlertAction *takeMediaAction = [UIAlertAction
|
|
|
|
|
actionWithTitle:NSLocalizedString(@"MEDIA_FROM_CAMERA_BUTTON", @"media picker option to take photo or video")
|
|
|
|
|
style:UIAlertActionStyleDefault
|
|
|
|
|
handler:^(UIAlertAction *_Nonnull action) {
|
|
|
|
|
[self takePictureOrVideo];
|
|
|
|
|
}];
|
2017-04-21 22:58:41 +02:00
|
|
|
|
UIImage *takeMediaImage = [UIImage imageNamed:@"actionsheet_camera_black"];
|
|
|
|
|
OWSAssert(takeMediaImage);
|
|
|
|
|
[takeMediaAction setValue:takeMediaImage forKey:@"image"];
|
2016-11-26 00:12:00 +01:00
|
|
|
|
[actionSheetController addAction:takeMediaAction];
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
UIAlertAction *chooseMediaAction = [UIAlertAction
|
|
|
|
|
actionWithTitle:NSLocalizedString(@"MEDIA_FROM_LIBRARY_BUTTON", @"media picker option to choose from library")
|
|
|
|
|
style:UIAlertActionStyleDefault
|
|
|
|
|
handler:^(UIAlertAction *_Nonnull action) {
|
|
|
|
|
[self chooseFromLibrary];
|
|
|
|
|
}];
|
2017-04-21 22:58:41 +02:00
|
|
|
|
UIImage *chooseMediaImage = [UIImage imageNamed:@"actionsheet_camera_roll_black"];
|
|
|
|
|
OWSAssert(chooseMediaImage);
|
|
|
|
|
[chooseMediaAction setValue:chooseMediaImage forKey:@"image"];
|
2016-11-26 00:12:00 +01:00
|
|
|
|
[actionSheetController addAction:chooseMediaAction];
|
2017-04-20 23:51:45 +02:00
|
|
|
|
|
|
|
|
|
UIAlertAction *chooseDocumentAction =
|
|
|
|
|
[UIAlertAction actionWithTitle:NSLocalizedString(@"MEDIA_FROM_DOCUMENT_PICKER_BUTTON",
|
|
|
|
|
@"action sheet button title when choosing attachment type")
|
|
|
|
|
style:UIAlertActionStyleDefault
|
|
|
|
|
handler:^(UIAlertAction *_Nonnull action) {
|
|
|
|
|
[self showAttachmentDocumentPicker];
|
|
|
|
|
}];
|
2017-04-21 22:58:41 +02:00
|
|
|
|
UIImage *chooseDocumentImage = [UIImage imageNamed:@"actionsheet_document_black"];
|
|
|
|
|
OWSAssert(chooseDocumentImage);
|
|
|
|
|
[chooseDocumentAction setValue:chooseDocumentImage forKey:@"image"];
|
2017-04-20 23:51:45 +02:00
|
|
|
|
[actionSheetController addAction:chooseDocumentAction];
|
|
|
|
|
|
2016-11-26 00:12:00 +01:00
|
|
|
|
[self presentViewController:actionSheetController animated:true completion:nil];
|
2014-11-25 16:38:33 +01:00
|
|
|
|
}
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (NSIndexPath *)lastVisibleIndexPath
|
|
|
|
|
{
|
|
|
|
|
NSIndexPath *lastVisibleIndexPath = nil;
|
|
|
|
|
for (NSIndexPath *indexPath in [self.collectionView indexPathsForVisibleItems]) {
|
|
|
|
|
if (!lastVisibleIndexPath || indexPath.row > lastVisibleIndexPath.row) {
|
|
|
|
|
lastVisibleIndexPath = indexPath;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return lastVisibleIndexPath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (nullable TSInteraction *)lastVisibleInteraction
|
2016-09-21 14:37:51 +02:00
|
|
|
|
{
|
2017-05-31 20:22:32 +02:00
|
|
|
|
NSIndexPath *lastVisibleIndexPath = [self lastVisibleIndexPath];
|
|
|
|
|
if (!lastVisibleIndexPath) {
|
|
|
|
|
return nil;
|
|
|
|
|
}
|
|
|
|
|
return [self interactionAtIndexPath:lastVisibleIndexPath];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)updateLastVisibleTimestamp
|
|
|
|
|
{
|
|
|
|
|
TSInteraction *lastVisibleInteraction = [self lastVisibleInteraction];
|
|
|
|
|
if (lastVisibleInteraction) {
|
|
|
|
|
uint64_t lastVisibleTimestamp = lastVisibleInteraction.timestampForSorting;
|
|
|
|
|
self.lastVisibleTimestamp = MAX(self.lastVisibleTimestamp, lastVisibleTimestamp);
|
|
|
|
|
}
|
2017-05-09 20:39:15 +02:00
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
[self ensureScrollDownButton];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)updateLastVisibleTimestamp:(uint64_t)timestamp
|
|
|
|
|
{
|
|
|
|
|
OWSAssert(timestamp > 0);
|
|
|
|
|
|
|
|
|
|
self.lastVisibleTimestamp = MAX(self.lastVisibleTimestamp, timestamp);
|
|
|
|
|
|
|
|
|
|
[self ensureScrollDownButton];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)markVisibleMessagesAsRead
|
|
|
|
|
{
|
|
|
|
|
[self updateLastVisibleTimestamp];
|
|
|
|
|
|
|
|
|
|
TSThread *thread = self.thread;
|
|
|
|
|
uint64_t lastVisibleTimestamp = self.lastVisibleTimestamp;
|
|
|
|
|
[self.editingDatabaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
|
|
|
|
|
NSMutableArray<id<OWSReadTracking>> *interactions = [NSMutableArray new];
|
2017-06-15 19:43:18 +02:00
|
|
|
|
[[TSDatabaseView unseenDatabaseViewExtension:transaction]
|
2017-05-31 20:22:32 +02:00
|
|
|
|
enumerateRowsInGroup:thread.uniqueId
|
|
|
|
|
usingBlock:^(
|
|
|
|
|
NSString *collection, NSString *key, id object, id metadata, NSUInteger index, BOOL *stop) {
|
|
|
|
|
|
|
|
|
|
TSInteraction *interaction = object;
|
|
|
|
|
if (interaction.timestampForSorting > lastVisibleTimestamp) {
|
|
|
|
|
*stop = YES;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
id<OWSReadTracking> possiblyRead = (id<OWSReadTracking>)object;
|
2017-06-12 17:52:21 +02:00
|
|
|
|
OWSAssert(!possiblyRead.read);
|
2017-05-31 20:22:32 +02:00
|
|
|
|
if (!possiblyRead.read) {
|
|
|
|
|
[interactions addObject:possiblyRead];
|
|
|
|
|
}
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
if (interactions.count < 1) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
DDLogError(@"Marking %zd messages as read.", interactions.count);
|
|
|
|
|
for (id<OWSReadTracking> possiblyRead in interactions) {
|
2017-06-12 19:28:29 +02:00
|
|
|
|
[possiblyRead markAsReadWithTransaction:transaction sendReadReceipt:YES updateExpiration:YES];
|
2017-05-31 20:22:32 +02:00
|
|
|
|
}
|
|
|
|
|
}];
|
2014-12-06 17:45:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
2017-06-13 21:09:47 +02:00
|
|
|
|
- (void)updateGroupModelTo:(TSGroupModel *)newGroupModel successCompletion:(void (^_Nullable)())successCompletion
|
2016-08-01 00:25:07 +02:00
|
|
|
|
{
|
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) {
|
2017-05-31 20:22:32 +02:00
|
|
|
|
groupThread = [TSGroupThread getOrCreateThreadWithGroupModel:newGroupModel transaction:transaction];
|
|
|
|
|
|
|
|
|
|
NSString *updateGroupInfo =
|
|
|
|
|
[groupThread.groupModel getInfoStringAboutUpdateTo:newGroupModel contactsManager:self.contactsManager];
|
2016-10-17 03:07:25 +02:00
|
|
|
|
|
|
|
|
|
groupThread.groupModel = newGroupModel;
|
|
|
|
|
[groupThread saveWithTransaction:transaction];
|
|
|
|
|
message = [[TSOutgoingMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
|
|
|
|
|
inThread:groupThread
|
2017-04-12 17:03:16 +02:00
|
|
|
|
groupMetaMessage:TSGroupMessageUpdate];
|
|
|
|
|
[message updateWithCustomMessage:updateGroupInfo transaction:transaction];
|
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
|
2017-05-15 16:47:27 +02:00
|
|
|
|
sourceFilename:nil
|
2016-10-14 22:59:58 +02:00
|
|
|
|
inMessage:message
|
|
|
|
|
success:^{
|
|
|
|
|
DDLogDebug(@"%@ Successfully sent group update with avatar", self.tag);
|
2017-06-13 21:09:47 +02:00
|
|
|
|
if (successCompletion) {
|
|
|
|
|
successCompletion();
|
|
|
|
|
}
|
2016-10-14 22:59:58 +02:00
|
|
|
|
}
|
|
|
|
|
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);
|
2017-06-13 21:09:47 +02:00
|
|
|
|
if (successCompletion) {
|
|
|
|
|
successCompletion();
|
|
|
|
|
}
|
2016-10-14 22:59:58 +02:00
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (void)popKeyBoard
|
|
|
|
|
{
|
2015-04-14 21:49:00 +02:00
|
|
|
|
[self.inputToolbar.contentView.textView becomeFirstResponder];
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (void)dismissKeyBoard
|
|
|
|
|
{
|
2015-01-30 23:28:05 +01:00
|
|
|
|
[self.inputToolbar.contentView.textView resignFirstResponder];
|
|
|
|
|
}
|
|
|
|
|
|
2015-03-01 00:04:39 +01:00
|
|
|
|
#pragma mark Drafts
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (void)loadDraftInCompose
|
|
|
|
|
{
|
2015-03-01 00:04:39 +01:00
|
|
|
|
__block NSString *placeholder;
|
|
|
|
|
[self.editingDatabaseConnection asyncReadWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
2017-05-31 20:22:32 +02:00
|
|
|
|
placeholder = [_thread currentDraftWithTransaction:transaction];
|
2015-12-22 12:45:09 +01:00
|
|
|
|
}
|
|
|
|
|
completionBlock:^{
|
2017-05-31 20:22:32 +02:00
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
|
[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
|
|
|
|
}
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (void)saveDraft
|
|
|
|
|
{
|
2015-03-01 00:04:39 +01:00
|
|
|
|
if (self.inputToolbar.hidden == NO) {
|
2017-05-31 20:22:32 +02:00
|
|
|
|
__block TSThread *thread = _thread;
|
2015-03-21 19:15:43 +01:00
|
|
|
|
__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) {
|
2017-05-09 16:33:35 +02:00
|
|
|
|
[thread setDraft:currentDraft transaction:transaction];
|
2015-03-01 00:04:39 +01:00
|
|
|
|
}];
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-05-09 16:33:35 +02:00
|
|
|
|
|
|
|
|
|
- (void)clearDraft
|
|
|
|
|
{
|
|
|
|
|
__block TSThread *thread = _thread;
|
|
|
|
|
[self.editingDatabaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
|
|
|
|
[thread setDraft:@"" 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();
|
2017-06-20 19:12:51 +02:00
|
|
|
|
self.backButtonUnreadCount = [self.messagesManager unreadMessagesCountExcept:self.thread];
|
2017-04-09 21:31:31 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (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);
|
2017-06-20 19:12:51 +02:00
|
|
|
|
|
|
|
|
|
// Max out the unread count at 99+.
|
|
|
|
|
const NSUInteger kMaxUnreadCount = 99;
|
2017-07-12 16:58:29 +02:00
|
|
|
|
_backButtonUnreadCountLabel.text = [ViewControllerUtils formatInt:(int) MIN(kMaxUnreadCount, unreadCount)];
|
2015-05-23 15:54:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
2015-12-26 17:27:27 +01:00
|
|
|
|
#pragma mark 3D Touch Preview Actions
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (NSArray<id<UIPreviewActionItem>> *)previewActionItems
|
|
|
|
|
{
|
2015-12-26 17:27:27 +01:00
|
|
|
|
return @[];
|
|
|
|
|
}
|
|
|
|
|
|
2017-02-15 22:09:57 +01:00
|
|
|
|
#pragma mark - Event Handling
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (void)navigationTitleTapped:(UIGestureRecognizer *)gestureRecognizer
|
|
|
|
|
{
|
2017-02-15 22:09:57 +01:00
|
|
|
|
if (gestureRecognizer.state == UIGestureRecognizerStateRecognized) {
|
|
|
|
|
[self showConversationSettings];
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-03-10 14:56:12 +01:00
|
|
|
|
|
2017-03-27 23:03:36 +02:00
|
|
|
|
#ifdef DEBUG
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (void)navigationTitleLongPressed:(UIGestureRecognizer *)gestureRecognizer
|
|
|
|
|
{
|
2017-03-27 23:03:36 +02:00
|
|
|
|
if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
|
2017-05-31 20:22:32 +02:00
|
|
|
|
[DebugUITableViewController presentDebugUIForThread:self.thread fromViewController:self];
|
2017-03-27 23:03:36 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
2017-03-10 14:56:12 +01:00
|
|
|
|
#pragma mark - JSQMessagesComposerTextViewPasteDelegate
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (BOOL)composerTextView:(JSQMessagesComposerTextView *)textView shouldPasteWithSender:(id)sender
|
|
|
|
|
{
|
2017-03-10 14:56:12 +01:00
|
|
|
|
return YES;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark - OWSTextViewPasteDelegate
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (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
|
2017-04-22 15:45:20 +02:00
|
|
|
|
{
|
|
|
|
|
[self tryToSendAttachmentIfApproved:attachment skipApprovalDialog:NO];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)tryToSendAttachmentIfApproved:(SignalAttachment *_Nullable)attachment
|
|
|
|
|
skipApprovalDialog:(BOOL)skipApprovalDialog
|
2017-04-07 04:04:10 +02:00
|
|
|
|
{
|
|
|
|
|
DDLogError(@"%@ %s", self.tag, __PRETTY_FUNCTION__);
|
|
|
|
|
|
|
|
|
|
DispatchMainThreadSafe(^{
|
2017-05-27 03:19:46 +02:00
|
|
|
|
__weak MessagesViewController *weakSelf = self;
|
2017-04-07 04:04:10 +02:00
|
|
|
|
if ([self isBlockedContactConversation]) {
|
|
|
|
|
[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;
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-07 21:31:54 +02:00
|
|
|
|
BOOL didShowSNAlert = [self
|
|
|
|
|
showSafetyNumberConfirmationIfNecessaryWithConfirmationText:[SafetyNumberStrings confirmSendButton]
|
|
|
|
|
completion:^(BOOL didConfirmIdentity) {
|
|
|
|
|
if (didConfirmIdentity) {
|
|
|
|
|
[weakSelf
|
|
|
|
|
tryToSendAttachmentIfApproved:attachment];
|
|
|
|
|
}
|
|
|
|
|
}];
|
|
|
|
|
if (didShowSNAlert) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2017-04-07 04:04:10 +02:00
|
|
|
|
if (attachment == nil || [attachment hasError]) {
|
|
|
|
|
DDLogWarn(@"%@ %s Invalid attachment: %@.",
|
|
|
|
|
self.tag,
|
|
|
|
|
__PRETTY_FUNCTION__,
|
|
|
|
|
attachment ? [attachment errorName] : @"Missing data");
|
|
|
|
|
[self showErrorAlertForAttachment:attachment];
|
2017-04-22 15:45:20 +02:00
|
|
|
|
} else if (skipApprovalDialog) {
|
|
|
|
|
[self sendMessageAttachment:attachment];
|
2017-04-07 04:04:10 +02:00
|
|
|
|
} else {
|
|
|
|
|
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-05-31 20:22:32 +02:00
|
|
|
|
- (void)showErrorAlertForAttachment:(SignalAttachment *_Nullable)attachment
|
|
|
|
|
{
|
2017-03-24 03:32:42 +01:00
|
|
|
|
OWSAssert(attachment == nil || [attachment hasError]);
|
2017-05-31 20:22:32 +02:00
|
|
|
|
|
|
|
|
|
NSString *errorMessage
|
|
|
|
|
= (attachment ? [attachment localizedErrorDescription] : [SignalAttachment missingDataErrorMessage]);
|
|
|
|
|
|
|
|
|
|
DDLogError(@"%@ %s: %@", self.tag, __PRETTY_FUNCTION__, errorMessage);
|
|
|
|
|
|
2017-03-24 03:32:42 +01:00
|
|
|
|
UIAlertController *controller =
|
2017-05-30 19:04:43 +02:00
|
|
|
|
[UIAlertController alertControllerWithTitle:NSLocalizedString(@"ATTACHMENT_ERROR_ALERT_TITLE",
|
|
|
|
|
@"The title of the 'attachment error' alert.")
|
|
|
|
|
message:errorMessage
|
|
|
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
2017-03-24 03:32:42 +01:00
|
|
|
|
[controller addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil)
|
|
|
|
|
style:UIAlertActionStyleDefault
|
|
|
|
|
handler:nil]];
|
2017-05-31 20:22:32 +02:00
|
|
|
|
[self presentViewController:controller animated:YES completion:nil];
|
2017-03-24 03:32:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
2017-06-22 16:55:26 +02:00
|
|
|
|
- (void)textViewDidChangeLayout
|
2017-05-16 19:46:57 +02:00
|
|
|
|
{
|
|
|
|
|
OWSAssert([NSThread isMainThread]);
|
|
|
|
|
|
|
|
|
|
BOOL wasAtBottom = [self isScrolledToBottom];
|
|
|
|
|
if (wasAtBottom) {
|
2017-05-16 21:52:19 +02:00
|
|
|
|
[self.scrollLaterTimer invalidate];
|
2017-05-20 00:28:40 +02:00
|
|
|
|
// We want to scroll to the bottom _after_ the layout has been updated.
|
2017-05-16 21:52:19 +02:00
|
|
|
|
self.scrollLaterTimer = [NSTimer weakScheduledTimerWithTimeInterval:0.001f
|
|
|
|
|
target:self
|
|
|
|
|
selector:@selector(scrollToBottomImmediately)
|
|
|
|
|
userInfo:nil
|
|
|
|
|
repeats:NO];
|
2017-05-16 19:46:57 +02:00
|
|
|
|
}
|
2017-06-22 16:55:26 +02:00
|
|
|
|
|
|
|
|
|
[self ensureScrollDownButton];
|
2017-05-16 19:46:57 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-05-16 20:04:44 +02:00
|
|
|
|
- (void)scrollToBottomImmediately
|
|
|
|
|
{
|
|
|
|
|
OWSAssert([NSThread isMainThread]);
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
[self scrollToBottomAnimated:NO];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)scrollToBottomAnimated:(BOOL)animated
|
|
|
|
|
{
|
2017-05-16 21:52:19 +02:00
|
|
|
|
[self.scrollLaterTimer invalidate];
|
|
|
|
|
self.scrollLaterTimer = nil;
|
2017-05-16 20:04:44 +02:00
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
if (self.isUserScrolling) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[super scrollToBottomAnimated:animated];
|
2017-05-16 20:04:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-05-30 17:04:58 +02:00
|
|
|
|
#pragma mark - OWSVoiceMemoGestureDelegate
|
2017-05-05 16:10:28 +02:00
|
|
|
|
|
2017-05-05 04:35:02 +02:00
|
|
|
|
- (void)voiceMemoGestureDidStart
|
2017-05-05 03:21:27 +02:00
|
|
|
|
{
|
2017-05-05 04:10:37 +02:00
|
|
|
|
OWSAssert([NSThread isMainThread]);
|
|
|
|
|
|
2017-05-05 04:35:02 +02:00
|
|
|
|
DDLogInfo(@"voiceMemoGestureDidStart");
|
2017-05-05 03:21:27 +02:00
|
|
|
|
|
2017-05-17 23:17:37 +02:00
|
|
|
|
const CGFloat kIgnoreMessageSendDoubleTapDurationSeconds = 2.f;
|
|
|
|
|
if (self.lastMessageSentDate &&
|
|
|
|
|
[[NSDate new] timeIntervalSinceDate:self.lastMessageSentDate] < kIgnoreMessageSendDoubleTapDurationSeconds) {
|
|
|
|
|
// If users double-taps the message send button, the second tap can look like a
|
|
|
|
|
// very short voice message gesture. We want to ignore such gestures.
|
|
|
|
|
[((OWSMessagesToolbarContentView *)self.inputToolbar.contentView)cancelVoiceMemoIfNecessary];
|
|
|
|
|
[((OWSMessagesInputToolbar *)self.inputToolbar) hideVoiceMemoUI:NO];
|
|
|
|
|
[self cancelRecordingVoiceMemo];
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-05 03:21:27 +02:00
|
|
|
|
[((OWSMessagesInputToolbar *)self.inputToolbar)showVoiceMemoUI];
|
2017-05-11 22:17:21 +02:00
|
|
|
|
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
|
2017-05-15 22:51:12 +02:00
|
|
|
|
[self requestRecordingVoiceMemo];
|
2017-05-05 03:21:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-05-05 04:35:02 +02:00
|
|
|
|
- (void)voiceMemoGestureDidEnd
|
2017-05-05 03:21:27 +02:00
|
|
|
|
{
|
2017-05-05 04:10:37 +02:00
|
|
|
|
OWSAssert([NSThread isMainThread]);
|
|
|
|
|
|
2017-05-05 04:35:02 +02:00
|
|
|
|
DDLogInfo(@"voiceMemoGestureDidEnd");
|
2017-05-05 03:21:27 +02:00
|
|
|
|
|
|
|
|
|
[((OWSMessagesInputToolbar *)self.inputToolbar) hideVoiceMemoUI:YES];
|
2017-05-05 04:10:37 +02:00
|
|
|
|
[self endRecordingVoiceMemo];
|
2017-05-11 22:17:21 +02:00
|
|
|
|
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
|
2017-05-05 03:21:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-05-05 04:35:02 +02:00
|
|
|
|
- (void)voiceMemoGestureDidCancel
|
2017-05-05 03:21:27 +02:00
|
|
|
|
{
|
2017-05-05 04:10:37 +02:00
|
|
|
|
OWSAssert([NSThread isMainThread]);
|
2017-05-05 03:21:27 +02:00
|
|
|
|
|
2017-05-05 04:35:02 +02:00
|
|
|
|
DDLogInfo(@"voiceMemoGestureDidCancel");
|
2017-05-05 04:10:37 +02:00
|
|
|
|
|
|
|
|
|
[((OWSMessagesInputToolbar *)self.inputToolbar) hideVoiceMemoUI:NO];
|
|
|
|
|
[self cancelRecordingVoiceMemo];
|
2017-05-11 22:17:21 +02:00
|
|
|
|
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
|
2017-05-05 04:10:37 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-05-05 04:35:02 +02:00
|
|
|
|
- (void)voiceMemoGestureDidChange:(CGFloat)cancelAlpha
|
|
|
|
|
{
|
|
|
|
|
OWSAssert([NSThread isMainThread]);
|
|
|
|
|
|
|
|
|
|
[((OWSMessagesInputToolbar *)self.inputToolbar) setVoiceMemoUICancelAlpha:cancelAlpha];
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-05 04:10:37 +02:00
|
|
|
|
- (void)cancelVoiceMemo
|
|
|
|
|
{
|
|
|
|
|
OWSAssert([NSThread isMainThread]);
|
|
|
|
|
|
2017-05-05 16:10:28 +02:00
|
|
|
|
[((OWSMessagesToolbarContentView *)self.inputToolbar.contentView)cancelVoiceMemoIfNecessary];
|
2017-05-05 03:21:27 +02:00
|
|
|
|
[((OWSMessagesInputToolbar *)self.inputToolbar) hideVoiceMemoUI:NO];
|
2017-05-05 04:10:37 +02:00
|
|
|
|
[self cancelRecordingVoiceMemo];
|
2017-05-05 03:21:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-05-05 16:10:28 +02:00
|
|
|
|
- (void)textViewDidChange:(UITextView *)textView
|
|
|
|
|
{
|
2017-05-08 17:13:51 +02:00
|
|
|
|
// Override.
|
|
|
|
|
//
|
2017-05-08 19:29:10 +02:00
|
|
|
|
// We want to show the "voice message" button if the text input is empty
|
2017-05-05 16:20:16 +02:00
|
|
|
|
// and the "send" button if it isn't.
|
2017-05-05 16:10:28 +02:00
|
|
|
|
[((OWSMessagesToolbarContentView *)self.inputToolbar.contentView)ensureEnabling];
|
2017-06-22 16:55:26 +02:00
|
|
|
|
|
|
|
|
|
[self ensureScrollDownButton];
|
2017-05-05 16:10:28 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-04-05 00:08:51 +02:00
|
|
|
|
#pragma mark - UIScrollViewDelegate
|
|
|
|
|
|
2017-05-31 20:22:32 +02:00
|
|
|
|
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
|
|
|
|
|
{
|
|
|
|
|
[self updateLastVisibleTimestamp];
|
|
|
|
|
}
|
|
|
|
|
|
2017-04-05 00:08:51 +02:00
|
|
|
|
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
|
|
|
|
|
{
|
|
|
|
|
self.userHasScrolled = YES;
|
2017-05-31 20:22:32 +02:00
|
|
|
|
self.isUserScrolling = YES;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
|
|
|
|
|
{
|
|
|
|
|
self.isUserScrolling = NO;
|
2017-04-05 00:08:51 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-04-28 18:18:42 +02:00
|
|
|
|
#pragma mark - OWSConversationSettingsViewDelegate
|
|
|
|
|
|
2017-06-13 21:09:47 +02:00
|
|
|
|
- (void)resendGroupUpdateForErrorMessage:(TSErrorMessage *)message
|
|
|
|
|
{
|
|
|
|
|
OWSAssert([NSThread isMainThread]);
|
|
|
|
|
OWSAssert([_thread isKindOfClass:[TSGroupThread class]]);
|
|
|
|
|
OWSAssert(message);
|
|
|
|
|
|
|
|
|
|
TSGroupThread *groupThread = (TSGroupThread *)self.thread;
|
|
|
|
|
TSGroupModel *groupModel = groupThread.groupModel;
|
|
|
|
|
[self updateGroupModelTo:groupModel
|
|
|
|
|
successCompletion:^{
|
|
|
|
|
DDLogInfo(@"Group updated, removing group creation error.");
|
|
|
|
|
|
|
|
|
|
[message remove];
|
|
|
|
|
}];
|
|
|
|
|
}
|
|
|
|
|
|
2017-04-28 18:18:42 +02:00
|
|
|
|
- (void)groupWasUpdated:(TSGroupModel *)groupModel
|
|
|
|
|
{
|
|
|
|
|
OWSAssert(groupModel);
|
|
|
|
|
|
|
|
|
|
NSMutableSet *groupMemberIds = [NSMutableSet setWithArray:groupModel.groupMemberIds];
|
|
|
|
|
[groupMemberIds addObject:[TSAccountManager localNumber]];
|
|
|
|
|
groupModel.groupMemberIds = [NSMutableArray arrayWithArray:[groupMemberIds allObjects]];
|
2017-06-13 21:09:47 +02:00
|
|
|
|
[self updateGroupModelTo:groupModel successCompletion:nil];
|
2017-07-25 18:52:30 +02:00
|
|
|
|
[self resetContentAndLayout];
|
2017-04-28 18:18:42 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)popAllConversationSettingsViews
|
|
|
|
|
{
|
2017-05-02 18:30:53 +02:00
|
|
|
|
if (self.presentedViewController) {
|
|
|
|
|
[self.presentedViewController
|
|
|
|
|
dismissViewControllerAnimated:YES
|
|
|
|
|
completion:^{
|
|
|
|
|
[self.navigationController popToViewController:self animated:YES];
|
|
|
|
|
}];
|
|
|
|
|
} else {
|
|
|
|
|
[self.navigationController popToViewController:self animated:YES];
|
|
|
|
|
}
|
2017-04-28 18:18:42 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-06-01 03:12:27 +02:00
|
|
|
|
#pragma mark - OWSMessagesCollectionViewFlowLayoutDelegate
|
|
|
|
|
|
2017-06-16 23:18:09 +02:00
|
|
|
|
- (BOOL)shouldShowCellDecorationsAtIndexPath:(NSIndexPath *)indexPath
|
2017-06-01 03:12:27 +02:00
|
|
|
|
{
|
|
|
|
|
TSInteraction *interaction = [self interactionAtIndexPath:indexPath];
|
2017-06-16 23:18:09 +02:00
|
|
|
|
|
|
|
|
|
// Show any top/bottom labels for all but the unread indicator
|
|
|
|
|
return ![interaction isKindOfClass:[TSUnreadIndicatorInteraction class]];
|
2017-06-01 03:12:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-07-25 18:52:30 +02:00
|
|
|
|
#pragma mark - Database Observation
|
|
|
|
|
|
|
|
|
|
- (void)setIsViewVisible:(BOOL)isViewVisible
|
|
|
|
|
{
|
|
|
|
|
_isViewVisible = isViewVisible;
|
|
|
|
|
|
|
|
|
|
[self updateShouldObserveDBModifications];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)setIsAppInBackground:(BOOL)isAppInBackground
|
|
|
|
|
{
|
|
|
|
|
_isAppInBackground = isAppInBackground;
|
|
|
|
|
|
|
|
|
|
[self updateShouldObserveDBModifications];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)updateShouldObserveDBModifications
|
|
|
|
|
{
|
|
|
|
|
self.shouldObserveDBModifications = self.isViewVisible && !self.isAppInBackground;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)setShouldObserveDBModifications:(BOOL)shouldObserveDBModifications
|
|
|
|
|
{
|
|
|
|
|
if (_shouldObserveDBModifications == shouldObserveDBModifications) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_shouldObserveDBModifications = shouldObserveDBModifications;
|
|
|
|
|
|
2017-07-26 18:39:43 +02:00
|
|
|
|
if (self.shouldObserveDBModifications) {
|
|
|
|
|
[self resetMappings];
|
2017-07-25 18:52:30 +02:00
|
|
|
|
}
|
2017-07-26 18:39:43 +02:00
|
|
|
|
}
|
2017-07-25 18:52:30 +02:00
|
|
|
|
|
2017-07-26 18:39:43 +02:00
|
|
|
|
- (void)resetMappings
|
|
|
|
|
{
|
2017-07-25 18:52:30 +02:00
|
|
|
|
// If we're entering "active" mode (e.g. view is visible and app is in foreground),
|
|
|
|
|
// reset all state updated by yapDatabaseModified:.
|
|
|
|
|
if (self.messageMappings != nil) {
|
|
|
|
|
// Before we begin observing database modifications, make sure
|
|
|
|
|
// our mapping and table state is up-to-date.
|
|
|
|
|
//
|
|
|
|
|
// We need to `beginLongLivedReadTransaction` before we update our
|
|
|
|
|
// mapping in order to jump to the most recent commit.
|
|
|
|
|
[self.uiDatabaseConnection beginLongLivedReadTransaction];
|
|
|
|
|
[self updateMessageMappingRangeOptions];
|
|
|
|
|
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
|
|
|
|
[self.messageMappings updateWithTransaction:transaction];
|
|
|
|
|
}];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.messageAdapterCache = [[NSCache alloc] init];
|
|
|
|
|
[self resetContentAndLayout];
|
|
|
|
|
[self updateLoadEarlierVisible];
|
|
|
|
|
[self ensureDynamicInteractions];
|
|
|
|
|
[self updateBackButtonUnreadCount];
|
|
|
|
|
[self updateNavigationBarSubtitleLabel];
|
|
|
|
|
}
|
|
|
|
|
|
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
|