session-ios/Signal/src/ViewControllers/ConversationView/ConversationViewController.m

5453 lines
215 KiB
Mathematica
Raw Normal View History

2014-10-29 21:58:58 +01:00
//
2019-01-04 15:19:41 +01:00
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
2014-10-29 21:58:58 +01:00
//
#import "ConversationViewController.h"
2014-10-29 21:58:58 +01:00
#import "AppDelegate.h"
#import "BlockListUIUtils.h"
#import "BlockListViewController.h"
#import "ContactsViewHelper.h"
2017-10-10 22:13:54 +02:00
#import "ConversationCollectionView.h"
#import "ConversationInputTextView.h"
#import "ConversationInputToolbar.h"
#import "ConversationScrollButton.h"
2017-10-10 22:13:54 +02:00
#import "ConversationViewCell.h"
#import "ConversationViewItem.h"
#import "ConversationViewLayout.h"
2018-10-31 15:05:24 +01:00
#import "ConversationViewModel.h"
#import "DateUtil.h"
#import "DebugUITableViewController.h"
2014-12-04 00:23:36 +01:00
#import "FingerprintViewController.h"
#import "NSAttributedString+OWS.h"
2018-02-23 21:44:46 +01:00
#import "OWSAudioPlayer.h"
#import "OWSContactOffersCell.h"
#import "OWSConversationSettingsViewController.h"
#import "OWSConversationSettingsViewDelegate.h"
Disappearing Messages * Per thread settings menu accessed by tapping on thread title This removed the toggle-phone behavior. You'll be able to see the phone number in the settings table view. This removed the "add contact" functionality, although it was already broken for ios>=9 (which is basically everybody). The group actions menu was absorbed into this screen * Added a confirm alert to leave group (fixes #938) * New Translation Strings * Extend "Add People" label to fit translations. * resolved issues with translations not fitting in group menu * Fix the long standing type warning where TSCalls were assigned to a TSMessageAdapter. * Can delete info messages Follow the JSQMVC pattern and put UIResponder-able content in the messageBubbleContainer. This gives us more functionality *and* allows us to delete some code. yay! It's still not yet possible to delete phone messages. =( * Fixed some compiler warnings. * xcode8 touching storyboard. So long xcode7! * Fixup multiline info messages. We were seeing info messages like "You set disappearing message timer to 10" instead of "You set disappearing message timer to 10 seconds." Admittedly this isn't a very good fix, as now one liners feel like they have too much padding. If the message is well over one line, we were wrapping properly, but there's a problem when the message is *just barely* two lines, the cell height grows, but the label still thinks it's just one line (as evinced by the one line appearing in the center of the label frame. The result being that the last word of the label is cropped. * Disable group actions after leaving group. // FREEBIE
2016-09-21 14:37:51 +02:00
#import "OWSDisappearingMessagesJob.h"
2017-10-25 00:27:07 +02:00
#import "OWSMath.h"
2017-10-17 06:05:29 +02:00
#import "OWSMessageCell.h"
#import "OWSSystemMessageCell.h"
2019-05-02 23:58:48 +02:00
#import "Session-Swift.h"
#import "SignalKeyingStorage.h"
#import "TSAttachmentPointer.h"
2016-09-02 16:22:06 +02:00
#import "TSCall.h"
#import "TSContactThread.h"
2014-11-25 16:38:33 +01:00
#import "TSDatabaseView.h"
2014-12-11 00:05:41 +01:00
#import "TSErrorMessage.h"
Disappearing Messages * Per thread settings menu accessed by tapping on thread title This removed the toggle-phone behavior. You'll be able to see the phone number in the settings table view. This removed the "add contact" functionality, although it was already broken for ios>=9 (which is basically everybody). The group actions menu was absorbed into this screen * Added a confirm alert to leave group (fixes #938) * New Translation Strings * Extend "Add People" label to fit translations. * resolved issues with translations not fitting in group menu * Fix the long standing type warning where TSCalls were assigned to a TSMessageAdapter. * Can delete info messages Follow the JSQMVC pattern and put UIResponder-able content in the messageBubbleContainer. This gives us more functionality *and* allows us to delete some code. yay! It's still not yet possible to delete phone messages. =( * Fixed some compiler warnings. * xcode8 touching storyboard. So long xcode7! * Fixup multiline info messages. We were seeing info messages like "You set disappearing message timer to 10" instead of "You set disappearing message timer to 10 seconds." Admittedly this isn't a very good fix, as now one liners feel like they have too much padding. If the message is well over one line, we were wrapping properly, but there's a problem when the message is *just barely* two lines, the cell height grows, but the label still thinks it's just one line (as evinced by the one line appearing in the center of the label frame. The result being that the last word of the label is cropped. * Disable group actions after leaving group. // FREEBIE
2016-09-21 14:37:51 +02:00
#import "TSGroupThread.h"
#import "TSIncomingMessage.h"
2016-09-02 16:22:06 +02:00
#import "TSInfoMessage.h"
#import "TSInvalidIdentityKeyErrorMessage.h"
#import "UIFont+OWS.h"
#import "UIViewController+Permissions.h"
#import "ViewControllerUtils.h"
#import <AVFoundation/AVFoundation.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 <MobileCoreServices/UTCoreTypes.h>
#import <PromiseKit/AnyPromise.h>
2020-06-05 02:38:44 +02:00
#import <SessionCoreKit/NSDate+OWS.h>
#import <SessionCoreKit/Threading.h>
2017-12-19 03:50:51 +01:00
#import <SignalMessaging/Environment.h>
2017-12-08 17:50:35 +01:00
#import <SignalMessaging/OWSContactOffersInteraction.h>
2017-12-19 03:50:51 +01:00
#import <SignalMessaging/OWSContactsManager.h>
#import <SignalMessaging/OWSFormat.h>
#import <SignalMessaging/OWSNavigationController.h>
2018-07-11 20:12:58 +02:00
#import <SignalMessaging/OWSUnreadIndicator.h>
#import <SignalMessaging/OWSUserProfile.h>
2018-08-07 23:37:12 +02:00
#import <SignalMessaging/SignalMessaging-Swift.h>
2017-12-08 17:50:35 +01:00
#import <SignalMessaging/ThreadUtil.h>
#import <SignalMessaging/UIUtil.h>
2018-03-01 20:42:54 +01:00
#import <SignalMessaging/UIViewController+OWS.h>
2020-06-05 02:38:44 +02:00
#import <SessionServiceKit/Contact.h>
#import <SessionServiceKit/ContactsUpdater.h>
#import <SessionServiceKit/MimeTypeUtil.h>
#import <SessionServiceKit/NSString+SSK.h>
#import <SessionServiceKit/NSTimer+OWS.h>
#import <SessionServiceKit/OWSAddToContactsOfferMessage.h>
#import <SessionServiceKit/OWSAddToProfileWhitelistOfferMessage.h>
#import <SessionServiceKit/OWSAttachmentDownloads.h>
#import <SessionServiceKit/OWSBlockingManager.h>
#import <SessionServiceKit/OWSDisappearingMessagesConfiguration.h>
#import <SessionServiceKit/OWSIdentityManager.h>
#import <SessionServiceKit/OWSMessageManager.h>
#import <SessionServiceKit/OWSMessageSender.h>
#import <SessionServiceKit/OWSMessageUtils.h>
#import <SessionServiceKit/OWSPrimaryStorage.h>
#import <SessionServiceKit/OWSPrimaryStorage+Loki.h>
#import <SessionServiceKit/OWSReadReceiptManager.h>
#import <SessionServiceKit/OWSVerificationStateChangeMessage.h>
2020-06-05 05:43:06 +02:00
#import <SessionServiceKit/SessionServiceKit-Swift.h>
2020-06-05 02:38:44 +02:00
#import <SessionServiceKit/TSAccountManager.h>
#import <SessionServiceKit/TSGroupModel.h>
#import <SessionServiceKit/TSInvalidIdentityKeyReceivingErrorMessage.h>
#import <SessionServiceKit/TSNetworkManager.h>
#import <SessionServiceKit/TSQuotedMessage.h>
2017-12-20 17:28:07 +01:00
#import <YapDatabase/YapDatabase.h>
2018-04-09 19:58:12 +02:00
#import <YapDatabase/YapDatabaseAutoView.h>
2017-12-15 17:16:07 +01:00
#import <YapDatabase/YapDatabaseViewChange.h>
#import <YapDatabase/YapDatabaseViewConnection.h>
2020-06-05 05:43:06 +02:00
#import <SessionMetadataKit/SessionMetadataKit-Swift.h>
@import Photos;
2017-10-10 22:13:54 +02:00
NS_ASSUME_NONNULL_BEGIN
static const CGFloat kLoadMoreHeaderHeight = 60.f;
2018-08-21 18:18:13 +02:00
static const CGFloat kToastInset = 10;
2014-10-29 21:58:58 +01:00
typedef enum : NSUInteger {
kMediaTypePicture,
kMediaTypeVideo,
} kMediaTypes;
typedef enum : NSUInteger {
kScrollContinuityBottom = 0,
kScrollContinuityTop,
} ScrollContinuity;
#pragma mark -
@interface ConversationViewController () <AttachmentApprovalViewControllerDelegate,
ContactShareApprovalViewControllerDelegate,
AVAudioPlayerDelegate,
CNContactViewControllerDelegate,
2018-05-01 22:38:54 +02:00
ContactEditingDelegate,
ContactsPickerDelegate,
ContactShareViewHelperDelegate,
2018-05-01 22:38:54 +02:00
ContactsViewHelperDelegate,
DisappearingTimerConfigurationViewDelegate,
OWSConversationSettingsViewDelegate,
2017-10-10 22:13:54 +02:00
ConversationViewLayoutDelegate,
ConversationViewCellDelegate,
ConversationInputTextViewDelegate,
ConversationSearchControllerDelegate,
LongTextViewDelegate,
MessageActionsDelegate,
MessageDetailViewDelegate,
MenuActionsViewControllerDelegate,
OWSMessageBubbleViewDelegate,
2017-10-10 22:13:54 +02:00
UICollectionViewDelegate,
UICollectionViewDataSource,
UIDocumentMenuDelegate,
UIDocumentPickerDelegate,
UIImagePickerControllerDelegate,
SendMediaNavDelegate,
UINavigationControllerDelegate,
UITextViewDelegate,
2017-10-10 22:13:54 +02:00
ConversationCollectionViewDelegate,
ConversationInputToolbarDelegate,
2018-10-31 15:05:24 +01:00
GifPickerViewControllerDelegate,
ConversationViewModelDelegate>
2014-10-29 21:58:58 +01:00
@property (nonatomic) TSThread *thread;
2018-10-31 15:05:24 +01:00
@property (nonatomic, readonly) ConversationViewModel *conversationViewModel;
2018-10-23 16:40:09 +02:00
@property (nonatomic, readonly) OWSAudioActivity *recordVoiceNoteAudioActivity;
2018-05-31 01:21:06 +02:00
@property (nonatomic, readonly) NSTimeInterval viewControllerCreatedAt;
2017-10-10 22:13:54 +02:00
@property (nonatomic, readonly) ConversationInputToolbar *inputToolbar;
@property (nonatomic, readonly) ConversationCollectionView *collectionView;
@property (nonatomic, readonly) UIProgressView *progressIndicatorView;
@property (nonatomic, readonly) ConversationViewLayout *layout;
2018-06-25 21:20:17 +02:00
@property (nonatomic, readonly) ConversationStyle *conversationStyle;
2017-10-10 22:13:54 +02:00
@property (nonatomic, nullable) AVAudioRecorder *audioRecorder;
2018-02-23 21:44:46 +01:00
@property (nonatomic, nullable) OWSAudioPlayer *audioAttachmentPlayer;
2017-10-10 22:13:54 +02:00
@property (nonatomic, nullable) NSUUID *voiceMessageUUID;
@property (nonatomic, nullable) NSTimer *readTimer;
2017-10-18 21:11:19 +02:00
@property (nonatomic) NSCache *cellMediaCache;
@property (nonatomic) LKConversationTitleView *headerView;
2017-10-10 22:13:54 +02:00
@property (nonatomic, nullable) UIView *bannerView;
2019-12-10 00:57:15 +01:00
@property (nonatomic, nullable) UIView *restoreSessionBannerView;
@property (nonatomic, nullable) OWSDisappearingMessagesConfiguration *disappearingMessagesConfiguration;
// Back Button Unread Count
@property (nonatomic, readonly) UIView *backButtonUnreadCountView;
@property (nonatomic, readonly) UILabel *backButtonUnreadCountLabel;
@property (nonatomic, readonly) NSUInteger backButtonUnreadCount;
2018-05-02 16:08:47 +02:00
@property (nonatomic) ConversationViewAction actionOnOpen;
2018-06-11 21:31:54 +02:00
@property (nonatomic) BOOL peek;
2015-01-31 12:00:58 +01:00
2017-10-10 22:13:54 +02:00
@property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper;
@property (nonatomic) BOOL userHasScrolled;
2017-10-12 22:19:07 +02:00
@property (nonatomic, nullable) NSDate *lastMessageSentDate;
2014-10-29 21:58:58 +01:00
@property (nonatomic, nullable) UIBarButtonItem *customBackButton;
@property (nonatomic) BOOL showLoadMoreHeader;
2017-11-16 22:43:41 +01:00
@property (nonatomic) UILabel *loadMoreHeader;
@property (nonatomic) uint64_t lastVisibleSortId;
@property (nonatomic) BOOL isUserScrolling;
@property (nonatomic) NSLayoutConstraint *scrollDownButtonButtomConstraint;
@property (nonatomic) ConversationScrollButton *scrollDownButton;
@property (nonatomic) BOOL isViewCompletelyAppeared;
@property (nonatomic) BOOL isViewVisible;
@property (nonatomic) BOOL shouldAnimateKeyboardChanges;
@property (nonatomic) BOOL viewHasEverAppeared;
@property (nonatomic) BOOL hasUnreadMessages;
2018-01-29 22:35:31 +01:00
@property (nonatomic) BOOL isPickingMediaAsDocument;
2018-02-22 17:03:53 +01:00
@property (nonatomic, nullable) NSNumber *viewHorizonTimestamp;
@property (nonatomic) ContactShareViewHelper *contactShareViewHelper;
2018-07-05 18:36:50 +02:00
@property (nonatomic) NSTimer *reloadTimer;
@property (nonatomic, nullable) NSDate *lastReloadDate;
2018-10-31 15:05:24 +01:00
@property (nonatomic) CGFloat scrollDistanceToBottomSnapshot;
@property (nonatomic, nullable) NSNumber *lastKnownDistanceFromBottom;
@property (nonatomic) ScrollContinuity scrollContinuity;
@property (nonatomic, nullable) NSTimer *autoLoadMoreTimer;
@property (nonatomic, readonly) ConversationSearchController *searchController;
@property (nonatomic, nullable) NSString *lastSearchedText;
@property (nonatomic) BOOL isShowingSearchUI;
2019-03-18 16:24:09 +01:00
@property (nonatomic, nullable) MenuActionsViewController *menuActionsViewController;
2019-03-19 16:13:06 +01:00
@property (nonatomic) CGFloat extraContentInsetPadding;
2019-03-22 21:53:55 +01:00
@property (nonatomic) CGFloat contentInsetBottom;
// Mentions
@property (nonatomic) NSInteger currentMentionStartIndex;
@property (nonatomic) NSMutableArray<LKMention *> *mentions;
@property (nonatomic) NSString *oldText;
2019-10-09 05:46:21 +02:00
2020-04-30 02:04:14 +02:00
// Status bar updating
/// Used to avoid duplicate status bar updates.
@property (nonatomic) NSMutableSet<NSNumber *> *handledMessageTimestamps;
@end
#pragma mark -
2017-09-06 20:13:18 +02:00
@implementation ConversationViewController
2014-10-29 21:58:58 +01:00
2017-10-10 22:13:54 +02:00
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
{
2018-08-27 16:29:51 +02:00
OWSFailDebug(@"Do not instantiate this view from coder");
self = [super initWithCoder:aDecoder];
if (!self) {
return self;
}
[self commonInit];
return self;
}
- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (!self) {
return self;
}
[self commonInit];
return self;
}
- (void)commonInit
{
2018-05-31 01:21:06 +02:00
_viewControllerCreatedAt = CACurrentMediaTime();
_contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self];
_contactShareViewHelper = [[ContactShareViewHelper alloc] initWithContactsManager:self.contactsManager];
_contactShareViewHelper.delegate = self;
NSString *audioActivityDescription = [NSString stringWithFormat:@"%@ voice note", self.logTag];
2018-10-23 16:40:09 +02:00
_recordVoiceNoteAudioActivity = [[OWSAudioActivity alloc] initWithAudioDescription:audioActivityDescription behavior:OWSAudioBehavior_PlayAndRecord];
self.scrollContinuity = kScrollContinuityBottom;
_currentMentionStartIndex = -1;
_mentions = [NSMutableArray new];
2019-10-11 01:45:24 +02:00
_oldText = @"";
}
#pragma mark - Dependencies
- (SSKMessageSenderJobQueue *)messageSenderJobQueue
{
return SSKEnvironment.shared.messageSenderJobQueue;
}
- (OWSSessionResetJobQueue *)sessionResetJobQueue
{
return AppEnvironment.shared.sessionResetJobQueue;
}
- (OWSAudioSession *)audioSession
{
return Environment.shared.audioSession;
}
- (OWSMessageSender *)messageSender
{
return SSKEnvironment.shared.messageSender;
}
- (OWSContactsManager *)contactsManager
{
return Environment.shared.contactsManager;
}
- (ContactsUpdater *)contactsUpdater
{
return SSKEnvironment.shared.contactsUpdater;
}
- (OWSBlockingManager *)blockingManager
{
return [OWSBlockingManager sharedManager];
}
- (OWSPrimaryStorage *)primaryStorage
{
return SSKEnvironment.shared.primaryStorage;
}
- (TSNetworkManager *)networkManager
{
return SSKEnvironment.shared.networkManager;
}
2020-02-06 00:34:13 +01:00
//- (OutboundCallInitiator *)outboundCallInitiator
//{
// return AppEnvironment.shared.outboundCallInitiator;
//}
- (id<OWSTypingIndicators>)typingIndicators
{
return SSKEnvironment.shared.typingIndicators;
}
2018-11-07 23:49:25 +01:00
- (OWSAttachmentDownloads *)attachmentDownloads
{
return SSKEnvironment.shared.attachmentDownloads;
}
2018-12-21 19:03:09 +01:00
- (TSAccountManager *)tsAccountManager
{
OWSAssertDebug(SSKEnvironment.shared.tsAccountManager);
return SSKEnvironment.shared.tsAccountManager;
}
2019-04-09 17:59:09 +02:00
- (OWSNotificationPresenter *)notificationPresenter
{
return AppEnvironment.shared.notificationPresenter;
}
#pragma mark -
- (void)addNotificationListeners
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(blockListDidChange:)
name:kNSNotificationName_BlockListDidChange
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(windowManagerCallDidChange:)
name:OWSWindowManagerCallDidChangeNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(identityStateDidChange:)
name:kNSNotificationName_IdentityStateDidChange
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didChangePreferredContentSize:)
name:UIContentSizeCategoryDidChangeNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillEnterForeground:)
name:OWSApplicationWillEnterForegroundNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidEnterBackground:)
name:OWSApplicationDidEnterBackgroundNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillResignActive:)
name:OWSApplicationWillResignActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidBecomeActive:)
name:OWSApplicationDidBecomeActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(cancelReadTimer)
name:OWSApplicationDidEnterBackgroundNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(otherUsersProfileDidChange:)
name:kNSNotificationName_OtherUsersProfileDidChange
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(profileWhitelistDidChange:)
name:kNSNotificationName_ProfileWhitelistDidChange
object:nil];
2019-01-10 15:45:48 +01:00
// Keyboard events.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillShow:)
name:UIKeyboardWillShowNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardDidShow:)
name:UIKeyboardDidShowNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardDidHide:)
name:UIKeyboardDidHideNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillChangeFrame:)
name:UIKeyboardWillChangeFrameNotification
object:nil];
2019-01-10 15:45:48 +01:00
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardDidChangeFrame:)
name:UIKeyboardDidChangeFrameNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleThreadSessionRestoreDevicesChangedNotifiaction:)
name:NSNotification.threadSessionRestoreDevicesChanged
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
2020-04-14 02:07:53 +02:00
selector:@selector(handleGroupThreadUpdatedNotification:)
name:NSNotification.groupThreadUpdated
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleCalculatingPoWNotification:)
name:NSNotification.calculatingPoW
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
2020-02-20 03:30:30 +01:00
selector:@selector(handleRoutingNotification:)
name:NSNotification.routing
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
2020-02-20 03:30:30 +01:00
selector:@selector(handleMessageSendingNotification:)
name:NSNotification.messageSending
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleMessageSentNotification:)
name:NSNotification.messageSent
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleMessageFailedNotification:)
name:NSNotification.messageFailed
object:nil];
// Device linking
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleUnexpectedDeviceLinkRequestReceivedNotification)
name:NSNotification.unexpectedDeviceLinkRequestReceived
object:nil];
}
2018-06-22 19:48:23 +02:00
- (BOOL)isGroupConversation
{
OWSAssertDebug(self.thread);
2018-06-22 19:48:23 +02:00
return self.thread.isGroupThread;
}
- (void)otherUsersProfileDidChange:(NSNotification *)notification
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
2017-08-21 23:27:30 +02:00
NSString *recipientId = notification.userInfo[kNSNotificationKey_ProfileRecipientId];
OWSAssertDebug(recipientId.length > 0);
if (recipientId.length > 0 && [self.thread.recipientIdentifiers containsObject:recipientId]) {
2017-09-06 20:13:18 +02:00
if (self.isGroupConversation) {
// Reload all cells if this is a group conversation,
// since we may need to update the sender names on the messages.
[self resetContentAndLayout];
}
}
}
- (void)profileWhitelistDidChange:(NSNotification *)notification
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
// If profile whitelist just changed, we may want to hide a profile whitelist offer.
2017-08-21 23:27:30 +02:00
NSString *_Nullable recipientId = notification.userInfo[kNSNotificationKey_ProfileRecipientId];
NSData *_Nullable groupId = notification.userInfo[kNSNotificationKey_ProfileGroupId];
if (recipientId.length > 0 && [self.thread.recipientIdentifiers containsObject:recipientId]) {
2019-02-19 23:49:40 +01:00
[self.conversationViewModel ensureDynamicInteractionsAndUpdateIfNecessary:YES];
} else if (groupId.length > 0 && self.thread.isGroupThread) {
TSGroupThread *groupThread = (TSGroupThread *)self.thread;
if ([groupThread.groupModel.groupId isEqualToData:groupId]) {
2019-02-19 23:49:40 +01:00
[self.conversationViewModel ensureDynamicInteractionsAndUpdateIfNecessary:YES];
[self ensureBannerState];
}
}
}
- (void)blockListDidChange:(id)notification
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
[self ensureBannerState];
}
- (void)identityStateDidChange:(NSNotification *)notification
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
[self ensureBannerState];
}
2020-04-14 02:07:53 +02:00
- (void)handleGroupThreadUpdatedNotification:(NSNotification *)notification
{
OWSAssertIsOnMainThread();
// Check thread
NSString *threadID = (NSString *)notification.object;
if (![threadID isEqualToString:self.thread.uniqueId]) { return; }
// Ensure thread instance is up to date
[self.thread reload];
// Update UI
[self hideInputIfNeeded];
2020-05-05 01:11:43 +02:00
[self.collectionView.collectionViewLayout invalidateLayout];
for (id<ConversationViewItem> item in self.viewItems) {
[item clearCachedLayoutState];
}
[self.conversationViewModel reloadViewItems];
[self.collectionView reloadData];
}
- (void)handleThreadSessionRestoreDevicesChangedNotifiaction:(NSNotification *)notification
{
// Check thread
NSString *threadID = (NSString *)notification.object;
if (![threadID isEqualToString:self.thread.uniqueId]) { return; }
// Ensure thread instance is up to date
[self.thread reload];
// Update UI
2019-12-10 00:57:15 +01:00
[self updateSessionRestoreBanner];
}
- (void)peekSetup
{
_peek = YES;
2018-05-02 16:08:47 +02:00
self.actionOnOpen = ConversationViewActionNone;
}
- (void)popped
{
_peek = NO;
[self hideInputIfNeeded];
}
2018-06-11 21:31:54 +02:00
- (void)configureForThread:(TSThread *)thread
action:(ConversationViewAction)action
focusMessageId:(nullable NSString *)focusMessageId
2017-04-18 22:08:01 +02:00
{
OWSAssertDebug(thread);
2018-06-11 21:31:54 +02:00
OWSLogInfo(@"configureForThread.");
2017-04-18 22:08:01 +02:00
_thread = thread;
2018-05-02 16:08:47 +02:00
self.actionOnOpen = action;
2017-10-18 21:11:19 +02:00
_cellMediaCache = [NSCache new];
// Cache the cell media for ~24 cells.
self.cellMediaCache.countLimit = 24;
2018-06-25 21:20:17 +02:00
_conversationStyle = [[ConversationStyle alloc] initWithThread:thread];
2014-11-26 16:00:10 +01:00
2018-10-31 15:05:24 +01:00
_conversationViewModel =
[[ConversationViewModel alloc] initWithThread:thread focusMessageIdOnOpen:focusMessageId isRSSFeed:self.isRSSFeed delegate:self];
_searchController = [[ConversationSearchController alloc] initWithThread:thread];
_searchController.delegate = self;
2018-07-05 18:36:50 +02:00
self.reloadTimer = [NSTimer weakScheduledTimerWithTimeInterval:1.f
target:self
selector:@selector(reloadTimerDidFire)
userInfo:nil
repeats:YES];
[LKMentionsManager populateUserPublicKeyCacheIfNeededFor:thread.uniqueId in:nil];
2018-07-05 18:36:50 +02:00
}
- (void)dealloc
{
[self.reloadTimer invalidate];
[self.autoLoadMoreTimer invalidate];
2018-07-05 18:36:50 +02:00
}
- (void)reloadTimerDidFire
{
OWSAssertIsOnMainThread();
if (self.isUserScrolling || !self.isViewCompletelyAppeared || !self.isViewVisible
|| !CurrentAppContext().isAppForegroundAndActive || !self.viewHasEverAppeared
|| OWSWindowManager.sharedManager.isPresentingMenuActions) {
2018-07-05 18:36:50 +02:00
return;
}
NSDate *now = [NSDate new];
if (self.lastReloadDate) {
NSTimeInterval timeSinceLastReload = [now timeIntervalSinceDate:self.lastReloadDate];
const NSTimeInterval kReloadFrequency = 60.f;
if (timeSinceLastReload < kReloadFrequency) {
return;
}
}
OWSLogVerbose(@"reloading conversation view contents.");
2018-07-05 18:36:50 +02:00
[self resetContentAndLayout];
}
Disappearing Messages * Per thread settings menu accessed by tapping on thread title This removed the toggle-phone behavior. You'll be able to see the phone number in the settings table view. This removed the "add contact" functionality, although it was already broken for ios>=9 (which is basically everybody). The group actions menu was absorbed into this screen * Added a confirm alert to leave group (fixes #938) * New Translation Strings * Extend "Add People" label to fit translations. * resolved issues with translations not fitting in group menu * Fix the long standing type warning where TSCalls were assigned to a TSMessageAdapter. * Can delete info messages Follow the JSQMVC pattern and put UIResponder-able content in the messageBubbleContainer. This gives us more functionality *and* allows us to delete some code. yay! It's still not yet possible to delete phone messages. =( * Fixed some compiler warnings. * xcode8 touching storyboard. So long xcode7! * Fixup multiline info messages. We were seeing info messages like "You set disappearing message timer to 10" instead of "You set disappearing message timer to 10 seconds." Admittedly this isn't a very good fix, as now one liners feel like they have too much padding. If the message is well over one line, we were wrapping properly, but there's a problem when the message is *just barely* two lines, the cell height grows, but the label still thinks it's just one line (as evinced by the one line appearing in the center of the label frame. The result being that the last word of the label is cropped. * Disable group actions after leaving group. // FREEBIE
2016-09-21 14:37:51 +02:00
- (BOOL)userLeftGroup
{
if (![_thread isKindOfClass:[TSGroupThread class]]) {
return NO;
}
TSGroupThread *groupThread = (TSGroupThread *)self.thread;
2019-01-04 15:19:41 +01:00
return !groupThread.isLocalUserInGroup;
Disappearing Messages * Per thread settings menu accessed by tapping on thread title This removed the toggle-phone behavior. You'll be able to see the phone number in the settings table view. This removed the "add contact" functionality, although it was already broken for ios>=9 (which is basically everybody). The group actions menu was absorbed into this screen * Added a confirm alert to leave group (fixes #938) * New Translation Strings * Extend "Add People" label to fit translations. * resolved issues with translations not fitting in group menu * Fix the long standing type warning where TSCalls were assigned to a TSMessageAdapter. * Can delete info messages Follow the JSQMVC pattern and put UIResponder-able content in the messageBubbleContainer. This gives us more functionality *and* allows us to delete some code. yay! It's still not yet possible to delete phone messages. =( * Fixed some compiler warnings. * xcode8 touching storyboard. So long xcode7! * Fixup multiline info messages. We were seeing info messages like "You set disappearing message timer to 10" instead of "You set disappearing message timer to 10 seconds." Admittedly this isn't a very good fix, as now one liners feel like they have too much padding. If the message is well over one line, we were wrapping properly, but there's a problem when the message is *just barely* two lines, the cell height grows, but the label still thinks it's just one line (as evinced by the one line appearing in the center of the label frame. The result being that the last word of the label is cropped. * Disable group actions after leaving group. // FREEBIE
2016-09-21 14:37:51 +02:00
}
2019-08-28 08:49:47 +02:00
- (BOOL)isRSSFeed
2019-08-28 07:49:16 +02:00
{
2019-08-28 08:49:47 +02:00
if (![_thread isKindOfClass:[TSGroupThread class]]) { return NO; }
TSGroupThread *thread = (TSGroupThread *)self.thread;
return thread.isRSSFeed;
2019-08-28 07:49:16 +02:00
}
- (void)hideInputIfNeeded
{
if (_peek) {
2017-10-10 22:13:54 +02:00
self.inputToolbar.hidden = YES;
[self dismissKeyBoard];
return;
}
2019-09-06 08:30:40 +02:00
if (self.userLeftGroup) {
2017-10-10 22:13:54 +02:00
self.inputToolbar.hidden = YES; // user has requested they leave the group. further sends disallowed
[self dismissKeyBoard];
} else {
2017-10-10 22:13:54 +02:00
self.inputToolbar.hidden = NO;
}
2019-09-06 08:30:40 +02:00
2020-05-05 01:11:43 +02:00
// Loki: In RSS feeds, don't hide the input bar entirely; just hide the text field inside.
2019-09-06 08:30:40 +02:00
if (self.isRSSFeed) {
[self.inputToolbar hideInputMethod];
}
}
2015-01-31 12:00:58 +01:00
- (void)viewDidLoad
{
[super viewDidLoad];
2017-10-10 22:13:54 +02:00
[self createContents];
2017-10-10 22:13:54 +02:00
[self registerCellClasses];
[self createConversationScrollButtons];
[self createHeaderViews];
2018-04-26 16:12:21 +02:00
if (@available(iOS 11, *)) {
// We use the default back button from home view, which animates nicely with interactive transitions like the
// interactive pop gesture and the "slide left" for info.
} else {
// On iOS9/10 the default back button is too wide, so we use a custom back button. This doesn't animate nicely
// with interactive transitions, but has the appropriate width.
[self createBackButton];
}
[self addNotificationListeners];
2017-11-01 17:01:42 +01:00
[self loadDraftInCompose];
2018-07-23 21:36:39 +02:00
[self applyTheme];
2018-10-31 15:05:24 +01:00
[self.conversationViewModel viewDidLoad];
// Loki: Set gradient background
self.collectionView.backgroundColor = UIColor.clearColor;
LKGradient *gradient = LKGradients.defaultLokiBackground;
self.view.backgroundColor = UIColor.clearColor;
[self.view setGradient:gradient];
// Loki: Set navigation bar background color
UINavigationBar *navigationBar = self.navigationController.navigationBar;
[navigationBar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];
navigationBar.shadowImage = [UIImage new];
[navigationBar setTranslucent:NO];
navigationBar.barTintColor = LKColors.navigationBarBackground;
// Loki: Set up navigation bar buttons
UIBarButtonItem *backButton = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Back", "") style:UIBarButtonItemStylePlain target:nil action:nil];
backButton.tintColor = LKColors.text;
self.navigationItem.backBarButtonItem = backButton;
UIBarButtonItem *settingsButton = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"Gear"] style:UIBarButtonItemStylePlain target:self action:@selector(showConversationSettings)];
settingsButton.tintColor = LKColors.text;
self.navigationItem.rightBarButtonItem = settingsButton;
2019-09-04 07:55:17 +02:00
if (self.thread.isGroupThread) {
TSGroupThread *thread = (TSGroupThread *)self.thread;
if (!thread.isPublicChat) { return; }
__block LKPublicChat *publicChat;
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
publicChat = [LKDatabaseUtilities getPublicChatForThreadID:thread.uniqueId transaction:transaction];
}];
2020-05-27 03:34:24 +02:00
[LKPublicChatAPI getInfoForChannelWithID:publicChat.channel onServer:publicChat.server]
.thenOn(dispatch_get_main_queue(), ^(id userCount) {
[self.headerView updateSubtitleForCurrentStatus];
});
2019-09-04 07:55:17 +02:00
}
2020-06-19 06:36:21 +02:00
if ([self.thread isKindOfClass:TSContactThread.class]) {
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[SSKEnvironment.shared.profileManager ensureProfileCachedForContactWithID:self.thread.contactIdentifier with:transaction];
}];
}
}
2017-10-10 22:13:54 +02:00
- (void)createContents
{
OWSAssertDebug(self.conversationStyle);
2018-06-22 19:48:23 +02:00
2018-08-09 16:47:43 +02:00
_layout = [[ConversationViewLayout alloc] initWithConversationStyle:self.conversationStyle];
2018-06-25 21:20:17 +02:00
self.conversationStyle.viewWidth = self.view.width;
2018-06-22 19:48:23 +02:00
self.layout.delegate = self;
2017-10-10 22:13:54 +02:00
// We use the root view bounds as the initial frame for the collection
// view so that its contents can be laid out immediately.
2019-01-08 22:38:18 +01:00
//
// TODO: To avoid relayout, it'd be better to take into account safeAreaInsets,
// but they're not yet set when this method is called.
_collectionView =
[[ConversationCollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:self.layout];
2017-10-10 22:13:54 +02:00
self.collectionView.layoutDelegate = self;
self.collectionView.delegate = self;
self.collectionView.dataSource = self;
self.collectionView.showsVerticalScrollIndicator = YES;
2017-10-10 22:13:54 +02:00
self.collectionView.showsHorizontalScrollIndicator = NO;
self.collectionView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive;
2019-01-08 01:57:22 +01:00
if (@available(iOS 10, *)) {
// To minimize time to initial apearance, we initially disable prefetching, but then
// re-enable it once the view has appeared.
self.collectionView.prefetchingEnabled = NO;
}
2017-10-10 22:13:54 +02:00
[self.view addSubview:self.collectionView];
2019-01-08 16:51:19 +01:00
[self.collectionView autoPinEdgeToSuperviewEdge:ALEdgeTop];
[self.collectionView autoPinEdgeToSuperviewEdge:ALEdgeBottom];
[self.collectionView autoPinEdgeToSuperviewSafeArea:ALEdgeLeading];
[self.collectionView autoPinEdgeToSuperviewSafeArea:ALEdgeTrailing];
2017-10-10 22:13:54 +02:00
_progressIndicatorView = [UIProgressView new];
[self.progressIndicatorView autoSetDimension:ALDimensionHeight toSize:LKValues.progressBarThickness];
self.progressIndicatorView.progressViewStyle = UIProgressViewStyleBar;
self.progressIndicatorView.progressTintColor = LKColors.accent;
self.progressIndicatorView.trackTintColor = UIColor.clearColor;
self.progressIndicatorView.alpha = 0;
[self.view addSubview:self.progressIndicatorView];
[self.progressIndicatorView autoPinEdgeToSuperviewEdge:ALEdgeTop];
[self.progressIndicatorView autoPinEdgeToSuperviewSafeArea:ALEdgeLeading];
[self.progressIndicatorView autoPinEdgeToSuperviewSafeArea:ALEdgeTrailing];
2018-04-09 20:56:33 +02:00
[self.collectionView applyScrollViewInsetsFix];
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, _collectionView);
2018-06-28 19:28:14 +02:00
_inputToolbar = [[ConversationInputToolbar alloc] initWithConversationStyle:self.conversationStyle];
2017-10-10 22:13:54 +02:00
self.inputToolbar.inputToolbarDelegate = self;
self.inputToolbar.inputTextViewDelegate = self;
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, _inputToolbar);
2017-11-16 22:43:41 +01:00
self.loadMoreHeader = [UILabel new];
2019-12-11 00:25:53 +01:00
self.loadMoreHeader.text = NSLocalizedString(@"CONVERSATION_VIEW_LOADING_MORE_MESSAGES", @"Indicates that the app is loading more messages in this conversation.");
self.loadMoreHeader.textColor = [LKColors.text colorWithAlphaComponent:0.8];
self.loadMoreHeader.textAlignment = NSTextAlignmentCenter;
2019-12-11 00:25:53 +01:00
self.loadMoreHeader.font = [UIFont boldSystemFontOfSize:LKValues.verySmallFontSize];
[self.collectionView addSubview:self.loadMoreHeader];
[self.loadMoreHeader autoPinWidthToWidthOfView:self.view];
[self.loadMoreHeader autoPinEdgeToSuperviewEdge:ALEdgeTop];
[self.loadMoreHeader autoSetDimension:ALDimensionHeight toSize:kLoadMoreHeaderHeight];
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, _loadMoreHeader);
2018-10-31 15:05:24 +01:00
[self updateShowLoadMoreHeader];
}
- (BOOL)becomeFirstResponder
{
OWSLogDebug(@"");
return [super becomeFirstResponder];
}
- (BOOL)resignFirstResponder
{
OWSLogDebug(@"");
return [super resignFirstResponder];
}
- (BOOL)canBecomeFirstResponder
{
return YES;
}
- (nullable UIView *)inputAccessoryView
{
if (self.isShowingSearchUI) {
return self.searchController.resultsBar;
} else {
return self.inputToolbar;
}
}
2017-10-10 22:13:54 +02:00
- (void)registerCellClasses
{
[self.collectionView registerClass:[OWSSystemMessageCell class]
forCellWithReuseIdentifier:[OWSSystemMessageCell cellReuseIdentifier]];
[self.collectionView registerClass:[OWSTypingIndicatorCell class]
forCellWithReuseIdentifier:[OWSTypingIndicatorCell cellReuseIdentifier]];
[self.collectionView registerClass:[OWSContactOffersCell class]
forCellWithReuseIdentifier:[OWSContactOffersCell cellReuseIdentifier]];
2017-10-17 06:05:29 +02:00
[self.collectionView registerClass:[OWSMessageCell class]
forCellWithReuseIdentifier:[OWSMessageCell cellReuseIdentifier]];
}
2017-05-26 18:59:31 +02:00
- (void)applicationWillEnterForeground:(NSNotification *)notification
{
[self startReadTimer];
[self updateCellsVisible];
2017-05-26 18:59:31 +02:00
}
- (void)applicationDidEnterBackground:(NSNotification *)notification
{
[self updateCellsVisible];
2017-10-18 21:11:19 +02:00
[self.cellMediaCache removeAllObjects];
}
2017-05-05 04:10:37 +02:00
- (void)applicationWillResignActive:(NSNotification *)notification
{
[self cancelVoiceMemo];
self.isUserScrolling = NO;
[self saveDraft];
[self markVisibleMessagesAsRead];
[self.cellMediaCache removeAllObjects];
[self cancelReadTimer];
[self dismissPresentedViewControllerIfNecessary];
}
- (void)applicationDidBecomeActive:(NSNotification *)notification
{
[self startReadTimer];
2020-08-03 02:50:15 +02:00
[self resetContentAndLayout];
}
- (void)dismissPresentedViewControllerIfNecessary
{
UIViewController *_Nullable presentedViewController = self.presentedViewController;
if (!presentedViewController) {
OWSLogDebug(@"presentedViewController was nil");
return;
}
if ([presentedViewController isKindOfClass:[UIAlertController class]]) {
OWSLogDebug(@"dismissing presentedViewController: %@", presentedViewController);
[self dismissViewControllerAnimated:NO completion:nil];
return;
}
if ([presentedViewController isKindOfClass:[UIImagePickerController class]]) {
OWSLogDebug(@"dismissing presentedViewController: %@", presentedViewController);
[self dismissViewControllerAnimated:NO completion:nil];
return;
}
2017-05-05 04:10:37 +02:00
}
- (void)viewWillAppear:(BOOL)animated
{
OWSLogDebug(@"viewWillAppear");
2017-06-19 23:10:34 +02:00
[self ensureBannerState];
2019-12-10 01:45:56 +01:00
[self updateSessionRestoreBanner];
[super viewWillAppear:animated];
Disappearing Messages * Per thread settings menu accessed by tapping on thread title This removed the toggle-phone behavior. You'll be able to see the phone number in the settings table view. This removed the "add contact" functionality, although it was already broken for ios>=9 (which is basically everybody). The group actions menu was absorbed into this screen * Added a confirm alert to leave group (fixes #938) * New Translation Strings * Extend "Add People" label to fit translations. * resolved issues with translations not fitting in group menu * Fix the long standing type warning where TSCalls were assigned to a TSMessageAdapter. * Can delete info messages Follow the JSQMVC pattern and put UIResponder-able content in the messageBubbleContainer. This gives us more functionality *and* allows us to delete some code. yay! It's still not yet possible to delete phone messages. =( * Fixed some compiler warnings. * xcode8 touching storyboard. So long xcode7! * Fixup multiline info messages. We were seeing info messages like "You set disappearing message timer to 10" instead of "You set disappearing message timer to 10 seconds." Admittedly this isn't a very good fix, as now one liners feel like they have too much padding. If the message is well over one line, we were wrapping properly, but there's a problem when the message is *just barely* two lines, the cell height grows, but the label still thinks it's just one line (as evinced by the one line appearing in the center of the label frame. The result being that the last word of the label is cropped. * Disable group actions after leaving group. // FREEBIE
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];
self.isViewVisible = YES;
// We should have already requested contact access at this point, so this should be a no-op
2017-09-06 19:59:39 +02:00
// unless it ever becomes possible to load this VC without going via the HomeViewController.
[self.contactsManager requestSystemContactsOnce];
[self updateDisappearingMessagesConfiguration];
[self updateBarButtonItems];
Disappearing Messages * Per thread settings menu accessed by tapping on thread title This removed the toggle-phone behavior. You'll be able to see the phone number in the settings table view. This removed the "add contact" functionality, although it was already broken for ios>=9 (which is basically everybody). The group actions menu was absorbed into this screen * Added a confirm alert to leave group (fixes #938) * New Translation Strings * Extend "Add People" label to fit translations. * resolved issues with translations not fitting in group menu * Fix the long standing type warning where TSCalls were assigned to a TSMessageAdapter. * Can delete info messages Follow the JSQMVC pattern and put UIResponder-able content in the messageBubbleContainer. This gives us more functionality *and* allows us to delete some code. yay! It's still not yet possible to delete phone messages. =( * Fixed some compiler warnings. * xcode8 touching storyboard. So long xcode7! * Fixup multiline info messages. We were seeing info messages like "You set disappearing message timer to 10" instead of "You set disappearing message timer to 10 seconds." Admittedly this isn't a very good fix, as now one liners feel like they have too much padding. If the message is well over one line, we were wrapping properly, but there's a problem when the message is *just barely* two lines, the cell height grows, but the label still thinks it's just one line (as evinced by the one line appearing in the center of the label frame. The result being that the last word of the label is cropped. * Disable group actions after leaving group. // FREEBIE
2016-09-21 14:37:51 +02:00
2018-08-03 19:10:11 +02:00
[self resetContentAndLayout];
// We want to set the initial scroll state the first time we enter the view.
if (!self.viewHasEverAppeared) {
[self scrollToDefaultPosition:NO];
2019-03-18 16:24:09 +01:00
} else if (self.menuActionsViewController != nil) {
2019-03-19 16:13:06 +01:00
[self scrollToMenuActionInteraction:NO];
}
[self updateLastVisibleSortId];
2018-05-31 01:21:06 +02:00
if (!self.viewHasEverAppeared) {
NSTimeInterval appearenceDuration = CACurrentMediaTime() - self.viewControllerCreatedAt;
OWSLogVerbose(@"First viewWillAppear took: %.2fms", appearenceDuration * 1000);
2018-05-31 01:21:06 +02:00
}
2020-05-05 01:11:43 +02:00
[self updateInputBarLayout];
}
2018-10-31 15:05:24 +01:00
- (NSArray<id<ConversationViewItem>> *)viewItems
{
2019-03-19 16:13:06 +01:00
return self.conversationViewModel.viewState.viewItems;
2018-10-31 15:05:24 +01:00
}
- (ThreadDynamicInteractions *)dynamicInteractions
{
return self.conversationViewModel.dynamicInteractions;
2018-10-31 15:05:24 +01:00
}
- (NSIndexPath *_Nullable)indexPathOfUnreadMessagesIndicator
{
2019-03-19 16:13:06 +01:00
NSNumber *_Nullable unreadIndicatorIndex = self.conversationViewModel.viewState.unreadIndicatorIndex;
if (unreadIndicatorIndex == nil) {
return nil;
}
2019-03-19 16:13:06 +01:00
return [NSIndexPath indexPathForRow:unreadIndicatorIndex.integerValue inSection:0];
}
2018-06-11 21:31:54 +02:00
- (NSIndexPath *_Nullable)indexPathOfMessageOnOpen
{
2018-10-31 15:05:24 +01:00
OWSAssertDebug(self.conversationViewModel.focusMessageIdOnOpen);
OWSAssertDebug(self.dynamicInteractions.focusMessagePosition);
2018-06-11 21:31:54 +02:00
2018-06-12 15:43:59 +02:00
if (!self.dynamicInteractions.focusMessagePosition) {
// This might happen if the focus message has disappeared
// before this view could appear.
OWSFailDebug(@"focus message has unknown position.");
2018-06-12 15:43:59 +02:00
return nil;
2018-06-11 21:31:54 +02:00
}
2018-06-12 15:43:59 +02:00
NSUInteger focusMessagePosition = self.dynamicInteractions.focusMessagePosition.unsignedIntegerValue;
if (focusMessagePosition >= self.viewItems.count) {
// This might happen if the focus message is outside the maximum
// valid load window size for this view.
OWSFailDebug(@"focus message has invalid position.");
2018-06-12 15:43:59 +02:00
return nil;
}
NSInteger row = (NSInteger)((self.viewItems.count - 1) - focusMessagePosition);
return [NSIndexPath indexPathForRow:row inSection:0];
2018-06-11 21:31:54 +02:00
}
- (void)scrollToDefaultPosition:(BOOL)isAnimated
{
if (self.isUserScrolling) {
return;
}
2018-06-11 21:31:54 +02:00
NSIndexPath *_Nullable indexPath = nil;
2018-10-31 15:05:24 +01:00
if (self.conversationViewModel.focusMessageIdOnOpen) {
2018-06-11 21:31:54 +02:00
indexPath = [self indexPathOfMessageOnOpen];
}
if (!indexPath) {
indexPath = [self indexPathOfUnreadMessagesIndicator];
}
if (indexPath) {
if (indexPath.section == 0 && indexPath.row == 0) {
[self.collectionView setContentOffset:CGPointZero animated:isAnimated];
} else {
[self.collectionView scrollToItemAtIndexPath:indexPath
atScrollPosition:UICollectionViewScrollPositionTop
animated:isAnimated];
}
} else {
[self scrollToBottomAnimated:isAnimated];
}
}
- (void)scrollToUnreadIndicatorAnimated
{
if (self.isUserScrolling) {
return;
}
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];
}
}
}
- (void)resetContentAndLayout
{
self.scrollContinuity = kScrollContinuityBottom;
2017-04-10 18:44:03 +02:00
// Avoid layout corrupt issues and out-of-date message subtitles.
2018-08-03 16:26:37 +02:00
self.lastReloadDate = [NSDate new];
2018-10-31 15:05:24 +01:00
[self.conversationViewModel viewDidResetContentAndLayout];
[self.collectionView.collectionViewLayout invalidateLayout];
[self.collectionView reloadData];
if (self.viewHasEverAppeared) {
// Try to update the lastKnownDistanceFromBottom; the content size may have changed.
[self updateLastKnownDistanceFromBottom];
}
}
- (void)setUserHasScrolled:(BOOL)userHasScrolled
{
_userHasScrolled = userHasScrolled;
[self ensureBannerState];
}
// 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) {
2018-01-30 22:41:25 +01:00
if ([[OWSIdentityManager sharedManager] verificationStateForRecipientId:recipientId]
== OWSVerificationStateNoLongerVerified) {
[result addObject:recipientId];
}
}
return [result copy];
}
2019-12-10 00:57:15 +01:00
- (void)updateSessionRestoreBanner {
BOOL isContactThread = [self.thread isKindOfClass:[TSContactThread class]];
2020-05-05 01:11:43 +02:00
BOOL shouldDetachBanner = !isContactThread;
2019-12-10 00:57:15 +01:00
if (isContactThread) {
TSContactThread *thread = (TSContactThread *)self.thread;
2020-05-26 06:22:18 +02:00
if (thread.sessionRestoreDevices.count > 0) {
2020-01-24 03:14:49 +01:00
if (self.restoreSessionBannerView == nil) {
LKSessionRestorationView *bannerView = [[LKSessionRestorationView alloc] initWithThread:thread];
2019-12-10 01:45:56 +01:00
[self.view addSubview:bannerView];
2020-01-24 03:14:49 +01:00
[bannerView autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:LKValues.mediumSpacing];
[bannerView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:LKValues.largeSpacing];
[bannerView autoPinEdgeToSuperviewEdge:ALEdgeRight withInset:LKValues.mediumSpacing];
2019-12-10 01:45:56 +01:00
[self.view layoutSubviews];
self.restoreSessionBannerView = bannerView;
[bannerView setOnRestore:^{
[self restoreSession];
}];
[bannerView setOnDismiss:^{
2019-12-10 05:59:20 +01:00
[thread removeAllSessionRestoreDevicesWithTransaction:nil];
2019-12-10 01:45:56 +01:00
}];
2019-12-10 00:57:15 +01:00
}
2020-05-26 06:22:18 +02:00
} else {
shouldDetachBanner = true;
}
2019-12-10 00:57:15 +01:00
}
2020-05-05 01:11:43 +02:00
if (shouldDetachBanner && self.restoreSessionBannerView != nil) {
2019-12-10 00:57:15 +01:00
[self.restoreSessionBannerView removeFromSuperview];
self.restoreSessionBannerView = nil;
}
}
- (void)ensureBannerState
{
// This method should be called rarely, so it's simplest to discard and
// rebuild the indicator view every time.
[self.bannerView removeFromSuperview];
self.bannerView = nil;
2019-12-10 05:59:20 +01:00
if (self.userHasScrolled) {
return;
}
2019-12-10 05:59:20 +01:00
NSArray<NSString *> *noLongerVerifiedRecipientIds = [self noLongerVerifiedRecipientIds];
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;
}
NSString *blockStateMessage = nil;
2018-09-09 19:46:23 +02:00
if ([self isBlockedConversation]) {
if (self.isGroupConversation) {
/*
2018-09-09 19:46:23 +02:00
blockStateMessage = NSLocalizedString(
@"MESSAGES_VIEW_GROUP_BLOCKED", @"Indicates that this group conversation has been blocked.");
*/
2018-09-09 19:46:23 +02:00
} else {
blockStateMessage = NSLocalizedString(
@"MESSAGES_VIEW_CONTACT_BLOCKED", @"Indicates that this 1:1 conversation has been blocked.");
}
} else if (self.isGroupConversation) {
/*
int blockedGroupMemberCount = [self blockedGroupMemberCount];
if (blockedGroupMemberCount == 1) {
2017-04-05 18:16:54 +02:00
blockStateMessage = NSLocalizedString(@"MESSAGES_VIEW_GROUP_1_MEMBER_BLOCKED",
@"Indicates that a single member of this group has been blocked.");
} else if (blockedGroupMemberCount > 1) {
blockStateMessage =
[NSString stringWithFormat:NSLocalizedString(@"MESSAGES_VIEW_GROUP_N_MEMBERS_BLOCKED_FORMAT",
@"Indicates that some members of this group has been blocked. Embeds "
@"{{the number of blocked users in this group}}."),
[OWSFormat formatInt:blockedGroupMemberCount]];
}
*/
}
if (blockStateMessage) {
[self createBannerWithTitle:blockStateMessage
2020-07-21 05:49:41 +02:00
bannerColor:LKColors.destructive
tapSelector:@selector(blockBannerViewWasTapped:)];
return;
}
2019-10-23 01:51:06 +02:00
/*
if ([ThreadUtil shouldShowGroupProfileBannerInThread:self.thread blockingManager:self.blockingManager]) {
[self createBannerWithTitle:
NSLocalizedString(@"MESSAGES_VIEW_GROUP_PROFILE_WHITELIST_BANNER",
@"Text for banner in group conversation view that offers to share your profile with this group.")
bannerColor:[UIColor ows_reminderDarkYellowColor]
tapSelector:@selector(groupProfileWhitelistBannerWasTapped:)];
return;
}
2019-10-23 01:51:06 +02:00
*/
}
- (void)createBannerWithTitle:(NSString *)title bannerColor:(UIColor *)bannerColor tapSelector:(SEL)tapSelector
{
OWSAssertDebug(title.length > 0);
OWSAssertDebug(bannerColor);
UIView *bannerView = [UIView containerView];
bannerView.backgroundColor = bannerColor;
bannerView.layer.cornerRadius = 2.5f;
// 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;
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];
[closeButton autoPinTrailingToSuperviewMarginWithInset:kBannerCloseButtonPadding];
[closeButton autoSetDimension:ALDimensionWidth toSize:closeIcon.size.width];
[closeButton autoSetDimension:ALDimensionHeight toSize:closeIcon.size.height];
[bannerView addSubview:label];
[label autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:5];
[label autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:5];
const CGFloat kBannerHPadding = 15.f;
[label autoPinLeadingToSuperviewMarginWithInset:kBannerHPadding];
const CGFloat kBannerHSpacing = 10.f;
[closeButton autoPinLeadingToTrailingEdgeOfView:label offset:kBannerHSpacing];
[bannerView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:tapSelector]];
bannerView.accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"banner_close");
[self.view addSubview:bannerView];
2020-06-05 05:43:06 +02:00
[bannerView autoPinEdge:ALEdgeTop toEdge:ALEdgeTop ofView:self.view withOffset:10.0f];
[bannerView autoHCenterInSuperview];
CGFloat labelDesiredWidth = [label sizeThatFits:CGSizeZero].width;
CGFloat bannerDesiredWidth
= (labelDesiredWidth + kBannerHPadding + kBannerHSpacing + closeIcon.size.width + kBannerCloseButtonPadding);
const CGFloat kMinBannerHMargin = 20.f;
if (bannerDesiredWidth + kMinBannerHMargin * 2.f >= self.view.width) {
2019-01-08 16:51:19 +01:00
[bannerView autoPinEdgeToSuperviewSafeArea:ALEdgeLeading withInset:kMinBannerHMargin];
[bannerView autoPinEdgeToSuperviewSafeArea:ALEdgeTrailing withInset:kMinBannerHMargin];
}
[self.view layoutSubviews];
self.bannerView = bannerView;
}
- (void)blockBannerViewWasTapped:(UIGestureRecognizer *)sender
{
if (sender.state != UIGestureRecognizerStateRecognized) {
return;
}
2018-09-09 19:46:23 +02:00
if ([self isBlockedConversation]) {
// If this a blocked conversation, offer to unblock.
[self showUnblockConversationUI:nil];
} else if (self.isGroupConversation) {
// 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];
}
}
}
- (void)groupProfileWhitelistBannerWasTapped:(UIGestureRecognizer *)sender
{
if (sender.state != UIGestureRecognizerStateRecognized) {
return;
}
[self presentAddThreadToProfileWhitelistWithSuccess:^{
[self ensureBannerState];
}];
}
2019-12-10 00:57:15 +01:00
- (void)restoreSession {
2020-04-30 00:51:19 +02:00
dispatch_async(dispatch_get_main_queue(), ^{
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
2020-06-30 08:05:35 +02:00
[LKSessionManagementProtocol startSessionResetInThread:self.thread transaction:transaction];
} error:nil];
2020-04-30 00:51:19 +02:00
});
2019-12-10 00:57:15 +01:00
}
- (void)noLongerVerifiedBannerViewWasTapped:(UIGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateRecognized) {
NSArray<NSString *> *noLongerVerifiedRecipientIds = [self noLongerVerifiedRecipientIds];
if (noLongerVerifiedRecipientIds.count < 1) {
return;
}
BOOL hasMultiple = noLongerVerifiedRecipientIds.count > 1;
UIAlertController *actionSheet = [UIAlertController alertControllerWithTitle:nil
message:nil
preferredStyle:UIAlertControllerStyleActionSheet];
2017-09-06 20:13:18 +02:00
__weak ConversationViewController *weakSelf = self;
UIAlertAction *verifyAction = [UIAlertAction
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 "
2018-06-22 19:48:23 +02:00
@"number of another user."))
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
[weakSelf showNoLongerVerifiedUI];
}];
[actionSheet addAction:verifyAction];
UIAlertAction *dismissAction =
[UIAlertAction actionWithTitle:CommonStrings.dismissButton
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"dismiss")
style:UIAlertActionStyleCancel
handler:^(UIAlertAction *action) {
[weakSelf resetVerificationStateToDefault];
}];
[actionSheet addAction:dismissAction];
[self dismissKeyBoard];
[self presentAlert:actionSheet];
}
}
- (void)resetVerificationStateToDefault
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
NSArray<NSString *> *noLongerVerifiedRecipientIds = [self noLongerVerifiedRecipientIds];
for (NSString *recipientId in noLongerVerifiedRecipientIds) {
OWSAssertDebug(recipientId.length > 0);
OWSRecipientIdentity *_Nullable recipientIdentity =
[[OWSIdentityManager sharedManager] recipientIdentityForRecipientId:recipientId];
OWSAssertDebug(recipientIdentity);
NSData *identityKey = recipientIdentity.identityKey;
OWSAssertDebug(identityKey.length > 0);
if (identityKey.length < 1) {
continue;
}
2018-02-02 20:07:13 +01:00
[OWSIdentityManager.sharedManager setVerificationState:OWSVerificationStateDefault
identityKey:identityKey
recipientId:recipientId
isUserInitiatedChange:YES];
}
}
2018-09-09 19:46:23 +02:00
- (void)showUnblockConversationUI:(nullable BlockActionCompletionBlock)completionBlock
{
2017-04-05 18:16:54 +02:00
self.userHasScrolled = NO;
2020-07-21 05:49:41 +02:00
[UIView setAnimationsEnabled:NO];
2017-04-05 18:16:54 +02:00
2018-09-09 19:46:23 +02:00
[BlockListUIUtils showUnblockThreadActionSheet:self.thread
fromViewController:self
blockingManager:self.blockingManager
contactsManager:self.contactsManager
completionBlock:completionBlock];
2020-07-21 05:49:41 +02:00
[UIView setAnimationsEnabled:YES];
}
2018-09-09 19:46:23 +02:00
- (BOOL)isBlockedConversation
{
2018-09-09 19:46:23 +02:00
return [self.blockingManager isThreadBlocked:self.thread];
}
- (int)blockedGroupMemberCount
{
OWSAssertDebug(self.isGroupConversation);
OWSAssertDebug([self.thread isKindOfClass:[TSGroupThread class]]);
TSGroupThread *groupThread = (TSGroupThread *)self.thread;
int blockedMemberCount = 0;
NSArray<NSString *> *blockedPhoneNumbers = [self.blockingManager blockedPhoneNumbers];
for (NSString *contactIdentifier in groupThread.groupModel.groupMemberIds) {
if ([blockedPhoneNumbers containsObject:contactIdentifier]) {
blockedMemberCount++;
}
}
return blockedMemberCount;
}
- (void)startReadTimer
{
[self.readTimer invalidate];
2017-05-31 20:27:46 +02:00
self.readTimer = [NSTimer weakScheduledTimerWithTimeInterval:3.f
target:self
selector:@selector(readTimerDidFire)
userInfo:nil
repeats:YES];
}
- (void)readTimerDidFire
{
[self markVisibleMessagesAsRead];
}
- (void)cancelReadTimer
{
[self.readTimer invalidate];
self.readTimer = nil;
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
2019-04-09 17:59:09 +02:00
// We don't present incoming message notifications for the presented
// conversation. But there's a narrow window *while* the conversationVC
// is being presented where a message notification for the not-quite-yet
// presented conversation can be shown. If that happens, dismiss it as soon
// as we enter the conversation.
[self.notificationPresenter cancelNotificationsWithThreadId:self.thread.uniqueId];
// recover status bar when returning from PhotoPicker, which is dark (uses light status bar)
[self setNeedsStatusBarAppearanceUpdate];
[ProfileFetcherJob runWithThread:self.thread];
[self markVisibleMessagesAsRead];
[self startReadTimer];
2018-06-11 22:20:37 +02:00
[self autoLoadMoreIfNecessary];
if (!self.viewHasEverAppeared) {
2019-01-08 01:57:22 +01:00
// To minimize time to initial apearance, we initially disable prefetching, but then
// re-enable it once the view has appeared.
if (@available(iOS 10, *)) {
self.collectionView.prefetchingEnabled = YES;
}
}
2018-10-31 15:05:24 +01:00
self.conversationViewModel.focusMessageIdOnOpen = nil;
2018-05-31 01:21:06 +02:00
self.isViewCompletelyAppeared = YES;
self.viewHasEverAppeared = YES;
self.shouldAnimateKeyboardChanges = YES;
// HACK: Because the inputToolbar is the inputAccessoryView, we make some special considertations WRT it's firstResponder status.
//
// When a view controller is presented, it is first responder. However if we resign first responder
// and the view re-appears, without being presented, the inputToolbar can become invisible.
// e.g. specifically works around the scenario:
// - Present this VC
// - Longpress on a message to show edit menu, which entails making the pressed view the first responder.
// - Begin presenting another view, e.g. swipe-left for details or swipe-right to go back, but quit part way, so that you remain on the conversation view
// - toolbar will be not be visible unless we reaquire first responder.
if (!self.isFirstResponder) {
// We don't have to worry about the input toolbar being visible if the inputToolbar.textView is first responder
// In fact doing so would unnecessarily dismiss the keyboard which is probably not desirable and at least
// a distracting animation.
BOOL shouldBecomeFirstResponder = NO;
if (self.isShowingSearchUI) {
shouldBecomeFirstResponder = !self.searchController.uiSearchController.searchBar.isFirstResponder;
} else {
shouldBecomeFirstResponder = !self.inputToolbar.isInputTextViewFirstResponder;
}
if (shouldBecomeFirstResponder) {
OWSLogDebug(@"reclaiming first responder to ensure toolbar is shown.");
[self becomeFirstResponder];
}
}
Faster conversation presentation. There are multiple places in the codebase we present a conversation. We used to have some very conservative machinery around how this was done, for fear of failing to present the call view controller, which would have left a hidden call in the background. We've since addressed that concern more thoroughly via the separate calling UIWindow. As such, the remaining presentation machinery is overly complex and inflexible for what we need. Sometimes we want to animate-push the conversation. (tap on home, tap on "send message" in contact card/group members) Sometimes we want to dismiss a modal, to reveal the conversation behind it (contact picker, group creation) Sometimes we want to present the conversation with no animation (becoming active from a notification) We also want to ensure that we're never pushing more than one conversation view controller, which was previously a problem since we were "pushing" a newly constructed VC in response to these myriad actions. It turned out there were certain code paths that caused multiple actions to be fired in rapid succession which pushed multiple ConversationVC's. The built-in method: `setViewControllers:animated` easily ensures we only have one ConversationVC on the stack, while being composable enough to faciliate the various more efficient animations we desire. The only thing lost with the complex methods is that the naive `presentViewController:` can fail, e.g. if another view is already presented. E.g. if an alert appears *just* before the user taps compose, the contact picker will fail to present. Since we no longer depend on this for presenting the CallViewController, this isn't catostrophic, and in fact, arguable preferable, since we want the user to read and dismiss any alert explicitly. // FREEBIE
2018-08-18 22:54:35 +02:00
switch (self.actionOnOpen) {
case ConversationViewActionNone:
break;
case ConversationViewActionCompose:
[self popKeyBoard];
break;
case ConversationViewActionAudioCall:
[self startAudioCall];
break;
case ConversationViewActionVideoCall:
[self startVideoCall];
break;
}
// Clear the "on open" state after the view has been presented.
self.actionOnOpen = ConversationViewActionNone;
2019-01-11 15:24:24 +01:00
2020-05-05 01:11:43 +02:00
[self updateInputBarLayout];
[self ensureScrollDownButton];
}
// `viewWillDisappear` is called whenever the view *starts* to disappear,
// but, as is the case with the "pan left for message details view" gesture,
// this can be canceled. As such, we shouldn't tear down anything expensive
// until `viewDidDisappear`.
- (void)viewWillDisappear:(BOOL)animated
{
OWSLogDebug(@"");
2017-06-19 23:10:34 +02:00
[super viewWillDisappear:animated];
self.isViewCompletelyAppeared = NO;
2019-03-18 16:24:09 +01:00
[self dismissMenuActions];
}
- (void)viewDidDisappear:(BOOL)animated
{
OWSLogDebug(@"");
[super viewDidDisappear:animated];
self.userHasScrolled = NO;
self.isViewVisible = NO;
self.shouldAnimateKeyboardChanges = NO;
[self.audioAttachmentPlayer stop];
self.audioAttachmentPlayer = nil;
[self cancelReadTimer];
[self saveDraft];
2017-05-31 20:27:46 +02:00
[self markVisibleMessagesAsRead];
2017-05-05 04:10:37 +02:00
[self cancelVoiceMemo];
2017-10-18 21:11:19 +02:00
[self.cellMediaCache removeAllObjects];
self.isUserScrolling = NO;
2014-10-29 21:58:58 +01:00
}
2018-08-30 21:18:03 +02:00
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
// We resize the inputToolbar whenever it's text is modified, including when setting saved draft-text.
// However it's possible this draft-text is set before the inputToolbar (an inputAccessoryView) is mounted
// in the view hierarchy. Since it's not in the view hierarchy, it hasn't been laid out and has no width,
// which is used to determine height.
// So here we unsure the proper height once we know everything's been layed out.
[self.inputToolbar ensureTextViewHeight];
}
#pragma mark - Initiliazers
- (void)createHeaderViews
{
LKConversationTitleView *headerView = [[LKConversationTitleView alloc] initWithThread:self.thread];
2018-04-26 16:12:21 +02:00
self.headerView = headerView;
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, headerView);
2018-04-26 16:12:21 +02:00
self.navigationItem.titleView = headerView;
2018-04-26 16:12:21 +02:00
if (@available(iOS 11, *)) {
// Do nothing, we use autolayout/intrinsic content size to grow
} else {
// Request "full width" title; the navigation bar will truncate this
// to fit between the left and right buttons.
CGSize screenSize = [UIScreen mainScreen].bounds.size;
CGRect headerFrame = CGRectMake(0, 0, screenSize.width, 44);
headerView.frame = headerFrame;
}
}
- (CGFloat)unreadCountViewDiameter
{
return 16;
}
- (void)createBackButton
Disappearing Messages * Per thread settings menu accessed by tapping on thread title This removed the toggle-phone behavior. You'll be able to see the phone number in the settings table view. This removed the "add contact" functionality, although it was already broken for ios>=9 (which is basically everybody). The group actions menu was absorbed into this screen * Added a confirm alert to leave group (fixes #938) * New Translation Strings * Extend "Add People" label to fit translations. * resolved issues with translations not fitting in group menu * Fix the long standing type warning where TSCalls were assigned to a TSMessageAdapter. * Can delete info messages Follow the JSQMVC pattern and put UIResponder-able content in the messageBubbleContainer. This gives us more functionality *and* allows us to delete some code. yay! It's still not yet possible to delete phone messages. =( * Fixed some compiler warnings. * xcode8 touching storyboard. So long xcode7! * Fixup multiline info messages. We were seeing info messages like "You set disappearing message timer to 10" instead of "You set disappearing message timer to 10 seconds." Admittedly this isn't a very good fix, as now one liners feel like they have too much padding. If the message is well over one line, we were wrapping properly, but there's a problem when the message is *just barely* two lines, the cell height grows, but the label still thinks it's just one line (as evinced by the one line appearing in the center of the label frame. The result being that the last word of the label is cropped. * Disable group actions after leaving group. // FREEBIE
2016-09-21 14:37:51 +02:00
{
2017-02-17 23:30:49 +01:00
UIBarButtonItem *backItem = [self createOWSBackButton];
self.customBackButton = backItem;
if (backItem.customView) {
// 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];
// 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.
[_backButtonUnreadCountView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:-6];
[_backButtonUnreadCountView autoPinLeadingToSuperviewMarginWithInset:1];
[_backButtonUnreadCountView autoSetDimension:ALDimensionHeight toSize:self.unreadCountViewDiameter];
// We set a min width, but we will also pin to our subview label, so we can grow to accommodate multiple digits.
[_backButtonUnreadCountView autoSetDimension:ALDimensionWidth
toSize:self.unreadCountViewDiameter
relation:NSLayoutRelationGreaterThanOrEqual];
[_backButtonUnreadCountView addSubview:_backButtonUnreadCountLabel];
[_backButtonUnreadCountLabel autoPinWidthToSuperviewWithMargin:4];
[_backButtonUnreadCountLabel autoPinHeightToSuperview];
}
self.navigationItem.leftBarButtonItem = backItem;
}
- (void)windowManagerCallDidChange:(NSNotification *)notification
{
[self updateBarButtonItems];
}
- (void)updateBarButtonItems
{
return; // Loki: Re-enable later?
self.navigationItem.hidesBackButton = NO;
if (self.customBackButton) {
self.navigationItem.leftBarButtonItem = self.customBackButton;
}
Disappearing Messages * Per thread settings menu accessed by tapping on thread title This removed the toggle-phone behavior. You'll be able to see the phone number in the settings table view. This removed the "add contact" functionality, although it was already broken for ios>=9 (which is basically everybody). The group actions menu was absorbed into this screen * Added a confirm alert to leave group (fixes #938) * New Translation Strings * Extend "Add People" label to fit translations. * resolved issues with translations not fitting in group menu * Fix the long standing type warning where TSCalls were assigned to a TSMessageAdapter. * Can delete info messages Follow the JSQMVC pattern and put UIResponder-able content in the messageBubbleContainer. This gives us more functionality *and* allows us to delete some code. yay! It's still not yet possible to delete phone messages. =( * Fixed some compiler warnings. * xcode8 touching storyboard. So long xcode7! * Fixup multiline info messages. We were seeing info messages like "You set disappearing message timer to 10" instead of "You set disappearing message timer to 10 seconds." Admittedly this isn't a very good fix, as now one liners feel like they have too much padding. If the message is well over one line, we were wrapping properly, but there's a problem when the message is *just barely* two lines, the cell height grows, but the label still thinks it's just one line (as evinced by the one line appearing in the center of the label frame. The result being that the last word of the label is cropped. * Disable group actions after leaving group. // FREEBIE
2016-09-21 14:37:51 +02:00
if (self.userLeftGroup) {
self.navigationItem.rightBarButtonItems = @[];
return;
}
if (self.isShowingSearchUI) {
self.navigationItem.rightBarButtonItems = @[];
self.navigationItem.leftBarButtonItem = nil;
self.navigationItem.hidesBackButton = YES;
return;
}
const CGFloat kBarButtonSize = 44;
Disappearing Messages * Per thread settings menu accessed by tapping on thread title This removed the toggle-phone behavior. You'll be able to see the phone number in the settings table view. This removed the "add contact" functionality, although it was already broken for ios>=9 (which is basically everybody). The group actions menu was absorbed into this screen * Added a confirm alert to leave group (fixes #938) * New Translation Strings * Extend "Add People" label to fit translations. * resolved issues with translations not fitting in group menu * Fix the long standing type warning where TSCalls were assigned to a TSMessageAdapter. * Can delete info messages Follow the JSQMVC pattern and put UIResponder-able content in the messageBubbleContainer. This gives us more functionality *and* allows us to delete some code. yay! It's still not yet possible to delete phone messages. =( * Fixed some compiler warnings. * xcode8 touching storyboard. So long xcode7! * Fixup multiline info messages. We were seeing info messages like "You set disappearing message timer to 10" instead of "You set disappearing message timer to 10 seconds." Admittedly this isn't a very good fix, as now one liners feel like they have too much padding. If the message is well over one line, we were wrapping properly, but there's a problem when the message is *just barely* two lines, the cell height grows, but the label still thinks it's just one line (as evinced by the one line appearing in the center of the label frame. The result being that the last word of the label is cropped. * Disable group actions after leaving group. // FREEBIE
2016-09-21 14:37:51 +02:00
NSMutableArray<UIBarButtonItem *> *barButtons = [NSMutableArray new];
if ([self canCall]) {
// 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];
UIImage *image = [[UIImage imageNamed:@"button_phone_white"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
[callButton setImage:image forState:UIControlStateNormal];
if (OWSWindowManager.sharedManager.hasCall) {
callButton.enabled = NO;
callButton.userInteractionEnabled = NO;
2018-07-13 15:50:49 +02:00
callButton.tintColor = [Theme.navbarIconColor colorWithAlphaComponent:0.7];
} else {
callButton.enabled = YES;
callButton.userInteractionEnabled = YES;
2018-07-13 15:50:49 +02:00
callButton.tintColor = Theme.navbarIconColor;
}
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.
2019-01-09 15:42:41 +01:00
BOOL hasCompactHeader = self.traitCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact;
if (!hasCompactHeader) {
imageEdgeInsets.left = round((kBarButtonSize - image.size.width) * 0.5f);
imageEdgeInsets.right = round((kBarButtonSize - (image.size.width + imageEdgeInsets.left)) * 0.5f);
imageEdgeInsets.top = round((kBarButtonSize - image.size.height) * 0.5f);
imageEdgeInsets.bottom = round(kBarButtonSize - (image.size.height + imageEdgeInsets.top));
}
callButton.imageEdgeInsets = imageEdgeInsets;
callButton.accessibilityLabel = NSLocalizedString(@"CALL_LABEL", "Accessibility label for placing call button");
2018-05-03 20:31:11 +02:00
[callButton addTarget:self action:@selector(startAudioCall) forControlEvents:UIControlEventTouchUpInside];
callButton.frame = CGRectMake(0,
0,
round(image.size.width + imageEdgeInsets.left + imageEdgeInsets.right),
round(image.size.height + imageEdgeInsets.top + imageEdgeInsets.bottom));
2019-06-14 07:25:39 +02:00
// Loki: Original code
// ========
// [barButtons
// addObject:[[UIBarButtonItem alloc] initWithCustomView:callButton
// accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"call")]];
// ========
}
if (self.disappearingMessagesConfiguration.isEnabled) {
DisappearingTimerConfigurationView *timerView = [[DisappearingTimerConfigurationView alloc]
initWithDurationSeconds:self.disappearingMessagesConfiguration.durationSeconds];
timerView.delegate = self;
2018-07-13 15:50:49 +02:00
timerView.tintColor = Theme.navbarIconColor;
// As of iOS11, we can size barButton item custom views with autoLayout.
// Before that, though we can still use autoLayout *within* the customView,
// setting the view's size with constraints causes the customView to be temporarily
// laid out with a misplaced origin.
if (@available(iOS 11.0, *)) {
[timerView autoSetDimensionsToSize:CGSizeMake(36, 44)];
} else {
timerView.frame = CGRectMake(0, 0, 36, 44);
}
[barButtons
addObject:[[UIBarButtonItem alloc] initWithCustomView:timerView
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"timer")]];
}
Disappearing Messages * Per thread settings menu accessed by tapping on thread title This removed the toggle-phone behavior. You'll be able to see the phone number in the settings table view. This removed the "add contact" functionality, although it was already broken for ios>=9 (which is basically everybody). The group actions menu was absorbed into this screen * Added a confirm alert to leave group (fixes #938) * New Translation Strings * Extend "Add People" label to fit translations. * resolved issues with translations not fitting in group menu * Fix the long standing type warning where TSCalls were assigned to a TSMessageAdapter. * Can delete info messages Follow the JSQMVC pattern and put UIResponder-able content in the messageBubbleContainer. This gives us more functionality *and* allows us to delete some code. yay! It's still not yet possible to delete phone messages. =( * Fixed some compiler warnings. * xcode8 touching storyboard. So long xcode7! * Fixup multiline info messages. We were seeing info messages like "You set disappearing message timer to 10" instead of "You set disappearing message timer to 10 seconds." Admittedly this isn't a very good fix, as now one liners feel like they have too much padding. If the message is well over one line, we were wrapping properly, but there's a problem when the message is *just barely* two lines, the cell height grows, but the label still thinks it's just one line (as evinced by the one line appearing in the center of the label frame. The result being that the last word of the label is cropped. * Disable group actions after leaving group. // FREEBIE
2016-09-21 14:37:51 +02:00
self.navigationItem.rightBarButtonItems = [barButtons copy];
}
#pragma mark - Identity
/**
* 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
*/
- (BOOL)showSafetyNumberConfirmationIfNecessaryWithConfirmationText:(NSString *)confirmationText
completion:(void (^)(BOOL didConfirmIdentity))completionHandler
{
return [SafetyNumberConfirmationAlert presentAlertIfNecessaryWithRecipientIds:self.thread.recipientIdentifiers
confirmationText:confirmationText
contactsManager:self.contactsManager
completion:^(BOOL didShowAlert) {
// Pre iOS-11, the keyboard and inputAccessoryView will obscure the alert if the keyboard is up when the
// alert is presented, so after hiding it, we regain first responder here.
if (@available(iOS 11.0, *)) {
// do nothing
} else {
[self becomeFirstResponder];
}
completionHandler(didShowAlert);
}
beforePresentationHandler:^(void) {
if (@available(iOS 11.0, *)) {
// do nothing
} else {
// Pre iOS-11, the keyboard and inputAccessoryView will obscure the alert if the keyboard is up when the
// alert is presented.
[self dismissKeyBoard];
[self resignFirstResponder];
}
}];
}
2014-10-29 21:58:58 +01:00
- (void)showFingerprintWithRecipientId:(NSString *)recipientId
{
// Ensure keyboard isn't hiding the "safety numbers changed" interaction when we
// return from FingerprintViewController.
[self dismissKeyBoard];
[FingerprintViewController presentFromViewController:self recipientId:recipientId];
}
#pragma mark - Calls
2018-05-03 20:31:11 +02:00
- (void)startAudioCall
2018-05-02 16:08:47 +02:00
{
[self callWithVideo:NO];
}
2018-05-03 20:31:11 +02:00
- (void)startVideoCall
2018-05-02 16:08:47 +02:00
{
[self callWithVideo:YES];
}
- (void)callWithVideo:(BOOL)isVideo
{
OWSAssertDebug([self.thread isKindOfClass:[TSContactThread class]]);
if (![self canCall]) {
OWSLogWarn(@"Tried to initiate a call but thread is not callable.");
return;
}
2017-09-06 20:13:18 +02:00
__weak ConversationViewController *weakSelf = self;
2018-09-09 19:46:23 +02:00
if ([self isBlockedConversation]) {
[self showUnblockConversationUI:^(BOOL isBlocked) {
if (!isBlocked) {
2018-05-02 16:08:47 +02:00
[weakSelf callWithVideo:isVideo];
}
}];
return;
}
BOOL didShowSNAlert =
[self showSafetyNumberConfirmationIfNecessaryWithConfirmationText:[CallStrings confirmAndCallButtonTitle]
completion:^(BOOL didConfirmIdentity) {
if (didConfirmIdentity) {
2018-05-02 16:08:47 +02:00
[weakSelf callWithVideo:isVideo];
}
}];
if (didShowSNAlert) {
return;
}
2020-02-06 00:34:13 +01:00
// [self.outboundCallInitiator initiateCallWithRecipientId:self.thread.contactIdentifier isVideo:isVideo];
}
- (BOOL)canCall
{
return !(self.isGroupConversation ||
2018-12-21 19:03:09 +01:00
[((TSContactThread *)self.thread).contactIdentifier isEqualToString:self.tsAccountManager.localNumber]);
}
2017-10-10 22:13:54 +02:00
#pragma mark - Dynamic Text
2017-10-10 22:13:54 +02:00
/**
Called whenever the user manually changes the dynamic type options inside Settings.
2017-10-10 22:13:54 +02:00
@param notification NSNotification with the dynamic type change information.
*/
- (void)didChangePreferredContentSize:(NSNotification *)notification
{
OWSLogInfo(@"didChangePreferredContentSize");
2019-01-08 22:38:18 +01:00
[self resetForSizeOrOrientationChange];
[self.inputToolbar updateFontSizes];
2017-10-10 22:13:54 +02:00
}
2017-10-10 22:13:54 +02:00
#pragma mark - Actions
2017-10-10 22:13:54 +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];
}
2017-10-10 22:13:54 +02:00
}
2017-10-10 22:13:54 +02:00
- (void)showConversationSettings
{
[self showConversationSettingsAndShowVerification:NO];
2014-10-29 21:58:58 +01:00
}
2017-10-10 22:13:54 +02:00
- (void)showConversationSettingsAndShowVerification:(BOOL)showVerification
{
2017-10-10 22:13:54 +02:00
OWSConversationSettingsViewController *settingsVC = [OWSConversationSettingsViewController new];
settingsVC.conversationSettingsViewDelegate = self;
[settingsVC configureWithThread:self.thread uiDatabaseConnection:self.uiDatabaseConnection];
2017-10-10 22:13:54 +02:00
settingsVC.showVerificationOnAppear = showVerification;
[self.navigationController pushViewController:settingsVC animated:YES];
}
#pragma mark - DisappearingTimerConfigurationViewDelegate
- (void)disappearingTimerConfigurationViewWasTapped:(DisappearingTimerConfigurationView *)disappearingTimerView
2017-10-10 22:13:54 +02:00
{
OWSLogDebug(@"Tapped timer in navbar");
2017-10-10 22:13:54 +02:00
[self showConversationSettings];
}
#pragma mark - Load More
- (void)autoLoadMoreIfNecessary
{
2018-05-31 22:36:16 +02:00
BOOL isMainAppAndActive = CurrentAppContext().isMainAppAndActive;
if (self.isUserScrolling || !self.isViewVisible || !isMainAppAndActive) {
return;
}
if (!self.showLoadMoreHeader) {
return;
}
CGSize screenSize = UIScreen.mainScreen.bounds.size;
CGFloat loadMoreThreshold = MAX(screenSize.width, screenSize.height);
if (self.collectionView.contentOffset.y < loadMoreThreshold) {
2018-10-31 15:05:24 +01:00
[self.conversationViewModel loadAnotherPageOfMessages];
}
}
- (void)updateShowLoadMoreHeader
{
2018-10-31 15:05:24 +01:00
OWSAssertDebug(self.conversationViewModel);
self.showLoadMoreHeader = self.conversationViewModel.canLoadMoreItems;
}
- (void)setShowLoadMoreHeader:(BOOL)showLoadMoreHeader
{
BOOL valueChanged = _showLoadMoreHeader != showLoadMoreHeader;
_showLoadMoreHeader = showLoadMoreHeader;
self.loadMoreHeader.hidden = !showLoadMoreHeader;
self.loadMoreHeader.userInteractionEnabled = showLoadMoreHeader;
if (valueChanged) {
2018-07-05 18:36:50 +02:00
[self resetContentAndLayout];
}
}
- (void)updateDisappearingMessagesConfiguration
{
2018-06-22 19:48:23 +02:00
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
self.disappearingMessagesConfiguration =
[OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:self.thread.uniqueId transaction:transaction];
}];
}
- (void)setDisappearingMessagesConfiguration:
(nullable OWSDisappearingMessagesConfiguration *)disappearingMessagesConfiguration
{
if (_disappearingMessagesConfiguration.isEnabled == disappearingMessagesConfiguration.isEnabled
&& _disappearingMessagesConfiguration.durationSeconds == disappearingMessagesConfiguration.durationSeconds) {
return;
}
_disappearingMessagesConfiguration = disappearingMessagesConfiguration;
[self updateBarButtonItems];
}
2014-12-11 00:05:41 +01:00
#pragma mark Bubble User Actions
- (void)handleFailedDownloadTapForMessage:(TSMessage *)message
{
2018-11-07 23:49:25 +01:00
OWSAssert(message);
2018-11-07 23:49:25 +01:00
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
[self.attachmentDownloads downloadAttachmentsForMessage:message
transaction:transaction
success:^(NSArray<TSAttachmentStream *> *attachmentStreams) {
OWSLogInfo(@"Successfully redownloaded attachment in thread: %@", message.thread);
}
failure:^(NSError *error) {
OWSLogWarn(@"Failed to redownload message with error: %@", error);
}];
}];
}
- (void)handleUnsentMessageTap:(TSOutgoingMessage *)message
{
UIAlertController *actionSheet = [UIAlertController alertControllerWithTitle:message.mostRecentFailureText
message:nil
preferredStyle:UIAlertControllerStyleActionSheet];
2016-11-26 00:12:00 +01:00
[actionSheet addAction:[OWSAlerts cancelAction]];
2016-11-26 00:12:00 +01:00
UIAlertAction *deleteMessageAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_DELETE_TITLE", @"")
style:UIAlertActionStyleDestructive
2018-06-22 19:48:23 +02:00
handler:^(UIAlertAction *action) {
2016-11-26 00:12:00 +01:00
[message remove];
}];
[actionSheet addAction:deleteMessageAction];
2016-11-26 00:12:00 +01:00
UIAlertAction *resendMessageAction = [UIAlertAction
actionWithTitle:NSLocalizedString(@"SEND_AGAIN_BUTTON", @"")
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"send_again")
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
[self.messageSenderJobQueue addMessage:message transaction:transaction];
}];
}];
2016-11-26 00:12:00 +01:00
[actionSheet addAction:resendMessageAction];
2016-11-26 00:12:00 +01:00
[self dismissKeyBoard];
[self presentAlert:actionSheet];
2014-12-11 00:05:41 +01:00
}
- (void)tappedNonBlockingIdentityChangeForRecipientId:(nullable NSString *)signalIdParam
{
if (signalIdParam == nil) {
if (self.thread.isGroupThread) {
// Before 2.13 we didn't track the recipient id in the identity change error.
OWSLogWarn(@"Ignoring tap on legacy nonblocking identity change since it has no signal id");
return;
} else {
OWSLogInfo(@"Assuming tap on legacy nonblocking identity change corresponds to current contact thread: %@",
self.thread.contactIdentifier);
signalIdParam = self.thread.contactIdentifier;
}
}
NSString *signalId = signalIdParam;
[self showFingerprintWithRecipientId:signalId];
}
- (void)tappedCorruptedMessage:(TSErrorMessage *)message
{
2016-11-26 00:12:00 +01:00
NSString *alertMessage = [NSString
stringWithFormat:NSLocalizedString(@"CORRUPTED_SESSION_DESCRIPTION", @"ActionSheet title"), self.thread.name];
UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil
message:alertMessage
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[OWSAlerts cancelAction]];
UIAlertAction *resetSessionAction = [UIAlertAction
actionWithTitle:NSLocalizedString(@"FINGERPRINT_SHRED_KEYMATERIAL_BUTTON", @"")
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"reset_session")
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
if (![self.thread isKindOfClass:[TSContactThread class]]) {
// Corrupt Message errors only appear in contact threads.
OWSLogError(@"Unexpected request to reset session in group thread. Refusing");
return;
}
TSContactThread *contactThread = (TSContactThread *)self.thread;
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
[self.sessionResetJobQueue addContactThread:contactThread transaction:transaction];
}];
}];
[alert addAction:resetSessionAction];
[self dismissKeyBoard];
[self presentAlert:alert];
}
- (void)tappedInvalidIdentityKeyErrorMessage:(TSInvalidIdentityKeyErrorMessage *)errorMessage
{
NSString *keyOwner = [self.contactsManager displayNameForPhoneIdentifier:errorMessage.theirSignalId];
NSString *titleFormat = NSLocalizedString(@"SAFETY_NUMBERS_ACTIONSHEET_TITLE", @"Action sheet heading");
NSString *titleText = [NSString stringWithFormat:titleFormat, keyOwner];
2016-11-26 00:12:00 +01:00
UIAlertController *actionSheet = [UIAlertController alertControllerWithTitle:titleText
message:nil
preferredStyle:UIAlertControllerStyleActionSheet];
[actionSheet addAction:[OWSAlerts cancelAction]];
2016-11-26 00:12:00 +01:00
UIAlertAction *showSafteyNumberAction =
[UIAlertAction actionWithTitle:NSLocalizedString(@"SHOW_SAFETY_NUMBER_ACTION", @"Action sheet item")
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"show_safety_number")
style:UIAlertActionStyleDefault
2018-06-22 19:48:23 +02:00
handler:^(UIAlertAction *action) {
OWSLogInfo(@"Remote Key Changed actions: Show fingerprint display");
[self showFingerprintWithRecipientId:errorMessage.theirSignalId];
}];
[actionSheet addAction:showSafteyNumberAction];
2017-09-06 20:13:18 +02:00
UIAlertAction *acceptSafetyNumberAction =
[UIAlertAction actionWithTitle:NSLocalizedString(@"ACCEPT_NEW_IDENTITY_ACTION", @"Action sheet item")
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"accept_safety_number")
2017-09-06 20:13:18 +02:00
style:UIAlertActionStyleDefault
2018-06-22 19:48:23 +02:00
handler:^(UIAlertAction *action) {
OWSLogInfo(@"Remote Key Changed actions: Accepted new identity key");
2017-09-06 20:13:18 +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
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
2017-09-06 20:13:18 +02:00
if ([errorMessage isKindOfClass:[TSInvalidIdentityKeyReceivingErrorMessage class]]) {
// Deliberately crash if the user fails to explicitly accept the new identity
2018-10-30 16:41:43 +01:00
// key. In practice we haven't been creating these messages in over a year.
[errorMessage throws_acceptNewIdentityKey];
#pragma clang diagnostic pop
2017-09-06 20:13:18 +02:00
}
}];
[actionSheet addAction:acceptSafetyNumberAction];
[self dismissKeyBoard];
[self presentAlert:actionSheet];
2014-10-29 21:58:58 +01:00
}
- (void)handleCallTap:(TSCall *)call
{
OWSAssertDebug(call);
if (![self.thread isKindOfClass:[TSContactThread class]]) {
OWSFailDebug(@"unexpected thread: %@", self.thread);
return;
}
TSContactThread *contactThread = (TSContactThread *)self.thread;
NSString *displayName = [self.contactsManager displayNameForPhoneIdentifier:contactThread.contactIdentifier];
UIAlertController *alert = [UIAlertController
alertControllerWithTitle:[CallStrings callBackAlertTitle]
message:[NSString stringWithFormat:[CallStrings callBackAlertMessageFormat], displayName]
preferredStyle:UIAlertControllerStyleAlert];
2017-09-06 20:13:18 +02:00
__weak ConversationViewController *weakSelf = self;
UIAlertAction *callAction = [UIAlertAction actionWithTitle:[CallStrings callBackAlertCallButton]
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"call_back")
style:UIAlertActionStyleDefault
2018-06-22 19:48:23 +02:00
handler:^(UIAlertAction *action) {
2018-05-03 20:31:11 +02:00
[weakSelf startAudioCall];
}];
[alert addAction:callAction];
[alert addAction:[OWSAlerts cancelAction]];
[self dismissKeyBoard];
[self presentAlert:alert];
}
#pragma mark - MessageActionsDelegate
2018-09-28 00:49:01 +02:00
- (void)messageActionsShowDetailsForItem:(id<ConversationViewItem>)conversationViewItem
{
[self showDetailViewForViewItem:conversationViewItem];
}
- (void)report:(id<ConversationViewItem>)conversationViewItem
{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Report?" message:@"If the message is found to violate the Session Public Chat code of conduct it will be removed." preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
uint64_t messageID = 0;
if ([conversationViewItem.interaction isKindOfClass:TSMessage.class]) {
2020-04-20 08:53:40 +02:00
messageID = ((TSMessage *)conversationViewItem.interaction).openGroupServerMessageID;
}
[LKPublicChatAPI reportMessageWithID:messageID inChannel:1 onServer:@"https://chat.getsession.org"];
}]];
[alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleDefault handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
}
2018-09-28 00:49:01 +02:00
- (void)messageActionsReplyToItem:(id<ConversationViewItem>)conversationViewItem
{
[self populateReplyForViewItem:conversationViewItem];
}
- (void)copyPublicKeyFor:(id<ConversationViewItem>)conversationViewItem
{
UIPasteboard.generalPasteboard.string = ((TSIncomingMessage *)conversationViewItem.interaction).authorId;
}
#pragma mark - MessageDetailViewDelegate
- (void)detailViewMessageWasDeleted:(MessageDetailViewController *)messageDetailViewController
{
OWSLogInfo(@"");
[self.navigationController popToViewController:self animated:YES];
}
#pragma mark - LongTextViewDelegate
- (void)longTextViewMessageWasDeleted:(LongTextViewController *)longTextViewController
{
OWSLogInfo(@"");
[self.navigationController popToViewController:self animated:YES];
}
#pragma mark - MenuActionsViewControllerDelegate
2019-03-18 16:24:09 +01:00
- (void)menuActionsWillPresent:(MenuActionsViewController *)menuActionsViewController
{
OWSLogVerbose(@"");
// While the menu actions are presented, temporarily use extra content
// inset padding so that interactions near the top or bottom of the
// collection view can be scrolled anywhere within the viewport.
//
// e.g. In a new conversation, there might be only a single message
// which we might want to scroll to the bottom of the screen to
// pin above the menu actions popup.
CGSize mainScreenSize = UIScreen.mainScreen.bounds.size;
2019-03-19 16:13:06 +01:00
self.extraContentInsetPadding = MAX(mainScreenSize.width, mainScreenSize.height);
2019-03-18 16:24:09 +01:00
UIEdgeInsets contentInset = self.collectionView.contentInset;
2019-03-19 16:13:06 +01:00
contentInset.top += self.extraContentInsetPadding;
contentInset.bottom += self.extraContentInsetPadding;
2019-03-18 16:24:09 +01:00
self.collectionView.contentInset = contentInset;
self.menuActionsViewController = menuActionsViewController;
}
- (void)menuActionsIsPresenting:(MenuActionsViewController *)menuActionsViewController
{
OWSLogVerbose(@"");
// Changes made in this "is presenting" callback are animated by the caller.
2019-03-19 16:13:06 +01:00
[self scrollToMenuActionInteraction:NO];
2019-03-18 16:24:09 +01:00
}
- (void)menuActionsDidPresent:(MenuActionsViewController *)menuActionsViewController
{
OWSLogVerbose(@"");
2019-03-19 16:13:06 +01:00
[self scrollToMenuActionInteraction:NO];
2019-03-18 16:24:09 +01:00
}
- (void)menuActionsIsDismissing:(MenuActionsViewController *)menuActionsViewController
{
2019-03-18 16:24:09 +01:00
OWSLogVerbose(@"");
// Changes made in this "is dismissing" callback are animated by the caller.
[self clearMenuActionsState];
}
- (void)menuActionsDidDismiss:(MenuActionsViewController *)menuActionsViewController
{
OWSLogVerbose(@"");
[self dismissMenuActions];
}
- (void)dismissMenuActions
{
OWSLogVerbose(@"");
[self clearMenuActionsState];
[[OWSWindowManager sharedManager] hideMenuActionsWindow];
}
2019-03-18 16:24:09 +01:00
- (void)clearMenuActionsState
{
2019-03-18 16:24:09 +01:00
OWSLogVerbose(@"");
2019-03-18 16:24:09 +01:00
if (self.menuActionsViewController == nil) {
return;
}
2019-03-18 16:24:09 +01:00
UIEdgeInsets contentInset = self.collectionView.contentInset;
2019-03-19 16:13:06 +01:00
contentInset.top -= self.extraContentInsetPadding;
contentInset.bottom -= self.extraContentInsetPadding;
2019-03-18 16:24:09 +01:00
self.collectionView.contentInset = contentInset;
2019-03-18 16:24:09 +01:00
self.menuActionsViewController = nil;
2019-03-19 16:13:06 +01:00
self.extraContentInsetPadding = 0;
2019-03-18 16:24:09 +01:00
}
2019-03-19 16:13:06 +01:00
- (void)scrollToMenuActionInteractionIfNecessary
2019-03-18 16:24:09 +01:00
{
if (self.menuActionsViewController != nil) {
2019-03-19 16:13:06 +01:00
[self scrollToMenuActionInteraction:NO];
2019-03-18 16:24:09 +01:00
}
}
2019-03-19 16:13:06 +01:00
- (void)scrollToMenuActionInteraction:(BOOL)animated
{
2019-03-19 16:13:06 +01:00
OWSAssertDebug(self.menuActionsViewController);
NSValue *_Nullable contentOffset = [self contentOffsetForMenuActionInteraction];
2019-03-18 16:24:09 +01:00
if (contentOffset == nil) {
2019-10-21 02:43:46 +02:00
// OWSFailDebug(@"Missing contentOffset.");
2019-03-18 16:24:09 +01:00
return;
}
[self.collectionView setContentOffset:contentOffset.CGPointValue animated:animated];
}
2019-03-19 16:13:06 +01:00
- (nullable NSValue *)contentOffsetForMenuActionInteraction
2019-03-18 16:24:09 +01:00
{
2019-03-19 16:13:06 +01:00
OWSAssertDebug(self.menuActionsViewController);
NSString *_Nullable menuActionInteractionId = self.menuActionsViewController.focusedInteraction.uniqueId;
if (menuActionInteractionId == nil) {
OWSFailDebug(@"Missing menu action interaction.");
2019-03-18 16:24:09 +01:00
return nil;
}
CGPoint modalTopWindow = [self.menuActionsViewController.focusUI convertPoint:CGPointZero toView:nil];
CGPoint modalTopLocal = [self.view convertPoint:modalTopWindow fromView:nil];
CGPoint offset = modalTopLocal;
CGFloat focusTop = offset.y - self.menuActionsViewController.vSpacing;
2019-03-19 16:13:06 +01:00
NSNumber *_Nullable interactionIndex
= self.conversationViewModel.viewState.interactionIndexMap[menuActionInteractionId];
if (interactionIndex == nil) {
// This is expected if the menu action interaction is being deleted.
2019-03-18 16:24:09 +01:00
return nil;
}
2019-03-19 16:13:06 +01:00
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:interactionIndex.integerValue inSection:0];
2019-03-18 16:24:09 +01:00
UICollectionViewLayoutAttributes *_Nullable layoutAttributes =
[self.layout layoutAttributesForItemAtIndexPath:indexPath];
if (layoutAttributes == nil) {
2019-10-21 02:43:46 +02:00
// OWSFailDebug(@"Missing layoutAttributes.");
2019-03-18 16:24:09 +01:00
return nil;
}
CGRect cellFrame = layoutAttributes.frame;
return [NSValue valueWithCGPoint:CGPointMake(0, CGRectGetMaxY(cellFrame) - focusTop)];
}
2019-03-18 16:24:09 +01:00
- (void)dismissMenuActionsIfNecessary
{
if (self.shouldDismissMenuActions) {
[self dismissMenuActions];
}
}
2019-03-18 16:24:09 +01:00
- (BOOL)shouldDismissMenuActions
{
if (!OWSWindowManager.sharedManager.isPresentingMenuActions) {
return NO;
}
2019-03-19 16:13:06 +01:00
NSString *_Nullable menuActionInteractionId = self.menuActionsViewController.focusedInteraction.uniqueId;
if (menuActionInteractionId == nil) {
2019-03-18 16:24:09 +01:00
return NO;
}
2019-03-19 16:13:06 +01:00
// Check whether there is still a view item for this interaction.
return (self.conversationViewModel.viewState.interactionIndexMap[menuActionInteractionId] == nil);
}
2017-10-10 22:13:54 +02:00
#pragma mark - ConversationViewCellDelegate
- (void)conversationCell:(ConversationViewCell *)cell
2018-12-18 19:35:49 +01:00
shouldAllowReply:(BOOL)shouldAllowReply
didLongpressMediaViewItem:(id<ConversationViewItem>)viewItem
{
2018-09-28 00:49:01 +02:00
NSArray<MenuAction *> *messageActions =
[ConversationViewItemActions mediaActionsWithConversationViewItem:viewItem
2018-12-18 19:35:49 +01:00
shouldAllowReply:shouldAllowReply
delegate:self];
2018-07-12 06:11:36 +02:00
[self presentMessageActions:messageActions withFocusedCell:cell];
}
- (void)conversationCell:(ConversationViewCell *)cell
2018-12-18 19:35:49 +01:00
shouldAllowReply:(BOOL)shouldAllowReply
didLongpressTextViewItem:(id<ConversationViewItem>)viewItem
2018-07-12 06:48:48 +02:00
{
2018-09-28 00:49:01 +02:00
NSArray<MenuAction *> *messageActions =
[ConversationViewItemActions textActionsWithConversationViewItem:viewItem
2018-12-18 19:35:49 +01:00
shouldAllowReply:shouldAllowReply
delegate:self];
2018-07-12 06:48:48 +02:00
[self presentMessageActions:messageActions withFocusedCell:cell];
}
- (void)conversationCell:(ConversationViewCell *)cell
2018-12-18 19:35:49 +01:00
shouldAllowReply:(BOOL)shouldAllowReply
didLongpressQuoteViewItem:(id<ConversationViewItem>)viewItem
{
2018-09-28 00:49:01 +02:00
NSArray<MenuAction *> *messageActions =
[ConversationViewItemActions quotedMessageActionsWithConversationViewItem:viewItem
2018-12-18 19:35:49 +01:00
shouldAllowReply:shouldAllowReply
delegate:self];
2018-07-12 06:11:36 +02:00
[self presentMessageActions:messageActions withFocusedCell:cell];
}
2018-09-28 00:49:01 +02:00
- (void)conversationCell:(ConversationViewCell *)cell
didLongpressSystemMessageViewItem:(id<ConversationViewItem>)viewItem
2018-07-12 06:11:36 +02:00
{
2018-09-28 00:49:01 +02:00
NSArray<MenuAction *> *messageActions =
[ConversationViewItemActions infoMessageActionsWithConversationViewItem:viewItem delegate:self];
2018-07-12 06:11:36 +02:00
[self presentMessageActions:messageActions withFocusedCell:cell];
}
- (void)presentMessageActions:(NSArray<MenuAction *> *)messageActions withFocusedCell:(ConversationViewCell *)cell
2018-07-10 23:34:22 +02:00
{
MenuActionsViewController *menuActionsViewController =
2019-03-18 16:24:09 +01:00
[[MenuActionsViewController alloc] initWithFocusedInteraction:cell.viewItem.interaction
focusedView:cell
actions:messageActions];
2018-07-11 01:31:11 +02:00
menuActionsViewController.delegate = self;
[[OWSWindowManager sharedManager] showMenuActionsWindow:menuActionsViewController];
2018-07-10 23:34:22 +02:00
}
- (NSAttributedString *)attributedContactOrProfileNameForPhoneIdentifier:(NSString *)recipientId
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
OWSAssertDebug(recipientId.length > 0);
return [self.contactsManager attributedContactOrProfileNameForPhoneIdentifier:recipientId];
}
- (void)tappedUnknownContactBlockOfferMessage:(OWSContactOffersInteraction *)interaction
{
if (![self.thread isKindOfClass:[TSContactThread class]]) {
OWSFailDebug(@"unexpected thread: %@", self.thread);
return;
}
TSContactThread *contactThread = (TSContactThread *)self.thread;
2017-08-21 23:13:36 +02:00
NSString *displayName = [self.contactsManager displayNameForPhoneIdentifier:interaction.recipientId];
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 *actionSheet =
[UIAlertController alertControllerWithTitle:title message:nil preferredStyle:UIAlertControllerStyleActionSheet];
[actionSheet addAction:[OWSAlerts cancelAction]];
UIAlertAction *blockAction =
[UIAlertAction actionWithTitle:NSLocalizedString(@"BLOCK_OFFER_ACTIONSHEET_BLOCK_ACTION",
@"Action sheet that will block an unknown user.")
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"block_user")
style:UIAlertActionStyleDestructive
handler:^(UIAlertAction *action) {
OWSLogInfo(@"Blocking an unknown user.");
[self.blockingManager addBlockedPhoneNumber:interaction.recipientId];
// Delete the offers.
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
contactThread.hasDismissedOffers = YES;
[contactThread saveWithTransaction:transaction];
[interaction removeWithTransaction:transaction];
} error:nil];
}];
[actionSheet addAction:blockAction];
[self dismissKeyBoard];
[self presentAlert:actionSheet];
}
- (void)tappedAddToContactsOfferMessage:(OWSContactOffersInteraction *)interaction
{
if (!self.contactsManager.supportsContactEditing) {
OWSFailDebug(@"Contact editing not supported");
return;
}
if (![self.thread isKindOfClass:[TSContactThread class]]) {
OWSFailDebug(@"unexpected thread: %@", [self.thread class]);
return;
}
TSContactThread *contactThread = (TSContactThread *)self.thread;
[self.contactsViewHelper presentContactViewControllerForRecipientId:contactThread.contactIdentifier
fromViewController:self
editImmediately:YES];
// Delete the offers.
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
contactThread.hasDismissedOffers = YES;
[contactThread saveWithTransaction:transaction];
[interaction removeWithTransaction:transaction];
} error:nil];
}
- (void)tappedAddToProfileWhitelistOfferMessage:(OWSContactOffersInteraction *)interaction
{
// This is accessed via the contact offer. Group whitelisting happens via a different interaction.
if (![self.thread isKindOfClass:[TSContactThread class]]) {
OWSFailDebug(@"unexpected thread: %@", [self.thread class]);
return;
}
TSContactThread *contactThread = (TSContactThread *)self.thread;
[self presentAddThreadToProfileWhitelistWithSuccess:^() {
// Delete the offers.
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
contactThread.hasDismissedOffers = YES;
[contactThread saveWithTransaction:transaction];
[interaction removeWithTransaction:transaction];
} error:nil];
}];
}
2017-11-08 20:04:51 +01:00
- (void)presentAddThreadToProfileWhitelistWithSuccess:(void (^)(void))successHandler
{
[[OWSProfileManager sharedManager] presentAddThreadToProfileWhitelist:self.thread
fromViewController:self
success:successHandler];
}
#pragma mark - OWSMessageBubbleViewDelegate
2017-10-10 22:13:54 +02:00
2018-09-28 00:49:01 +02:00
- (void)didTapImageViewItem:(id<ConversationViewItem>)viewItem
2017-10-10 22:13:54 +02:00
attachmentStream:(TSAttachmentStream *)attachmentStream
imageView:(UIView *)imageView
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
OWSAssertDebug(viewItem);
OWSAssertDebug(attachmentStream);
OWSAssertDebug(imageView);
2017-10-10 22:13:54 +02:00
[self dismissKeyBoard];
// In case we were presenting edit menu, we need to become first responder before presenting another VC
// else UIKit won't restore first responder status to us when the presented VC is dismissed.
if (!self.isFirstResponder) {
[self becomeFirstResponder];
}
MediaGallery *mediaGallery =
[[MediaGallery alloc] initWithThread:self.thread
options:MediaGalleryOptionSliderEnabled | MediaGalleryOptionShowAllMediaButton];
2018-11-07 18:00:34 +01:00
[mediaGallery presentDetailViewFromViewController:self mediaAttachment:attachmentStream replacingView:imageView];
2017-10-10 22:13:54 +02:00
}
2018-09-28 00:49:01 +02:00
- (void)didTapVideoViewItem:(id<ConversationViewItem>)viewItem
attachmentStream:(TSAttachmentStream *)attachmentStream
imageView:(UIImageView *)imageView
2017-10-10 22:13:54 +02:00
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
OWSAssertDebug(viewItem);
OWSAssertDebug(attachmentStream);
2017-10-10 22:13:54 +02:00
[self dismissKeyBoard];
// In case we were presenting edit menu, we need to become first responder before presenting another VC
// else UIKit won't restore first responder status to us when the presented VC is dismissed.
if (!self.isFirstResponder) {
[self becomeFirstResponder];
}
MediaGallery *mediaGallery =
[[MediaGallery alloc] initWithThread:self.thread
options:MediaGalleryOptionSliderEnabled | MediaGalleryOptionShowAllMediaButton];
2018-11-07 18:00:34 +01:00
[mediaGallery presentDetailViewFromViewController:self mediaAttachment:attachmentStream replacingView:imageView];
2017-10-10 22:13:54 +02:00
}
2018-09-28 00:49:01 +02:00
- (void)didTapAudioViewItem:(id<ConversationViewItem>)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream
2017-10-10 22:13:54 +02:00
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
OWSAssertDebug(viewItem);
OWSAssertDebug(attachmentStream);
2017-10-10 22:13:54 +02:00
NSFileManager *fileManager = [NSFileManager defaultManager];
2018-09-04 16:25:42 +02:00
if (![fileManager fileExistsAtPath:attachmentStream.originalFilePath]) {
OWSFailDebug(@"Missing video file: %@", attachmentStream.originalMediaURL);
2017-10-10 22:13:54 +02:00
}
[self dismissKeyBoard];
if (self.audioAttachmentPlayer) {
// Is this player associated with this media adapter?
if (self.audioAttachmentPlayer.owner == viewItem) {
// Tap to pause & unpause.
[self.audioAttachmentPlayer togglePlayState];
return;
}
[self.audioAttachmentPlayer stop];
self.audioAttachmentPlayer = nil;
}
2018-10-23 01:17:05 +02:00
2018-09-04 16:25:42 +02:00
self.audioAttachmentPlayer =
2018-10-23 16:40:09 +02:00
[[OWSAudioPlayer alloc] initWithMediaUrl:attachmentStream.originalMediaURL audioBehavior:OWSAudioBehavior_AudioMessagePlayback delegate:viewItem];
2017-10-10 22:13:54 +02:00
// Associate the player with this media adapter.
self.audioAttachmentPlayer.owner = viewItem;
2018-10-23 16:40:09 +02:00
[self.audioAttachmentPlayer play];
2017-10-10 22:13:54 +02:00
}
2018-09-28 00:49:01 +02:00
- (void)didTapTruncatedTextMessage:(id<ConversationViewItem>)conversationItem
2017-10-10 22:13:54 +02:00
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
OWSAssertDebug(conversationItem);
OWSAssertDebug([conversationItem.interaction isKindOfClass:[TSMessage class]]);
2017-10-10 22:13:54 +02:00
LongTextViewController *viewController = [[LongTextViewController alloc] initWithViewItem:conversationItem];
viewController.delegate = self;
[self.navigationController pushViewController:viewController animated:YES];
2017-10-10 22:13:54 +02:00
}
2018-09-28 00:49:01 +02:00
- (void)didTapContactShareViewItem:(id<ConversationViewItem>)conversationItem
2018-05-01 19:39:48 +02:00
{
OWSAssertIsOnMainThread();
OWSAssertDebug(conversationItem);
OWSAssertDebug(conversationItem.contactShare);
OWSAssertDebug([conversationItem.interaction isKindOfClass:[TSMessage class]]);
2018-05-01 19:39:48 +02:00
ContactViewController *view = [[ContactViewController alloc] initWithContactShare:conversationItem.contactShare];
2018-05-01 19:39:48 +02:00
[self.navigationController pushViewController:view animated:YES];
}
2018-05-08 23:22:27 +02:00
- (void)didTapSendMessageToContactShare:(ContactShareViewModel *)contactShare
{
OWSAssertIsOnMainThread();
OWSAssertDebug(contactShare);
[self.contactShareViewHelper sendMessageWithContactShare:contactShare fromViewController:self];
}
2018-05-08 23:22:27 +02:00
- (void)didTapSendInviteToContactShare:(ContactShareViewModel *)contactShare
{
OWSAssertIsOnMainThread();
OWSAssertDebug(contactShare);
2018-05-09 17:13:29 +02:00
[self.contactShareViewHelper showInviteContactWithContactShare:contactShare fromViewController:self];
}
2018-05-08 23:22:27 +02:00
- (void)didTapShowAddToContactUIForContactShare:(ContactShareViewModel *)contactShare
{
OWSAssertIsOnMainThread();
OWSAssertDebug(contactShare);
2018-05-09 17:13:29 +02:00
[self.contactShareViewHelper showAddToContactsWithContactShare:contactShare fromViewController:self];
}
2018-09-28 00:49:01 +02:00
- (void)didTapFailedIncomingAttachment:(id<ConversationViewItem>)viewItem
2017-10-10 22:13:54 +02:00
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
OWSAssertDebug(viewItem);
2017-10-10 22:13:54 +02:00
// Restart failed downloads
TSMessage *message = (TSMessage *)viewItem.interaction;
2018-11-07 23:49:25 +01:00
[self handleFailedDownloadTapForMessage:message];
2017-10-10 22:13:54 +02:00
}
- (void)didTapFailedOutgoingMessage:(TSOutgoingMessage *)message
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
OWSAssertDebug(message);
2017-10-10 22:13:54 +02:00
[self handleUnsentMessageTap:message];
}
2018-09-28 00:49:01 +02:00
- (void)didTapConversationItem:(id<ConversationViewItem>)viewItem
quotedReply:(OWSQuotedReplyModel *)quotedReply
failedThumbnailDownloadAttachmentPointer:(TSAttachmentPointer *)attachmentPointer
2018-04-09 19:58:12 +02:00
{
OWSAssertIsOnMainThread();
OWSAssertDebug(viewItem);
OWSAssertDebug(attachmentPointer);
TSMessage *message = (TSMessage *)viewItem.interaction;
if (![message isKindOfClass:[TSMessage class]]) {
OWSFailDebug(@"message had unexpected class: %@", message.class);
return;
}
2018-11-07 23:49:25 +01:00
[self.uiDatabaseConnection asyncReadWithBlock:^(YapDatabaseReadTransaction *transaction) {
[self.attachmentDownloads downloadAttachmentPointer:attachmentPointer
2018-11-02 17:11:15 +01:00
success:^(NSArray<TSAttachmentStream *> *attachmentStreams) {
OWSAssertDebug(attachmentStreams.count == 1);
TSAttachmentStream *attachmentStream = attachmentStreams.firstObject;
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *postSuccessTransaction) {
[message setQuotedMessageThumbnailAttachmentStream:attachmentStream];
[message saveWithTransaction:postSuccessTransaction];
} error:nil];
}
2018-06-22 19:48:23 +02:00
failure:^(NSError *error) {
OWSLogWarn(@"Failed to redownload thumbnail with error: %@", error);
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *postSuccessTransaction) {
[message touchWithTransaction:postSuccessTransaction];
} error:nil];
}];
}];
}
2018-09-28 00:49:01 +02:00
- (void)didTapConversationItem:(id<ConversationViewItem>)viewItem quotedReply:(OWSQuotedReplyModel *)quotedReply
{
OWSAssertIsOnMainThread();
OWSAssertDebug(viewItem);
OWSAssertDebug(quotedReply);
OWSAssertDebug(quotedReply.timestamp > 0);
OWSAssertDebug(quotedReply.authorId.length > 0);
2018-04-09 19:58:12 +02:00
2018-10-31 15:05:24 +01:00
NSIndexPath *_Nullable indexPath = [self.conversationViewModel ensureLoadWindowContainsQuotedReply:quotedReply];
if (!indexPath) {
[self presentRemotelySourcedQuotedReplyToast];
return;
}
2018-04-10 15:45:58 +02:00
[self.collectionView scrollToItemAtIndexPath:indexPath
atScrollPosition:UICollectionViewScrollPositionTop
animated:YES];
2018-04-09 19:58:12 +02:00
2018-04-10 15:45:58 +02:00
// TODO: Highlight the quoted message?
2018-04-09 19:58:12 +02:00
}
2019-01-18 16:23:07 +01:00
- (void)didTapConversationItem:(id<ConversationViewItem>)viewItem linkPreview:(OWSLinkPreview *)linkPreview
{
OWSAssertIsOnMainThread();
2019-01-22 17:18:39 +01:00
NSURL *_Nullable url = [NSURL URLWithString:linkPreview.urlString];
if (!url) {
OWSFailDebug(@"Invalid link preview URL.");
return;
}
[UIApplication.sharedApplication openURL:url];
2019-01-18 16:23:07 +01:00
}
2018-09-28 00:49:01 +02:00
- (void)showDetailViewForViewItem:(id<ConversationViewItem>)conversationItem
2017-10-10 22:13:54 +02:00
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
OWSAssertDebug(conversationItem);
OWSAssertDebug([conversationItem.interaction isKindOfClass:[TSMessage class]]);
2017-10-10 22:13:54 +02:00
TSMessage *message = (TSMessage *)conversationItem.interaction;
MessageDetailViewController *detailVC =
2017-10-26 18:09:36 +02:00
[[MessageDetailViewController alloc] initWithViewItem:conversationItem
message:message
2018-06-22 19:48:23 +02:00
thread:self.thread
2017-10-26 18:09:36 +02:00
mode:MessageMetadataViewModeFocusOnMetadata];
detailVC.delegate = self;
[self.navigationController pushViewController:detailVC animated:YES];
2017-10-10 22:13:54 +02:00
}
2018-09-28 00:49:01 +02:00
- (void)populateReplyForViewItem:(id<ConversationViewItem>)conversationItem
{
OWSLogDebug(@"user did tap reply");
__block OWSQuotedReplyModel *quotedReply;
2018-06-22 19:48:23 +02:00
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
quotedReply = [OWSQuotedReplyModel quotedReplyForSendingWithConversationViewItem:conversationItem
threadId:conversationItem.interaction.uniqueThreadId
transaction:transaction];
}];
if (![quotedReply isKindOfClass:[OWSQuotedReplyModel class]]) {
OWSFailDebug(@"unexpected quotedMessage: %@", quotedReply.class);
return;
}
self.inputToolbar.quotedReply = quotedReply;
[self.inputToolbar beginEditingTextMessage];
}
#pragma mark - ContactEditingDelegate
- (void)didFinishEditingContact
{
OWSLogDebug(@"");
[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.
OWSLogDebug(@"completed editing contact.");
[self dismissViewControllerAnimated:NO completion:nil];
} else {
OWSLogDebug(@"canceled editing contact.");
[self dismissViewControllerAnimated:YES completion:nil];
}
}
#pragma mark - ContactsViewHelperDelegate
- (void)contactsViewHelperDidUpdateContacts
{
2019-02-19 23:49:40 +01:00
[self.conversationViewModel ensureDynamicInteractionsAndUpdateIfNecessary:YES];
}
- (void)createConversationScrollButtons
2017-10-10 22:13:54 +02:00
{
2019-12-11 00:25:53 +01:00
self.scrollDownButton = [[ConversationScrollButton alloc] initWithIconText:@"\uf107"];
[self.scrollDownButton addTarget:self
action:@selector(scrollDownButtonTapped)
forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.scrollDownButton];
2017-11-22 15:39:11 +01:00
[self.scrollDownButton autoSetDimension:ALDimensionWidth toSize:ConversationScrollButton.buttonSize];
[self.scrollDownButton autoSetDimension:ALDimensionHeight toSize:ConversationScrollButton.buttonSize];
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, _scrollDownButton);
// The "scroll down" button layout tracks the content inset of the collection view,
// so pin to the edge of the collection view.
self.scrollDownButtonButtomConstraint =
[self.scrollDownButton autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:self.view];
2019-01-08 16:51:19 +01:00
[self.scrollDownButton autoPinEdgeToSuperviewSafeArea:ALEdgeTrailing];
[self updateScrollDownButtonLayout];
}
- (void)updateScrollDownButtonLayout
{
CGFloat inset = -(self.collectionView.contentInset.bottom + self.bottomLayoutGuide.length);
self.scrollDownButtonButtomConstraint.constant = inset;
[self.scrollDownButton.superview setNeedsLayout];
2017-10-10 22:13:54 +02:00
}
- (void)setHasUnreadMessages:(BOOL)hasUnreadMessages
{
if (_hasUnreadMessages == hasUnreadMessages) {
return;
}
2017-10-10 22:13:54 +02:00
_hasUnreadMessages = hasUnreadMessages;
self.scrollDownButton.hasUnreadMessages = hasUnreadMessages;
2019-02-19 23:49:40 +01:00
[self.conversationViewModel ensureDynamicInteractionsAndUpdateIfNecessary:YES];
}
- (void)scrollDownButtonTapped
{
NSIndexPath *indexPathOfUnreadMessagesIndicator = [self indexPathOfUnreadMessagesIndicator];
if (indexPathOfUnreadMessagesIndicator != nil) {
NSInteger unreadRow = indexPathOfUnreadMessagesIndicator.row;
BOOL isScrolledAboveUnreadIndicator = YES;
NSArray<NSIndexPath *> *visibleIndices = self.collectionView.indexPathsForVisibleItems;
for (NSIndexPath *indexPath in visibleIndices) {
if (indexPath.row > unreadRow) {
isScrolledAboveUnreadIndicator = NO;
break;
}
}
if (isScrolledAboveUnreadIndicator) {
// Only scroll as far as the unread indicator if we're scrolled above the unread indicator.
[[self collectionView] scrollToItemAtIndexPath:indexPathOfUnreadMessagesIndicator
atScrollPosition:UICollectionViewScrollPositionTop
animated:YES];
return;
}
}
[self scrollToBottomAnimated:YES];
}
- (void)ensureScrollDownButton
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
BOOL shouldShowScrollDownButton = NO;
2017-10-10 22:13:54 +02:00
CGFloat scrollSpaceToBottom = (self.safeContentHeight + 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-10-19 15:53:35 +02:00
if (self.viewItems.count > 0) {
2018-09-28 00:49:01 +02:00
id<ConversationViewItem> lastViewItem = [self.viewItems lastObject];
OWSAssertDebug(lastViewItem);
if (lastViewItem.interaction.sortId > self.lastVisibleSortId) {
shouldShowScrollDownButton = YES;
} else if (isScrolledUp) {
shouldShowScrollDownButton = YES;
}
}
self.scrollDownButton.hidden = !shouldShowScrollDownButton;
}
2018-05-01 22:38:54 +02:00
#pragma mark - Attachment Picking: Contacts
2018-05-03 16:47:42 +02:00
2018-05-01 22:38:54 +02:00
- (void)chooseContactForSending
{
ContactsPicker *contactsPicker =
[[ContactsPicker alloc] initWithAllowsMultipleSelection:NO subtitleCellType:SubtitleCellValueNone];
contactsPicker.contactsPickerDelegate = self;
2018-05-01 22:38:54 +02:00
contactsPicker.title
= NSLocalizedString(@"CONTACT_PICKER_TITLE", @"navbar title for contact picker when sharing a contact");
OWSNavigationController *navigationController =
[[OWSNavigationController alloc] initWithRootViewController:contactsPicker];
2018-05-01 22:38:54 +02:00
[self dismissKeyBoard];
[self presentViewController:navigationController animated:YES completion:nil];
}
#pragma mark - Attachment Picking: Documents
- (void)showAttachmentDocumentPickerMenu
{
NSString *allItems = (__bridge NSString *)kUTTypeItem;
NSArray<NSString *> *documentTypes = @[ allItems ];
// UIDocumentPickerModeImport copies to a temp file within our container.
// It uses more memory than "open" but lets us avoid working with security scoped URLs.
UIDocumentPickerMode pickerMode = UIDocumentPickerModeImport;
2018-01-29 22:35:31 +01:00
// TODO: UIDocumentMenuViewController is deprecated; we should use UIDocumentPickerViewController
// instead.
UIDocumentMenuViewController *menuController =
[[UIDocumentMenuViewController alloc] initWithDocumentTypes:documentTypes inMode:pickerMode];
menuController.delegate = self;
2018-01-29 22:35:31 +01:00
UIImage *takeMediaImage = [UIImage imageNamed:@"actionsheet_camera_black"];
OWSAssertDebug(takeMediaImage);
2018-01-29 22:35:31 +01:00
[menuController addOptionWithTitle:NSLocalizedString(
@"MEDIA_FROM_LIBRARY_BUTTON", @"media picker option to choose from library")
image:takeMediaImage
order:UIDocumentMenuOrderFirst
handler:^{
[self chooseFromLibraryAsDocument];
}];
[self dismissKeyBoard];
[self presentViewController:menuController animated:YES completion:nil];
}
2017-09-26 18:37:00 +02:00
#pragma mark - Attachment Picking: GIFs
2020-02-01 04:31:12 +01:00
- (void)showGIFMetadataWarning
{
NSString *title = NSLocalizedString(@"Search GIFs?", @"");
2020-02-01 23:46:58 +01:00
NSString *message = NSLocalizedString(@"You will not have full metadata protection when sending GIFs.", @"");
2020-02-01 04:31:12 +01:00
UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
[self showGifPicker];
}]];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"") style:UIAlertActionStyleDefault handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
}
2017-09-26 18:37:00 +02:00
- (void)showGifPicker
{
GifPickerViewController *view =
[[GifPickerViewController alloc] initWithThread:self.thread messageSender:self.messageSender];
view.delegate = self;
OWSNavigationController *navigationController = [[OWSNavigationController alloc] initWithRootViewController:view];
[self dismissKeyBoard];
2017-09-26 18:37:00 +02:00
[self presentViewController:navigationController animated:YES completion:nil];
}
#pragma mark GifPickerViewControllerDelegate
- (void)gifPickerDidSelectWithAttachment:(SignalAttachment *)attachment
{
OWSAssertDebug(attachment);
[self showApprovalDialogForAttachment:attachment];
[ThreadUtil addThreadToProfileWhitelistIfEmptyContactThread:self.thread];
2019-02-19 23:49:40 +01:00
[self.conversationViewModel ensureDynamicInteractionsAndUpdateIfNecessary:YES];
}
- (void)messageWasSent:(TSOutgoingMessage *)message
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
OWSAssertDebug(message);
self.lastMessageSentDate = [NSDate new];
2018-10-31 15:05:24 +01:00
[self.conversationViewModel clearUnreadMessagesIndicator];
self.inputToolbar.quotedReply = nil;
2018-08-31 19:44:13 +02:00
if (!Environment.shared.preferences.hasSentAMessage) {
[Environment.shared.preferences setHasSentAMessage:YES];
2018-08-07 23:36:34 +02:00
}
2018-08-31 19:44:13 +02:00
if ([Environment.shared.preferences soundInForeground]) {
2018-06-01 20:20:48 +02:00
SystemSoundID soundId = [OWSSounds systemSoundIDForSound:OWSSound_MessageSent quiet:YES];
2018-06-01 23:51:18 +02:00
AudioServicesPlaySystemSound(soundId);
}
[self.typingIndicators didSendOutgoingMessageInThread:self.thread];
}
#pragma mark UIDocumentMenuDelegate
- (void)documentMenu:(UIDocumentMenuViewController *)documentMenu
didPickDocumentPicker:(UIDocumentPickerViewController *)documentPicker
{
documentPicker.delegate = self;
[self dismissKeyBoard];
[self presentViewController:documentPicker animated:YES completion:nil];
}
#pragma mark UIDocumentPickerDelegate
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url
{
OWSLogDebug(@"Picked document at url: %@", url);
NSString *type;
NSError *typeError;
[url getResourceValue:&type forKey:NSURLTypeIdentifierKey error:&typeError];
if (typeError) {
OWSFailDebug(@"Determining type of picked document at url: %@ failed with error: %@", url, typeError);
}
if (!type) {
OWSFailDebug(@"falling back to default filetype for picked document at url: %@", url);
type = (__bridge NSString *)kUTTypeData;
}
NSNumber *isDirectory;
NSError *isDirectoryError;
[url getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:&isDirectoryError];
if (isDirectoryError) {
OWSFailDebug(@"Determining if picked document was a directory failed with error: %@", isDirectoryError);
} else if ([isDirectory boolValue]) {
OWSLogInfo(@"User picked directory.");
dispatch_async(dispatch_get_main_queue(), ^{
[OWSAlerts
showAlertWithTitle:
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")];
});
return;
}
NSString *filename = url.lastPathComponent;
if (!filename) {
OWSFailDebug(@"Unable to determine filename");
filename = NSLocalizedString(
@"ATTACHMENT_DEFAULT_FILENAME", @"Generic filename for an attachment with no known name");
}
OWSAssertDebug(type);
OWSAssertDebug(filename);
2018-07-30 16:56:19 +02:00
DataSource *_Nullable dataSource = [DataSourcePath dataSourceWithURL:url shouldDeleteOnDeallocation:NO];
if (!dataSource) {
OWSFailDebug(@"attachment data was unexpectedly empty for picked document");
dispatch_async(dispatch_get_main_queue(), ^{
[OWSAlerts showAlertWithTitle:NSLocalizedString(@"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE",
@"Alert title when picking a document fails for an unknown reason")];
});
return;
}
[dataSource setSourceFilename:filename];
// Although we want to be able to send higher quality attachments through the document picker
// it's more imporant that we ensure the sent format is one all clients can accept (e.g. *not* quicktime .mov)
if ([SignalAttachment isInvalidVideoWithDataSource:dataSource dataUTI:type]) {
[self showApprovalDialogAfterProcessingVideoURL:url filename:filename];
return;
}
// "Document picker" attachments _SHOULD NOT_ be resized, if possible.
2017-12-07 23:29:47 +01:00
SignalAttachment *attachment =
[SignalAttachment attachmentWithDataSource:dataSource dataUTI:type imageQuality:TSImageQualityOriginal];
[self showApprovalDialogForAttachment:attachment];
}
2014-10-29 21:58:58 +01:00
#pragma mark - UIImagePickerController
/*
* Presenting UIImagePickerController
*/
- (void)takePictureOrVideo
{
2019-03-08 06:05:58 +01:00
[self ows_askForCameraPermissions:^(BOOL cameraGranted) {
if (!cameraGranted) {
OWSLogWarn(@"camera permission denied.");
return;
}
2019-03-08 06:05:58 +01:00
[self ows_askForMicrophonePermissions:^(BOOL micGranted) {
if (!micGranted) {
OWSLogWarn(@"proceeding, though mic permission denied.");
// We can still continue without mic permissions, but any captured video will
// be silent.
}
2019-01-08 17:18:05 +01:00
2019-03-08 06:05:58 +01:00
UIViewController *pickerModal;
if (SSKFeatureFlags.useCustomPhotoCapture) {
2019-03-28 20:02:09 +01:00
SendMediaNavigationController *navController = [SendMediaNavigationController showingCameraFirst];
navController.sendMediaNavDelegate = self;
2019-03-08 06:05:58 +01:00
pickerModal = navController;
} else {
UIImagePickerController *picker = [OWSImagePickerController new];
pickerModal = picker;
picker.sourceType = UIImagePickerControllerSourceTypeCamera;
picker.mediaTypes = @[ (__bridge NSString *)kUTTypeImage, (__bridge NSString *)kUTTypeMovie ];
picker.allowsEditing = NO;
picker.delegate = self;
}
OWSAssertDebug(pickerModal);
[self dismissKeyBoard];
2020-03-23 00:31:08 +01:00
pickerModal.modalPresentationStyle = UIModalPresentationFullScreen;
2019-03-08 06:05:58 +01:00
[self presentViewController:pickerModal animated:YES completion:nil];
}];
}];
}
2018-01-29 22:35:31 +01:00
- (void)chooseFromLibraryAsDocument
{
OWSAssertIsOnMainThread();
2018-01-30 16:15:15 +01:00
[self chooseFromLibraryAsDocument:YES];
2018-01-29 22:35:31 +01:00
}
- (void)chooseFromLibraryAsMedia
{
OWSAssertIsOnMainThread();
2018-01-30 16:15:15 +01:00
[self chooseFromLibraryAsDocument:NO];
2018-01-29 22:35:31 +01:00
}
2018-01-30 16:15:15 +01:00
- (void)chooseFromLibraryAsDocument:(BOOL)shouldTreatAsDocument
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
2018-01-29 22:35:31 +01:00
self.isPickingMediaAsDocument = shouldTreatAsDocument;
[self ows_askForMediaLibraryPermissions:^(BOOL granted) {
if (!granted) {
OWSLogWarn(@"Media Library permission denied.");
return;
}
2019-03-28 20:02:09 +01:00
SendMediaNavigationController *pickerModal = [SendMediaNavigationController showingMediaLibraryFirst];
pickerModal.sendMediaNavDelegate = self;
[self dismissKeyBoard];
2020-03-23 00:31:08 +01:00
pickerModal.modalPresentationStyle = UIModalPresentationFullScreen;
[self presentViewController:pickerModal animated:YES completion:nil];
}];
2014-10-29 21:58:58 +01:00
}
/*
* Dismissing UIImagePickerController
*/
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
{
2014-10-29 21:58:58 +01:00
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)resetFrame
{
2015-01-31 09:38:52 +01:00
// fixes bug on frame being off after this selection
2018-06-01 20:43:04 +02:00
CGRect frame = [UIScreen mainScreen].bounds;
2015-01-31 09:38:52 +01:00
self.view.frame = frame;
}
#pragma mark - SendMediaNavDelegate
- (void)sendMediaNavDidCancel:(SendMediaNavigationController *)sendMediaNavigationController
{
[self dismissViewControllerAnimated:YES completion:^{
if (!self.isFirstResponder) {
[self becomeFirstResponder];
}
}];
}
- (void)sendMediaNav:(SendMediaNavigationController *)sendMediaNavigationController
didApproveAttachments:(NSArray<SignalAttachment *> *)attachments
messageText:(nullable NSString *)messageText
{
[self tryToSendAttachments:attachments messageText:messageText];
2019-03-28 20:02:09 +01:00
[self.inputToolbar clearTextMessageAnimated:NO];
2019-10-11 06:52:56 +02:00
[self resetMentions];
2019-03-28 20:02:09 +01:00
// we want to already be at the bottom when the user returns, rather than have to watch
// the new message scroll into view.
[self scrollToBottomAnimated:NO];
[self dismissViewControllerAnimated:YES completion:^{
if (!self.isFirstResponder) {
[self becomeFirstResponder];
}
if (@available(iOS 10, *)) {
// do nothing
} else {
[self reloadInputViews];
}
}];
}
2019-03-28 20:02:09 +01:00
- (nullable NSString *)sendMediaNavInitialMessageText:(SendMediaNavigationController *)sendMediaNavigationController
{
return self.inputToolbar.messageText;
}
- (void)sendMediaNav:(SendMediaNavigationController *)sendMediaNavigationController
didChangeMessageText:(nullable NSString *)messageText
{
[self.inputToolbar setMessageText:messageText animated:NO];
}
#pragma mark - UIImagePickerControllerDelegate
2014-10-29 21:58:58 +01:00
/*
* Fetching data from UIImagePickerController
2014-10-29 21:58:58 +01:00
*/
- (void)imagePickerController:(UIImagePickerController *)picker
didFinishPickingMediaWithInfo:(NSDictionary<NSString *, id> *)info
{
2015-01-31 09:38:52 +01:00
[self resetFrame];
2017-04-13 18:55:21 +02:00
NSURL *referenceURL = [info valueForKey:UIImagePickerControllerReferenceURL];
if (!referenceURL) {
OWSLogVerbose(@"Could not retrieve reference URL for picked asset");
2017-04-13 22:06:23 +02:00
[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) {
2018-08-27 16:29:51 +02:00
OWSCFailDebug(@"Error retrieving filename for asset: %@", error);
2017-04-13 18:55:21 +02:00
}];
}
- (void)imagePickerController:(UIImagePickerController *)picker
didFinishPickingMediaWithInfo:(NSDictionary<NSString *, id> *)info
2017-11-08 18:56:55 +01:00
filename:(NSString *_Nullable)filename
2017-04-13 18:55:21 +02:00
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
2017-04-13 18:55:21 +02:00
void (^failedToPickAttachment)(NSError *error) = ^void(NSError *error) {
OWSLogError(@"failed to pick attachment with error: %@", error);
};
NSString *mediaType = info[UIImagePickerControllerMediaType];
if ([mediaType isEqualToString:(__bridge NSString *)kUTTypeMovie]) {
// Video picked from library or captured with camera
NSURL *videoURL = info[UIImagePickerControllerMediaURL];
[self dismissViewControllerAnimated:YES
completion:^{
[self showApprovalDialogAfterProcessingVideoURL:videoURL filename:filename];
}];
} else if (picker.sourceType == UIImagePickerControllerSourceTypeCamera) {
// Static Image captured from camera
UIImage *imageFromCamera = [info[UIImagePickerControllerOriginalImage] normalizedImage];
2017-04-13 18:55:21 +02:00
[self dismissViewControllerAnimated:YES
completion:^{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
if (imageFromCamera) {
// "Camera" attachments _SHOULD_ be resized, if possible.
2017-04-13 18:55:21 +02:00
SignalAttachment *attachment =
[SignalAttachment imageAttachmentWithImage:imageFromCamera
dataUTI:(NSString *)kUTTypeJPEG
filename:filename
2017-12-07 23:29:47 +01:00
imageQuality:TSImageQualityCompact];
if (!attachment || [attachment hasError]) {
OWSLogWarn(@"Invalid attachment: %@.",
attachment ? [attachment errorName] : @"Missing data");
[self showErrorAlertForAttachment:attachment];
failedToPickAttachment(nil);
} else {
[self showApprovalDialogForAttachment:attachment];
}
} else {
failedToPickAttachment(nil);
}
}];
} else {
// Non-Video image picked from library
OWSFailDebug(
@"Only use UIImagePicker for camera/video capture. Picking media from UIImagePicker is not supported. ");
// To avoid re-encoding GIF and PNG's as JPEG we have to get the raw data of
// the selected item vs. using the UIImagePickerControllerOriginalImage
NSURL *assetURL = info[UIImagePickerControllerReferenceURL];
PHAsset *asset = [[PHAsset fetchAssetsWithALAssetURLs:@[ assetURL ] options:nil] lastObject];
if (!asset) {
return failedToPickAttachment(nil);
}
// Images chosen from the "attach document" UI should be sent as originals;
// images chosen from the "attach media" UI should be resized to "medium" size;
TSImageQuality imageQuality = (self.isPickingMediaAsDocument ? TSImageQualityOriginal : TSImageQualityMedium);
PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];
options.synchronous = YES; // We're only fetching one asset.
options.networkAccessAllowed = YES; // iCloud OK
options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat; // Don't need quick/dirty version
[[PHImageManager defaultManager]
requestImageDataForAsset:asset
options:options
resultHandler:^(NSData *_Nullable imageData,
NSString *_Nullable dataUTI,
UIImageOrientation orientation,
NSDictionary *_Nullable assetInfo) {
NSError *assetFetchingError = assetInfo[PHImageErrorKey];
if (assetFetchingError || !imageData) {
return failedToPickAttachment(assetFetchingError);
}
OWSAssertIsOnMainThread();
DataSource *_Nullable dataSource =
[DataSourceValue dataSourceWithData:imageData utiType:dataUTI];
[dataSource setSourceFilename:filename];
SignalAttachment *attachment = [SignalAttachment attachmentWithDataSource:dataSource
dataUTI:dataUTI
imageQuality:imageQuality];
[self dismissViewControllerAnimated:YES
completion:^{
OWSAssertIsOnMainThread();
if (!attachment || [attachment hasError]) {
OWSLogWarn(@"Invalid attachment: %@.",
attachment ? [attachment errorName] : @"Missing data");
[self showErrorAlertForAttachment:attachment];
failedToPickAttachment(nil);
} else {
[self showApprovalDialogForAttachment:attachment];
}
}];
}];
}
}
2018-05-05 04:32:29 +02:00
- (void)sendContactShare:(ContactShareViewModel *)contactShare
2018-05-03 17:33:01 +02:00
{
OWSAssertIsOnMainThread();
OWSAssertDebug(contactShare);
2018-05-03 17:33:01 +02:00
OWSLogVerbose(@"Sending contact share.");
2018-05-03 17:33:01 +02:00
BOOL didAddToProfileWhitelist = [ThreadUtil addThreadToProfileWhitelistIfEmptyContactThread:self.thread];
[self.editingDatabaseConnection
asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
// TODO - in line with QuotedReply and other message attachments, saving should happen as part of sending
// preparation rather than duplicated here and in the SAE
if (contactShare.avatarImage) {
[contactShare.dbRecord saveAvatarImage:contactShare.avatarImage transaction:transaction];
}
2018-05-05 04:32:29 +02:00
}
completionBlock:^{
TSOutgoingMessage *message =
[ThreadUtil enqueueMessageWithContactShare:contactShare.dbRecord inThread:self.thread];
2018-05-05 04:32:29 +02:00
[self messageWasSent:message];
2018-05-03 17:33:01 +02:00
2018-05-07 18:32:31 +02:00
if (didAddToProfileWhitelist) {
2019-02-19 23:49:40 +01:00
[self.conversationViewModel ensureDynamicInteractionsAndUpdateIfNecessary:YES];
2018-05-07 18:32:31 +02:00
}
}];
2018-05-03 17:33:01 +02:00
}
- (void)showApprovalDialogAfterProcessingVideoURL:(NSURL *)movieURL filename:(nullable NSString *)filename
2017-04-13 18:55:21 +02:00
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
[ModalActivityIndicatorViewController
presentFromViewController:self
canCancel:YES
2017-09-18 21:35:14 +02:00
backgroundBlock:^(ModalActivityIndicatorViewController *modalActivityIndicator) {
2018-07-30 16:56:19 +02:00
DataSource *dataSource =
[DataSourcePath dataSourceWithURL:movieURL shouldDeleteOnDeallocation:NO];
dataSource.sourceFilename = filename;
VideoCompressionResult *compressionResult =
[SignalAttachment compressVideoAsMp4WithDataSource:dataSource
dataUTI:(NSString *)kUTTypeMPEG4];
[compressionResult.attachmentPromise.then(^(SignalAttachment *attachment) {
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
OWSAssertDebug([attachment isKindOfClass:[SignalAttachment class]]);
if (modalActivityIndicator.wasCancelled) {
return;
}
[modalActivityIndicator dismissWithCompletion:^{
if (!attachment || [attachment hasError]) {
OWSLogError(@"Invalid attachment: %@.",
attachment ? [attachment errorName] : @"Missing data");
[self showErrorAlertForAttachment:attachment];
} else {
[self showApprovalDialogForAttachment:attachment];
2017-09-18 21:35:14 +02:00
}
}];
}) retainUntilComplete];
2017-09-18 21:35:14 +02:00
}];
2014-11-25 16:38:33 +01:00
}
2018-05-01 22:38:54 +02:00
#pragma mark - Storage access
2014-11-25 16:38:33 +01:00
- (YapDatabaseConnection *)uiDatabaseConnection
{
return OWSPrimaryStorage.sharedManager.uiDatabaseConnection;
2014-11-25 16:38:33 +01:00
}
- (YapDatabaseConnection *)editingDatabaseConnection
{
return OWSPrimaryStorage.sharedManager.dbReadWriteConnection;
}
2018-10-31 15:05:24 +01:00
#pragma mark - Audio
- (void)requestRecordingVoiceMemo
{
2018-10-31 15:05:24 +01:00
OWSAssertIsOnMainThread();
NSUUID *voiceMessageUUID = [NSUUID UUID];
self.voiceMessageUUID = voiceMessageUUID;
__weak typeof(self) weakSelf = self;
[self ows_askForMicrophonePermissions:^(BOOL granted) {
__strong typeof(self) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
if (strongSelf.voiceMessageUUID != voiceMessageUUID) {
// This voice message recording has been cancelled
// before recording could begin.
return;
}
if (granted) {
[strongSelf startRecordingVoiceMemo];
} else {
OWSLogInfo(@"we do not have recording permission.");
[strongSelf cancelVoiceMemo];
[OWSAlerts showNoMicrophonePermissionAlert];
}
}];
}
2018-10-31 15:05:24 +01:00
- (void)startRecordingVoiceMemo
{
OWSAssertIsOnMainThread();
2018-10-31 15:05:24 +01:00
OWSLogInfo(@"startRecordingVoiceMemo");
2018-10-31 15:05:24 +01:00
// Cancel any ongoing audio playback.
[self.audioAttachmentPlayer stop];
self.audioAttachmentPlayer = nil;
2018-10-31 15:05:24 +01:00
NSString *temporaryDirectory = OWSTemporaryDirectory();
NSString *filename = [NSString stringWithFormat:@"%lld.m4a", [NSDate ows_millisecondTimeStamp]];
NSString *filepath = [temporaryDirectory stringByAppendingPathComponent:filename];
NSURL *fileURL = [NSURL fileURLWithPath:filepath];
2018-10-31 15:05:24 +01:00
// Setup audio session
BOOL configuredAudio = [self.audioSession startAudioActivity:self.recordVoiceNoteAudioActivity];
if (!configuredAudio) {
OWSFailDebug(@"Couldn't configure audio session");
[self cancelVoiceMemo];
return;
}
2018-10-31 15:05:24 +01:00
NSError *error;
// Initiate and prepare the recorder
self.audioRecorder = [[AVAudioRecorder alloc] initWithURL:fileURL
settings:@{
AVFormatIDKey : @(kAudioFormatMPEG4AAC),
AVSampleRateKey : @(44100),
AVNumberOfChannelsKey : @(2),
AVEncoderBitRateKey : @(128 * 1024),
}
error:&error];
if (error) {
OWSFailDebug(@"Couldn't create audioRecorder: %@", error);
[self cancelVoiceMemo];
2015-01-31 12:00:58 +01:00
return;
}
2018-10-31 15:05:24 +01:00
self.audioRecorder.meteringEnabled = YES;
if (![self.audioRecorder prepareToRecord]) {
OWSFailDebug(@"audioRecorder couldn't prepareToRecord.");
[self cancelVoiceMemo];
2017-05-05 04:10:37 +02:00
return;
}
if (![self.audioRecorder record]) {
OWSFailDebug(@"audioRecorder couldn't record.");
2017-05-08 17:13:51 +02:00
[self cancelVoiceMemo];
2017-05-05 04:10:37 +02:00
return;
}
}
- (void)endRecordingVoiceMemo
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
2017-05-05 04:10:37 +02:00
OWSLogInfo(@"endRecordingVoiceMemo");
2017-05-05 04:10:37 +02:00
self.voiceMessageUUID = nil;
2017-05-05 04:10:37 +02:00
if (!self.audioRecorder) {
// No voice message recording is in progress.
// We may be cancelling before the recording could begin.
OWSLogError(@"Missing audioRecorder");
2017-05-05 04:10:37 +02:00
return;
}
2017-11-01 16:36:58 +01:00
NSTimeInterval durationSeconds = self.audioRecorder.currentTime;
[self stopRecording];
2017-05-05 04:10:37 +02:00
const NSTimeInterval kMinimumRecordingTimeSeconds = 1.f;
2017-11-01 16:36:58 +01:00
if (durationSeconds < kMinimumRecordingTimeSeconds) {
OWSLogInfo(@"Discarding voice message; too short.");
self.audioRecorder = nil;
2017-05-08 19:29:10 +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.")];
return;
}
2018-07-30 16:56:19 +02:00
DataSource *_Nullable dataSource =
[DataSourcePath dataSourceWithURL:self.audioRecorder.url shouldDeleteOnDeallocation:YES];
2017-05-05 16:29:27 +02:00
self.audioRecorder = nil;
if (!dataSource) {
OWSFailDebug(@"Couldn't load audioRecorder data");
self.audioRecorder = nil;
return;
}
2017-05-08 19:29:10 +02:00
NSString *filename = [NSLocalizedString(@"VOICE_MESSAGE_FILE_NAME", @"Filename for voice messages.")
stringByAppendingPathExtension:@"m4a"];
[dataSource setSourceFilename:filename];
SignalAttachment *attachment =
[SignalAttachment voiceMessageAttachmentWithDataSource:dataSource dataUTI:(NSString *)kUTTypeMPEG4Audio];
OWSLogVerbose(@"voice memo duration: %f, file size: %zd", durationSeconds, [dataSource dataLength]);
2017-05-05 04:10:37 +02:00
if (!attachment || [attachment hasError]) {
OWSLogWarn(@"Invalid attachment: %@.", attachment ? [attachment errorName] : @"Missing data");
2017-05-05 04:10:37 +02:00
[self showErrorAlertForAttachment:attachment];
} else {
[self tryToSendAttachments:@[ attachment ] messageText:nil];
}
}
- (void)stopRecording
2017-05-05 04:10:37 +02:00
{
[self.audioRecorder stop];
[self.audioSession endAudioActivity:self.recordVoiceNoteAudioActivity];
2017-05-05 04:10:37 +02:00
}
- (void)cancelRecordingVoiceMemo
2017-05-05 04:10:37 +02:00
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
OWSLogDebug(@"cancelRecordingVoiceMemo");
2017-05-05 04:10:37 +02:00
[self stopRecording];
2017-05-05 04:10:37 +02:00
self.audioRecorder = nil;
self.voiceMessageUUID = nil;
2017-05-05 04:10:37 +02:00
}
2017-10-12 22:19:07 +02:00
- (void)setAudioRecorder:(nullable 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-10-10 22:13:54 +02:00
- (void)attachmentButtonPressed
{
[self dismissKeyBoard];
2017-09-06 20:13:18 +02:00
__weak ConversationViewController *weakSelf = self;
2018-09-09 19:46:23 +02:00
if ([self isBlockedConversation]) {
[self showUnblockConversationUI:^(BOOL isBlocked) {
if (!isBlocked) {
2017-10-10 22:13:54 +02:00
[weakSelf attachmentButtonPressed];
}
}];
return;
}
BOOL didShowSNAlert =
[self showSafetyNumberConfirmationIfNecessaryWithConfirmationText:
NSLocalizedString(@"CONFIRMATION_TITLE", @"Generic button text to proceed with an action")
completion:^(BOOL didConfirmIdentity) {
if (didConfirmIdentity) {
2017-10-10 22:13:54 +02:00
[weakSelf attachmentButtonPressed];
}
}];
if (didShowSNAlert) {
return;
}
UIAlertController *actionSheet =
[UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
[actionSheet addAction:[OWSAlerts cancelAction]];
UIAlertAction *takeMediaAction =
[UIAlertAction actionWithTitle:NSLocalizedString(
@"MEDIA_FROM_CAMERA_BUTTON", @"media picker option to take photo or video")
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"send_camera")
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
[self takePictureOrVideo];
}];
UIImage *takeMediaImage = [UIImage imageNamed:@"actionsheet_camera_black"];
OWSAssertDebug(takeMediaImage);
[takeMediaAction setValue:takeMediaImage forKey:@"image"];
[actionSheet addAction:takeMediaAction];
2016-11-26 00:12:00 +01:00
UIAlertAction *chooseMediaAction =
[UIAlertAction actionWithTitle:NSLocalizedString(
@"MEDIA_FROM_LIBRARY_BUTTON", @"media picker option to choose from library")
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"send_choose_media")
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
[self chooseFromLibraryAsMedia];
}];
UIImage *chooseMediaImage = [UIImage imageNamed:@"actionsheet_camera_roll_black"];
OWSAssertDebug(chooseMediaImage);
[chooseMediaAction setValue:chooseMediaImage forKey:@"image"];
[actionSheet addAction:chooseMediaAction];
UIAlertAction *gifAction =
[UIAlertAction actionWithTitle:NSLocalizedString(@"SELECT_GIF_BUTTON",
@"Label for 'select GIF to attach' action sheet button")
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"send_gif")
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
2020-05-19 01:12:41 +02:00
NSUserDefaults *userDefaults = NSUserDefaults.standardUserDefaults;
BOOL hasSeenGIFMetadataWarning = [userDefaults boolForKey:@"hasSeenGIFMetadataWarning"];
if (!hasSeenGIFMetadataWarning) {
[self showGIFMetadataWarning];
[userDefaults setBool:YES forKey:@"hasSeenGIFMetadataWarning"];
} else {
[self showGifPicker];
}
}];
2018-05-04 19:06:26 +02:00
UIImage *gifImage = [UIImage imageNamed:@"actionsheet_gif_black"];
OWSAssertDebug(gifImage);
2018-05-04 19:06:26 +02:00
[gifAction setValue:gifImage forKey:@"image"];
[actionSheet addAction:gifAction];
2018-05-04 19:06:26 +02:00
UIAlertAction *chooseDocumentAction =
[UIAlertAction actionWithTitle:NSLocalizedString(@"MEDIA_FROM_DOCUMENT_PICKER_BUTTON",
@"action sheet button title when choosing attachment type")
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"send_document")
style:UIAlertActionStyleDefault
2018-06-22 19:48:23 +02:00
handler:^(UIAlertAction *action) {
[self showAttachmentDocumentPickerMenu];
}];
UIImage *chooseDocumentImage = [UIImage imageNamed:@"actionsheet_document_black"];
OWSAssertDebug(chooseDocumentImage);
[chooseDocumentAction setValue:chooseDocumentImage forKey:@"image"];
[actionSheet addAction:chooseDocumentAction];
2019-10-23 04:35:15 +02:00
/*
2018-05-03 22:25:50 +02:00
if (kIsSendingContactSharesEnabled) {
UIAlertAction *chooseContactAction =
[UIAlertAction actionWithTitle:NSLocalizedString(@"ATTACHMENT_MENU_CONTACT_BUTTON",
@"attachment menu option to send contact")
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"send_contact")
2018-05-03 22:25:50 +02:00
style:UIAlertActionStyleDefault
2018-06-22 19:48:23 +02:00
handler:^(UIAlertAction *action) {
2018-05-03 22:25:50 +02:00
[self chooseContactForSending];
}];
2018-05-09 20:56:11 +02:00
UIImage *chooseContactImage = [UIImage imageNamed:@"actionsheet_contact"];
OWSAssertDebug(takeMediaImage);
2018-05-03 22:25:50 +02:00
[chooseContactAction setValue:chooseContactImage forKey:@"image"];
[actionSheet addAction:chooseContactAction];
2018-05-03 22:25:50 +02:00
}
2019-10-17 04:07:38 +02:00
*/
2018-05-01 22:38:54 +02:00
[self dismissKeyBoard];
[self presentAlert:actionSheet];
2014-11-25 16:38:33 +01:00
}
- (nullable NSIndexPath *)lastVisibleIndexPath
2017-10-20 15:53:33 +02:00
{
NSIndexPath *_Nullable lastVisibleIndexPath = nil;
2017-10-20 15:53:33 +02:00
for (NSIndexPath *indexPath in [self.collectionView indexPathsForVisibleItems]) {
if (!lastVisibleIndexPath || indexPath.row > lastVisibleIndexPath.row) {
lastVisibleIndexPath = indexPath;
}
}
2018-05-11 16:43:33 +02:00
if (lastVisibleIndexPath && lastVisibleIndexPath.row >= (NSInteger)self.viewItems.count) {
return (self.viewItems.count > 0 ? [NSIndexPath indexPathForRow:(NSInteger)self.viewItems.count - 1 inSection:0]
: nil);
}
2017-10-20 15:53:33 +02:00
return lastVisibleIndexPath;
}
2018-09-28 00:49:01 +02:00
- (nullable id<ConversationViewItem>)lastVisibleViewItem
2017-10-20 15:53:33 +02:00
{
NSIndexPath *_Nullable lastVisibleIndexPath = [self lastVisibleIndexPath];
2017-10-20 15:53:33 +02:00
if (!lastVisibleIndexPath) {
return nil;
}
return [self viewItemForIndex:lastVisibleIndexPath.row];
}
// In the case where we explicitly scroll to bottom, we want to synchronously
// update the UI to reflect that, since the "mark as read" logic is asynchronous
// and won't update the UI state immediately.
- (void)didScrollToBottom
{
2018-09-28 00:49:01 +02:00
id<ConversationViewItem> _Nullable lastVisibleViewItem = [self.viewItems lastObject];
if (lastVisibleViewItem) {
uint64_t lastVisibleSortId = lastVisibleViewItem.interaction.sortId;
self.lastVisibleSortId = MAX(self.lastVisibleSortId, lastVisibleSortId);
}
self.scrollDownButton.hidden = YES;
self.hasUnreadMessages = NO;
}
- (void)updateLastVisibleSortId
{
2018-09-28 00:49:01 +02:00
id<ConversationViewItem> _Nullable lastVisibleViewItem = [self lastVisibleViewItem];
2017-10-20 15:53:33 +02:00
if (lastVisibleViewItem) {
uint64_t lastVisibleSortId = lastVisibleViewItem.interaction.sortId;
self.lastVisibleSortId = MAX(self.lastVisibleSortId, lastVisibleSortId);
}
[self ensureScrollDownButton];
__block NSUInteger numberOfUnreadMessages;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
numberOfUnreadMessages =
[[transaction ext:TSUnreadDatabaseViewExtensionName] numberOfItemsInGroup:self.thread.uniqueId];
}];
self.hasUnreadMessages = numberOfUnreadMessages > 0;
}
- (void)markVisibleMessagesAsRead
{
if (self.presentedViewController) {
OWSLogInfo(@"Not marking messages as read; another view is presented.");
return;
}
if (OWSWindowManager.sharedManager.shouldShowCallView) {
OWSLogInfo(@"Not marking messages as read; call view is presented.");
return;
}
if (self.navigationController.topViewController != self) {
OWSLogInfo(@"Not marking messages as read; another view is pushed.");
return;
}
[self updateLastVisibleSortId];
uint64_t lastVisibleSortId = self.lastVisibleSortId;
if (lastVisibleSortId == 0) {
2017-09-28 15:31:17 +02:00
// No visible messages yet. New Thread.
return;
}
[OWSReadReceiptManager.sharedManager markAsReadLocallyBeforeSortId:self.lastVisibleSortId thread:self.thread];
}
2017-11-08 20:04:51 +01:00
- (void)updateGroupModelTo:(TSGroupModel *)newGroupModel successCompletion:(void (^_Nullable)(void))successCompletion
{
__block TSGroupThread *groupThread;
__block TSOutgoingMessage *message;
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
groupThread = [TSGroupThread getOrCreateThreadWithGroupModel:newGroupModel transaction:transaction];
NSString *updateGroupInfo =
[groupThread.groupModel getInfoStringAboutUpdateTo:newGroupModel contactsManager:self.contactsManager];
groupThread.groupModel = newGroupModel;
[groupThread saveWithTransaction:transaction];
uint32_t expiresInSeconds = [groupThread disappearingMessagesDurationWithTransaction:transaction];
message = [TSOutgoingMessage outgoingMessageInThread:groupThread
2018-08-31 18:43:05 +02:00
groupMetaMessage:TSGroupMetaMessageUpdate
expiresInSeconds:expiresInSeconds];
[message updateWithCustomMessage:updateGroupInfo transaction:transaction];
} error:nil];
[groupThread fireAvatarChangedNotification];
if (newGroupModel.groupImage) {
NSData *data = UIImagePNGRepresentation(newGroupModel.groupImage);
2018-07-30 17:00:56 +02:00
DataSource *_Nullable dataSource = [DataSourceValue dataSourceWithData:data fileExtension:@"png"];
// DURABLE CLEANUP - currently one caller uses the completion handler to delete the tappable error message
// which causes this code to be called. Once we're more aggressive about durable sending retry,
// we could get rid of this "retryable tappable error message".
[self.messageSender sendTemporaryAttachment:dataSource
contentType:OWSMimeTypeImagePng
inMessage:message
success:^{
OWSLogDebug(@"Successfully sent group update with avatar");
if (successCompletion) {
successCompletion();
}
}
2018-06-22 19:48:23 +02:00
failure:^(NSError *error) {
OWSLogError(@"Failed to send group avatar update with error: %@", error);
}];
} else {
// DURABLE CLEANUP - currently one caller uses the completion handler to delete the tappable error message
// which causes this code to be called. Once we're more aggressive about durable sending retry,
// we could get rid of this "retryable tappable error message".
[self.messageSender sendMessage:message
success:^{
OWSLogDebug(@"Successfully sent group update");
if (successCompletion) {
successCompletion();
}
}
2018-06-22 19:48:23 +02:00
failure:^(NSError *error) {
OWSLogError(@"Failed to send group update with error: %@", error);
}];
}
self.thread = groupThread;
}
- (void)popKeyBoard
{
2017-10-10 22:13:54 +02:00
[self.inputToolbar beginEditingTextMessage];
}
- (void)dismissKeyBoard
{
2017-10-10 22:13:54 +02:00
[self.inputToolbar endEditingTextMessage];
}
#pragma mark Drafts
- (void)loadDraftInCompose
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
2017-10-12 22:19:07 +02:00
2017-10-10 22:13:54 +02:00
__block NSString *draft;
2017-10-12 22:19:07 +02:00
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
2018-07-11 20:12:58 +02:00
draft = [self.thread currentDraftWithTransaction:transaction];
2017-10-12 22:19:07 +02:00
}];
2018-07-03 06:42:32 +02:00
[self.inputToolbar setMessageText:draft animated:NO];
}
- (void)saveDraft
{
if (!self.inputToolbar.hidden) {
__block TSThread *thread = _thread;
2017-10-10 22:13:54 +02:00
__block NSString *currentDraft = [self.inputToolbar messageText];
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[thread setDraft:currentDraft transaction:transaction];
}];
}
}
#pragma mark 3D Touch Preview Actions
- (NSArray<id<UIPreviewActionItem>> *)previewActionItems
{
return @[];
}
#pragma mark - ConversationHeaderViewDelegate
- (void)didTapConversationHeaderView:(ConversationHeaderView *)conversationHeaderView
{
[self showConversationSettings];
}
2017-09-01 20:30:39 +02:00
#ifdef USE_DEBUG_UI
- (void)navigationTitleLongPressed:(UIGestureRecognizer *)gestureRecognizer
{
2017-03-27 23:03:36 +02:00
if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
[DebugUITableViewController presentDebugUIForThread:self.thread fromViewController:self];
2017-03-27 23:03:36 +02:00
}
}
#endif
2017-10-10 22:13:54 +02:00
#pragma mark - ConversationInputTextViewDelegate
- (void)textViewDidChange:(UITextView *)textView
{
// Prepare
NSString *newText = textView.text;
// Typing indicators
if (newText.length > 0) {
2018-10-31 17:19:07 +01:00
[self.typingIndicators didStartTypingOutgoingInputInThread:self.thread];
}
// Mentions
BOOL isBackspace = newText.length < self.oldText.length;
if (isBackspace) {
self.currentMentionStartIndex = -1;
2019-10-11 06:52:56 +02:00
[self.inputToolbar hideMentionCandidateSelectionView];
2019-11-07 01:59:11 +01:00
NSArray *mentionsToRemove = [self.mentions filtered:^BOOL(LKMention *mention) {
2019-10-15 02:31:59 +02:00
return ![mention isContainedIn:newText];
}];
[self.mentions removeObjectsInArray:mentionsToRemove];
}
if (newText.length > 0) {
2019-10-11 06:52:56 +02:00
NSUInteger lastCharacterIndex = newText.length - 1;
unichar lastCharacter = [newText characterAtIndex:lastCharacterIndex];
2019-11-13 04:54:26 +01:00
// Check if there is a whitespace before '@' or the '@' is the first character
2019-11-18 04:32:35 +01:00
unichar secondToLastCharacter = ' ';
2019-11-13 04:54:26 +01:00
if (lastCharacterIndex > 0) {
2019-11-18 04:32:35 +01:00
secondToLastCharacter = [newText characterAtIndex:lastCharacterIndex - 1];
2019-11-13 04:54:26 +01:00
}
2019-11-18 04:32:35 +01:00
if (lastCharacter == '@' && [NSCharacterSet.whitespaceAndNewlineCharacterSet characterIsMember:secondToLastCharacter]) {
NSArray<LKMention *> *mentionCandidates = [LKMentionsManager getMentionCandidatesFor:@"" in:self.thread.uniqueId];
2019-10-11 06:52:56 +02:00
self.currentMentionStartIndex = (NSInteger)lastCharacterIndex;
[self.inputToolbar showMentionCandidateSelectionViewFor:mentionCandidates in:self.thread];
} else if ([NSCharacterSet.whitespaceAndNewlineCharacterSet characterIsMember:lastCharacter]) {
self.currentMentionStartIndex = -1;
2019-10-11 06:52:56 +02:00
[self.inputToolbar hideMentionCandidateSelectionView];
} else {
if (self.currentMentionStartIndex != -1) {
NSString *query = [newText substringFromIndex:(NSUInteger)self.currentMentionStartIndex + 1]; // + 1 to get rid of the @
NSArray<LKMention *> *mentionCandidates = [LKMentionsManager getMentionCandidatesFor:query in:self.thread.uniqueId];
2019-10-11 06:52:56 +02:00
[self.inputToolbar showMentionCandidateSelectionViewFor:mentionCandidates in:self.thread];
}
2019-10-09 05:46:21 +02:00
}
}
self.oldText = newText;
2019-10-09 05:46:21 +02:00
}
2019-10-11 06:52:56 +02:00
- (void)handleMentionCandidateSelected:(LKMention *)mentionCandidate from:(LKMentionCandidateSelectionView *)mentionCandidateSelectionView
2019-10-09 05:46:21 +02:00
{
NSUInteger mentionStartIndex = (NSUInteger)self.currentMentionStartIndex;
2019-10-11 06:52:56 +02:00
[self.mentions addObject:mentionCandidate];
2019-10-09 05:46:21 +02:00
NSString *oldText = self.inputToolbar.messageText;
2020-06-23 06:25:13 +02:00
NSString *newText = [oldText stringByReplacingCharactersInRange:NSMakeRange(mentionStartIndex, oldText.length - mentionStartIndex) withString:[NSString stringWithFormat:@"@%@ ", mentionCandidate.displayName]];
2019-10-09 05:46:21 +02:00
[self.inputToolbar setMessageText:newText animated:NO];
2019-10-11 05:57:06 +02:00
self.currentMentionStartIndex = -1;
2019-10-11 06:52:56 +02:00
[self.inputToolbar hideMentionCandidateSelectionView];
self.oldText = newText;
}
2019-10-11 01:45:24 +02:00
- (NSString *)getSendText
{
NSString *result = self.inputToolbar.messageText;
for (LKMention *mention in self.mentions) {
2019-10-11 06:52:56 +02:00
NSRange range = [result rangeOfString:[NSString stringWithFormat:@"@%@", mention.displayName]];
2020-07-20 03:02:58 +02:00
result = [result stringByReplacingCharactersInRange:range withString:[[NSString alloc] initWithFormat:@"@%@", mention.publicKey]];
}
return result;
}
2019-10-11 06:52:56 +02:00
- (void)resetMentions
2019-10-11 01:45:24 +02:00
{
self.oldText = @"";
self.currentMentionStartIndex = -1;
self.mentions = @[].mutableCopy;
}
- (void)inputTextViewSendMessagePressed
{
[self sendButtonPressed];
}
- (void)didPasteAttachment:(SignalAttachment *_Nullable)attachment
{
OWSLogError(@"");
[self showApprovalDialogForAttachment:attachment];
}
- (void)showApprovalDialogForAttachment:(SignalAttachment *_Nullable)attachment
{
2018-11-06 20:30:35 +01:00
if (attachment == nil) {
2018-11-26 20:31:03 +01:00
OWSFailDebug(@"attachment was unexpectedly nil");
2018-11-06 20:30:35 +01:00
[self showErrorAlertForAttachment:nil];
return;
}
[self showApprovalDialogForAttachments:@[ attachment ]];
}
- (void)showApprovalDialogForAttachments:(NSArray<SignalAttachment *> *)attachments
2018-11-06 20:30:35 +01:00
{
OWSNavigationController *modal =
[AttachmentApprovalViewController wrappedInNavControllerWithAttachments:attachments approvalDelegate:self];
2018-11-06 20:30:35 +01:00
[self presentViewController:modal animated:YES completion:nil];
2018-11-06 20:30:35 +01:00
}
- (void)tryToSendAttachments:(NSArray<SignalAttachment *> *)attachments messageText:(NSString *_Nullable)messageText
{
DispatchMainThreadSafe(^{
2017-09-06 20:13:18 +02:00
__weak ConversationViewController *weakSelf = self;
2018-09-09 19:46:23 +02:00
if ([self isBlockedConversation]) {
[self showUnblockConversationUI:^(BOOL isBlocked) {
if (!isBlocked) {
[weakSelf tryToSendAttachments:attachments messageText:messageText];
}
}];
return;
}
BOOL didShowSNAlert = [self
showSafetyNumberConfirmationIfNecessaryWithConfirmationText:[SafetyNumberStrings confirmSendButton]
completion:^(BOOL didConfirmIdentity) {
if (didConfirmIdentity) {
[weakSelf tryToSendAttachments:attachments
messageText:messageText];
}
}];
if (didShowSNAlert) {
return;
}
2018-11-06 20:30:35 +01:00
for (SignalAttachment *attachment in attachments) {
if ([attachment hasError]) {
OWSLogWarn(@"Invalid attachment: %@.", attachment ? [attachment errorName] : @"Missing data");
[self showErrorAlertForAttachment:attachment];
return;
}
}
BOOL didAddToProfileWhitelist = [ThreadUtil addThreadToProfileWhitelistIfEmptyContactThread:self.thread];
2019-02-20 18:32:41 +01:00
__block TSOutgoingMessage *message;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
message = [ThreadUtil enqueueMessageWithText:messageText
mediaAttachments:attachments
inThread:self.thread
quotedReplyModel:self.inputToolbar.quotedReply
linkPreviewDraft:nil
transaction:transaction];
}];
[self messageWasSent:message];
if (didAddToProfileWhitelist) {
2019-02-19 23:49:40 +01:00
[self.conversationViewModel ensureDynamicInteractionsAndUpdateIfNecessary:YES];
}
2020-07-21 07:05:16 +02:00
if ([self.thread isKindOfClass:TSContactThread.class]) {
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[LKSessionManagementProtocol sendSessionRequestIfNeededToPublicKey:self.thread.contactIdentifier transaction:transaction];
}];
}
});
}
2019-01-10 15:45:48 +01:00
- (void)keyboardWillShow:(NSNotification *)notification
{
[self handleKeyboardNotification:notification];
}
- (void)keyboardDidShow:(NSNotification *)notification
{
[self handleKeyboardNotification:notification];
}
- (void)keyboardWillHide:(NSNotification *)notification
{
[self handleKeyboardNotification:notification];
}
- (void)keyboardDidHide:(NSNotification *)notification
{
[self handleKeyboardNotification:notification];
}
- (void)keyboardWillChangeFrame:(NSNotification *)notification
{
2019-01-10 15:45:48 +01:00
[self handleKeyboardNotification:notification];
}
- (void)keyboardDidChangeFrame:(NSNotification *)notification
{
[self handleKeyboardNotification:notification];
}
- (void)handleKeyboardNotification:(NSNotification *)notification
{
2018-04-11 21:17:34 +02:00
OWSAssertIsOnMainThread();
NSDictionary *userInfo = [notification userInfo];
NSValue *_Nullable keyboardBeginFrameValue = userInfo[UIKeyboardFrameBeginUserInfoKey];
if (!keyboardBeginFrameValue) {
OWSFailDebug(@"Missing keyboard begin frame");
return;
}
NSValue *_Nullable keyboardEndFrameValue = userInfo[UIKeyboardFrameEndUserInfoKey];
if (!keyboardEndFrameValue) {
OWSFailDebug(@"Missing keyboard end frame");
return;
}
CGRect keyboardEndFrame = [keyboardEndFrameValue CGRectValue];
CGRect keyboardEndFrameConverted = [self.view convertRect:keyboardEndFrame fromView:nil];
UIEdgeInsets oldInsets = self.collectionView.contentInset;
UIEdgeInsets newInsets = oldInsets;
2019-03-22 21:53:55 +01:00
// Measures how far the keyboard "intrudes" into the collection view's content region.
// Indicates how large the bottom content inset should be in order to avoid the keyboard
// from hiding the conversation content.
//
2019-03-22 21:53:55 +01:00
// NOTE: we can ignore the "bottomLayoutGuide" (i.e. the notch); this will be accounted
// for by the "adjustedContentInset".
CGFloat keyboardContentOverlap
= MAX(0, self.view.height - self.bottomLayoutGuide.length - keyboardEndFrameConverted.origin.y);
// For the sake of continuity, we want to maintain the same contentInsetBottom when the
// the keyboard/input accessory are hidden, e.g. during dismissal animations, when
// presenting popups like the attachment picker, etc.
//
2019-03-22 21:53:55 +01:00
// Therefore, we only zero out the contentInsetBottom if the inputAccessoryView is nil.
if (self.inputAccessoryView == nil || keyboardContentOverlap > 0) {
self.contentInsetBottom = keyboardContentOverlap;
} else if (!CurrentAppContext().isAppForegroundAndActive) {
// If app is not active, we'll dismiss the keyboard
// so only reserve enough space for the input accessory
// view. Otherwise, the content will animate into place
// when the app returns from the background.
//
// NOTE: There are two separate cases. If the keyboard is
// dismissed, the inputAccessoryView grows to allow
// space for the notch. In this case, we need to
// subtract bottomLayoutGuide. However, if the
// keyboard is presented we don't want to do that.
// I don't see a simple, safe way to distinguish
// these two cases. Therefore, I'm _always_
// subtracting bottomLayoutGuide. This will cause
// a slight animation when returning to the app
// but it will "match" the presentation animation
// of the input accessory.
self.contentInsetBottom = MAX(0, self.inputAccessoryView.height - self.bottomLayoutGuide.length);
2019-03-22 21:53:55 +01:00
}
2019-03-22 21:53:55 +01:00
newInsets.top = 0 + self.extraContentInsetPadding;
newInsets.bottom = self.contentInsetBottom + self.extraContentInsetPadding;
2019-03-18 16:24:09 +01:00
BOOL wasScrolledToBottom = [self isScrolledToBottom];
void (^adjustInsets)(void) = ^(void) {
2018-08-03 22:30:13 +02:00
if (!UIEdgeInsetsEqualToEdgeInsets(self.collectionView.contentInset, newInsets)) {
self.collectionView.contentInset = newInsets;
}
self.collectionView.scrollIndicatorInsets = newInsets;
// Note there is a bug in iOS11.2 which where switching to the emoji keyboard
// does not fire a UIKeyboardFrameWillChange notification. In that case, the scroll
// down button gets mostly obscured by the keyboard.
// RADAR: #36297652
[self updateScrollDownButtonLayout];
// Update the layout of the scroll down button immediately.
// This change might be animated by the keyboard notification.
[self.scrollDownButton.superview layoutIfNeeded];
// Adjust content offset to prevent the presented keyboard from obscuring content.
if (!self.viewHasEverAppeared) {
[self scrollToDefaultPosition:NO];
} else if (wasScrolledToBottom) {
// If we were scrolled to the bottom, don't do any fancy math. Just stay at the bottom.
[self scrollToBottomAnimated:NO];
} else if (self.isViewCompletelyAppeared) {
// If we were scrolled away from the bottom, shift the content in lockstep with the
// keyboard, up to the limits of the content bounds.
CGFloat insetChange = newInsets.bottom - oldInsets.bottom;
CGFloat oldYOffset = self.collectionView.contentOffset.y;
2018-04-11 20:18:09 +02:00
CGFloat newYOffset = CGFloatClamp(oldYOffset + insetChange, 0, self.safeContentHeight);
CGPoint newOffset = CGPointMake(0, newYOffset);
// If the user is dismissing the keyboard via interactive scrolling, any additional conset offset feels
// redundant, so we only adjust content offset when *presenting* the keyboard (i.e. when insetChange > 0).
if (insetChange > 0 && newYOffset > keyboardEndFrame.origin.y) {
[self.collectionView setContentOffset:newOffset animated:NO];
}
}
};
if (self.shouldAnimateKeyboardChanges && CurrentAppContext().isAppForegroundAndActive) {
adjustInsets();
} else {
// Even though we are scrolling without explicitly animating, the notification seems to occur within the context
// of a system animation, which is desirable when the view is visible, because the user sees the content rise
// in sync with the keyboard. However, when the view hasn't yet been presented, the animation conflicts and the
// result is that initial load causes the collection cells to visably "animate" to their final position once the
// view appears.
[UIView performWithoutAnimation:adjustInsets];
}
}
2018-07-23 21:36:39 +02:00
- (void)applyTheme
{
OWSAssertIsOnMainThread();
// make sure toolbar extends below iPhoneX home button.
self.view.backgroundColor = Theme.toolbarBackgroundColor;
self.collectionView.backgroundColor = Theme.backgroundColor;
}
#pragma mark - AttachmentApprovalViewControllerDelegate
2018-11-06 20:30:35 +01:00
- (void)attachmentApproval:(AttachmentApprovalViewController *)attachmentApproval
didApproveAttachments:(NSArray<SignalAttachment *> *)attachments
messageText:(NSString *_Nullable)messageText
{
[self tryToSendAttachments:attachments messageText:messageText];
2019-03-28 20:02:09 +01:00
[self.inputToolbar clearTextMessageAnimated:NO];
2019-10-11 06:52:56 +02:00
[self resetMentions];
[self dismissViewControllerAnimated:YES completion:nil];
2019-03-28 20:02:09 +01:00
// We always want to scroll to the bottom of the conversation after the local user
// sends a message. Normally, this is taken care of in yapDatabaseModified:, but
// we don't listen to db modifications when this view isn't visible, i.e. when the
// attachment approval view is presented.
[self scrollToBottomAnimated:NO];
}
- (void)attachmentApprovalDidCancel:(AttachmentApprovalViewController *)attachmentApproval
{
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)attachmentApproval:(AttachmentApprovalViewController *)attachmentApproval
didChangeMessageText:(nullable NSString *)newMessageText
{
[self.inputToolbar setMessageText:newMessageText animated:NO];
}
#pragma mark -
- (void)showErrorAlertForAttachment:(SignalAttachment *_Nullable)attachment
{
OWSAssertDebug(attachment == nil || [attachment hasError]);
NSString *errorMessage
= (attachment ? [attachment localizedErrorDescription] : [SignalAttachment missingDataErrorMessage]);
OWSLogError(@": %@", errorMessage);
2017-09-08 16:28:21 +02:00
[OWSAlerts showAlertWithTitle:NSLocalizedString(
@"ATTACHMENT_ERROR_ALERT_TITLE", @"The title of the 'attachment error' alert.")
message:errorMessage];
}
2017-10-10 22:13:54 +02:00
- (CGFloat)safeContentHeight
{
// Don't use self.collectionView.contentSize.height as the collection view's
// content size might not be set yet.
//
// We can safely call prepareLayout to ensure the layout state is up-to-date
// since our layout uses a dirty flag internally to debounce redundant work.
[self.layout prepareLayout];
2017-10-10 22:13:54 +02:00
return [self.collectionView.collectionViewLayout collectionViewContentSize].height;
}
- (void)scrollToBottomAnimated:(BOOL)animated
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
if (self.isUserScrolling) {
return;
}
2018-01-16 21:27:53 +01:00
// Ensure the view is fully layed out before we try to scroll to the bottom, since
// we use the collectionView bounds to determine where the "bottom" is.
[self.view layoutIfNeeded];
const CGFloat topInset = ^{
if (@available(iOS 11, *)) {
return -self.collectionView.adjustedContentInset.top;
} else {
return -self.collectionView.contentInset.top;
}
}();
const CGFloat bottomInset = ^{
if (@available(iOS 11, *)) {
return -self.collectionView.adjustedContentInset.bottom;
} else {
return -self.collectionView.contentInset.bottom;
}
}();
const CGFloat firstContentPageTop = topInset;
const CGFloat collectionViewUnobscuredHeight = self.collectionView.bounds.size.height + bottomInset;
const CGFloat lastContentPageTop = self.safeContentHeight - collectionViewUnobscuredHeight;
CGFloat dstY = MAX(firstContentPageTop, lastContentPageTop);
[self.collectionView setContentOffset:CGPointMake(0, dstY) animated:animated];
[self didScrollToBottom];
}
- (void)scrollToFirstUnreadMessage:(BOOL)isAnimated
{
[self scrollToDefaultPosition:isAnimated];
}
#pragma mark - UIScrollViewDelegate
- (void)updateLastKnownDistanceFromBottom
{
// Never update the lastKnownDistanceFromBottom,
// if we're presenting the menu actions which
// temporarily meddles with the content insets.
if (!OWSWindowManager.sharedManager.isPresentingMenuActions) {
self.lastKnownDistanceFromBottom = @(self.safeDistanceFromBottom);
}
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
// Constantly try to update the lastKnownDistanceFromBottom.
[self updateLastKnownDistanceFromBottom];
[self updateLastVisibleSortId];
[self.autoLoadMoreTimer invalidate];
self.autoLoadMoreTimer = [NSTimer weakScheduledTimerWithTimeInterval:0.1f
target:self
selector:@selector(autoLoadMoreTimerDidFire)
userInfo:nil
repeats:NO];
}
- (void)autoLoadMoreTimerDidFire
{
[self autoLoadMoreIfNecessary];
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
self.userHasScrolled = YES;
self.isUserScrolling = YES;
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
self.isUserScrolling = NO;
}
#pragma mark - OWSConversationSettingsViewDelegate
- (void)resendGroupUpdateForErrorMessage:(TSErrorMessage *)message
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
OWSAssertDebug([_thread isKindOfClass:[TSGroupThread class]]);
OWSAssertDebug(message);
TSGroupThread *groupThread = (TSGroupThread *)self.thread;
TSGroupModel *groupModel = groupThread.groupModel;
[self updateGroupModelTo:groupModel
successCompletion:^{
OWSLogInfo(@"Group updated, removing group creation error.");
[message remove];
}];
}
2018-06-28 19:28:14 +02:00
- (void)conversationColorWasUpdated
{
[self.conversationStyle updateProperties];
2018-08-03 19:10:11 +02:00
[self resetContentAndLayout];
2018-06-28 19:28:14 +02:00
}
- (void)groupWasUpdated:(TSGroupModel *)groupModel
{
OWSAssertDebug(groupModel);
NSMutableSet *groupMemberIds = [NSMutableSet setWithArray:groupModel.groupMemberIds];
2018-12-21 19:03:09 +01:00
[groupMemberIds addObject:self.tsAccountManager.localNumber];
groupModel.groupMemberIds = [NSMutableArray arrayWithArray:[groupMemberIds allObjects]];
[self updateGroupModelTo:groupModel successCompletion:nil];
}
- (void)popAllConversationSettingsViewsWithCompletion:(void (^_Nullable)(void))completionBlock
{
2017-05-02 18:30:53 +02:00
if (self.presentedViewController) {
[self.presentedViewController dismissViewControllerAnimated:YES
completion:^{
[self.navigationController
popToViewController:self
animated:YES
completion:completionBlock];
}];
2017-05-02 18:30:53 +02:00
} else {
[self.navigationController popToViewController:self animated:YES completion:completionBlock];
}
}
#pragma mark - Conversation Search
- (void)conversationSettingsDidRequestConversationSearch:(OWSConversationSettingsViewController *)conversationSettingsViewController
{
[self showSearchUI];
[self popAllConversationSettingsViewsWithCompletion:^{
// This delay is unfortunate, but without it, self.searchController.uiSearchController.searchBar
// isn't yet ready to become first responder. Presumably we're still mid transition.
// A hardcorded constant like this isn't great because it's either too slow, making our users
// wait, or too fast, and fails to wait long enough to be ready to become first responder.
// Luckily in this case the stakes aren't catastrophic. In the case that we're too aggressive
// the user will just have to manually tap into the search field before typing.
// Leaving this assert in as proof that we're not ready to become first responder yet.
// If this assert fails, *great* maybe we can get rid of this delay.
OWSAssertDebug(![self.searchController.uiSearchController.searchBar canBecomeFirstResponder]);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.searchController.uiSearchController.searchBar becomeFirstResponder];
});
}];
}
- (void)showSearchUI
{
self.isShowingSearchUI = YES;
UISearchBar *searchBar = self.searchController.uiSearchController.searchBar;
searchBar.searchBarStyle = UISearchBarStyleMinimal;
searchBar.barStyle = UIBarStyleBlack;
searchBar.tintColor = LKColors.accent;
UIImage *searchImage = [[UIImage imageNamed:@"searchbar_search"] asTintedImageWithColor:LKColors.searchBarPlaceholder];
[searchBar setImage:searchImage forSearchBarIcon:UISearchBarIconSearch state:UIControlStateNormal];
UIImage *clearImage = [[UIImage imageNamed:@"searchbar_clear"] asTintedImageWithColor:LKColors.searchBarPlaceholder];
[searchBar setImage:clearImage forSearchBarIcon:UISearchBarIconClear state:UIControlStateNormal];
2020-01-21 01:30:01 +01:00
UITextField *searchTextField;
if (@available(iOS 13, *)) {
searchTextField = searchBar.searchTextField;
} else {
searchTextField = (UITextField *)[searchBar valueForKey:@"_searchField"];
}
searchTextField.backgroundColor = LKColors.searchBarBackground;
searchTextField.textColor = LKColors.text;
searchTextField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:NSLocalizedString(@"Search", @"") attributes:@{ NSForegroundColorAttributeName : LKColors.searchBarPlaceholder }];
2020-03-17 06:18:53 +01:00
searchBar.keyboardAppearance = LKAppModeUtilities.isLightMode ? UIKeyboardAppearanceDefault : UIKeyboardAppearanceDark;
[searchBar setPositionAdjustment:UIOffsetMake(4, 0) forSearchBarIcon:UISearchBarIconSearch];
[searchBar setSearchTextPositionAdjustment:UIOffsetMake(2, 0)];
[searchBar setPositionAdjustment:UIOffsetMake(-4, 0) forSearchBarIcon:UISearchBarIconClear];
// Note: setting a searchBar as the titleView causes UIKit to render the navBar
// *slightly* taller (44pt -> 56pt)
self.navigationItem.titleView = searchBar;
[self updateBarButtonItems];
// Hack so that the ResultsBar stays on the screen when dismissing the search field
// keyboard.
//
// Details:
//
// When the search UI is activated, both the SearchField and the ConversationVC
// have the resultsBar as their inputAccessoryView.
//
// So when the SearchField is first responder, the ResultsBar is shown on top of the keyboard.
// When the ConversationVC is first responder, the ResultsBar is shown at the bottom of the
// screen.
//
// When the user swipes to dismiss the keyboard, trying to see more of the content while
// searching, we want the ResultsBar to stay at the bottom of the screen - that is, we
// want the ConversationVC to becomeFirstResponder.
//
// If the SearchField were a subview of ConversationVC.view, this would all be automatic,
// as first responder status is percolated up the responder chain via `nextResponder`, which
// basically travereses each superView, until you're at a rootView, at which point the next
// responder is the ViewController which controls that View.
//
// However, because SearchField lives in the Navbar, it's "controlled" by the
// NavigationController, not the ConversationVC.
//
// So here we stub the next responder on the navBar so that when the searchBar resigns
// first responder, the ConversationVC will be in it's responder chain - keeeping the
// ResultsBar on the bottom of the screen after dismissing the keyboard.
if (![self.navigationController.navigationBar isKindOfClass:[OWSNavigationBar class]]) {
OWSFailDebug(@"unexpected navigationController: %@", self.navigationController);
return;
}
OWSNavigationBar *navBar = (OWSNavigationBar *)self.navigationController.navigationBar;
navBar.stubbedNextResponder = self;
}
- (void)hideSearchUI
{
self.isShowingSearchUI = NO;
self.navigationItem.titleView = self.headerView;
[self updateBarButtonItems];
if (![self.navigationController.navigationBar isKindOfClass:[OWSNavigationBar class]]) {
OWSFailDebug(@"unexpected navigationController: %@", self.navigationController);
return;
}
OWSNavigationBar *navBar = (OWSNavigationBar *)self.navigationController.navigationBar;
OWSAssertDebug(navBar.stubbedNextResponder == self);
navBar.stubbedNextResponder = nil;
// restore first responder to VC
[self becomeFirstResponder];
2019-03-29 18:07:15 +01:00
if (@available(iOS 10, *)) {
[self reloadInputViews];
} else {
// We want to change the inputAccessoryView from SearchResults -> MessageInput
// reloading too soon on an old iOS9 device caused the inputAccessoryView to go from
// SearchResults -> MessageInput -> SearchResults
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self reloadInputViews];
});
}
}
#pragma mark ConversationSearchControllerDelegate
- (void)didDismissSearchController:(UISearchController *)searchController
{
OWSLogVerbose(@"");
OWSAssertIsOnMainThread();
[self hideSearchUI];
}
- (void)conversationSearchController:(ConversationSearchController *)conversationSearchController
didUpdateSearchResults:(nullable ConversationScreenSearchResultSet *)conversationScreenSearchResultSet
{
OWSAssertIsOnMainThread();
OWSLogInfo(@"conversationScreenSearchResultSet: %@", conversationScreenSearchResultSet.debugDescription);
self.lastSearchedText = conversationScreenSearchResultSet.searchText;
[UIView performWithoutAnimation:^{
[self.collectionView reloadItemsAtIndexPaths:self.collectionView.indexPathsForVisibleItems];
}];
if (conversationScreenSearchResultSet) {
[BenchManager completeEventWithEventId:self.lastSearchedText];
2017-05-02 18:30:53 +02:00
}
}
- (void)conversationSearchController:(ConversationSearchController *)conversationSearchController
didSelectMessageId:(NSString *)messageId
{
OWSLogDebug(@"messageId: %@", messageId);
[self scrollToInteractionId:messageId];
[BenchManager completeEventWithEventId:[NSString stringWithFormat:@"Conversation Search Nav: %@", messageId]];
}
- (void)scrollToInteractionId:(NSString *)interactionId
{
NSIndexPath *_Nullable indexPath = [self.conversationViewModel ensureLoadWindowContainsInteractionId:interactionId];
if (!indexPath) {
OWSFailDebug(@"unable to find indexPath");
return;
}
[self.collectionView scrollToItemAtIndexPath:indexPath
atScrollPosition:UICollectionViewScrollPositionCenteredVertically
animated:YES];
}
2017-10-10 22:13:54 +02:00
#pragma mark - ConversationViewLayoutDelegate
- (NSArray<id<ConversationViewLayoutItem>> *)layoutItems
{
return self.viewItems;
}
- (CGFloat)layoutHeaderHeight
{
return (self.showLoadMoreHeader ? kLoadMoreHeaderHeight : 0.f);
}
2017-10-10 22:13:54 +02:00
#pragma mark - ConversationInputToolbarDelegate
- (void)sendButtonPressed
{
[BenchManager startEventWithTitle:@"Send Message" eventId:@"message-send"];
[BenchManager startEventWithTitle:@"Send Message milestone: clearTextMessageAnimated completed"
eventId:@"fromSendUntil_clearTextMessageAnimated"];
[BenchManager startEventWithTitle:@"Send Message milestone: toggleDefaultKeyboard completed"
eventId:@"fromSendUntil_toggleDefaultKeyboard"];
2019-10-11 06:52:56 +02:00
[self.inputToolbar hideMentionCandidateSelectionView];
2019-10-11 01:45:24 +02:00
[self tryToSendTextMessage:[self getSendText] updateKeyboardState:YES];
2017-10-10 22:13:54 +02:00
}
- (void)tryToSendTextMessage:(NSString *)text updateKeyboardState:(BOOL)updateKeyboardState
{
OWSAssertIsOnMainThread();
2017-10-10 22:13:54 +02:00
__weak ConversationViewController *weakSelf = self;
2018-09-09 19:46:23 +02:00
if ([self isBlockedConversation]) {
[self showUnblockConversationUI:^(BOOL isBlocked) {
2017-10-10 22:13:54 +02:00
if (!isBlocked) {
[weakSelf tryToSendTextMessage:text updateKeyboardState:NO];
}
}];
return;
}
BOOL didShowSNAlert =
[self showSafetyNumberConfirmationIfNecessaryWithConfirmationText:[SafetyNumberStrings confirmSendButton]
completion:^(BOOL didConfirmIdentity) {
if (didConfirmIdentity) {
[weakSelf tryToSendTextMessage:text
updateKeyboardState:NO];
}
}];
if (didShowSNAlert) {
return;
}
2017-10-18 20:53:31 +02:00
text = [text ows_stripped];
2017-10-10 22:13:54 +02:00
if (text.length < 1) {
return;
}
// Limit outgoing text messages to 16kb.
//
// We convert large text messages to attachments
// which are presented as normal text messages.
BOOL didAddToProfileWhitelist = [ThreadUtil addThreadToProfileWhitelistIfEmptyContactThread:self.thread];
__block TSOutgoingMessage *message;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
message = [ThreadUtil enqueueMessageWithText:text
inThread:self.thread
quotedReplyModel:self.inputToolbar.quotedReply
linkPreviewDraft:self.inputToolbar.linkPreviewDraft
transaction:transaction];
}];
[self.conversationViewModel appendUnsavedOutgoingTextMessage:message];
2017-10-10 22:13:54 +02:00
[self messageWasSent:message];
// Clearing the text message is a key part of the send animation.
// It takes 10-15ms, but we do it inline rather than dispatch async
// since the send can't feel "complete" without it.
[BenchManager benchWithTitle:@"clearTextMessageAnimated"
block:^{
[self.inputToolbar clearTextMessageAnimated:YES];
2019-10-11 06:52:56 +02:00
[self resetMentions];
}];
[BenchManager completeEventWithEventId:@"fromSendUntil_clearTextMessageAnimated"];
dispatch_async(dispatch_get_main_queue(), ^{
// After sending we want to return from the numeric keyboard to the
// alphabetical one. Because this is so slow (40-50ms), we prefer it
// happens async, after any more essential send UI work is done.
[BenchManager benchWithTitle:@"toggleDefaultKeyboard"
block:^{
[self.inputToolbar toggleDefaultKeyboard];
}];
[BenchManager completeEventWithEventId:@"fromSendUntil_toggleDefaultKeyboard"];
});
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[self.thread setDraft:@"" transaction:transaction];
}];
2017-10-10 22:13:54 +02:00
if (didAddToProfileWhitelist) {
2019-02-19 23:49:40 +01:00
[self.conversationViewModel ensureDynamicInteractionsAndUpdateIfNecessary:YES];
2017-10-10 22:13:54 +02:00
}
2020-07-21 07:05:16 +02:00
if ([self.thread isKindOfClass:TSContactThread.class]) {
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[LKSessionManagementProtocol sendSessionRequestIfNeededToPublicKey:self.thread.contactIdentifier transaction:transaction];
}];
}
2017-10-10 22:13:54 +02:00
}
- (void)voiceMemoGestureDidStart
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
2017-10-10 22:13:54 +02:00
OWSLogInfo(@"voiceMemoGestureDidStart");
2017-10-10 22:13:54 +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.
[self.inputToolbar cancelVoiceMemoIfNecessary];
[self.inputToolbar hideVoiceMemoUI:NO];
[self cancelRecordingVoiceMemo];
return;
}
[self.inputToolbar showVoiceMemoUI];
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
[self requestRecordingVoiceMemo];
}
2019-02-06 06:36:03 +01:00
- (void)voiceMemoGestureDidComplete
2017-10-10 22:13:54 +02:00
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
2017-10-10 22:13:54 +02:00
2019-02-06 06:36:03 +01:00
OWSLogInfo(@"");
2017-10-10 22:13:54 +02:00
[self.inputToolbar hideVoiceMemoUI:YES];
[self endRecordingVoiceMemo];
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
}
2019-02-06 06:36:03 +01:00
- (void)voiceMemoGestureDidLock
{
OWSAssertIsOnMainThread();
OWSLogInfo(@"");
[self.inputToolbar lockVoiceMemoUI];
}
2017-10-10 22:13:54 +02:00
- (void)voiceMemoGestureDidCancel
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
2017-10-10 22:13:54 +02:00
OWSLogInfo(@"voiceMemoGestureDidCancel");
2017-10-10 22:13:54 +02:00
[self.inputToolbar hideVoiceMemoUI:NO];
[self cancelRecordingVoiceMemo];
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
}
2019-02-06 06:36:03 +01:00
- (void)voiceMemoGestureDidUpdateCancelWithRatioComplete:(CGFloat)cancelAlpha
2017-10-10 22:13:54 +02:00
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
2017-10-10 22:13:54 +02:00
[self.inputToolbar setVoiceMemoUICancelAlpha:cancelAlpha];
}
2017-10-10 22:13:54 +02:00
- (void)cancelVoiceMemo
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
2017-10-10 22:13:54 +02:00
[self.inputToolbar cancelVoiceMemoIfNecessary];
[self.inputToolbar hideVoiceMemoUI:NO];
[self cancelRecordingVoiceMemo];
}
#pragma mark - Database Observation
- (void)setIsUserScrolling:(BOOL)isUserScrolling
{
_isUserScrolling = isUserScrolling;
[self autoLoadMoreIfNecessary];
}
- (void)setIsViewVisible:(BOOL)isViewVisible
{
_isViewVisible = isViewVisible;
[self updateCellsVisible];
}
- (void)updateCellsVisible
{
BOOL isAppInBackground = CurrentAppContext().isInBackground;
BOOL isCellVisible = self.isViewVisible && !isAppInBackground;
for (ConversationViewCell *cell in self.collectionView.visibleCells) {
cell.isCellVisible = isCellVisible;
}
}
2018-02-22 17:03:53 +01:00
- (nullable NSIndexPath *)firstIndexPathAtViewHorizonTimestamp
{
2018-02-22 17:03:53 +01:00
if (!self.viewHorizonTimestamp) {
return nil;
}
if (self.viewItems.count < 1) {
return nil;
}
2018-02-22 17:03:53 +01:00
uint64_t viewHorizonTimestamp = self.viewHorizonTimestamp.unsignedLongLongValue;
// Binary search for the first view item whose timestamp >= the "view horizon" timestamp.
2018-02-22 16:53:13 +01:00
// We want to move "left" rightward, discarding interactions before this cutoff.
// We want to move "right" leftward, discarding all-but-the-first interaction after this cutoff.
// In the end, if we converge on an item _after_ this cutoff, it's the one we want.
// If we converge on an item _before_ this cutoff, there was no interaction that fit our criteria.
NSUInteger left = 0, right = self.viewItems.count - 1;
while (left != right) {
OWSAssertDebug(left < right);
NSUInteger mid = (left + right) / 2;
OWSAssertDebug(left <= mid);
OWSAssertDebug(mid < right);
2018-09-28 00:49:01 +02:00
id<ConversationViewItem> viewItem = self.viewItems[mid];
2018-02-22 17:03:53 +01:00
if (viewItem.interaction.timestamp >= viewHorizonTimestamp) {
right = mid;
} else {
// This is an optimization; it also ensures that we converge.
left = mid + 1;
}
}
OWSAssertDebug(left == right);
2018-09-28 00:49:01 +02:00
id<ConversationViewItem> viewItem = self.viewItems[left];
2018-02-22 17:03:53 +01:00
if (viewItem.interaction.timestamp >= viewHorizonTimestamp) {
OWSLogInfo(@"firstIndexPathAtViewHorizonTimestamp: %zd / %zd", left, self.viewItems.count);
return [NSIndexPath indexPathForRow:(NSInteger) left inSection:0];
} else {
OWSLogInfo(@"firstIndexPathAtViewHorizonTimestamp: none / %zd", self.viewItems.count);
return nil;
}
}
2017-10-10 22:13:54 +02:00
#pragma mark - ConversationCollectionViewDelegate
2019-01-08 22:38:18 +01:00
- (void)collectionViewWillChangeSizeFrom:(CGSize)oldSize to:(CGSize)newSize
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
}
2019-01-08 22:38:18 +01:00
- (void)collectionViewDidChangeSizeFrom:(CGSize)oldSize to:(CGSize)newSize
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
2019-01-09 15:27:35 +01:00
if (oldSize.width != newSize.width) {
[self resetForSizeOrOrientationChange];
}
2019-01-09 15:15:33 +01:00
[self updateLastVisibleSortId];
}
2017-10-10 22:13:54 +02:00
#pragma mark - View Items
2018-09-28 00:49:01 +02:00
- (nullable id<ConversationViewItem>)viewItemForIndex:(NSInteger)index
2017-10-10 22:13:54 +02:00
{
2017-10-19 15:53:35 +02:00
if (index < 0 || index >= (NSInteger)self.viewItems.count) {
OWSFailDebug(@"Invalid view item index: %lu", (unsigned long)index);
2017-10-12 22:19:07 +02:00
return nil;
2017-10-10 22:13:54 +02:00
}
2017-10-20 15:53:33 +02:00
return self.viewItems[(NSUInteger)index];
2017-10-10 22:13:54 +02:00
}
#pragma mark - UICollectionViewDataSource
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return (NSInteger)self.viewItems.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
2018-09-28 00:49:01 +02:00
id<ConversationViewItem> _Nullable viewItem = [self viewItemForIndex:indexPath.row];
2017-10-10 22:13:54 +02:00
ConversationViewCell *cell = [viewItem dequeueCellForCollectionView:self.collectionView indexPath:indexPath];
if (!cell) {
OWSFailDebug(@"Could not dequeue cell.");
2017-10-10 22:13:54 +02:00
return cell;
}
cell.viewItem = viewItem;
cell.delegate = self;
if ([cell isKindOfClass:[OWSMessageCell class]]) {
OWSMessageCell *messageCell = (OWSMessageCell *)cell;
messageCell.messageBubbleView.delegate = self;
}
2018-06-25 21:20:17 +02:00
cell.conversationStyle = self.conversationStyle;
2017-10-10 22:13:54 +02:00
2018-08-09 16:47:43 +02:00
[cell loadForDisplay];
2017-10-10 22:13:54 +02:00
// TODO: Confirm with nancy if this will work.
NSString *cellName = [NSString stringWithFormat:@"interaction.%@", NSUUID.UUID.UUIDString];
cell.accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, cellName);
2017-10-10 22:13:54 +02:00
return cell;
}
#pragma mark - UICollectionViewDelegate
2017-10-10 22:13:54 +02:00
- (void)collectionView:(UICollectionView *)collectionView
willDisplayCell:(UICollectionViewCell *)cell
forItemAtIndexPath:(NSIndexPath *)indexPath
{
OWSAssertDebug([cell isKindOfClass:[ConversationViewCell class]]);
2017-10-10 22:13:54 +02:00
ConversationViewCell *conversationViewCell = (ConversationViewCell *)cell;
conversationViewCell.isCellVisible = YES;
}
2017-10-10 22:13:54 +02:00
- (void)collectionView:(UICollectionView *)collectionView
didEndDisplayingCell:(nonnull UICollectionViewCell *)cell
forItemAtIndexPath:(nonnull NSIndexPath *)indexPath
{
OWSAssertDebug([cell isKindOfClass:[ConversationViewCell class]]);
2017-10-10 22:13:54 +02:00
ConversationViewCell *conversationViewCell = (ConversationViewCell *)cell;
conversationViewCell.isCellVisible = NO;
}
// We use this hook to ensure scroll state continuity. As the collection
// view's content size changes, we want to keep the same cells in view.
- (CGPoint)collectionView:(UICollectionView *)collectionView
targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
{
2019-10-04 08:52:38 +02:00
if (@available(iOS 13, *)) {
2019-10-04 08:52:38 +02:00
} else {
if (self.menuActionsViewController != nil) {
NSValue *_Nullable contentOffset = [self contentOffsetForMenuActionInteraction];
if (contentOffset != nil) {
return contentOffset.CGPointValue;
}
2019-03-18 16:24:09 +01:00
}
}
if (self.scrollContinuity == kScrollContinuityBottom && self.lastKnownDistanceFromBottom) {
NSValue *_Nullable contentOffset =
[self contentOffsetForLastKnownDistanceFromBottom:self.lastKnownDistanceFromBottom.floatValue];
if (contentOffset) {
proposedContentOffset = contentOffset.CGPointValue;
}
}
return proposedContentOffset;
}
// We use this hook to ensure scroll state continuity. As the collection
// view's content size changes, we want to keep the same cells in view.
- (nullable NSValue *)contentOffsetForLastKnownDistanceFromBottom:(CGFloat)lastKnownDistanceFromBottom
{
// Adjust the content offset to reflect the "last known" distance
// from the bottom of the content.
CGFloat contentOffsetYBottom = self.maxContentOffsetY;
CGFloat contentOffsetY = contentOffsetYBottom - MAX(0, lastKnownDistanceFromBottom);
CGFloat minContentOffsetY;
if (@available(iOS 11, *)) {
minContentOffsetY = -self.collectionView.safeAreaInsets.top;
} else {
minContentOffsetY = 0.f;
}
contentOffsetY = MAX(minContentOffsetY, contentOffsetY);
return [NSValue valueWithCGPoint:CGPointMake(0, contentOffsetY)];
}
#pragma mark - Scroll State
- (BOOL)isScrolledToBottom
{
CGFloat distanceFromBottom = self.safeDistanceFromBottom;
const CGFloat kIsAtBottomTolerancePts = 5;
BOOL isScrolledToBottom = distanceFromBottom <= kIsAtBottomTolerancePts;
return isScrolledToBottom;
}
- (CGFloat)safeDistanceFromBottom
{
// This is a bit subtle.
//
// The _wrong_ way to determine if we're scrolled to the bottom is to
// measure whether the collection view's content is "near" the bottom edge
// of the collection view. This is wrong because the collection view
// might not have enough content to fill the collection view's bounds
// _under certain conditions_ (e.g. with the keyboard dismissed).
//
// What we're really interested in is something a bit more subtle:
// "Is the scroll view scrolled down as far as it can, "at rest".
//
// To determine that, we find the appropriate "content offset y" if
// the scroll view were scrolled down as far as possible. IFF the
// actual "content offset y" is "near" that value, we return YES.
CGFloat maxContentOffsetY = self.maxContentOffsetY;
CGFloat distanceFromBottom = maxContentOffsetY - self.collectionView.contentOffset.y;
return distanceFromBottom;
}
- (CGFloat)maxContentOffsetY
{
CGFloat contentHeight = self.safeContentHeight;
UIEdgeInsets adjustedContentInset;
if (@available(iOS 11, *)) {
adjustedContentInset = self.collectionView.adjustedContentInset;
} else {
adjustedContentInset = self.collectionView.contentInset;
}
// Note the usage of MAX() to handle the case where there isn't enough
// content to fill the collection view at its current size.
CGFloat maxContentOffsetY = contentHeight + adjustedContentInset.bottom - self.collectionView.bounds.size.height;
return maxContentOffsetY;
}
2018-05-01 22:38:54 +02:00
#pragma mark - ContactsPickerDelegate
- (void)contactsPickerDidCancel:(ContactsPicker *)contactsPicker
{
OWSLogDebug(@"");
[self dismissViewControllerAnimated:YES completion:nil];
2018-05-01 22:38:54 +02:00
}
- (void)contactsPicker:(ContactsPicker *)contactsPicker contactFetchDidFail:(NSError *)error
{
OWSLogDebug(@"with error %@", error);
[self dismissViewControllerAnimated:YES completion:nil];
2018-05-01 22:38:54 +02:00
}
- (void)contactsPicker:(ContactsPicker *)contactsPicker didSelectContact:(Contact *)contact
{
OWSAssertDebug(contact);
2018-06-18 23:20:27 +02:00
CNContact *_Nullable cnContact = [self.contactsManager cnContactWithId:contact.cnContactId];
if (!cnContact) {
OWSFailDebug(@"Could not load system contact.");
2018-06-18 23:20:27 +02:00
return;
}
2018-05-03 16:47:42 +02:00
OWSLogDebug(@"with contact: %@", contact);
2018-05-01 22:38:54 +02:00
2018-06-18 23:20:27 +02:00
OWSContact *_Nullable contactShareRecord = [OWSContacts contactForSystemContact:cnContact];
2018-05-05 04:32:29 +02:00
if (!contactShareRecord) {
OWSFailDebug(@"Could not convert system contact.");
2018-05-03 16:47:42 +02:00
return;
}
2018-05-05 04:32:29 +02:00
BOOL isProfileAvatar = NO;
NSData *_Nullable avatarImageData = [self.contactsManager avatarDataForCNContactId:cnContact.identifier];
2018-05-04 17:40:23 +02:00
for (NSString *recipientId in contact.textSecureIdentifiers) {
2018-05-10 03:10:23 +02:00
if (avatarImageData) {
2018-05-04 17:40:23 +02:00
break;
}
2018-05-10 03:10:23 +02:00
avatarImageData = [self.contactsManager profileImageDataForPhoneIdentifier:recipientId];
if (avatarImageData) {
2018-05-04 17:40:23 +02:00
isProfileAvatar = YES;
2018-05-05 04:32:29 +02:00
}
}
2018-05-04 17:40:23 +02:00
contactShareRecord.isProfileAvatar = isProfileAvatar;
2018-05-05 04:32:29 +02:00
ContactShareViewModel *contactShare =
2018-05-10 03:10:23 +02:00
[[ContactShareViewModel alloc] initWithContactShareRecord:contactShareRecord avatarImageData:avatarImageData];
2018-05-05 04:32:29 +02:00
2018-05-03 16:47:42 +02:00
// TODO: We should probably show this in the same navigation view controller.
ContactShareApprovalViewController *approveContactShare =
[[ContactShareApprovalViewController alloc] initWithContactShare:contactShare
contactsManager:self.contactsManager
delegate:self];
OWSAssertDebug(contactsPicker.navigationController);
[contactsPicker.navigationController pushViewController:approveContactShare animated:YES];
2018-05-01 22:38:54 +02:00
}
- (void)contactsPicker:(ContactsPicker *)contactsPicker didSelectMultipleContacts:(NSArray<Contact *> *)contacts
{
OWSFailDebug(@"with contacts: %@", contacts);
[self dismissViewControllerAnimated:YES completion:nil];
2018-05-01 22:38:54 +02:00
}
- (BOOL)contactsPicker:(ContactsPicker *)contactsPicker shouldSelectContact:(Contact *)contact
{
// Any reason to preclude contacts?
return YES;
}
#pragma mark - ContactShareApprovalViewControllerDelegate
2018-05-03 16:47:42 +02:00
- (void)approveContactShare:(ContactShareApprovalViewController *)approveContactShare
2018-05-05 04:32:29 +02:00
didApproveContactShare:(ContactShareViewModel *)contactShare
2018-05-03 16:47:42 +02:00
{
OWSLogInfo(@"");
2018-05-03 16:47:42 +02:00
2018-05-03 17:33:01 +02:00
[self dismissViewControllerAnimated:YES
completion:^{
[self sendContactShare:contactShare];
}];
2018-05-03 16:47:42 +02:00
}
- (void)approveContactShare:(ContactShareApprovalViewController *)approveContactShare
2018-05-05 04:32:29 +02:00
didCancelContactShare:(ContactShareViewModel *)contactShare
2018-05-03 17:33:01 +02:00
{
OWSLogInfo(@"");
2018-05-03 17:33:01 +02:00
[self dismissViewControllerAnimated:YES completion:nil];
}
2018-05-03 16:47:42 +02:00
#pragma mark - ContactShareViewHelperDelegate
- (void)didCreateOrEditContact
{
OWSLogInfo(@"");
[self dismissViewControllerAnimated:YES completion:nil];
}
#pragma mark - Toast
- (void)presentMissingQuotedReplyToast
{
OWSLogInfo(@"");
NSString *toastText = NSLocalizedString(@"QUOTED_REPLY_ORIGINAL_MESSAGE_DELETED",
@"Toast alert text shown when tapping on a quoted message which we cannot scroll to because the local copy of "
@"the message was since deleted.");
ToastController *toastController = [[ToastController alloc] initWithText:toastText];
2018-08-21 18:18:13 +02:00
CGFloat bottomInset = kToastInset + self.collectionView.contentInset.bottom + self.view.layoutMargins.bottom;
[toastController presentToastViewFromBottomOfView:self.view inset:bottomInset];
}
- (void)presentRemotelySourcedQuotedReplyToast
{
OWSLogInfo(@"");
NSString *toastText = NSLocalizedString(@"QUOTED_REPLY_ORIGINAL_MESSAGE_REMOTELY_SOURCED",
@"Toast alert text shown when tapping on a quoted message which we cannot scroll to because the local copy of "
@"the message didn't exist when the quote was received.");
ToastController *toastController = [[ToastController alloc] initWithText:toastText];
2018-08-21 18:18:13 +02:00
CGFloat bottomInset = kToastInset + self.collectionView.contentInset.bottom + self.view.layoutMargins.bottom;
[toastController presentToastViewFromBottomOfView:self.view inset:bottomInset];
}
#pragma mark -
- (void)presentViewController:(UIViewController *)viewController
animated:(BOOL)animated
completion:(void (^__nullable)(void))completion
{
// Ensure that we are first responder before presenting other views.
// This ensures that the input toolbar will be restored after the
// presented view is dismissed.
2018-06-01 18:01:20 +02:00
if (![self isFirstResponder]) {
[self becomeFirstResponder];
}
[super presentViewController:viewController animated:animated completion:completion];
}
2018-10-31 15:05:24 +01:00
#pragma mark - ConversationViewModelDelegate
- (void)conversationViewModelWillUpdate
{
OWSAssertIsOnMainThread();
OWSAssertDebug(self.conversationViewModel);
// 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
//
// NOTE: It's critical we do this before beginLongLivedReadTransaction.
// We want to relayout our contents using the old message mappings and
// view items before they are updated.
[self.collectionView layoutIfNeeded];
// ENDHACK to work around radar #28167779
}
- (void)conversationViewModelDidUpdate:(ConversationUpdate *)conversationUpdate
{
OWSAssertIsOnMainThread();
OWSAssertDebug(conversationUpdate);
OWSAssertDebug(self.conversationViewModel);
2019-03-19 16:13:06 +01:00
if (!self.viewLoaded) {
2019-03-19 16:15:09 +01:00
// It's safe to ignore updates before the view loads;
// viewWillAppear will call resetContentAndLayout.
2019-03-19 16:13:06 +01:00
return;
}
2019-03-18 16:24:09 +01:00
[self dismissMenuActionsIfNecessary];
2018-10-31 15:05:24 +01:00
if (self.isGroupConversation) {
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
[self.thread reloadWithTransaction:transaction];
}];
}
[self updateDisappearingMessagesConfiguration];
if (conversationUpdate.conversationUpdateType == ConversationUpdateType_Minor) {
return;
} else if (conversationUpdate.conversationUpdateType == ConversationUpdateType_Reload) {
[self resetContentAndLayout];
2018-12-17 20:54:44 +01:00
[self updateLastVisibleSortId];
2018-10-31 15:05:24 +01:00
return;
}
OWSAssertDebug(conversationUpdate.conversationUpdateType == ConversationUpdateType_Diff);
OWSAssertDebug(conversationUpdate.updateItems);
// We want to auto-scroll to the bottom of the conversation
// if the user is inserting new interactions.
__block BOOL scrollToBottom = NO;
2018-10-31 15:05:24 +01:00
self.scrollContinuity = ([self isScrolledToBottom] ? kScrollContinuityBottom : kScrollContinuityTop);
2018-10-31 15:05:24 +01:00
void (^batchUpdates)(void) = ^{
OWSAssertIsOnMainThread();
const NSUInteger section = 0;
BOOL hasInserted = NO, hasUpdated = NO;
for (ConversationUpdateItem *updateItem in conversationUpdate.updateItems) {
switch (updateItem.updateItemType) {
case ConversationUpdateItemType_Delete: {
// Always perform deletes before inserts and updates.
OWSAssertDebug(!hasInserted && !hasUpdated);
[self.collectionView deleteItemsAtIndexPaths:@[
[NSIndexPath indexPathForRow:(NSInteger)updateItem.oldIndex inSection:section]
]];
break;
}
case ConversationUpdateItemType_Insert: {
// Always perform inserts before updates.
OWSAssertDebug(!hasUpdated);
[self.collectionView insertItemsAtIndexPaths:@[
[NSIndexPath indexPathForRow:(NSInteger)updateItem.newIndex inSection:section]
]];
hasInserted = YES;
id<ConversationViewItem> viewItem = updateItem.viewItem;
OWSAssertDebug(viewItem);
if ([viewItem.interaction isKindOfClass:[TSOutgoingMessage class]]) {
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)viewItem.interaction;
if (!outgoingMessage.isFromLinkedDevice) {
scrollToBottom = YES;
}
}
break;
}
case ConversationUpdateItemType_Update: {
[self.collectionView reloadItemsAtIndexPaths:@[
[NSIndexPath indexPathForRow:(NSInteger)updateItem.oldIndex inSection:section]
2018-10-31 15:05:24 +01:00
]];
hasUpdated = YES;
break;
}
}
}
};
BOOL shouldAnimateUpdates = conversationUpdate.shouldAnimateUpdates;
void (^batchUpdatesCompletion)(BOOL) = ^(BOOL finished) {
OWSAssertIsOnMainThread();
if (!finished) {
OWSLogInfo(@"performBatchUpdates did not finish");
}
2018-12-17 20:54:44 +01:00
[self updateLastVisibleSortId];
2018-10-31 15:05:24 +01:00
if (scrollToBottom) {
[self scrollToBottomAnimated:NO];
2018-10-31 15:05:24 +01:00
}
// Try to update the lastKnownDistanceFromBottom; the content size may have changed.
[self updateLastKnownDistanceFromBottom];
2018-10-31 15:05:24 +01:00
};
@try {
if (shouldAnimateUpdates) {
[self.collectionView performBatchUpdates:batchUpdates completion:batchUpdatesCompletion];
} else {
// HACK: We use `UIView.animateWithDuration:0` rather than `UIView.performWithAnimation` to work around a
// UIKit Crash like:
//
// *** Assertion failure in -[ConversationViewLayout prepareForCollectionViewUpdates:],
// /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit_Sim/UIKit-3600.7.47/UICollectionViewLayout.m:760
// *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'While
// preparing update a visible view at <NSIndexPath: 0xc000000011c00016> {length = 2, path = 0 - 142}
// wasn't found in the current data model and was not in an update animation. This is an internal
// error.'
//
// I'm unclear if this is a bug in UIKit, or if we're doing something crazy in
// ConversationViewLayout#prepareLayout. To reproduce, rapidily insert and delete items into the
// conversation. See `DebugUIMessages#thrashCellsInThread:`
[UIView
animateWithDuration:0.0
animations:^{
[self.collectionView performBatchUpdates:batchUpdates completion:batchUpdatesCompletion];
if (scrollToBottom) {
[self scrollToBottomAnimated:NO];
2018-10-31 15:05:24 +01:00
}
2018-12-12 19:52:35 +01:00
[BenchManager completeEventWithEventId:@"message-send"];
2018-10-31 15:05:24 +01:00
}];
}
} @catch (NSException *exception) {
OWSFailDebug(@"exception: %@ of type: %@ with reason: %@, user info: %@.",
exception.description,
exception.name,
exception.reason,
exception.userInfo);
for (ConversationUpdateItem *updateItem in conversationUpdate.updateItems) {
switch (updateItem.updateItemType) {
case ConversationUpdateItemType_Delete:
OWSLogWarn(@"ConversationUpdateItemType_Delete class: %@, itemId: %@, oldIndex: %lu, "
@"newIndex: %lu",
[updateItem.viewItem class],
updateItem.viewItem.itemId,
(unsigned long)updateItem.oldIndex,
(unsigned long)updateItem.newIndex);
break;
case ConversationUpdateItemType_Insert:
OWSLogWarn(@"ConversationUpdateItemType_Insert class: %@, itemId: %@, oldIndex: %lu, "
@"newIndex: %lu",
[updateItem.viewItem class],
updateItem.viewItem.itemId,
(unsigned long)updateItem.oldIndex,
(unsigned long)updateItem.newIndex);
break;
case ConversationUpdateItemType_Update:
OWSLogWarn(@"ConversationUpdateItemType_Update class: %@, itemId: %@, oldIndex: %lu, "
@"newIndex: %lu",
[updateItem.viewItem class],
updateItem.viewItem.itemId,
(unsigned long)updateItem.oldIndex,
(unsigned long)updateItem.newIndex);
break;
}
}
@throw exception;
}
self.lastReloadDate = [NSDate new];
}
- (void)conversationViewModelWillLoadMoreItems
{
OWSAssertIsOnMainThread();
OWSAssertDebug(self.conversationViewModel);
// 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.
self.scrollDistanceToBottomSnapshot = self.safeContentHeight - self.collectionView.contentOffset.y;
}
- (void)conversationViewModelDidLoadMoreItems
{
OWSAssertIsOnMainThread();
OWSAssertDebug(self.conversationViewModel);
[self.layout prepareLayout];
self.collectionView.contentOffset = CGPointMake(0, self.safeContentHeight - self.scrollDistanceToBottomSnapshot);
}
- (void)conversationViewModelDidLoadPrevPage
{
OWSAssertIsOnMainThread();
OWSAssertDebug(self.conversationViewModel);
[self scrollToUnreadIndicatorAnimated];
}
- (void)conversationViewModelRangeDidChange
{
OWSAssertIsOnMainThread();
if (!self.conversationViewModel) {
return;
}
[self updateShowLoadMoreHeader];
}
- (void)conversationViewModelDidReset
{
OWSAssertIsOnMainThread();
// Scroll to bottom to get view back to a known good state.
[self scrollToBottomAnimated:NO];
}
2019-01-08 16:44:22 +01:00
#pragma mark - Orientation
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
OWSAssertIsOnMainThread();
2019-01-08 16:44:22 +01:00
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
// The "message actions" window tries to pin the message
// in the content of this view. It's easier to dismiss the
// "message actions" window when the device changes orientation
// than to try to ensure this works in that case.
Bigger hack to fix problem with lesser hack. There were two symptoms to this bad "leave app while dismissing keyboard" state... The first, most noticeable symptom was that the main window no longer respected the device orientation. This was caused by UIKit temporarily disabling autorotate during an interactive keyboard dismissal, and not cleaning up after itself when we hid the window mid dismissal due to our screen protection feature. This was solved previously in: ca0a555f8 The second symptom remained, and is solved by this commit. Wherein after getting in this bad state, the interactive keyboard dismiss function behaves oddly. Normally when interactively dismissing the keyboard in a scroll view, the keyboard top follows your finger, until you lift up your finger, at which point, depending on how close you are to the bottom, the keyboard should completely dismiss, or cancel and return to its fully popped position. In the degraded state, the keyboard would follow your finger, but when you lifted your finger, it would stay where your finger left it, it would not complete/cancel the dismiss. The solution is, instead of only re-enabling autorotate, to use a higher level private method which is called upon complete/cancellation of the interactive dismissal. The method, `UIScrollToDismissSupport#finishScrollViewTransition`, as well as re-enabling autorotate, does some other work to restore the UI to it's normal post interactive-keyboard-dismiss gesture state. For posterity here's the decompiled pseudocode: ``` /* @class UIScrollToDismissSupport */ -(void)finishScrollViewTransition { *(int8_t *)&self->_scrollViewTransitionFinishing = 0x0; [self->_controller setInterfaceAutorotationDisabled:0x0]; [self hideScrollViewHorizontalScrollIndicator:0x0]; ebx = *ivar_offset(_scrollViewNotificationInfo); [*(self + ebx) release]; *(self + ebx) = 0x0; esi = *ivar_offset(_scrollViewForTransition); [*(self + esi) release]; *(self + esi) = 0x0; return; } ```
2019-03-20 22:45:43 +01:00
if (OWSWindowManager.sharedManager.isPresentingMenuActions) {
[self dismissMenuActions];
}
2019-01-11 22:45:02 +01:00
// Snapshot the "last visible row".
NSIndexPath *_Nullable lastVisibleIndexPath = self.lastVisibleIndexPath;
2019-01-08 22:38:18 +01:00
__weak ConversationViewController *weakSelf = self;
[coordinator
animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
2019-01-11 22:45:02 +01:00
if (lastVisibleIndexPath) {
[self.collectionView scrollToItemAtIndexPath:lastVisibleIndexPath
atScrollPosition:UICollectionViewScrollPositionBottom
animated:NO];
}
2019-01-08 22:38:18 +01:00
}
completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
ConversationViewController *strongSelf = weakSelf;
if (!strongSelf) {
return;
}
2019-01-09 15:27:35 +01:00
// When transition animation is complete, update layout to reflect
// new size.
[strongSelf resetForSizeOrOrientationChange];
2020-05-05 01:11:43 +02:00
[strongSelf updateInputBarLayout];
2019-01-11 15:24:24 +01:00
2019-03-18 16:24:09 +01:00
if (self.menuActionsViewController != nil) {
2019-03-19 16:13:06 +01:00
[self scrollToMenuActionInteraction:NO];
2019-03-18 16:24:09 +01:00
} else if (lastVisibleIndexPath) {
2019-01-11 22:45:02 +01:00
[strongSelf.collectionView scrollToItemAtIndexPath:lastVisibleIndexPath
atScrollPosition:UICollectionViewScrollPositionBottom
animated:NO];
}
2019-01-08 22:38:18 +01:00
}];
2019-01-08 16:44:22 +01:00
}
- (void)traitCollectionDidChange:(nullable UITraitCollection *)previousTraitCollection
{
[super traitCollectionDidChange:previousTraitCollection];
2019-01-08 16:51:19 +01:00
[self ensureBannerState];
2019-01-09 15:42:41 +01:00
[self updateBarButtonItems];
2019-01-08 16:44:22 +01:00
}
2019-01-08 22:38:18 +01:00
- (void)resetForSizeOrOrientationChange
{
self.scrollContinuity = kScrollContinuityBottom;
self.conversationStyle.viewWidth = self.collectionView.width;
// Evacuate cached cell sizes.
for (id<ConversationViewItem> viewItem in self.viewItems) {
[viewItem clearCachedLayoutState];
}
[self.collectionView.collectionViewLayout invalidateLayout];
[self.collectionView reloadData];
if (self.viewHasEverAppeared) {
// Try to update the lastKnownDistanceFromBottom; the content size may have changed.
[self updateLastKnownDistanceFromBottom];
}
2020-05-05 01:11:43 +02:00
[self updateInputBarLayout];
2019-01-11 15:24:24 +01:00
}
- (void)viewSafeAreaInsetsDidChange
{
[super viewSafeAreaInsetsDidChange];
2020-05-05 01:11:43 +02:00
[self updateInputBarLayout];
2019-01-11 15:24:24 +01:00
}
2020-05-05 01:11:43 +02:00
- (void)updateInputBarLayout
2019-01-11 15:24:24 +01:00
{
UIEdgeInsets safeAreaInsets = UIEdgeInsetsZero;
if (@available(iOS 11, *)) {
safeAreaInsets = self.view.safeAreaInsets;
}
2019-01-15 22:33:54 +01:00
[self.inputToolbar updateLayoutWithSafeAreaInsets:safeAreaInsets];
// Scroll button layout depends on input toolbar size.
[self updateScrollDownButtonLayout];
2019-01-08 22:38:18 +01:00
}
- (void)handleCalculatingPoWNotification:(NSNotification *)notification
{
NSNumber *timestamp = (NSNumber *)notification.object;
[self setProgressIfNeededTo:0.25f forMessageWithTimestamp:timestamp];
}
2020-02-20 03:30:30 +01:00
- (void)handleRoutingNotification:(NSNotification *)notification
{
NSNumber *timestamp = (NSNumber *)notification.object;
[self setProgressIfNeededTo:0.50f forMessageWithTimestamp:timestamp];
}
2020-02-20 03:30:30 +01:00
- (void)handleMessageSendingNotification:(NSNotification *)notification
{
NSNumber *timestamp = (NSNumber *)notification.object;
[self setProgressIfNeededTo:0.75f forMessageWithTimestamp:timestamp];
}
- (void)handleMessageSentNotification:(NSNotification *)notification
{
NSNumber *timestamp = (NSNumber *)notification.object;
[self setProgressIfNeededTo:1.0f forMessageWithTimestamp:timestamp];
2020-04-30 02:04:14 +02:00
[self.handledMessageTimestamps addObject:timestamp];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^(void) {
[self hideProgressIndicatorViewForMessageWithTimestamp:timestamp];
});
}
- (void)handleMessageFailedNotification:(NSNotification *)notification
{
NSNumber *timestamp = (NSNumber *)notification.object;
[self hideProgressIndicatorViewForMessageWithTimestamp:timestamp];
}
- (void)setProgressIfNeededTo:(float)progress forMessageWithTimestamp:(NSNumber *)timestamp
{
2020-04-30 02:04:14 +02:00
if ([self.handledMessageTimestamps contains:^BOOL(NSNumber *t) {
return [t isEqual:timestamp];
}]) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
2020-04-30 07:08:33 +02:00
__block TSInteraction *targetInteraction;
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
2020-04-30 07:08:33 +02:00
[self.thread enumerateInteractionsWithTransaction:transaction usingBlock:^(TSInteraction *interaction, YapDatabaseReadTransaction *t) {
if (interaction.timestamp == timestamp.unsignedLongLongValue) {
targetInteraction = interaction;
}
}];
} error:nil];
2020-04-30 07:08:33 +02:00
if (targetInteraction == nil || targetInteraction.interactionType != OWSInteractionType_OutgoingMessage) { return; }
NSString *hexEncodedPublicKey = targetInteraction.thread.contactIdentifier;
if (hexEncodedPublicKey == nil) { return; }
__block NSString *masterHexEncodedPublicKey;
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
masterHexEncodedPublicKey = [LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:hexEncodedPublicKey in:transaction] ?: hexEncodedPublicKey;
}];
BOOL isSlaveDevice = ![masterHexEncodedPublicKey isEqual:hexEncodedPublicKey];
if (isSlaveDevice) { return; }
if (progress <= self.progressIndicatorView.progress) { return; }
self.progressIndicatorView.alpha = 1;
[self.progressIndicatorView setProgress:progress animated:YES];
});
}
- (void)hideProgressIndicatorViewForMessageWithTimestamp:(NSNumber *)timestamp
{
__block TSInteraction *targetInteraction;
[self.thread enumerateInteractionsUsingBlock:^(TSInteraction *interaction) {
if (interaction.timestamp == timestamp.unsignedLongLongValue) {
targetInteraction = interaction;
}
}];
if (targetInteraction == nil || targetInteraction.interactionType != OWSInteractionType_OutgoingMessage) { return; }
dispatch_async(dispatch_get_main_queue(), ^{
[UIView animateWithDuration:0.25 animations:^{
self.progressIndicatorView.alpha = 0;
} completion:^(BOOL finished) {
[self.progressIndicatorView setProgress:0.0f];
}];
});
}
- (void)handleUnexpectedDeviceLinkRequestReceivedNotification
{
if (!LKDeviceLinkingUtilities.shouldShowUnexpectedDeviceLinkRequestReceivedAlert) { return; }
dispatch_async(dispatch_get_main_queue(), ^{
2020-03-27 00:49:53 +01:00
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Device Link Request Received" message:@"Open the device link screen by going to \"Settings\" > \"Devices\" > \"Link a Device\" to link your devices." preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
});
}
2014-10-29 21:58:58 +01:00
@end
2017-10-10 22:13:54 +02:00
NS_ASSUME_NONNULL_END