#import "TGModernConversationController.h" #import "FreedomUIKit.h" #import "TGPeerIdAdapter.h" #import "TGUpdateStateRequestBuilder.h" #import "TGModernConversationCompanion.h" #import "TGImagePickerController.h" #import "TGModernConversationCollectionView.h" #import "TGModernConversationViewLayout.h" #import "TGModernConversationItem.h" #import "TGModernFlatteningViewModel.h" #import "TGModernTemporaryView.h" #import "TGImageUtils.h" #import "TGPhoneUtils.h" #import "TGStringUtils.h" #import "TGFileUtils.h" #import "TGFont.h" #import "TGHacks.h" #import "TGObserverProxy.h" #import "TGActionSheet.h" #import "HPGrowingTextView.h" #import "HPTextViewInternal.h" #import "TGMessageModernConversationItem.h" #import "TGMessage.h" #import "TGAppDelegate.h" #import "TGApplication.h" #import "TGInterfaceManager.h" #import "TGModernConversationTitleView.h" #import "TGModernConversationAvatarButton.h" #import "TGModernConversationInputTextPanel.h" #import "TGModernConversationEditingPanel.h" #import "TGModernConversationTitlePanel.h" #import "TGModernConversationEmptyListPlaceholderView.h" #import "TGOverlayControllerWindow.h" #import "TGModernGalleryController.h" #import "TGGenericPeerMediaGalleryModel.h" #import "TGGroupAvatarGalleryModel.h" #import "TGGroupAvatarGalleryItem.h" #import "TGSecretPeerMediaGalleryModel.h" #import "TGSecretInfiniteLifetimePeerMediaGalleryModel.h" #import "TGGenericPeerGalleryItem.h" #import "TGModernGalleryVideoItemView.h" #import "TGDropboxHelper.h" #import "TGICloudItem.h" #import "TGGoogleDriveController.h" #import "TGLocationViewController.h" #import "TGLocationPickerController.h" #import "TGMapViewController.h" #import "TGWebSearchController.h" #import "TGLegacyCameraController.h" #import "TGDocumentController.h" #import "TGForwardContactPickerController.h" #import "TGAudioRecorder.h" #import "TGModernConversationAudioPlayer.h" #import "TGAttachmentSheetRecentCameraView.h" #import "PGCamera.h" #import "TGCameraPreviewView.h" #import "TGCameraController.h" #import "TGMediaFoldersController.h" #import "TGModernMediaPickerController.h" #import "TGMediaPickerAssetsLibrary.h" #import "TGAssetImageManager.h" #import "TGAccessChecker.h" #import "UIDevice+PlatformInfo.h" #import "TGMediaItem.h" #import "TGDatabase.h" #import "TGTelegraph.h" #import "TGGenericModernConversationCompanion.h" #import "TGModernConversationEmptyListPlaceholderView.h" #import "TGRemoteImageView.h" #import "TGMenuView.h" #import "TGAlertView.h" #import "TGWallpaperManager.h" #import #import #import #import "TGVideoConverter.h" #import "TGGiphySearchResultItem.h" #import "TGBingSearchResultItem.h" #import "TGWebSearchInternalImageResult.h" #import "TGWebSearchInternalGifResult.h" #import "TGAttachmentSheetWindow.h" #import "TGAttachmentSheetRecentItemView.h" #import "TGAttachmentSheetButtonItemView.h" #import "TGAttachmentSheetRecentControlledButtonItemView.h" #import "TGAttachmentSheetFileInstructionItemView.h" #import "ATQueue.h" #import "TGAssetImageManager.h" #import "TGProgressWindow.h" #import "TGModernConversationControllerDynamicTypeSignals.h" #import "TGMessageViewModel.h" #import "TGModenConcersationReplyAssociatedPanel.h" #import "TGStickerAssociatedInputPanel.h" #import "TGModernConversationMentionsAssociatedPanel.h" #import "TGModernConversationHashtagsAssociatedPanel.h" #import "TGModernConversationForwardInputPanel.h" #import "TGModernConversationWebPreviewInputPanel.h" #import "TGExternalGalleryModel.h" #import "TGStickersSignals.h" #import "TGCommandKeyboardView.h" #import "TGModernConversationCommandsAssociatedPanel.h" #import "TGEmbedPreviewController.h" #import "TGSearchBar.h" #import "TGGlobalMessageSearchSignals.h" #import "TGModernConversationSearchInputPanel.h" #import "TGAttachmentSheetEmbedItemView.h" #import "TGModernDateHeaderView.h" #import "TGModernConversationControllerView.h" #import #if TARGET_IPHONE_SIMULATOR NSInteger TGModernConversationControllerUnloadHistoryLimit = 500; NSInteger TGModernConversationControllerUnloadHistoryThreshold = 200; #else NSInteger TGModernConversationControllerUnloadHistoryLimit = 500; NSInteger TGModernConversationControllerUnloadHistoryThreshold = 200; #endif #define TGModernConversationControllerLogCellOperations false typedef enum { TGModernConversationActivityChangeAuto = 0, TGModernConversationActivityChangeActive = 1, TGModernConversationActivityChangeInactive = 2 } TGModernConversationActivityChange; typedef enum { TGModernConversationPanelAnimationNone = 0, TGModernConversationPanelAnimationSlide = 1, TGModernConversationPanelAnimationFade = 2, TGModernConversationPanelAnimationSlideFar = 3 } TGModernConversationPanelAnimation; @interface TGModernConversationController () { bool _alreadyHadWillAppear; bool _alreadyHadDidAppear; NSTimeInterval _willAppearTimestamp; bool _receivedWillDisappear; bool _didDisappearBeforeAppearing; NSString *_initialInputText; NSArray *_initialForwardMessages; bool _shouldHaveTitlePanelLoaded; bool _editingMode; NSMutableArray *_items; NSMutableSet *_collectionRegisteredIdentifiers; TGModernConversationControllerView *_view; TGModernConversationCollectionView *_collectionView; TGModernConversationViewLayout *_collectionLayout; UIScrollView *_collectionViewScrollToTopProxy; TGModernViewStorage *_viewStorage; NSMutableArray *_itemsBoundToTemporaryContainer; bool _disableItemBinding; CGImageRef _snapshotImage; TGModernTemporaryView *_snapshotBackgroundView; UIView *_snapshotImageView; UIImageView *_backgroundView; TGModernConversationInputTextPanel *_inputTextPanel; TGModernConversationInputPanel *_currentInputPanel; TGModernConversationInputPanel *_customInputPanel; UIView *_titlePanelWrappingView; TGModernConversationTitlePanel *_primaryTitlePanel; TGModernConversationTitlePanel *_secondaryTitlePanel; TGModernConversationTitlePanel *_currentTitlePanel; TGModernConversationEmptyListPlaceholderView *_emptyListPlaceholder; bool _isRotating; CGFloat _keyboardHeight; CGFloat _halfTransitionKeyboardHeight; TGObserverProxy *_keyboardWillHideProxy; TGObserverProxy *_keyboardWillChangeFrameProxy; TGObserverProxy *_keyboardDidChangeFrameProxy; TGObserverProxy *_applicationWillResignActiveProxy; TGObserverProxy *_applicationDidEnterBackgroundProxy; TGObserverProxy *_applicationDidBecomeActiveProxy; TGObserverProxy *_screenshotProxy; TGObserverProxy *_dropboxProxy; CGPoint _collectionPanTouchContentOffset; bool _collectionPanStartedAtBottom; TGModernConversationTitleView *_titleView; TGModernConversationAvatarButton *_avatarButton; UIBarButtonItem *_avatarButtonItem; UIBarButtonItem *_infoButtonItem; TGMenuContainerView *_menuContainerView; UIButton *_unseenMessagesButton; bool _disableScrollProcessing; bool _enableAboveHistoryRequests; bool _enableBelowHistoryRequests; bool _enableUnloadHistoryRequests; bool _canReadHistory; TGAudioRecorder *_currentAudioRecorder; TGModernConversationAudioPlayer *_currentAudioPlayer; int32_t _currentAudioPlayerMessageId; bool _streamAudioItems; int32_t _currentStreamingAudioMessageId; NSUserActivity *_currentActivity; TGAttachmentSheetWindow *_attachmentSheetWindow; UIDocumentInteractionController *_interactionController; TGICloudItemRequest *_currentICloudItemRequest; SDisposableSet *_disposable; int32_t _temporaryHighlightMessageIdUponDisplay; bool _hasUnseenMessagesBelow; int32_t _scrollBackMessageId; bool _canCheckScrollBackMessageId; NSString *_currentLinkParseLink; SMetaDisposable *_currentLinkParseDisposable; bool _disableLinkPreviewsForMessage; TGBotReplyMarkup *_replyMarkup; bool _hasBots; bool _canBroadcast; bool _isBroadcasting; bool _isAlwaysBroadcasting; bool _inputDisabled; bool _isChannel; UIView *_conversationHeader; NSNumber *_scrollingToBottom; TGSearchBar *_searchBar; TGModernConversationSearchInputPanel *_searchPanel; NSString *_query; SMetaDisposable *_searchDisposable; NSArray *_searchResultsIds; NSUInteger _searchResultsOffset; int32_t _messageIdForVisibleHoleDirection; } @end @implementation TGModernConversationController - (instancetype)init { self = [super init]; if (self != nil) { _actionHandle = [[ASHandle alloc] initWithDelegate:self releaseOnMainThread:true]; self.automaticallyManageScrollViewInsets = false; self.adjustControllerInsetWhenStartingRotation = true; _items = [[NSMutableArray alloc] init]; _keyboardWillHideProxy = [[TGObserverProxy alloc] initWithTarget:self targetSelector:@selector(keyboardWillHide:) name:@"UIDeviceOrientationDidChangeNotification"]; _keyboardWillChangeFrameProxy = [[TGObserverProxy alloc] initWithTarget:self targetSelector:@selector(keyboardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification]; _keyboardDidChangeFrameProxy = [[TGObserverProxy alloc] initWithTarget:self targetSelector:@selector(keyboardDidChangeFrame:) name:UIKeyboardDidChangeFrameNotification]; _applicationWillResignActiveProxy = [[TGObserverProxy alloc] initWithTarget:self targetSelector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification]; _applicationDidEnterBackgroundProxy = [[TGObserverProxy alloc] initWithTarget:self targetSelector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification]; _applicationDidBecomeActiveProxy = [[TGObserverProxy alloc] initWithTarget:self targetSelector:@selector(applicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification]; if (iosMajorVersion() >= 7) { _screenshotProxy = [[TGObserverProxy alloc] initWithTarget:self targetSelector:@selector(serviceNotificationReceived:) name:UIApplicationUserDidTakeScreenshotNotification]; } _titleView = [[TGModernConversationTitleView alloc] initWithFrame:CGRectMake(0, 0, 1, 1)]; _titleView.delegate = self; [self setTitleView:_titleView]; _avatarButton = [[TGModernConversationAvatarButton alloc] initWithFrame:CGRectMake(0, 0, 1, 1)]; [_avatarButton addTarget:self action:@selector(avatarPressed) forControlEvents:UIControlEventTouchUpInside]; _avatarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:_avatarButton]; [self setRightBarButtonItem:[self defaultRightBarButtonItem]]; [self setLeftBarButtonItem:[self defaultLeftBarButtonItem]]; _canReadHistory = true; _enableUnloadHistoryRequests = true; self.dismissPresentedControllerWhenRemovedFromNavigationStack = true; _didDisappearBeforeAppearing = false; _streamAudioItems = TGAppDelegateInstance.autoPlayAudio; _disposable = [[SDisposableSet alloc] init]; if (iosMajorVersion() >= 7) { __weak TGModernConversationController *weakSelf = self; [_disposable add:[[TGModernConversationControllerDynamicTypeSignals dynamicTypeBaseFontPointSize] startWithNext:^(NSNumber *pointSize) { TGUpdateMessageViewModelLayoutConstants([pointSize floatValue]); __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) [strongSelf refreshMetrics]; } error:nil completed:nil]]; } } return self; } - (void)dealloc { [_disposable dispose]; [_actionHandle reset]; _collectionView.delegate = nil; _collectionView.dataSource = nil; if (_snapshotImage != nil) { CGImageRelease(_snapshotImage); _snapshotImage = nil; } [_companion unbindController]; if (_attachmentSheetWindow != nil) _attachmentSheetWindow.rootViewController = nil; } - (NSInteger)_indexForCollectionView { return 1; } - (void)_resetCollectionView { [self _resetCollectionView:false]; } - (void)_resetCollectionView:(bool)resetPositioning { if (_collectionView != nil) { _collectionView.delegate = nil; _collectionView.dataSource = nil; [_collectionView removeFromSuperview]; UICollectionView *collectionView = _collectionView; dispatch_async(dispatch_get_main_queue(), ^ { TGLog(@"***** replacing collection view %@", collectionView); }); } if (_collectionViewScrollToTopProxy != nil) { _collectionViewScrollToTopProxy.delegate = nil; [_collectionViewScrollToTopProxy removeFromSuperview]; _collectionViewScrollToTopProxy = nil; } _collectionRegisteredIdentifiers = [[NSMutableSet alloc] init]; CGSize collectionViewSize = _view.bounds.size; _collectionLayout = [[TGModernConversationViewLayout alloc] init]; _collectionView = [[TGModernConversationCollectionView alloc] initWithFrame:CGRectMake(0, -210.0f, collectionViewSize.width, collectionViewSize.height + 210.0f) collectionViewLayout:_collectionLayout]; [_companion _setControllerWidthForItemCalculation:_collectionView.frame.size.width]; _collectionView.headerView = _conversationHeader; _collectionView.transform = CGAffineTransformMakeRotation((CGFloat)M_PI); _collectionView.backgroundColor = nil; _collectionView.opaque = false; _collectionView.scrollsToTop = false; _collectionView.dataSource = self; _collectionView.delegate = self; _collectionView.delaysContentTouches = false; if (iosMajorVersion() >= 7) { UIPanGestureRecognizer *collectionPanRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(collectionViewPan:)]; collectionPanRecognizer.delegate = self; [_collectionView addGestureRecognizer:collectionPanRecognizer]; } _collectionView.unreadMessageRange = [_companion unreadMessageRange]; _collectionView.alwaysBounceVertical = true; [_collectionView registerClass:[TGModernCollectionCell class] forCellWithReuseIdentifier:@"_empty"]; UIEdgeInsets contentInset = _collectionView.contentInset; contentInset.bottom = 210.0f + [_collectionView implicitTopInset]; contentInset.top = _keyboardHeight + _currentInputPanel.frame.size.height; _collectionView.contentInset = contentInset; [self _adjustCollectionInset]; [_emptyListPlaceholder adjustLayoutForSize:collectionViewSize contentInsets:UIEdgeInsetsMake(_collectionView.contentInset.bottom - 210.0f - [_collectionView implicitTopInset], 0.0f, _collectionView.contentInset.top, 0.0f) duration:0.0f curve:0]; [_view insertSubview:_collectionView atIndex:[self _indexForCollectionView]]; _collectionViewScrollToTopProxy = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, _view.frame.size.width, 8)]; _collectionViewScrollToTopProxy.autoresizingMask = UIViewAutoresizingFlexibleWidth; _collectionViewScrollToTopProxy.delegate = self; _collectionViewScrollToTopProxy.scrollsToTop = true; _collectionViewScrollToTopProxy.contentSize = CGSizeMake(1, 16); _collectionViewScrollToTopProxy.contentOffset = CGPointMake(0, 8); [_view insertSubview:_collectionViewScrollToTopProxy belowSubview:_collectionView]; if (resetPositioning) { int32_t messageId = [_companion initialPositioningMessageId]; TGInitialScrollPosition scrollPosition = [_companion initialPositioningScrollPosition]; if (messageId != 0) { _collectionView.contentOffset = CGPointMake(0.0f, [self contentOffsetForMessageId:messageId scrollPosition:scrollPosition initial:true additionalOffset:0.0f]); } } [_collectionView layoutSubviews]; [self _updateVisibleItemIndices:nil]; } - (CGFloat)contentOffsetForMessageId:(int32_t)messageId scrollPosition:(TGInitialScrollPosition)scrollPosition initial:(bool)initial additionalOffset:(CGFloat)additionalOffset { if (![_collectionLayout hasLayoutAttributes]) [_collectionLayout prepareLayout]; CGFloat contentOffsetY = _collectionView.contentOffset.y; int index = -1; for (TGMessageModernConversationItem *messageItem in _items) { index++; if (messageItem->_message.mid == messageId) { if (index == 0 && initial) { return -_collectionView.contentInset.top; } UICollectionViewLayoutAttributes *attributes = [_collectionLayout layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0]]; if (attributes != nil) { switch (scrollPosition) { case TGInitialScrollPositionTop: contentOffsetY = CGRectGetMaxY(attributes.frame) + _collectionView.contentInset.bottom - [_collectionView implicitTopInset] - _collectionView.frame.size.height + [_companion initialPositioningOverflowForScrollPosition:scrollPosition]; break; case TGInitialScrollPositionCenter: { CGFloat visibleHeight = _collectionView.frame.size.height - _collectionView.contentInset.top - _collectionView.contentInset.bottom + [_collectionView implicitTopInset]; contentOffsetY = CGFloor(CGRectGetMidY(attributes.frame) - visibleHeight / 2.0f - _collectionView.contentInset.top); break; } case TGInitialScrollPositionBottom: contentOffsetY = attributes.frame.origin.y - _collectionView.contentInset.top - [_companion initialPositioningOverflowForScrollPosition:scrollPosition]; break; default: break; } } break; } } contentOffsetY += additionalOffset; if (contentOffsetY > _collectionLayout.collectionViewContentSize.height + _collectionView.contentInset.bottom - _collectionView.frame.size.height) { contentOffsetY = _collectionLayout.collectionViewContentSize.height + _collectionView.contentInset.bottom - _collectionView.frame.size.height; } if (contentOffsetY < -_collectionView.contentInset.top) { contentOffsetY = -_collectionView.contentInset.top; } return contentOffsetY; } - (void)setInitialSnapshot:(CGImageRef)image backgroundView:(TGModernTemporaryView *)backgroundView viewStorage:(TGModernViewStorage *)viewStorage topEdge:(CGFloat)topEdge { if (_viewStorage == nil && viewStorage != nil) _viewStorage = viewStorage; if (_snapshotImage != NULL) { CGImageRelease(_snapshotImage); _snapshotImage = nil; } if (_snapshotBackgroundView != nil) { [_snapshotBackgroundView unbindItems]; [_snapshotBackgroundView removeFromSuperview]; _snapshotBackgroundView = nil; } if (image != NULL) _snapshotImage = CGImageRetain(image); if (self.isViewLoaded) { if (_snapshotImage != NULL) { if (_snapshotImageView == nil) { _snapshotImageView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CGImageGetWidth(_snapshotImage) * (TGIsRetina() ? 0.5f : 1.0f), CGImageGetHeight(_snapshotImage) * (TGIsRetina() ? 0.5f : 1.0f))]; _snapshotImageView.userInteractionEnabled = false; _snapshotImageView.transform = CGAffineTransformMakeRotation((CGFloat)M_PI); [_view insertSubview:_snapshotImageView atIndex:[self _indexForCollectionView]]; } _snapshotBackgroundView = backgroundView; if (_snapshotBackgroundView != nil) { [_view insertSubview:_snapshotBackgroundView belowSubview:_snapshotImageView]; } _snapshotImageView.layer.contents = (__bridge id)_snapshotImage; if (_conversationHeader != nil) { [_view addSubview:_conversationHeader]; CGRect headerFrame = _conversationHeader.frame; headerFrame.origin.x = CGFloor((_view.frame.size.width - headerFrame.size.width) / 2.0f); headerFrame.origin.y = _view.frame.size.height - _conversationHeader.frame.size.height - topEdge - 4.0f; CGFloat visibleHeight = _view.frame.size.height - self.controllerInset.top - self.controllerInset.bottom - 44.0f; headerFrame.origin.y = MIN(headerFrame.origin.y, self.controllerInset.top + CGFloor((visibleHeight - headerFrame.size.height) / 2.0f)); _conversationHeader.frame = headerFrame; } } else { if (_snapshotImageView != nil) { [_snapshotImageView removeFromSuperview]; _snapshotImageView = nil; } if (_collectionView == nil) [self _resetCollectionView:true]; } } } - (UIBarButtonItem *)defaultLeftBarButtonItem { if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) return nil; if (self.isFirstInStack) { return [[UIBarButtonItem alloc] initWithTitle:TGLocalized(@"Common.Close") style:UIBarButtonItemStylePlain target:self action:@selector(closeButtonPressed)]; } return nil; } - (void)setIsFirstInStack:(bool)isFirstInStack { if (isFirstInStack != self.isFirstInStack) { [super setIsFirstInStack:isFirstInStack]; [self setLeftBarButtonItem:[self defaultLeftBarButtonItem] animated:false]; } } - (UIBarButtonItem *)defaultRightBarButtonItem { if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) return _avatarButtonItem; else { if (_infoButtonItem == nil && _companion != nil) { _infoButtonItem = [[UIBarButtonItem alloc] initWithTitle:[_companion _controllerInfoButtonText] style:UIBarButtonItemStylePlain target:self action:@selector(infoButtonPressed)]; } return _infoButtonItem; } } - (void)loadView { [super loadView]; [self check3DTouch]; _view = [[TGModernConversationControllerView alloc] initWithFrame:self.view.bounds]; _view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; __weak TGModernConversationController *weakSelf = self; _view.layoutForSize = ^(CGSize size) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { if (strongSelf->_keyboardHeight < FLT_EPSILON) { [strongSelf _performSizeChangesWithDuration:strongSelf->_isRotating ? 0.3 : 0.0 size:size]; } } }; [self.view addSubview:_view]; _view.clipsToBounds = true; _view.backgroundColor = [UIColor whiteColor]; _backgroundView = [[UIImageView alloc] initWithFrame:_view.bounds]; UIImage *wallpaperImage = [[TGWallpaperManager instance] currentWallpaperImage]; _backgroundView.image = wallpaperImage; _backgroundView.clipsToBounds = true; _backgroundView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; _backgroundView.contentMode = UIViewContentModeScaleAspectFill; [_view addSubview:_backgroundView]; _inputTextPanel = [[TGModernConversationInputTextPanel alloc] initWithFrame:CGRectMake(0, _view.frame.size.height - 45, _view.frame.size.width, 45) accessoryView:[_companion _controllerInputTextPanelAccessoryView]]; if (_initialInputText.length != 0) { [_inputTextPanel.inputField setText:_initialInputText]; _initialInputText = nil; } _inputTextPanel.delegate = self; [_inputTextPanel setReplyMarkup:_replyMarkup]; [_inputTextPanel setHasBots:_hasBots]; [_inputTextPanel setCanBroadcast:_canBroadcast]; [_inputTextPanel setIsBroadcasting:_isBroadcasting]; [_inputTextPanel setIsAlwaysBroadcasting:_isAlwaysBroadcasting]; [_inputTextPanel setInputDisabled:_inputDisabled]; [_inputTextPanel setIsChannel:_isChannel]; if (_initialForwardMessages != nil) { TGModernConversationForwardInputPanel *panel = [[TGModernConversationForwardInputPanel alloc] initWithMessages:_initialForwardMessages]; __weak TGModernConversationController *weakSelf = self; panel.dismiss = ^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf->_inputTextPanel setPrimaryExtendedPanel:nil animated:true]; [strongSelf->_inputTextPanel setSecondaryExtendedPanel:nil animated:true]; } }; [_inputTextPanel setPrimaryExtendedPanel:panel animated:true]; } _initialForwardMessages = nil; if (_customInputPanel != nil) [self setInputPanel:_customInputPanel animated:false]; else [self setInputPanel:[self defaultInputPanel] animated:false]; if (_currentTitlePanel != nil) { id currentTitlePanel = _currentTitlePanel; _currentTitlePanel = nil; [self setCurrentTitlePanel:currentTitlePanel animation:TGModernConversationPanelAnimationNone]; } [_view insertSubview:_emptyListPlaceholder belowSubview:_currentInputPanel]; if (![self _updateControllerInset:false]) [self controllerInsetUpdated:UIEdgeInsetsZero]; [_emptyListPlaceholder adjustLayoutForSize:_view.bounds.size contentInsets:UIEdgeInsetsMake(self.controllerInset.top, 0.0f, _currentInputPanel.frame.size.height, 0.0f) duration:0.0f curve:0]; } - (void)viewWillAppear:(BOOL)animated { TGLog(@"willAppear"); [self setRightBarButtonItem:[self defaultRightBarButtonItem]]; if (self.navigationController.viewControllers.count >= 2) { UIViewController *previousController = [self.navigationController.viewControllers objectAtIndex:self.navigationController.viewControllers.count - 2]; [_titleView setBackButtonTitle:previousController.navigationItem.backBarButtonItem.title.length == 0 ? TGLocalized(@"Common.Back") : previousController.navigationItem.backBarButtonItem.title]; } if (_didDisappearBeforeAppearing) _keyboardHeight = 0; //else if (_receivedWillDisappear) // _keyboardHeight = _halfTransitionKeyboardHeight; _receivedWillDisappear = false; _inputTextPanel.maybeInputField.internalTextView.enableFirstResponder = false; _willAppearTimestamp = CFAbsoluteTimeGetCurrent(); CGSize collectionViewSize = _view.bounds.size; if (_collectionView != nil) { if (ABS(collectionViewSize.width - _collectionView.frame.size.width) > FLT_EPSILON) [self _performSizeChangesWithDuration:0.0f size:collectionViewSize]; else { [_currentInputPanel adjustForSize:collectionViewSize keyboardHeight:_keyboardHeight duration:0.0 animationCurve:0]; [self _adjustCollectionViewForSize:collectionViewSize keyboardHeight:_keyboardHeight inputContainerHeight:_currentInputPanel.frame.size.height duration:0.0 animationCurve:0]; } } else [_currentInputPanel adjustForSize:collectionViewSize keyboardHeight:_keyboardHeight duration:0.0 animationCurve:0]; [_titleView setOrientation:self.interfaceOrientation]; [_titleView resumeAnimations]; [_avatarButton setOrientation:self.interfaceOrientation]; [super viewWillAppear:animated]; bool beingAnimated = animated; if (_shouldIgnoreAppearAnimationOnce) { beingAnimated = false; _shouldIgnoreAppearAnimationOnce = false; } [_companion _controllerWillAppearAnimated:beingAnimated firstTime:!_alreadyHadWillAppear]; if (_alreadyHadWillAppear) { [self _updateCanRegroupIncomingUnreadMessages]; } _alreadyHadWillAppear = true; NSDictionary *userActivityData = [_companion userActivityData]; if (false && iosMajorVersion() >= 8 && _currentActivity == nil && userActivityData != nil) { NSMutableDictionary *mutableUserActivityData = [[NSMutableDictionary alloc] initWithDictionary:userActivityData]; if (_inputTextPanel.maybeInputField.text.length != 0) mutableUserActivityData[@"text"] = _inputTextPanel.maybeInputField.text; _currentActivity = [[NSUserActivity alloc] initWithActivityType:@"org.telegram.conversation"]; _currentActivity.userInfo = mutableUserActivityData; _currentActivity.webpageURL = [NSURL URLWithString:@"https://telegram.org/dl"]; _currentActivity.delegate = self; _currentActivity.supportsContinuationStreams = true; [_currentActivity becomeCurrent]; } } - (void)viewDidAppear:(BOOL)animated { if (animated && _didDisappearBeforeAppearing) { dispatch_async(dispatch_get_main_queue(), ^ { _inputTextPanel.maybeInputField.internalTextView.enableFirstResponder = true; }); } else _inputTextPanel.maybeInputField.internalTextView.enableFirstResponder = true; [_companion _controllerDidAppear:!_alreadyHadDidAppear]; _alreadyHadDidAppear = true; [self _updateCanReadHistory:TGModernConversationActivityChangeActive]; _companion.viewContext.animationsEnabled = true; [self _updateItemsAnimationsEnabled]; [super viewDidAppear:animated]; if (_shouldOpenKeyboardOnce) { _inputTextPanel.inputField.internalTextView.enableFirstResponder = true; _shouldOpenKeyboardOnce = false; [self openKeyboard]; } _inputTextPanel.canShowKeyboardAutomatically = true; _inputTextPanel.enableKeyboard = true; } - (void)applicationWillResignActive:(NSNotification *)__unused notification { [self _updateCanReadHistory:TGModernConversationActivityChangeInactive]; [self stopInlineMedia]; } - (void)applicationDidEnterBackground:(NSNotification *)__unused notification { [self _updateCanReadHistory:TGModernConversationActivityChangeInactive]; } - (void)applicationDidBecomeActive:(NSNotification *)__unused notification { [self _updateCanRegroupIncomingUnreadMessages]; [self _updateCanReadHistory:TGModernConversationActivityChangeAuto]; } - (void)serviceNotificationReceived:(NSNotification *)__unused notification { if (self.navigationController.topViewController == self && self.presentedViewController == nil) { for (UIWindow *window in [[UIApplication sharedApplication] windows]) { if ([window isKindOfClass:[TGOverlayControllerWindow class]]) { TGOverlayControllerWindow *overlayControllerWindow = (TGOverlayControllerWindow *)window; if ([overlayControllerWindow.rootViewController isKindOfClass:[TGModernGalleryController class]]) { TGModernGalleryController *galleryController = (TGModernGalleryController *)overlayControllerWindow.rootViewController; if ([galleryController isFullyOpaque]) return; } } } } else return; NSMutableArray *messageIds = [[NSMutableArray alloc] init]; for (TGModernCollectionCell *cell in _collectionView.visibleCells) { TGMessageModernConversationItem *item = cell.boundItem; if (item != nil) [messageIds addObject:@(item->_message.mid)]; } [_companion serviceNotificationsForMessageIds:messageIds]; } - (void)viewWillDisappear:(BOOL)animated { _inputTextPanel.enableKeyboard = false; _inputTextPanel.canShowKeyboardAutomatically = true; _didDisappearBeforeAppearing = false; _receivedWillDisappear = true; freedomUIKitTest4_1(); [self stopInlineMediaIfPlaying]; [_collectionView stopScrollingAnimation]; [self _updateCanReadHistory:TGModernConversationActivityChangeInactive]; [self stopInlineMedia]; _companion.viewContext.animationsEnabled = false; [self _updateItemsAnimationsEnabled]; if (iosMajorVersion() >= 8) { [_currentActivity invalidate]; _currentActivity = nil; } if (_attachmentSheetWindow != nil) [_attachmentSheetWindow dismissAnimated:animated completion:nil]; _dropboxProxy = nil; [super viewWillDisappear:animated]; } - (void)viewDidDisappear:(BOOL)animated { _receivedWillDisappear = false; _didDisappearBeforeAppearing = true; _keyboardHeight = 0.0f; [_currentInputPanel adjustForSize:_view.bounds.size keyboardHeight:_keyboardHeight duration:0.0 animationCurve:0]; [self _adjustCollectionViewForSize:_view.bounds.size keyboardHeight:_keyboardHeight inputContainerHeight:_currentInputPanel.frame.size.height duration:0.0 animationCurve:0]; [_inputTextPanel.maybeInputField.internalTextView resignFirstResponder]; [_titleView suspendAnimations]; [self _leaveEditingModeAnimated:false]; [_companion updateControllerInputText:_inputTextPanel.maybeInputField.text]; [super viewDidDisappear:animated]; } - (BOOL)shouldAutorotate { bool tracking = _collectionView.isTracking; return !tracking && [super shouldAutorotate] && _currentAudioRecorder == nil; } - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration]; _isRotating = true; if (_keyboardHeight < FLT_EPSILON) { //[self _performSizeChangesWithDuration:duration size:_view.bounds.size]; } } - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation { [super didRotateFromInterfaceOrientation:fromInterfaceOrientation]; _isRotating = false; } - (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { [_titleView setOrientation:toInterfaceOrientation]; [_avatarButton setOrientation:self.interfaceOrientation]; [super willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration]; } - (void)controllerInsetUpdated:(UIEdgeInsets)previousInset { [super controllerInsetUpdated:previousInset]; if (![self viewControllerIsChangingInterfaceOrientation] && _collectionView != nil) [self _adjustCollectionInset]; if (_menuContainerView != nil) { _menuContainerView.frame = CGRectMake(0, self.controllerInset.top, _view.frame.size.width, _view.frame.size.height - self.controllerInset.top - self.controllerInset.bottom); } if (![self viewControllerIsChangingInterfaceOrientation]) { if (_titlePanelWrappingView != nil) { _titlePanelWrappingView.frame = CGRectMake(0.0f, self.controllerInset.top, _view.frame.size.width, _titlePanelWrappingView.frame.size.height); } } } - (void)_adjustCollectionInset { UIEdgeInsets contentInset = _collectionView.contentInset; if (ABS(contentInset.bottom - self.controllerInset.top) > FLT_EPSILON) { contentInset.bottom = self.controllerInset.top + 210.0f + [_collectionView implicitTopInset]; _collectionView.contentInset = contentInset; [self _updateUnseenMessagesButton]; [_emptyListPlaceholder adjustLayoutForSize:_view.bounds.size contentInsets:UIEdgeInsetsMake(_collectionView.contentInset.bottom - 210.0f - [_collectionView implicitTopInset], 0.0f, _collectionView.contentInset.top, 0.0f) duration:0.0f curve:0]; } } - (void)stopAudioRecording { if (_currentAudioRecorder != nil) { _currentAudioRecorder.delegate = nil; [_currentAudioRecorder cancel]; _currentAudioRecorder = nil; if ([self shouldAutorotate]) [TGViewController attemptAutorotation]; } } - (void)stopInlineMediaIfPlaying { if (_currentAudioPlayer != nil) { [_currentAudioPlayer stop]; _currentAudioPlayer = nil; _currentAudioPlayerMessageId = 0; _currentStreamingAudioMessageId = 0; [self updateInlineMediaContexts]; } } - (void)touchedTableBackground { if (_menuContainerView.isShowingMenu) return; if ([_inputTextPanel.maybeInputField.internalTextView isFirstResponder]) [_inputTextPanel.maybeInputField.internalTextView resignFirstResponder]; [_searchBar resignFirstResponder]; } - (void)tableTouchesCancelled { for (TGModernCollectionCell *cell in _collectionView.visibleCells) { TGMessageModernConversationItem *item = cell.boundItem; if (item != nil) [item clearHighlights]; } } - (void)navigationBarAction { [_menuContainerView hideMenu]; } - (void)avatarPressed { [_companion _controllerAvatarPressed]; } - (void)closeButtonPressed { [_companion _dismissController]; } - (void)infoButtonPressed { [self avatarPressed]; } - (void)setInputPanel:(TGModernConversationInputPanel *)panel animated:(bool)animated { if (!self.isViewLoaded) return; if (panel == _currentInputPanel) return; if (animated) { TGModernConversationInputPanel *previousPanel = _currentInputPanel; _currentInputPanel = nil; int curve = iosMajorVersion() < 7 ? 0 : 7; _currentInputPanel = panel; if (_currentInputPanel != nil) { [_view addSubview:_currentInputPanel]; [_currentInputPanel adjustForSize:_view.bounds.size keyboardHeight:_keyboardHeight duration:0.0 animationCurve:0]; _currentInputPanel.frame = CGRectMake(_currentInputPanel.frame.origin.x, _view.frame.size.height, _currentInputPanel.frame.size.width, _currentInputPanel.frame.size.height); } [_currentInputPanel adjustForSize:_view.bounds.size keyboardHeight:_keyboardHeight duration:0.3 animationCurve:curve]; [self _adjustCollectionViewForSize:_view.bounds.size keyboardHeight:_keyboardHeight inputContainerHeight:_currentInputPanel.frame.size.height duration:0.3 animationCurve:curve]; [previousPanel endEditing:true]; [UIView animateWithDuration:0.22 delay:0.00 options:0 animations:^ { previousPanel.frame = CGRectMake(0.0f, _view.frame.size.height, previousPanel.frame.size.width, previousPanel.frame.size.height); } completion:^(__unused BOOL finished) { [previousPanel removeFromSuperview]; }]; } else { [_currentInputPanel removeFromSuperview]; _currentInputPanel = panel; if (_currentInputPanel != nil) { [_view addSubview:_currentInputPanel]; [_currentInputPanel adjustForSize:_view.bounds.size keyboardHeight:_keyboardHeight duration:0.0 animationCurve:0]; _currentInputPanel.frame = CGRectMake(_currentInputPanel.frame.origin.x, _view.frame.size.height - _currentInputPanel.frame.size.height, _currentInputPanel.frame.size.width, _currentInputPanel.frame.size.height); [self _adjustCollectionViewForSize:_view.bounds.size keyboardHeight:_keyboardHeight inputContainerHeight:_currentInputPanel.frame.size.height duration:0.0 animationCurve:0]; } } } #pragma mark - - (NSArray *)items { return _items; } - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { if (collectionView == _collectionView) return 1; return 0; } - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { if (collectionView == _collectionView && section == 0) return _items.count; return 0; } - (void)_updateVisibleItemIndices:(TGModernCollectionCell *)additionalCell { if (!_disableScrollProcessing) { if (_messageIdForVisibleHoleDirection != 0 && !_enableBelowHistoryRequests && _collectionView.contentOffset.y <= -_collectionView.contentInset.top + 10.0f) { _messageIdForVisibleHoleDirection = 0; } } NSMutableArray *sortedHoles = nil; NSMutableArray *currentMessageIds = nil; for (TGModernCollectionCell *cell in [_collectionView visibleCells]) { TGMessageModernConversationItem *item = cell.boundItem; if (item != nil) { if (item->_message.hole != nil) { if (sortedHoles == nil) { sortedHoles = [[NSMutableArray alloc] init]; } TGVisibleMessageHoleDirection direction = TGVisibleMessageHoleDirectionEarlier; int32_t sortDistance = 0; if (_messageIdForVisibleHoleDirection != 0 && _messageIdForVisibleHoleDirection <= ABS(item->_message.mid)) { direction = TGVisibleMessageHoleDirectionLater; } else { direction = TGVisibleMessageHoleDirectionEarlier; } if (_messageIdForVisibleHoleDirection != 0) { sortDistance = ABS(_messageIdForVisibleHoleDirection - ABS(item->_message.mid)); } else { sortDistance = INT32_MAX - ABS(item->_message.mid); } NSInteger index = -1; bool added = false; for (TGVisibleMessageHole *currentHole in sortedHoles) { index++; int32_t currentDistance = 0; if (_messageIdForVisibleHoleDirection != 0) { currentDistance = ABS(_messageIdForVisibleHoleDirection - currentHole.hole.maxId); } else { currentDistance = INT32_MAX - currentHole.hole.maxId; } if (currentDistance > sortDistance) { [sortedHoles insertObject:[[TGVisibleMessageHole alloc] initWithHole:item->_message.hole direction:direction] atIndex:index]; added = true; break; } } if (!added) { [sortedHoles addObject:[[TGVisibleMessageHole alloc] initWithHole:item->_message.hole direction:direction]]; } } if (item->_message.viewCount != 0 && item->_message.mid < TGMessageLocalMidBaseline) { if (currentMessageIds == nil) { currentMessageIds = [[NSMutableArray alloc] init]; } [currentMessageIds addObject:@(item->_message.mid)]; } } } if (additionalCell != nil) { TGMessageModernConversationItem *item = additionalCell.boundItem; if (item != nil) { if (item->_message.hole != nil) { if (sortedHoles == nil) { sortedHoles = [[NSMutableArray alloc] init]; } TGVisibleMessageHoleDirection direction = TGVisibleMessageHoleDirectionEarlier; int32_t sortDistance = 0; if (_messageIdForVisibleHoleDirection != 0 && _messageIdForVisibleHoleDirection <= ABS(item->_message.mid)) { direction = TGVisibleMessageHoleDirectionLater; } else { direction = TGVisibleMessageHoleDirectionEarlier; } if (_messageIdForVisibleHoleDirection != 0) { sortDistance = ABS(_messageIdForVisibleHoleDirection - ABS(item->_message.mid)); } else { sortDistance = INT32_MAX - ABS(item->_message.mid); } NSInteger index = -1; bool added = false; for (TGVisibleMessageHole *currentHole in sortedHoles) { index++; int32_t currentDistance = 0; if (_messageIdForVisibleHoleDirection != 0) { currentDistance = ABS(_messageIdForVisibleHoleDirection - currentHole.hole.maxId); } else { currentDistance = INT32_MAX - currentHole.hole.maxId; } if (currentDistance > sortDistance) { [sortedHoles insertObject:[[TGVisibleMessageHole alloc] initWithHole:item->_message.hole direction:direction] atIndex:index]; added = true; break; } } if (!added) { [sortedHoles addObject:[[TGVisibleMessageHole alloc] initWithHole:item->_message.hole direction:direction]]; } } if (item->_message.viewCount != 0 && item->_message.mid < TGMessageLocalMidBaseline) { if (currentMessageIds == nil) { currentMessageIds = [[NSMutableArray alloc] init]; } [currentMessageIds addObject:@(item->_message.mid)]; } } } [_companion _controllerDidUpdateVisibleHoles:sortedHoles]; if (currentMessageIds != nil) { [_companion _controllerDidUpdateVisibleUnseenMessageIds:currentMessageIds]; } if (_collectionView.contentSize.height > FLT_EPSILON && !_disableScrollProcessing) { if ((NSInteger)_items.count >= TGModernConversationControllerUnloadHistoryLimit + TGModernConversationControllerUnloadHistoryThreshold) [self _maybeUnloadHistory]; if (_enableAboveHistoryRequests && _collectionView.contentOffset.y > _collectionView.contentSize.height - 800 * 2.0f && _collectionView.contentSize.height > FLT_EPSILON) [_companion loadMoreMessagesAbove]; if (_enableBelowHistoryRequests && _collectionView.contentOffset.y < 600 * 2.0f) [_companion loadMoreMessagesBelow]; } } - (void)collectionView:(UICollectionView *)__unused collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)__unused indexPath { if (_temporaryHighlightMessageIdUponDisplay != 0) { TGMessageModernConversationItem *item = [(TGModernCollectionCell *)cell boundItem]; if (item->_message.mid == _temporaryHighlightMessageIdUponDisplay) { _temporaryHighlightMessageIdUponDisplay = 0; [item setTemporaryHighlighted:true viewStorage:_viewStorage]; TGDispatchAfter(0.6, dispatch_get_main_queue(), ^ { [item setTemporaryHighlighted:false viewStorage:_viewStorage]; }); } } [self _updateVisibleItemIndices:(TGModernCollectionCell *)cell]; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == 0) { TGModernConversationItem *item = indexPath.row < (NSInteger)_items.count ? [_items objectAtIndex:indexPath.row] : nil; if (item != nil) { __block TGModernCollectionCell *cell = nil; [UIView performWithoutAnimation:^ { cell = [item dequeueCollectionCell:collectionView registeredIdentifiers:_collectionRegisteredIdentifiers forIndexPath:indexPath]; if (cell.boundItem != nil) { TGModernConversationItem *item = cell.boundItem; [item unbindCell:_viewStorage]; } if (!_disableItemBinding) [self _bindItem:item toCell:cell atIndexPath:indexPath]; }]; return cell; } } return [collectionView dequeueReusableCellWithReuseIdentifier:@"_empty" forIndexPath:indexPath]; } - (void)_bindItem:(TGModernConversationItem *)item toCell:(TGModernCollectionCell *)cell atIndexPath:(NSIndexPath *)__unused indexPath { bool movedFromTemporaryContainer = false; if (_itemsBoundToTemporaryContainer != nil && [_itemsBoundToTemporaryContainer containsObject:item]) { [_itemsBoundToTemporaryContainer removeObject:item]; [item moveToCell:cell]; movedFromTemporaryContainer = true; #if TGModernConversationControllerLogCellOperations TGLog(@"(restore item %d)", indexPath.item); #endif } if (!movedFromTemporaryContainer) { #if TGModernConversationControllerLogCellOperations TGLog(@"dequeue cell at %d (bind)", indexPath.item); #endif if (item.boundCell != nil) [item unbindCell:_viewStorage]; [item bindCell:cell viewStorage:_viewStorage]; } } - (void)collectionView:(UICollectionView *)__unused collectionView didEndDisplayingCell:(TGModernCollectionCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == 0) { if (cell.boundItem != nil) { #if TGModernConversationControllerLogCellOperations TGLog(@"enqueue cell at %d (unbind)", indexPath.item); #endif TGModernConversationItem *item = cell.boundItem; [item unbindCell:_viewStorage]; } else { #if TGModernConversationControllerLogCellOperations TGLog(@"enqueue cell at %d (clear)", indexPath.item); #endif } } [self _updateVisibleItemIndices:nil]; } #pragma mark - - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { if (scrollView == _collectionView) { //if (!_disableScrollProcessing) // _disableScrollProcessing = false; } } - (void)_checkScrollBackMessage { if (_scrollBackMessageId != 0 && _canCheckScrollBackMessageId) { bool found = false; for (TGModernCollectionCell *cell in _collectionView.visibleCells) { TGMessageModernConversationItem *item = cell.boundItem; if (item != nil && item->_message.mid == _scrollBackMessageId) { found = true; [self setScrollBackMessageId:0]; break; } } if (!found) [self setScrollBackButtonVisible:true]; } } - (void)scrollViewDidScroll:(UIScrollView *)scrollView { if (scrollView == _collectionView) { if (_unseenMessagesButton != nil && _unseenMessagesButton.superview != nil && scrollView.contentOffset.y <= -scrollView.contentInset.top) { [self setHasUnseenMessagesBelow:false]; } if (_scrollBackMessageId != 0) [self _checkScrollBackMessage]; if (scrollView.contentSize.height > FLT_EPSILON && !_disableScrollProcessing) { if ((NSInteger)_items.count >= TGModernConversationControllerUnloadHistoryLimit + TGModernConversationControllerUnloadHistoryThreshold) [self _maybeUnloadHistory]; if (_enableAboveHistoryRequests && scrollView.contentOffset.y > scrollView.contentSize.height - 800 * 2.0f && scrollView.contentSize.height > FLT_EPSILON) [_companion loadMoreMessagesAbove]; if (_enableBelowHistoryRequests && scrollView.contentOffset.y < 600 * 2.0f) [_companion loadMoreMessagesBelow]; } } } - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)__unused scrollView { _scrollingToBottom = nil; _canCheckScrollBackMessageId = true; [self _checkScrollBackMessage]; } - (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView { if (scrollView == _collectionViewScrollToTopProxy) { [_collectionView scrollToTopIfNeeded]; return false; } return false; } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)__unused gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)__unused otherGestureRecognizer { return true; } - (void)collectionViewPan:(UIPanGestureRecognizer *)__unused recognizer { } #pragma mark - - (NSArray *)_currentItems { return _items; } - (void)replaceItems:(NSArray *)newItems { _messageIdForVisibleHoleDirection = 0; [_items removeAllObjects]; [_items addObjectsFromArray:newItems]; if (self.isViewLoaded) { [_collectionView reloadData]; } } - (void)addScaleAnimation:(CALayer *)layer delay:(NSTimeInterval)delay { CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.scale"]; animation.fromValue = @(0.95f); animation.toValue = @(1.0f); animation.duration = 0.35 * TGAnimationSpeedFactor(); animation.beginTime = CACurrentMediaTime() + delay; animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; animation.removedOnCompletion = true; animation.fillMode = kCAFillModeBoth; [layer addAnimation:animation forKey:@"transform.scale"]; } - (void)addAlphaAnimation:(CALayer *)layer delay:(NSTimeInterval)delay { CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"opacity"]; animation.fromValue = @(0.0f); animation.toValue = @(1.0f); animation.duration = 0.31 * TGAnimationSpeedFactor(); animation.beginTime = CACurrentMediaTime() + delay; animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; animation.removedOnCompletion = true; animation.fillMode = kCAFillModeBoth; [layer addAnimation:animation forKey:@"opacity"]; } - (void)addOffsetAnimation:(CALayer *)layer delay:(NSTimeInterval)delay offset:(CGFloat)offset { CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"]; animation.fromValue = [NSValue valueWithCGPoint:CGPointMake(layer.position.x, layer.position.y + offset)]; animation.toValue = [NSValue valueWithCGPoint:layer.position]; animation.duration = 0.31 * TGAnimationSpeedFactor(); animation.beginTime = CACurrentMediaTime() + delay; animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; animation.removedOnCompletion = true; animation.fillMode = kCAFillModeBoth; [layer addAnimation:animation forKey:@"position"]; } - (TGMessage *)latestVisibleMessage { TGMessage *latestMessage = nil; for (TGModernCollectionCell *cell in _collectionView.visibleCells) { TGMessageModernConversationItem *item = cell.boundItem; if (item != nil) { if (latestMessage == nil || (int32_t)item->_message.date > (int32_t)latestMessage.date || ((int32_t)item->_message.date == (int32_t)latestMessage.date && ABS(item->_message.mid) > ABS(latestMessage.mid))) { latestMessage = item->_message; } } } return latestMessage; } - (NSArray *)visibleMessageIds { NSMutableArray *messageIds = [[NSMutableArray alloc] init]; for (TGModernCollectionCell *cell in _collectionView.visibleCells) { TGMessageModernConversationItem *item = cell.boundItem; if (item != nil) { [messageIds addObject:@(item->_message.mid)]; } } return messageIds; } - (void)replaceItems:(NSArray *)newItems positionAtMessageId:(int32_t)positionAtMessageId expandAt:(int32_t)__unused expandMessageId jump:(bool)jump { _messageIdForVisibleHoleDirection = positionAtMessageId; NSMutableDictionary *storedViewFrames = [[NSMutableDictionary alloc] init]; NSMutableDictionary *storedDecorationViewFrames = [[NSMutableDictionary alloc] init]; NSMutableDictionary *storedViews = [[NSMutableDictionary alloc] init]; NSMutableDictionary *storedDecorationViews = [[NSMutableDictionary alloc] init]; NSMutableArray *previousGroups = [[NSMutableArray alloc] init]; int32_t minVisiblePreviousMid = INT32_MAX; int32_t maxVisiblePreviousMid = 0; for (TGModernCollectionCell *cell in [_collectionView visibleCells]) { TGMessageModernConversationItem *item = [cell boundItem]; if (item != nil) { if (item->_message.group != nil) { [previousGroups addObject:item->_message.group]; } storedViewFrames[@(item->_message.mid)] = [NSValue valueWithCGRect:[_view convertRect:cell.frame fromView:_collectionView]]; storedViews[@(item->_message.mid)] = [cell snapshotViewAfterScreenUpdates:false]; minVisiblePreviousMid = MIN(minVisiblePreviousMid, ABS(item->_message.mid)); maxVisiblePreviousMid = MAX(maxVisiblePreviousMid, ABS(item->_message.mid)); } } for (UIView *decorationView in [_collectionView visibleDecorations]) { if ([decorationView isKindOfClass:[TGModernDateHeaderView class]]) { TGModernDateHeaderView *dateHeader = (TGModernDateHeaderView *)decorationView; storedDecorationViewFrames[@(dateHeader.date)] = [NSValue valueWithCGRect:[_view convertRect:dateHeader.frame fromView:_collectionView]]; storedDecorationViews[@(dateHeader.date)] = [dateHeader snapshotViewAfterScreenUpdates:false]; } } NSMutableDictionary *previousItemsByMessageId = [[NSMutableDictionary alloc] init]; for (TGMessageModernConversationItem *item in _items) { previousItemsByMessageId[@(item->_message.mid)] = item; } [_items removeAllObjects]; [_items addObjectsFromArray:newItems]; for (NSUInteger i = 0; i < _items.count; i++) { TGMessageModernConversationItem *currentItem = _items[i]; TGMessageModernConversationItem *previousItem = previousItemsByMessageId[@(currentItem->_message.mid)]; if (previousItem != nil) { [_items replaceObjectAtIndex:i withObject:previousItem]; } } if (self.isViewLoaded) { _disableScrollProcessing = true; [self _beginReloadDataWithTemporaryContainer]; NSInteger index = -1; CGFloat offsetDifference = 0.0f; for (TGMessageModernConversationItem *item in _items) { index++; if (item->_message.mid == positionAtMessageId) { UICollectionViewLayoutAttributes *layoutAttributes = [_collectionLayout layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0]]; NSValue *previousFrame = storedViewFrames[@(item->_message.mid)]; if (layoutAttributes != nil && previousFrame != nil) { CGRect updatedFrame = [_view convertRect:layoutAttributes.frame fromView:_collectionView]; offsetDifference = updatedFrame.origin.y - [previousFrame CGRectValue].origin.y; } break; } } CGFloat contentOffsetY = _collectionView.contentOffset.y - offsetDifference; if (expandMessageId != 0) { TGMessageGroup *expandGroup = nil; for (TGMessageGroup *group in previousGroups) { if (ABS(expandMessageId) >= group.minId && ABS(expandMessageId) <= group.maxId) { expandGroup = group; break; } } if (expandGroup != nil) { CGFloat lastItemOffsetY = 0.0; for (TGMessageModernConversationItem *item in _items) { if (item->_message.mid <= expandGroup.maxId) { lastItemOffsetY = [self contentOffsetForMessageId:item->_message.mid scrollPosition:TGInitialScrollPositionBottom initial:true additionalOffset:0.0f]; break; } } if (contentOffsetY > lastItemOffsetY) { for (TGMessageModernConversationItem *item in _items.reverseObjectEnumerator) { if (item->_message.mid >= expandGroup.minId) { contentOffsetY = MIN(contentOffsetY, [self contentOffsetForMessageId:item->_message.mid scrollPosition:TGInitialScrollPositionTop initial:true additionalOffset:100.0f]); break; } } } } } if (jump) { contentOffsetY = [self contentOffsetForMessageId:positionAtMessageId scrollPosition:TGInitialScrollPositionCenter initial:true additionalOffset:0.0f]; } if (contentOffsetY > _collectionLayout.collectionViewContentSize.height + _collectionView.contentInset.bottom - _collectionView.frame.size.height) { contentOffsetY = _collectionLayout.collectionViewContentSize.height + _collectionView.contentInset.bottom - _collectionView.frame.size.height; } if (contentOffsetY < -_collectionView.contentInset.top) { contentOffsetY = -_collectionView.contentInset.top; } [_collectionView setContentOffset:CGPointMake(0.0f, contentOffsetY) animated:false]; [_collectionView setNeedsLayout]; [_collectionView layoutSubviews]; [self _endReloadDataWithTemporaryContainer]; _disableScrollProcessing = false; NSMutableArray *visibleMids = [[NSMutableArray alloc] init]; CGFloat minY = 0.0; CGFloat maxY = 0.0; int32_t minVisibleMid = INT32_MAX; int32_t maxVisibleMid = 0; NSMutableArray *currentGroups = [[NSMutableArray alloc] init]; for (TGModernCollectionCell *cell in [_collectionView visibleCells]) { TGMessageModernConversationItem *item = [cell boundItem]; if (item != nil) { if (ABS(minY) < FLT_EPSILON || cell.frame.origin.y < minY) { minY = cell.frame.origin.y; } if (ABS(maxY) < FLT_EPSILON || CGRectGetMaxY(cell.frame) > maxY) { maxY = CGRectGetMaxY(cell.frame); } int32_t mid = item->_message.mid; minVisibleMid = MIN(minVisibleMid, ABS(mid)); maxVisibleMid = MAX(maxVisibleMid, ABS(mid)); if (item->_message.group != nil) { [currentGroups addObject:item->_message.group]; } } } for (TGModernCollectionCell *cell in [_collectionView visibleCells]) { TGMessageModernConversationItem *item = [cell boundItem]; if (item != nil) { [visibleMids addObject:@(item->_message.mid)]; NSValue *previousFrame = nil; int32_t mid = item->_message.mid; TGMessageGroup *group = nil; for (TGMessageGroup *currentGroup in previousGroups) { if (mid >= currentGroup.minId && mid <= currentGroup.maxId) { group = currentGroup; } } bool grouped = false; if (group != nil && storedViewFrames[@(-group.maxId)] != nil) { previousFrame = storedViewFrames[@(-group.maxId)]; grouped = true; } else { previousFrame = storedViewFrames[@(item->_message.mid)]; } if (previousFrame == nil && item->_message.group != nil) { int32_t minId = item->_message.group.minId; int32_t maxId = item->_message.group.maxId; __block int32_t selectedId = 0; __block NSValue *resultFrame = nil; [storedViewFrames enumerateKeysAndObjectsUsingBlock:^(NSNumber *nMid, NSValue *nFrame, __unused BOOL *stop) { int32_t mid = [nMid intValue]; if (mid >= minId && mid <= maxId) { if (selectedId == 0 || mid > selectedId) { selectedId = mid; resultFrame = nFrame; } } }]; previousFrame = resultFrame; } CGRect frame = cell.frame; if (previousFrame != nil) { CGRect updatedFrame = [_view convertRect:[previousFrame CGRectValue] toView:_collectionView]; if (!grouped) { updatedFrame.size = frame.size; cell.frame = updatedFrame; [UIView animateWithDuration:0.25 delay:0.0 options:7 << 16 animations:^{ cell.frame = frame; } completion:nil]; if (item->_message.group != nil) { [self addAlphaAnimation:cell.layer delay:0.0]; } } else { CGFloat distance = ABS(updatedFrame.origin.y - frame.origin.y) / _collectionView.frame.size.height; NSTimeInterval delay = MIN(distance / 1.5f, 0.25); [self addAlphaAnimation:cell.layer delay:delay]; [self addScaleAnimation:cell.layer delay:delay]; if (ABS(updatedFrame.origin.y - frame.origin.y) > 5.0f) { if (updatedFrame.origin.y < frame.origin.y) { [self addOffsetAnimation:cell.layer delay:delay * 0.7 offset:-8.0f]; } else { [self addOffsetAnimation:cell.layer delay:delay * 0.7 offset:8.0f]; } } } } else { CGRect offsetFrame = cell.frame; if (ABS(mid) < minVisiblePreviousMid) { offsetFrame.origin.y = maxY + 1000.0f; } else if (ABS(mid) > maxVisiblePreviousMid) { offsetFrame.origin.y = minY - 1000.0f; } cell.frame = offsetFrame; [UIView animateWithDuration:0.25 delay:0.0 options:7 << 16 animations:^{ cell.frame = frame; } completion:nil]; [self addAlphaAnimation:cell.layer delay:0.0]; } } } [storedViews removeObjectsForKeys:visibleMids]; [storedViews enumerateKeysAndObjectsUsingBlock:^(NSNumber *nMid, UIView *cell, __unused BOOL *stop) { NSValue *previousFrame = nil; int32_t mid = [nMid intValue]; TGMessageGroup *group = nil; for (TGMessageGroup *currentGroup in currentGroups) { if (mid >= currentGroup.minId && mid <= currentGroup.maxId) { group = currentGroup; } } previousFrame = storedViewFrames[@(mid)]; if (previousFrame != nil) { CGRect updatedFrame = [_view convertRect:[previousFrame CGRectValue] toView:_collectionView]; cell.frame = updatedFrame; [_collectionView insertSubview:cell atIndex:0]; CGRect offsetFrame = updatedFrame; if (mid > 0 && group == nil) { if (ABS(mid) < minVisibleMid) { offsetFrame.origin.y = maxY + 1000.0f; } else { offsetFrame.origin.y = minY - 1000.0f; } } [UIView animateWithDuration:0.2 delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^{ cell.alpha = 0.0f; cell.frame = offsetFrame; } completion:^(__unused BOOL finished){ [cell removeFromSuperview]; }]; } }]; NSMutableArray *visibleDecorationViewIds = [[NSMutableArray alloc] init]; for (UIView *decorationView in [_collectionView visibleDecorations]) { if ([decorationView isKindOfClass:[TGModernDateHeaderView class]]) { TGModernDateHeaderView *dateView = (TGModernDateHeaderView *)decorationView; NSValue *previousFrame = storedDecorationViewFrames[@(dateView.date)]; CGRect frame = dateView.frame; if (previousFrame != nil) { CGRect updatedFrame = [_view convertRect:[previousFrame CGRectValue] toView:_collectionView]; updatedFrame.size = frame.size; dateView.frame = updatedFrame; [UIView animateWithDuration:0.25 delay:0.0 options:7 << 16 animations:^{ dateView.frame = frame; } completion:nil]; } } } [storedDecorationViews removeObjectsForKeys:visibleDecorationViewIds]; [self _updateVisibleItemIndices:nil]; } } - (void)replaceItemsWithFastScroll:(NSArray *)newItems intent:(TGModernConversationInsertItemIntent)intent scrollToMessageId:(int32_t)scrollToMessageId scrollBackMessageId:(int32_t)scrollBackMessageId animated:(bool)animated { bool scrollDown = true; if (scrollToMessageId != 0) { bool allMessagesAreBelow = true; for (TGMessageModernConversationItem *item in _items) { if (item->_message.mid > scrollToMessageId) { allMessagesAreBelow = false; break; } } scrollDown = allMessagesAreBelow; } NSMutableArray *storedCells = [[NSMutableArray alloc] init]; for (TGModernCollectionCell *cell in _collectionView.visibleCells) { if (cell.boundItem != nil) { TGModernCollectionCell *cellCopy = [[TGModernCollectionCell alloc] initWithFrame:[_collectionView convertRect:cell.frame toView:_view]]; [(TGMessageModernConversationItem *)cell.boundItem moveToCell:cellCopy]; [storedCells addObject:cellCopy]; } } if (animated) _temporaryHighlightMessageIdUponDisplay = scrollToMessageId; [_items removeAllObjects]; [_collectionView reloadData]; if (storedCells.count != 0) { if (intent == TGModernConversationInsertItemIntentSendTextMessage) { _inputTextPanel.maybeInputField.oneTimeLongAnimation = true; [_inputTextPanel setPrimaryExtendedPanel:nil animated:true skipHeightAnimation:_inputTextPanel.maybeInputField.text.length != 0]; [_inputTextPanel setSecondaryExtendedPanel:nil animated:true skipHeightAnimation:_inputTextPanel.maybeInputField.text.length != 0]; [_inputTextPanel.maybeInputField setText:@"" animated:true]; } else if (intent == TGModernConversationInsertItemIntentSendOtherMessage) { if ([self currentReplyMessageId] != 0) { [_inputTextPanel setPrimaryExtendedPanel:nil animated:true skipHeightAnimation:false]; [_inputTextPanel setSecondaryExtendedPanel:nil animated:true skipHeightAnimation:false]; } } [_items addObjectsFromArray:newItems]; [_collectionView reloadData]; if (scrollToMessageId) { TGMessageModernConversationItem *selectedItem = nil; for (TGMessageModernConversationItem *messageItem in _items) { if (messageItem->_message.mid == scrollToMessageId) { selectedItem = messageItem; break; } } if (selectedItem != nil) { _scrollingToBottom = @false; [_collectionView setContentOffset:CGPointMake(0.0f, [self contentOffsetForMessageId:scrollToMessageId scrollPosition:TGInitialScrollPositionCenter initial:false additionalOffset:0.0f]) animated:false]; } else { if (_collectionView.contentOffset.y > -_collectionView.contentInset.top) { [_collectionView setContentOffset:CGPointMake(0.0f, -_collectionView.contentInset.top) animated:true]; _scrollingToBottom = @true; } } } else { _scrollingToBottom = nil; [_collectionView setContentOffset:CGPointMake(0.0f, -_collectionView.contentInset.top) animated:false]; } NSMutableArray *currentCellsWithFrames = [[NSMutableArray alloc] init]; CGFloat minStoredCellY = CGFLOAT_MAX; CGFloat maxStoredCellY = CGFLOAT_MIN; for (TGModernCollectionCell *cell in storedCells) { cell.frame = [_collectionView convertRect:cell.frame fromView:_view]; minStoredCellY = MIN(minStoredCellY, cell.frame.origin.y); maxStoredCellY = MAX(maxStoredCellY, CGRectGetMaxY(cell.frame)); [_collectionView addSubview:cell]; } [_collectionView layoutSubviews]; CGFloat maxCurrentCellY = CGFLOAT_MIN; CGFloat minCurrentCellY = CGFLOAT_MAX; for (TGModernCollectionCell *cell in _collectionView.visibleCells) { maxCurrentCellY = MAX(maxCurrentCellY, CGRectGetMaxY(cell.frame)); minCurrentCellY = MIN(minCurrentCellY, cell.frame.origin.y); } CGFloat offsetDifference = 0.0f; if (scrollDown) offsetDifference = minStoredCellY - maxCurrentCellY; else offsetDifference = maxStoredCellY - minCurrentCellY; for (TGModernCollectionCell *cell in _collectionView.visibleCells) { maxCurrentCellY = MAX(maxCurrentCellY, CGRectGetMaxY(cell.frame)); [currentCellsWithFrames addObject:@[cell, [NSValue valueWithCGRect:cell.frame]]]; cell.frame = CGRectOffset(cell.frame, 0.0f, offsetDifference); } NSMutableArray *currentDecorationsWithFrames = [[NSMutableArray alloc] init]; for (UIView *decoration in [_collectionView visibleDecorations]) { [currentDecorationsWithFrames addObject:@[decoration, [NSValue valueWithCGRect:decoration.frame]]]; decoration.frame = CGRectOffset(decoration.frame, 0.0f, offsetDifference); } [UIView animateWithDuration:animated ? 0.3 : 0.0 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^ { for (TGModernCollectionCell *cell in storedCells) { cell.frame = CGRectOffset(cell.frame, 0.0f, -offsetDifference); } for (NSArray *desc in currentCellsWithFrames) { TGModernCollectionCell *cell = desc[0]; cell.frame = [(NSValue *)desc[1] CGRectValue]; } for (NSArray *desc in currentDecorationsWithFrames) { UIView *decoration = desc[0]; decoration.frame = [(NSValue *)desc[1] CGRectValue]; } } completion:^(__unused BOOL finished) { for (TGModernCollectionCell *cell in storedCells) { [cell removeFromSuperview]; } }]; } else { if (intent == TGModernConversationInsertItemIntentSendTextMessage) { [_inputTextPanel setPrimaryExtendedPanel:nil animated:true skipHeightAnimation:false]; [_inputTextPanel setSecondaryExtendedPanel:nil animated:true skipHeightAnimation:false]; [_inputTextPanel.maybeInputField setText:@"" animated:false]; } else if (intent == TGModernConversationInsertItemIntentSendOtherMessage) { if ([self currentReplyMessageId] != 0) { [_inputTextPanel setPrimaryExtendedPanel:nil animated:true skipHeightAnimation:false]; [_inputTextPanel setSecondaryExtendedPanel:nil animated:true skipHeightAnimation:false]; } } _scrollingToBottom = nil; [_collectionView setContentOffset:CGPointMake(0.0f, -_collectionView.contentInset.top) animated:false]; } [self setScrollBackMessageId:scrollBackMessageId]; if (scrollBackMessageId != 0) [self setScrollBackButtonVisible:true]; [self _updateVisibleItemIndices:nil]; } - (void)replaceItems:(NSArray *)items atIndices:(NSIndexSet *)indices { [_items replaceObjectsAtIndexes:indices withObjects:items]; [_collectionView reloadData]; [self _updateVisibleItemIndices:nil]; } - (void)deleteItemsAtIndices:(NSIndexSet *)indexSet animated:(bool)animated { [self _deleteItemsAtIndices:indexSet animated:animated animationFactor:0.7f]; } - (void)_deleteItemsAtIndices:(NSIndexSet *)indexSet animated:(bool)animated animationFactor:(CGFloat)animationFactor { NSMutableIndexSet *indexSetAnimated = [[NSMutableIndexSet alloc] initWithIndexSet:indexSet]; if (true) { CGFloat referenceContentOffset = _collectionView.contentOffset.y; CGFloat referenceContentBoundsOffset = referenceContentOffset + _collectionView.bounds.size.height; NSUInteger lastVisibleOfCurrentIndices = NSNotFound; NSUInteger farthestVisibleOfCurrentIndices = NSNotFound; int currentItemCount = (int)_items.count; for (int i = 0; i < currentItemCount; i++) { UICollectionViewLayoutAttributes *attributes = [_collectionLayout layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]]; CGRect itemFrame = attributes.frame; if (CGRectGetMaxY(itemFrame) > referenceContentOffset + FLT_EPSILON) { if (i != 0 || itemFrame.origin.y < referenceContentOffset - FLT_EPSILON) lastVisibleOfCurrentIndices = i; break; } } for (int i = currentItemCount - 1; i >= 0; i--) { UICollectionViewLayoutAttributes *attributes = [_collectionLayout layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]]; CGRect itemFrame = attributes.frame; if (itemFrame.origin.y < referenceContentBoundsOffset + FLT_EPSILON) { if (i != currentItemCount - 1 || CGRectGetMaxY(itemFrame) > referenceContentBoundsOffset - FLT_EPSILON) farthestVisibleOfCurrentIndices = i; break; } } if (lastVisibleOfCurrentIndices != NSNotFound) { bool partialReloadRequired = false; NSMutableIndexSet *indicesToRemoveWithoutAnimation = [[NSMutableIndexSet alloc] init]; NSUInteger indexCount = indexSetAnimated.count; for (NSUInteger i = 0; i < indexCount; i++) { NSUInteger currentIndex = [indexSetAnimated firstIndex]; if (currentIndex == NSNotFound || currentIndex >= lastVisibleOfCurrentIndices) break; else { [indicesToRemoveWithoutAnimation addIndex:currentIndex]; [indexSetAnimated removeIndex:currentIndex]; partialReloadRequired = true; } } if (indicesToRemoveWithoutAnimation.count != 0) { [indexSetAnimated shiftIndexesStartingAtIndex:[indicesToRemoveWithoutAnimation firstIndex] by:-indicesToRemoveWithoutAnimation.count]; [_items removeObjectsAtIndexes:indicesToRemoveWithoutAnimation]; } if (partialReloadRequired) { CGFloat previousContentHeight = _collectionLayout.collectionViewContentSize.height; [_collectionLayout prepareLayout]; CGFloat currentContentHeight = _collectionLayout.collectionViewContentSize.height; [self _beginReloadDataWithTemporaryContainer]; _collectionView.contentOffset = CGPointMake(0.0f, _collectionView.contentOffset.y + (currentContentHeight - previousContentHeight)); [self _endReloadDataWithTemporaryContainer]; [_collectionView updateRelativeBounds]; } } if (farthestVisibleOfCurrentIndices != NSNotFound) { bool partialReloadRequired = false; NSMutableIndexSet *indicesToRemoveWithoutAnimation = [[NSMutableIndexSet alloc] init]; NSUInteger indexCount = indexSetAnimated.count; for (NSUInteger i = 0; i < indexCount; i++) { NSUInteger currentIndex = [indexSetAnimated lastIndex]; if (currentIndex == NSNotFound || currentIndex <= farthestVisibleOfCurrentIndices) break; else { [indicesToRemoveWithoutAnimation addIndex:currentIndex]; [indexSetAnimated removeIndex:currentIndex]; partialReloadRequired = true; } } if (indicesToRemoveWithoutAnimation.count != 0) [_items removeObjectsAtIndexes:indicesToRemoveWithoutAnimation]; if (partialReloadRequired) { [self _beginReloadDataWithTemporaryContainer]; [self _endReloadDataWithTemporaryContainer]; [_collectionView updateRelativeBounds]; } } } if (indexSetAnimated.count != 0) { [_items removeObjectsAtIndexes:indexSetAnimated]; if (animated && indexSetAnimated.count < 100) { if (iosMajorVersion() >= 7) [TGHacks setSecondaryAnimationDurationFactor:(float)animationFactor]; else [TGHacks setAnimationDurationFactor:(float)animationFactor]; #ifndef DEBUG @try #endif { NSUInteger indexPathCount = indexSetAnimated.count; NSMutableArray *indexPaths = [[NSMutableArray alloc] initWithCapacity:indexPathCount]; NSUInteger currentIndex = [indexSetAnimated firstIndex]; for (NSUInteger i = 0; i < indexPathCount; i++) { [indexPaths addObject:[NSIndexPath indexPathForItem:currentIndex inSection:0]]; currentIndex = [indexSetAnimated indexGreaterThanIndex:currentIndex]; } [_collectionView performBatchUpdates:^ { [_collectionView deleteItemsAtIndexPaths:indexPaths]; } completion:nil beforeDecorations:nil animated:true animationFactor:(float)animationFactor]; } #ifndef DEBUG @catch (NSException *e) { TGLog(@"%@", e); [self _resetCollectionView]; } #endif [TGHacks setSecondaryAnimationDurationFactor:1.0f]; [TGHacks setAnimationDurationFactor:1.0f]; [_collectionView updateRelativeBounds]; } else { [self _beginReloadDataWithTemporaryContainer]; [self _endReloadDataWithTemporaryContainer]; [_collectionView updateRelativeBounds]; } } [self _updateVisibleItemIndices:nil]; } - (void)moveItems:(NSArray *)moveIndexPairs { NSMutableArray *movingItems = [[NSMutableArray alloc] init]; for (NSArray *pair in moveIndexPairs) { id item = _items[[pair[0] intValue]]; [movingItems addObject:item]; [_items removeObjectAtIndex:[pair[0] intValue]]; } int index = (int)movingItems.count; for (NSArray *pair in moveIndexPairs.reverseObjectEnumerator) { index--; [_items insertObject:movingItems[index] atIndex:[pair[1] intValue]]; } #ifndef DEBUG @try #endif { [_collectionView performBatchUpdates:^ { for (NSArray *pair in moveIndexPairs) { [_collectionView moveItemAtIndexPath:[NSIndexPath indexPathForItem:[pair[0] intValue] inSection:0] toIndexPath:[NSIndexPath indexPathForItem:[pair[1] intValue] inSection:0]]; } } completion:nil]; } #ifndef DEBUG @catch (NSException *e) { TGLog(@"%@", e); [self _resetCollectionView]; } #endif [_collectionView updateRelativeBounds]; } - (void)insertItems:(NSArray *)itemsArray atIndices:(NSIndexSet *)indexSet animated:(bool)animated intent:(TGModernConversationInsertItemIntent)intent { [self insertItems:itemsArray atIndices:indexSet animated:animated intent:intent removeAtIndices:nil]; } - (void)insertItems:(NSArray *)itemsArray atIndices:(NSIndexSet *)indexSet animated:(bool)animated intent:(TGModernConversationInsertItemIntent)intent removeAtIndices:(NSIndexSet *)removeIndexSet { if (removeIndexSet.count != 0) { [self _deleteItemsAtIndices:removeIndexSet animated:animated animationFactor:0.7f]; } bool scrollToBottom = [_scrollingToBottom boolValue]; if (indexSet.count != itemsArray.count) { TGLog(@"***** %s:%s: indices.count != insertedItems.count", __FILE__, __PRETTY_FUNCTION__); return; } NSMutableArray *insertItemsAnimated = [[NSMutableArray alloc] initWithArray:itemsArray]; NSMutableIndexSet *insertIndicesAnimated = [[NSMutableIndexSet alloc] initWithIndexSet:indexSet]; if (true) { CGFloat referenceContentOffset = _collectionView.contentOffset.y + _collectionView.contentInset.top; NSUInteger lastVisibleOfCurrentIndices = NSNotFound; NSUInteger currentItemCount = _items.count; for (NSUInteger i = 0; i < currentItemCount; i++) { UICollectionViewLayoutAttributes *attributes = [_collectionLayout layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]]; CGRect itemFrame = attributes.frame; if (CGRectGetMaxY(itemFrame) > referenceContentOffset + FLT_EPSILON) { if (i != 0 || itemFrame.origin.y < referenceContentOffset - FLT_EPSILON) lastVisibleOfCurrentIndices = i; break; } } bool partialReloadRequired = false; if (lastVisibleOfCurrentIndices != NSNotFound || intent == TGModernConversationInsertItemIntentLoadMoreMessagesBelow) { bool hadIncomingUnread = false; NSUInteger modifiedLastVisibleOfCurrentIndices = lastVisibleOfCurrentIndices; NSUInteger insertedItems = 0; for (NSUInteger i = 0; i < insertItemsAnimated.count; i++) { NSUInteger currentIndex = [insertIndicesAnimated firstIndex]; if ((intent != TGModernConversationInsertItemIntentLoadMoreMessagesBelow && currentIndex > modifiedLastVisibleOfCurrentIndices) || currentIndex == NSNotFound) break; else { [_items insertObject:insertItemsAnimated[i] atIndex:currentIndex]; insertedItems++; if (intent != TGModernConversationInsertItemIntentLoadMoreMessagesBelow) { TGMessageModernConversationItem *messageItem = insertItemsAnimated[i]; if (!messageItem->_message.outgoing && messageItem->_message.unread) hadIncomingUnread = true; } [insertIndicesAnimated removeIndex:currentIndex]; if (modifiedLastVisibleOfCurrentIndices != NSNotFound) modifiedLastVisibleOfCurrentIndices++; partialReloadRequired = true; } } if (insertedItems != 0) [insertItemsAnimated removeObjectsInRange:NSMakeRange(0, insertedItems)]; if (hadIncomingUnread) [self setHasUnseenMessagesBelow:true]; } if (partialReloadRequired) { CGFloat previousContentHeight = _collectionLayout.collectionViewContentSize.height; [_collectionLayout prepareLayout]; CGFloat currentContentHeight = _collectionLayout.collectionViewContentSize.height; [self _beginReloadDataWithTemporaryContainer]; if (intent == TGModernConversationInsertItemIntentLoadMoreMessagesBelow) { _collectionView.contentOffset = CGPointMake(0.0f, _collectionView.contentOffset.y + (currentContentHeight - previousContentHeight)); } else { _collectionView.contentOffset = CGPointMake(0.0f, _collectionView.contentOffset.y + (currentContentHeight - previousContentHeight)); } [self _endReloadDataWithTemporaryContainer]; } } if (insertIndicesAnimated.count != 0) { [_items insertObjects:insertItemsAnimated atIndexes:insertIndicesAnimated]; if (animated) { if (iosMajorVersion() >= 7) [TGHacks setSecondaryAnimationDurationFactor:0.7f]; else [TGHacks setAnimationDurationFactor:0.7f]; #ifndef DEBUG @try #endif { NSUInteger indexPathCount = insertIndicesAnimated.count; NSMutableArray *indexPaths = [[NSMutableArray alloc] initWithCapacity:indexPathCount]; NSUInteger currentIndex = [insertIndicesAnimated firstIndex]; for (NSUInteger i = 0; i < indexPathCount; i++) { [indexPaths addObject:[NSIndexPath indexPathForItem:currentIndex inSection:0]]; currentIndex = [insertIndicesAnimated indexGreaterThanIndex:currentIndex]; } [_collectionView performBatchUpdates:^ { [_collectionView insertItemsAtIndexPaths:indexPaths]; } completion:nil beforeDecorations:^ { if (intent == TGModernConversationInsertItemIntentSendTextMessage || intent == TGModernConversationInsertItemIntentSendOtherMessage) { if (intent == TGModernConversationInsertItemIntentSendTextMessage) { _inputTextPanel.maybeInputField.oneTimeLongAnimation = true; [_inputTextPanel setPrimaryExtendedPanel:nil animated:true skipHeightAnimation:_inputTextPanel.maybeInputField.text.length != 0]; [_inputTextPanel setSecondaryExtendedPanel:nil animated:true skipHeightAnimation:_inputTextPanel.maybeInputField.text.length != 0]; [_inputTextPanel.maybeInputField setText:@""]; } else if (intent == TGModernConversationInsertItemIntentSendOtherMessage) { if ([self currentReplyMessageId] != 0) { [_inputTextPanel setPrimaryExtendedPanel:nil animated:true skipHeightAnimation:false]; [_inputTextPanel setSecondaryExtendedPanel:nil animated:true skipHeightAnimation:false]; } } if (_collectionView.contentOffset.y > -_collectionView.contentInset.top) { [_collectionView setContentOffset:CGPointMake(0.0f, -_collectionView.contentInset.top) animated:true]; _scrollingToBottom = @true; } //[_collectionView scrollRectToVisible:CGRectMake(0.0f, 0.0f, 1.0f, 1.0f) animated:true]; } } animated:true animationFactor:0.7f]; } #ifndef DEBUG @catch (NSException *e) { TGLog(@"%@", e); [self _resetCollectionView]; } #endif [TGHacks setSecondaryAnimationDurationFactor:1.0f]; [TGHacks setAnimationDurationFactor:1.0f]; [_collectionView updateRelativeBounds]; } else { [self _beginReloadDataWithTemporaryContainer]; [self _endReloadDataWithTemporaryContainer]; if (intent == TGModernConversationInsertItemIntentLoadMoreMessagesBelow) { } [_collectionView updateRelativeBounds]; if (intent == TGModernConversationInsertItemIntentSendTextMessage || intent == TGModernConversationInsertItemIntentSendOtherMessage) { if (intent == TGModernConversationInsertItemIntentSendTextMessage) { _inputTextPanel.maybeInputField.oneTimeLongAnimation = true; [_inputTextPanel setPrimaryExtendedPanel:nil animated:true skipHeightAnimation:_inputTextPanel.maybeInputField.text.length != 0]; [_inputTextPanel setSecondaryExtendedPanel:nil animated:true skipHeightAnimation:_inputTextPanel.maybeInputField.text.length != 0]; [_inputTextPanel.maybeInputField setText:@"" animated:true]; } else if (intent == TGModernConversationInsertItemIntentSendOtherMessage) { if ([self currentReplyMessageId] != 0) { [_inputTextPanel setPrimaryExtendedPanel:nil animated:true skipHeightAnimation:false]; [_inputTextPanel setSecondaryExtendedPanel:nil animated:true skipHeightAnimation:false]; } } [_collectionView scrollRectToVisible:CGRectMake(0.0f, 0.0f, 1.0f, 1.0f) animated:true]; } } } else if (intent == TGModernConversationInsertItemIntentSendTextMessage || intent == TGModernConversationInsertItemIntentSendOtherMessage) { if (intent == TGModernConversationInsertItemIntentSendTextMessage) { [_inputTextPanel setPrimaryExtendedPanel:nil animated:true skipHeightAnimation:false]; [_inputTextPanel setSecondaryExtendedPanel:nil animated:true skipHeightAnimation:false]; [_inputTextPanel.maybeInputField setText:@"" animated:false]; } else if (intent == TGModernConversationInsertItemIntentSendOtherMessage) { if ([self currentReplyMessageId] != 0) { [_inputTextPanel setPrimaryExtendedPanel:nil animated:true skipHeightAnimation:false]; [_inputTextPanel setSecondaryExtendedPanel:nil animated:true skipHeightAnimation:false]; } } if (_collectionView.contentOffset.y > -_collectionView.contentInset.top) { [_collectionView setContentOffset:CGPointMake(0.0f, -_collectionView.contentInset.top) animated:true]; _scrollingToBottom = @true; } } if (_enableUnloadHistoryRequests && (NSInteger)_items.count >= TGModernConversationControllerUnloadHistoryLimit + TGModernConversationControllerUnloadHistoryThreshold) [self _maybeUnloadHistory]; if (intent == TGModernConversationInsertItemIntentGeneric && _items.count != 0 && _streamAudioItems && _currentStreamingAudioMessageId == 0) { for (TGMessageModernConversationItem *item in _items.reverseObjectEnumerator) { if (!item->_message.outgoing && [itemsArray containsObject:item]) { for (id attachment in item->_message.mediaAttachments) { if ([attachment isKindOfClass:[TGAudioMediaAttachment class]]) { _currentStreamingAudioMessageId = item->_message.mid; if (item->_mediaAvailabilityStatus) [self playAudioFromMessage:item->_message.mid media:attachment]; break; } } } } } if (scrollToBottom) { if (_collectionView.contentOffset.y > -_collectionView.contentInset.top) { [_collectionView setContentOffset:CGPointMake(0.0f, -_collectionView.contentInset.top) animated:true]; _scrollingToBottom = @true; } } if ([self currentReplyMessageId] == 0 && intent == TGModernConversationInsertItemIntentGeneric) { for (TGMessageModernConversationItem *item in itemsArray) { if (item->_message.forceReply) { [self setReplyMessage:item->_message animated:true]; break; } } } [self _updateVisibleItemIndices:nil]; } - (void)updateItemAtIndex:(NSUInteger)index toItem:(TGModernConversationItem *)updatedItem { CGFloat containerWidth = _collectionView == nil ? _view.frame.size.width : _collectionView.frame.size.width; bool sizeChanged = false; CGSize lastSize = [(TGMessageModernConversationItem *)_items[index] sizeForContainerSize:CGSizeMake(containerWidth, CGFLOAT_MAX)]; [_items[index] updateToItem:updatedItem viewStorage:_viewStorage sizeChanged:&sizeChanged]; CGSize updatedSize = lastSize; if (sizeChanged) { updatedSize = [(TGMessageModernConversationItem *)_items[index] sizeForContainerSize:CGSizeMake(containerWidth, CGFLOAT_MAX)]; } TGMessageModernConversationItem *item = (TGMessageModernConversationItem *)updatedItem; if (_streamAudioItems && _currentStreamingAudioMessageId == item->_message.mid && item->_mediaAvailabilityStatus) { for (id attachment in item->_message.mediaAttachments) { if ([attachment isKindOfClass:[TGAudioMediaAttachment class]]) { [self playAudioFromMessage:item->_message.mid media:attachment]; break; } } } if (sizeChanged && ABS(lastSize.height - updatedSize.height) != 0) { if (_collectionView.isDecelerating) { [_collectionLayout invalidateLayout]; [_collectionView layoutSubviews]; } else { std::vector decorationAttributes; NSArray *layoutAttributes = [_collectionLayout layoutAttributesForItems:_items containerWidth:containerWidth maxHeight:FLT_MAX decorationViewAttributes:&decorationAttributes contentHeight:NULL]; [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^ { for (NSIndexPath *indexPath in _collectionView.indexPathsForVisibleItems) { TGModernCollectionCell *cell = (TGModernCollectionCell *)[_collectionView cellForItemAtIndexPath:indexPath]; TGMessageModernConversationItem *cellItem = cell.boundItem; if (cellItem != nil) { UICollectionViewLayoutAttributes *attributes = layoutAttributes[indexPath.row]; cell.frame = attributes.frame; } } } completion:^(BOOL finished) { if (finished) [_collectionLayout invalidateLayout]; }]; [_collectionView updateRelativeBounds]; } } } - (void)updateItemProgressAtIndex:(NSUInteger)index toProgress:(CGFloat)progress animated:(bool)animated { if (index > _items.count) return; [_items[index] updateProgress:(float)progress viewStorage:_viewStorage animated:animated]; } - (void)imageDataInvalidated:(NSString *)imageUrl { if (_collectionView != nil) { for (TGModernCollectionCell *cell in [_collectionView visibleCells]) { [(TGMessageModernConversationItem *)cell.boundItem imageDataInvalidated:imageUrl]; } } else { for (TGMessageModernConversationItem *messageItem in _items) { [messageItem imageDataInvalidated:imageUrl]; } } } - (void)updateCheckedMessages { if (_editingMode) { for (TGModernCollectionCell *cell in _collectionView.visibleCells) { [(TGMessageModernConversationItem *)cell.boundItem updateEditingState:_viewStorage animationDelay:0.0]; } [self _updateEditingPanel]; } } - (void)updateMessageAttributes:(int32_t)messageId { for (TGMessageModernConversationItem *messageItem in _items) { if (messageItem->_message.mid == messageId) { [messageItem updateMessageAttributes]; break; } } } - (void)setHasUnseenMessagesBelow:(bool)hasUnseenMessagesBelow { if (!self.isViewLoaded) return; _hasUnseenMessagesBelow = hasUnseenMessagesBelow; [self setScrollBackButtonVisible:_hasUnseenMessagesBelow || _scrollBackMessageId != 0]; } - (void)setUnreadMessageRangeIfAppropriate:(TGMessageRange)unreadMessageRange { if (!TGMessageRangeEquals(_collectionView.unreadMessageRange, unreadMessageRange)) { _collectionView.unreadMessageRange = unreadMessageRange; [_collectionView reloadData]; [_collectionView layoutSubviews]; int32_t minMessageId = INT32_MAX; for (TGMessageModernConversationItem *item in _items) { if (item->_message.mid >= unreadMessageRange.firstMessageId) { if (item->_message.mid < minMessageId || minMessageId == INT32_MIN) { minMessageId = item->_message.mid; } } } if (minMessageId != INT32_MAX) { CGFloat contentOffset = [self contentOffsetForMessageId:minMessageId scrollPosition:TGInitialScrollPositionTop initial:false additionalOffset:0.0f]; [_collectionView setContentOffset:CGPointMake(0.0f, contentOffset) animated:false]; } } } - (void)setScrollBackMessageId:(int32_t)scrollBackMessageId { if (!self.isViewLoaded) return; _scrollBackMessageId = scrollBackMessageId; } - (void)setScrollBackButtonVisible:(bool)scrollBackButtonVisible { if (scrollBackButtonVisible) { if (_unseenMessagesButton == nil) { UIImage *image = [UIImage imageNamed:@"ModernConversationUnseenMessagesButton.png"]; _unseenMessagesButton = [[UIButton alloc] initWithFrame:CGRectMake(0.0f, 0.0f, image.size.width, image.size.height)]; [_unseenMessagesButton setImage:image forState:UIControlStateNormal]; _unseenMessagesButton.adjustsImageWhenHighlighted = false; [_unseenMessagesButton addTarget:self action:@selector(unseenMessagesButtonPressed) forControlEvents:UIControlEventTouchUpInside]; } if (_unseenMessagesButton.superview == nil) { [_view insertSubview:_unseenMessagesButton aboveSubview:_collectionView]; [self _updateUnseenMessagesButton]; } } else if (_unseenMessagesButton != nil) { [_unseenMessagesButton removeFromSuperview]; } } - (void)_updateUnseenMessagesButton { if (_unseenMessagesButton.superview != nil) { CGSize collectionViewSize = _view.bounds.size; CGSize buttonSize = _unseenMessagesButton.frame.size; _unseenMessagesButton.frame = CGRectMake(collectionViewSize.width - buttonSize.width - 6, collectionViewSize.height - buttonSize.height - _collectionView.contentInset.top - 6, buttonSize.width, buttonSize.height); } } - (void)_updateEditingPanel { if ([_currentInputPanel isKindOfClass:[TGModernConversationEditingPanel class]]) { TGModernConversationEditingPanel *editingPanel = (TGModernConversationEditingPanel *)_currentInputPanel; [editingPanel setActionsEnabled:[_companion checkedMessageCount] != 0]; [editingPanel setDeleteEnabled:[self canDeleteSelectedMessages]]; } } - (void)_beginReloadDataWithTemporaryContainer { _itemsBoundToTemporaryContainer = [[NSMutableArray alloc] init]; UIView *tempContainer = [[UIView alloc] init]; for (NSIndexPath *indexPath in [_collectionView indexPathsForVisibleItems]) { TGModernCollectionCell *cell = (TGModernCollectionCell *)[_collectionView cellForItemAtIndexPath:indexPath]; TGModernConversationItem *item = [cell boundItem]; if (item != nil) { [item temporaryMoveToView:tempContainer]; [_itemsBoundToTemporaryContainer addObject:item]; #if TGModernConversationControllerLogCellOperations TGLog(@"(store item %d)", indexPath.item); #endif } } _disableItemBinding = true; [_collectionView reloadData]; } - (void)_endReloadDataWithTemporaryContainer { [_collectionView updateVisibleItemsNow]; [_collectionView layoutIfNeeded]; _disableItemBinding = false; for (NSIndexPath *indexPath in _collectionView.indexPathsForVisibleItems) { TGModernCollectionCell *cell = (TGModernCollectionCell *)[_collectionView cellForItemAtIndexPath:indexPath]; if (cell.boundItem == nil && indexPath.row < (NSInteger)_items.count) { [self _bindItem:_items[indexPath.row] toCell:cell atIndexPath:indexPath]; } } for (TGModernConversationItem *item in _itemsBoundToTemporaryContainer) { [item unbindCell:_viewStorage]; } _itemsBoundToTemporaryContainer = nil; } - (void)updateItems:(NSArray *)updatedItems atIndices:(NSArray *)indices { if (indices.count == 0) return; if (indices.count != updatedItems.count) { TGLog(@"***** %s:%s: indices.count != updatedItems", __FILE__, __PRETTY_FUNCTION__); return; } int index = -1; bool sizeChanged = false; for (NSNumber *nIndex in indices) { index++; [(TGModernConversationItem *)_items[[nIndex intValue]] updateToItem:updatedItems[index] viewStorage:_viewStorage sizeChanged:&sizeChanged]; } } - (void)scrollToMessage:(int32_t)messageId sourceMessageId:(int32_t)sourceMessageId animated:(bool)animated { TGMessageModernConversationItem *selectedItem = nil; for (TGMessageModernConversationItem *messageItem in _items) { if (messageItem->_message.mid == messageId) { selectedItem = messageItem; break; } } if (selectedItem != nil) { bool foundCell = false; for (TGModernCollectionCell *cell in _collectionView.visibleCells) { if (((TGMessageModernConversationItem *)cell.boundItem)->_message.mid == messageId) { foundCell = true; break; } } if (animated) { if (foundCell) { [selectedItem setTemporaryHighlighted:true viewStorage:_viewStorage]; TGDispatchAfter(0.6, dispatch_get_main_queue(), ^ { [selectedItem setTemporaryHighlighted:false viewStorage:_viewStorage]; }); } else _temporaryHighlightMessageIdUponDisplay = messageId; } CGFloat contentOffset = [self contentOffsetForMessageId:messageId scrollPosition:TGInitialScrollPositionCenter initial:false additionalOffset:0.0f]; if (ABS(contentOffset - _collectionView.contentOffset.y) > FLT_EPSILON) { _canCheckScrollBackMessageId = false; [self setScrollBackMessageId:sourceMessageId]; [_collectionView setContentOffset:CGPointMake(0.0f, contentOffset) animated:animated]; } else { [self setScrollBackMessageId:0]; [self setScrollBackButtonVisible:false]; } } } - (void)openMediaFromMessage:(int32_t)messageId instant:(bool)instant { TGMessageModernConversationItem *mediaMessageItem = nil; TGModernCollectionCell *mediaItemCell = nil; int index = -1; for (TGMessageModernConversationItem *messageItem in _items) { index++; if (messageItem->_message.mid == messageId) { TGModernCollectionCell *cell = (TGModernCollectionCell *)[_collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0]]; if (messageItem->_mediaAvailabilityStatus && cell != nil) { mediaMessageItem = messageItem; mediaItemCell = cell; } break; } } if (mediaMessageItem != nil && index >= 0) { TGUser *author = nil; if (mediaMessageItem->_message.fromUid == 0) { author = [[TGUser alloc] init]; author.uid = 0; author.firstName = _titleView.title; } else if (!TGPeerIdIsChannel(mediaMessageItem->_message.fromUid)) { author = [TGDatabaseInstance() loadUser:mediaMessageItem->_message.outgoing ? TGTelegraphInstance.clientUserId : (int32_t)mediaMessageItem->_message.fromUid]; } else { author = [[TGUser alloc] init]; author.uid = 0; author.firstName = _titleView.title; } if (author == nil) return; bool isGallery = false; bool isAvatar = false; TGImageInfo *avatarImageInfo = nil; TGWebPageMediaAttachment *webPage = nil; int32_t webPageMessageId = 0; bool foundMedia = false; for (TGMediaAttachment *attachment in mediaMessageItem->_message.mediaAttachments) { switch (attachment.type) { case TGVideoMediaAttachmentType: case TGImageMediaAttachmentType: { foundMedia = true; isGallery = true; break; } case TGWebPageMediaAttachmentType: { webPage = ((TGWebPageMediaAttachment *)attachment); foundMedia = true; webPageMessageId = mediaMessageItem->_message.mid; break; } case TGActionMediaAttachmentType: { TGActionMediaAttachment *actionAttachment = (TGActionMediaAttachment *)attachment; switch (actionAttachment.actionType) { case TGMessageActionChatEditPhoto: { foundMedia = true; TGImageMediaAttachment *photo = actionAttachment.actionData[@"photo"]; isAvatar = true; avatarImageInfo = photo.imageInfo; break; } default: break; } break; } case TGLocationMediaAttachmentType: { int64_t peerId = mediaMessageItem->_message.fromUid; if (mediaMessageItem->_message.forwardPeerId != 0) { peerId = mediaMessageItem->_message.forwardPeerId; } TGLocationMediaAttachment *mapAttachment = (TGLocationMediaAttachment *)attachment; __weak TGModernConversationController *weakSelf = self; id peer = nil; if (TGPeerIdIsChannel(peerId)) { peer = [TGDatabaseInstance() loadChannels:@[@(peerId)]][@(peerId)]; } else { peer = [TGDatabaseInstance() loadUser:(int32_t)peerId]; } TGLocationViewController *controller = [[TGLocationViewController alloc] initWithCoordinate:CLLocationCoordinate2DMake(mapAttachment.latitude, mapAttachment.longitude) venue:mapAttachment.venue peer:peer]; controller.calloutPressed = ^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { if ([peer isKindOfClass:[TGConversation class]]) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf.navigationController popViewControllerAnimated:true]; } } else if ([peer isKindOfClass:[TGUser class]]) { [[TGInterfaceManager instance] navigateToProfileOfUser:((TGUser *)peer).uid]; } } }; if ([_companion allowMessageForwarding]) { controller.forwardPressed = ^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) [strongSelf.companion controllerWantsToForwardMessages:@[@(mediaMessageItem->_message.mid)]]; }; } if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) { [self.navigationController pushViewController:controller animated:true]; } else { TGNavigationController *navigationController = [TGNavigationController navigationControllerWithControllers:@[controller]]; navigationController.presentationStyle = TGNavigationControllerPresentationStyleInFormSheet; navigationController.modalPresentationStyle = UIModalPresentationFormSheet; [self presentViewController:navigationController animated:true completion:nil]; } break; } case TGDocumentMediaAttachmentType: { TGDocumentMediaAttachment *documentAttachment = (TGDocumentMediaAttachment *)attachment; /*if ([documentAttachment.mimeType isEqualToString:@"audio/mpeg"]) { if (_currentAudioPlayerMessageId != messageId) [self playAudioFromMessage:messageId media:documentAttachment]; else [_currentAudioPlayer play]; break; }*/ if ([[[documentAttachment.fileName pathExtension] lowercaseString] isEqualToString:@"strings"]) { [[[TGActionSheet alloc] initWithTitle:nil actions:@[ [[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Conversation.ApplyLocalization") action:@"applyLocalization"], [[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Conversation.OpenFile") action:@"open"], [[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Common.Cancel") action:@"cancel" type:TGActionSheetActionTypeCancel], ] actionBlock:^(TGModernConversationController *controller, NSString *action) { if ([action isEqualToString:@"applyLocalization"]) { NSBundle *referenceBundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"en" ofType:@"lproj"]]; NSDictionary *referenceDict = [NSDictionary dictionaryWithContentsOfFile:[referenceBundle pathForResource:@"Localizable" ofType:@"strings"]]; NSDictionary *localizationDict = [NSDictionary dictionaryWithContentsOfFile:[_companion fileUrlForDocumentMedia:documentAttachment].path]; __block bool valid = true; NSMutableArray *missingKeys = [[NSMutableArray alloc] init]; NSMutableArray *invalidFormatKeys = [[NSMutableArray alloc] init]; NSString *invalidFileString = nil; if (localizationDict != nil && referenceDict != nil) { [referenceDict enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *sourceValue, __unused BOOL *stop) { NSString *targetValue = localizationDict[key]; if (targetValue == nil) { [missingKeys addObject:key]; } else { for (int i = 0; i < 2; i++) { NSString *firstValue = i == 0 ? sourceValue : targetValue; NSString *secondValue = i == 0 ? targetValue : sourceValue; NSRange firstRange = NSMakeRange(0, firstValue.length); NSRange secondRange = NSMakeRange(0, secondValue.length); while (firstRange.length != 0) { NSRange range = [firstValue rangeOfString:@"%" options:0 range:firstRange]; if (range.location == NSNotFound || range.location == firstValue.length - 1) break; else { firstRange.location = range.location + range.length; firstRange.length = firstValue.length - firstRange.location; NSString *findPositionalString = nil; NSString *findFreeString = nil; unichar c = [firstValue characterAtIndex:range.location + 1]; if (c == 'd' || c == 'f') findPositionalString = [[NSString alloc] initWithFormat:@"%%%c", (char)c]; else if (c >= '0' && c <= '9') { if (range.location + 3 < firstValue.length) { if ([firstValue characterAtIndex:range.location + 2] == '$') { unichar formatChar = [firstValue characterAtIndex:range.location + 3]; findFreeString = [[NSString alloc] initWithFormat:@"%%%c$%c", (char)c, (char)formatChar]; } } } if (findPositionalString != nil) { NSRange foundRange = [secondValue rangeOfString:findPositionalString options:0 range:secondRange]; if (foundRange.location != NSNotFound) { secondRange.location = foundRange.location + foundRange.length; secondRange.length = secondValue.length - secondRange.location; } else { valid = false; [invalidFormatKeys addObject:key]; break; } } else if (findFreeString != nil) { if ([secondValue rangeOfString:findFreeString].location == NSNotFound) { valid = false; [invalidFormatKeys addObject:key]; break; } } } } } } }]; } else { valid = false; invalidFileString = @"invalid localization file format"; } if (valid) { NSMutableString *missingKeysString = [[NSMutableString alloc] init]; static const int maxKeys = 5; for (int i = 0; i < maxKeys && i < (int)missingKeys.count; i++) { if (missingKeysString.length != 0) [missingKeysString appendString:@", "]; [missingKeysString appendString:missingKeys[i]]; if (i == maxKeys - 1 && maxKeys < (int)missingKeys.count) [missingKeysString appendFormat:@" and %d more", (int)(missingKeys.count - maxKeys)]; } if (missingKeysString.length != 0) { TGAlertView *alertView = [[TGAlertView alloc] initWithTitle:nil message:[[NSString alloc] initWithFormat:@"Localization file is valid, but the following keys are missing: %@", missingKeysString] delegate:nil cancelButtonTitle:TGLocalized(@"Common.OK") otherButtonTitles:nil]; [alertView show]; } [controller.companion controllerWantsToApplyLocalization:[_companion fileUrlForDocumentMedia:documentAttachment].path]; } else { NSString *reasonString = nil; if (invalidFileString.length != 0) reasonString = invalidFileString; else if (invalidFormatKeys.count != 0) { NSMutableString *invalidFormatKeysString = [[NSMutableString alloc] init]; static const int maxKeys = 5; for (int i = 0; i < maxKeys && i < (int)invalidFormatKeys.count; i++) { if (invalidFormatKeysString.length != 0) [invalidFormatKeysString appendString:@", "]; [invalidFormatKeysString appendString:invalidFormatKeys[i]]; if (i == maxKeys - 1 && maxKeys < (int)invalidFormatKeys.count) [invalidFormatKeysString appendFormat:@" and %d more", (int)(invalidFormatKeys.count - maxKeys)]; } reasonString = [[NSString alloc] initWithFormat:@"invalid value format for keys %@", invalidFormatKeysString]; } TGAlertView *alertView = [[TGAlertView alloc] initWithTitle:nil message:[[NSString alloc] initWithFormat:@"Invalid localization file: %@", reasonString] delegate:nil cancelButtonTitle:TGLocalized(@"Common.OK") otherButtonTitles:nil]; [alertView show]; } } else if ([action isEqualToString:@"open"]) { TGDocumentController *documentController = [[TGDocumentController alloc] initWithURL:[controller.companion fileUrlForDocumentMedia:documentAttachment] messageId:mediaMessageItem->_message.mid]; if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) [controller.navigationController pushViewController:documentController animated:true]; else { if (iosMajorVersion() >= 8) { documentController.modalPresentationStyle = UIModalPresentationFormSheet; [controller presentViewController:documentController animated:false completion:nil]; } else { TGNavigationController *navigationController = [TGNavigationController navigationControllerWithControllers:@[documentController]]; navigationController.presentationStyle = TGNavigationControllerPresentationStyleInFormSheet; navigationController.modalPresentationStyle = UIModalPresentationFormSheet; [controller presentViewController:navigationController animated:true completion:nil]; } } [controller.companion updateMediaAccessTimeForMessageId:messageId]; } } target:self] showInView:_view]; break; } TGDocumentController *documentController = [[TGDocumentController alloc] initWithURL:[_companion fileUrlForDocumentMedia:documentAttachment] messageId:mediaMessageItem->_message.mid]; if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) [self.navigationController pushViewController:documentController animated:true]; else { if (iosMajorVersion() >= 8) { documentController.modalPresentationStyle = UIModalPresentationFormSheet; [self presentViewController:documentController animated:false completion:nil]; } else { TGNavigationController *navigationController = [TGNavigationController navigationControllerWithControllers:@[documentController]]; navigationController.presentationStyle = TGNavigationControllerPresentationStyleInFormSheet; navigationController.modalPresentationStyle = UIModalPresentationFormSheet; [self presentViewController:navigationController animated:true completion:nil]; } } [_companion updateMediaAccessTimeForMessageId:messageId]; break; } case TGAudioMediaAttachmentType: { TGAudioMediaAttachment *audioAttachment = (TGAudioMediaAttachment *)attachment; if (_currentAudioPlayerMessageId != messageId) [self playAudioFromMessage:messageId media:audioAttachment]; else [_currentAudioPlayer play]; break; } default: break; } if (foundMedia) break; } if (!foundMedia) return; [self stopInlineMediaIfPlaying]; TGModernGalleryController *modernGallery = [[TGModernGalleryController alloc] init]; if (webPage != nil) { modernGallery.model = [[TGExternalGalleryModel alloc] initWithWebPage:webPage]; } else if (isGallery) { if (mediaMessageItem->_message.messageLifetime > 0 && mediaMessageItem->_message.messageLifetime <= 60 && mediaMessageItem->_message.layer >= 17) { modernGallery.model = [[TGSecretPeerMediaGalleryModel alloc] initWithPeerId:((TGGenericModernConversationCompanion *)_companion).conversationId messageId:mediaMessageItem->_message.mid]; } else if (!_companion.allowMessageForwarding) { modernGallery.model = [[TGSecretInfiniteLifetimePeerMediaGalleryModel alloc] initWithPeerId:((TGGenericModernConversationCompanion *)_companion).conversationId atMessageId:mediaMessageItem->_message.mid allowActions:_companion.allowMessageForwarding important:TGMessageSortKeySpace(mediaMessageItem->_message.sortKey) == TGMessageSpaceImportant]; } else { modernGallery.model = [[TGGenericPeerMediaGalleryModel alloc] initWithPeerId:((TGGenericModernConversationCompanion *)_companion).conversationId atMessageId:mediaMessageItem->_message.mid allowActions:_companion.allowMessageForwarding important:TGMessageSortKeySpace(mediaMessageItem->_message.sortKey) == TGMessageSpaceImportant]; } } else if (isAvatar) { NSString *legacyThumbnailUrl = [avatarImageInfo closestImageUrlWithSize:CGSizeZero resultingSize:NULL]; NSString *legacyUrl = [avatarImageInfo imageUrlForLargestSize:NULL]; modernGallery.model = [[TGGroupAvatarGalleryModel alloc] initWithMessageId:mediaMessageItem->_message.mid legacyThumbnailUrl:legacyThumbnailUrl legacyUrl:legacyUrl imageSize:CGSizeMake(640.0f, 640.0f)]; } __weak TGModernConversationController *weakSelf = self; modernGallery.itemFocused = ^(id item) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { if ([item conformsToProtocol:@protocol(TGGenericPeerGalleryItem)]) { id concreteItem = (id)item; int32_t messageId = [concreteItem messageId]; strongSelf.companion.mediaHiddenMessageId = messageId; for (TGModernCollectionCell *cell in strongSelf->_collectionView.visibleCells) { [(TGMessageModernConversationItem *)[cell boundItem] updateMediaVisibility]; } } else if ([item isKindOfClass:[TGGroupAvatarGalleryItem class]]) { int32_t messageId = ((TGGroupAvatarGalleryItem *)item).messageId; strongSelf.companion.mediaHiddenMessageId = messageId; for (TGModernCollectionCell *cell in strongSelf->_collectionView.visibleCells) { [(TGMessageModernConversationItem *)[cell boundItem] updateMediaVisibility]; } } else { int32_t messageId = webPageMessageId; strongSelf.companion.mediaHiddenMessageId = messageId; for (TGModernCollectionCell *cell in strongSelf->_collectionView.visibleCells) { [(TGMessageModernConversationItem *)[cell boundItem] updateMediaVisibility]; } } } }; modernGallery.finishedTransitionIn = ^(__unused id item, TGModernGalleryItemView *itemView) { if ([itemView isKindOfClass:[TGModernGalleryVideoItemView class]]) { [((TGModernGalleryVideoItemView *)itemView) play]; } }; modernGallery.beginTransitionIn = ^UIView *(id item, TGModernGalleryItemView *itemView) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { if ([item conformsToProtocol:@protocol(TGGenericPeerGalleryItem)]) { if ([itemView isKindOfClass:[TGModernGalleryVideoItemView class]]) { [((TGModernGalleryVideoItemView *)itemView) hidePlayButton]; } id concreteItem = (id)item; int32_t messageId = [concreteItem messageId]; for (TGModernCollectionCell *cell in strongSelf->_collectionView.visibleCells) { TGMessageModernConversationItem *cellItem = [cell boundItem]; if (cellItem != nil && cellItem->_message.mid == messageId) { return [cellItem referenceViewForImageTransition]; } } } else if ([item isKindOfClass:[TGGroupAvatarGalleryItem class]]) { int32_t messageId = ((TGGroupAvatarGalleryItem *)item).messageId; for (TGModernCollectionCell *cell in strongSelf->_collectionView.visibleCells) { TGMessageModernConversationItem *cellItem = [cell boundItem]; if (cellItem != nil && cellItem->_message.mid == messageId) { return [cellItem referenceViewForImageTransition]; } } } else { int32_t messageId = webPageMessageId; for (TGModernCollectionCell *cell in strongSelf->_collectionView.visibleCells) { TGMessageModernConversationItem *cellItem = [cell boundItem]; if (cellItem != nil && cellItem->_message.mid == messageId) { return [cellItem referenceViewForImageTransition]; } } } } return nil; }; modernGallery.beginTransitionOut = ^UIView *(id item) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { if ([item conformsToProtocol:@protocol(TGGenericPeerGalleryItem)]) { id concreteItem = (id)item; int32_t messageId = [concreteItem messageId]; for (TGModernCollectionCell *cell in strongSelf->_collectionView.visibleCells) { TGMessageModernConversationItem *cellItem = [cell boundItem]; if (cellItem != nil && cellItem->_message.mid == messageId) { return [cellItem referenceViewForImageTransition]; } } } else if ([item isKindOfClass:[TGGroupAvatarGalleryItem class]]) { int32_t messageId = ((TGGroupAvatarGalleryItem *)item).messageId; for (TGModernCollectionCell *cell in strongSelf->_collectionView.visibleCells) { TGMessageModernConversationItem *cellItem = [cell boundItem]; if (cellItem != nil && cellItem->_message.mid == messageId) { return [cellItem referenceViewForImageTransition]; } } } else { int32_t messageId = webPageMessageId; for (TGModernCollectionCell *cell in strongSelf->_collectionView.visibleCells) { TGMessageModernConversationItem *cellItem = [cell boundItem]; if (cellItem != nil && cellItem->_message.mid == messageId) { return [cellItem referenceViewForImageTransition]; } } } } return nil; }; modernGallery.completedTransitionOut = ^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { strongSelf.companion.mediaHiddenMessageId = 0; for (TGModernCollectionCell *cell in strongSelf->_collectionView.visibleCells) { [(TGMessageModernConversationItem *)[cell boundItem] updateMediaVisibility]; } } }; modernGallery.animateTransition = !instant; modernGallery.showInterface = !instant; [self closeExistingMediaGallery]; TGOverlayControllerWindow *controllerWindow = [[TGOverlayControllerWindow alloc] initWithParentController:self contentController:modernGallery]; controllerWindow.hidden = false; } } - (void)audioPlayerDidFinish { int32_t currentStreamingAudioMessageId = _currentStreamingAudioMessageId; [self stopInlineMediaIfPlaying]; if (_streamAudioItems && currentStreamingAudioMessageId != 0) { int32_t lastIncomingAudioMessageId = 0; TGAudioMediaAttachment *lastIncomingAudioMessageMedia = nil; for (TGMessageModernConversationItem *item in _items) { if (item->_message.outgoing) continue; if (item->_message.mid <= currentStreamingAudioMessageId) break; for (id attachment in item->_message.mediaAttachments) { if ([attachment isKindOfClass:[TGAudioMediaAttachment class]]) { lastIncomingAudioMessageId = item->_message.mid; if (item->_mediaAvailabilityStatus) lastIncomingAudioMessageMedia = attachment; break; } } } if (lastIncomingAudioMessageId != 0) { _currentStreamingAudioMessageId = lastIncomingAudioMessageId; if (lastIncomingAudioMessageMedia != nil) [self playAudioFromMessage:lastIncomingAudioMessageId media:lastIncomingAudioMessageMedia]; } else _currentStreamingAudioMessageId = 0; } } - (void)playAudioFromMessage:(int32_t)messageId media:(id)media { if (_currentAudioRecorder != nil) return; [self stopAudioRecording]; [self stopInlineMediaIfPlaying]; NSString *localFilePath = nil; if ([media isKindOfClass:[TGAudioMediaAttachment class]]) localFilePath = ((TGAudioMediaAttachment *)media).localFilePath; else if ([media isKindOfClass:[TGDocumentMediaAttachment class]]) localFilePath = [[_companion fileUrlForDocumentMedia:media] path]; _currentAudioPlayer = [[TGModernConversationAudioPlayer alloc] initWithFilePath:localFilePath]; _currentAudioPlayer.delegate = self; _currentAudioPlayerMessageId = messageId; [_currentAudioPlayer play:0.0f]; [self updateInlineMediaContexts]; _streamAudioItems = TGAppDelegateInstance.autoPlayAudio; if (_streamAudioItems) _currentStreamingAudioMessageId = messageId; else _currentStreamingAudioMessageId = 0; [_companion updateMediaAccessTimeForMessageId:messageId]; [_companion markMessagesAsViewed:@[@(messageId)]]; } - (void)closeExistingMediaGallery { for (UIWindow *window in [self.associatedWindowStack copy]) { if ([window isKindOfClass:[TGOverlayControllerWindow class]]) { if ([window.rootViewController isKindOfClass:[TGModernGalleryController class]]) { [((TGModernGalleryController *)window.rootViewController) dismiss]; } } } } - (void)closeMediaFromMessage:(int32_t)__unused messageId instant:(bool)__unused instant { [self closeExistingMediaGallery]; self.associatedWindowStack = nil; _companion.mediaHiddenMessageId = 0; for (TGModernCollectionCell *cell in _collectionView.visibleCells) { [(TGMessageModernConversationItem *)[cell boundItem] updateMediaVisibility]; } } - (void)stopInlineMedia { for (TGMessageModernConversationItem *item in _items) { [item stopInlineMedia]; } } - (void)updateInlineMediaContexts { for (TGMessageModernConversationItem *item in _items) { [item updateInlineMediaContext]; } } - (void)openBrowserFromMessage:(int32_t)__unused messageId url:(NSString *)url { [(TGApplication *)[TGApplication sharedApplication] openURL:[NSURL URLWithString:url] forceNative:true]; } - (void)showActionsMenuForUnsentMessage:(int32_t)messageId { TGMessageModernConversationItem *unsentMessageItem = nil; int unsentMessageCount = 0; for (TGMessageModernConversationItem *messageItem in _items) { if (messageItem->_message.mid == messageId) unsentMessageItem = messageItem; if (messageItem->_message.deliveryState == TGMessageDeliveryStateFailed) unsentMessageCount++; } if (unsentMessageItem != nil && unsentMessageCount != 0) { NSMutableArray *actions = [[NSMutableArray alloc] init]; if (unsentMessageItem->_message.text.length != 0) [actions addObject:[[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Conversation.MessageDialogEdit") action:@"editMessage"]]; [actions addObject:[[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Conversation.MessageDialogRetry") action:@"resendMessage"]]; if (unsentMessageCount > 1) [actions addObject:[[TGActionSheetAction alloc] initWithTitle:[[NSString alloc] initWithFormat:TGLocalized(@"Conversation.MessageDialogRetryAll"), unsentMessageCount] action:@"resendAllMessages"]]; [actions addObject:[[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Conversation.MessageDialogDelete") action:@"deleteMessage"]]; [actions addObject:[[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Common.Cancel") action:@"cancel" type:TGActionSheetActionTypeCancel]]; [[[TGActionSheet alloc] initWithTitle:TGLocalized(@"Conversation.MessageDeliveryFailed") actions:actions actionBlock:^(TGModernConversationController *controller, NSString *action) { if ([action isEqualToString:@"editMessage"]) { [controller.companion _deleteMessages:@[@(messageId)] animated:true]; [controller.companion controllerDeletedMessages:@[@(messageId)] completion:nil]; _inputTextPanel.inputField.text = unsentMessageItem->_message.text; } else if ([action isEqualToString:@"resendMessage"]) { [controller.companion controllerWantsToResendMessages:@[@(messageId)]]; } else if ([action isEqualToString:@"resendAllMessages"]) { NSMutableArray *messageIds = [[NSMutableArray alloc] init]; for (TGMessageModernConversationItem *messageItem in controller->_items.reverseObjectEnumerator) { if (messageItem->_message.deliveryState == TGMessageDeliveryStateFailed) { [messageIds addObject:@(messageItem->_message.mid)]; } } [controller.companion controllerWantsToResendMessages:messageIds]; } else if ([action isEqualToString:@"deleteMessage"]) { [controller.companion _deleteMessages:@[@(messageId)] animated:true]; [controller.companion controllerDeletedMessages:@[@(messageId)] completion:nil]; } } target:self] showInView:_view]; } } - (void)highlightAndShowActionsMenuForMessage:(int32_t)messageId { TGMessageModernConversationItem *highlightedItem = nil; for (TGModernCollectionCell *cell in _collectionView.visibleCells) { TGMessageModernConversationItem *messageItem = cell.boundItem; if (messageItem != nil && messageItem->_message.mid == messageId) { CGRect contentFrame = [[cell contentViewForBinding] convertRect:[messageItem effectiveContentFrame] toView:_view]; if (CGRectIsNull(contentFrame) || CGRectIsEmpty(contentFrame)) break; contentFrame = CGRectIntersection(contentFrame, CGRectMake(0, 0, _view.frame.size.width, _currentInputPanel == nil ? _view.frame.size.height : _currentInputPanel.frame.origin.y)); if (CGRectIsNull(contentFrame) || CGRectIsEmpty(contentFrame)) break; if (_menuContainerView == nil) _menuContainerView = [[TGMenuContainerView alloc] init]; if (_menuContainerView.superview != _view) [_view addSubview:_menuContainerView]; _menuContainerView.frame = CGRectMake(0, self.controllerInset.top, _view.frame.size.width, _view.frame.size.height - self.controllerInset.top - self.controllerInset.bottom); NSMutableArray *actions = [[NSMutableArray alloc] init]; if ([_companion allowReplies] && messageItem->_message.mid < TGMessageLocalMidBaseline) { [actions addObject:[[NSDictionary alloc] initWithObjectsAndKeys:TGLocalized(@"Conversation.ContextMenuReply"), @"title", @"reply", @"action", nil]]; if ([_companion canDeleteMessage:messageItem->_message] && messageItem->_message.actionInfo != nil) { [actions addObject:[[NSDictionary alloc] initWithObjectsAndKeys:TGLocalized(@"Conversation.ContextMenuDelete"), @"title", @"delete", @"action", nil]]; } } else { if ([_companion canDeleteMessage:messageItem->_message]) { [actions addObject:[[NSDictionary alloc] initWithObjectsAndKeys:TGLocalized(@"Conversation.ContextMenuDelete"), @"title", @"delete", @"action", nil]]; } } bool hasCaption = false; for (TGMediaAttachment *attachment in messageItem->_message.mediaAttachments) { if ([attachment isKindOfClass:[TGImageMediaAttachment class]]) { TGImageMediaAttachment *imageAttachment = (TGImageMediaAttachment *)attachment; hasCaption = (imageAttachment.caption.length > 0); } } bool isDocument = false; id stickerPackReference = nil; for (id attachment in messageItem->_message.mediaAttachments) { if ([attachment isKindOfClass:[TGDocumentMediaAttachment class]]) { NSString *localFilePath = [[_companion fileUrlForDocumentMedia:attachment] path]; if ([[NSFileManager defaultManager] fileExistsAtPath:localFilePath isDirectory:NULL]) isDocument = true; for (id attribute in ((TGDocumentMediaAttachment *)attachment).attributes) { if ([attribute isKindOfClass:[TGDocumentAttributeSticker class]]) { stickerPackReference = ((TGDocumentAttributeSticker *)attribute).packReference; break; } } break; } } bool addedForward = false; if (TGPeerIdIsChannel(messageItem->_message.fromUid) && [_companion allowReplies]) { addedForward = true; } if (!addedForward && TGPeerIdIsChannel(messageItem->_message.fromUid) && messageItem->_message.actionInfo == nil) { addedForward = true; [actions addObject:[[NSDictionary alloc] initWithObjectsAndKeys:TGLocalized(@"Conversation.ContextMenuForward"), @"title", @"forward", @"action", nil]]; } if (messageItem->_message.text.length != 0 || hasCaption) [actions addObject:[[NSDictionary alloc] initWithObjectsAndKeys:TGLocalized(@"Conversation.ContextMenuCopy"), @"title", @"copy", @"action", nil]]; else if (stickerPackReference != nil) { if ([TGStickersSignals isStickerPackInstalled:stickerPackReference]) { [actions addObject:[[NSDictionary alloc] initWithObjectsAndKeys:TGLocalized(@"Conversation.ContextMenuStickerPackInfo"), @"title", @"stickerPackInfo", @"action", nil]]; } else { [actions addObject:[[NSDictionary alloc] initWithObjectsAndKeys:TGLocalized(@"Conversation.ContextMenuStickerPackAdd"), @"title", @"stickerPackInfo", @"action", nil]]; } } else if (messageItem->_message.actionInfo == nil && [_companion allowMessageForwarding] && !addedForward) { [actions addObject:[[NSDictionary alloc] initWithObjectsAndKeys:TGLocalized(@"Conversation.ContextMenuForward"), @"title", @"forward", @"action", nil]]; } if (messageItem->_message.actionInfo == nil) [actions addObject:[[NSDictionary alloc] initWithObjectsAndKeys:TGLocalized(@"Conversation.ContextMenuMore"), @"title", @"select", @"action", nil]]; if (TGIsArabic()) { NSMutableArray *reversedActions = [[NSMutableArray alloc] init]; for (id item in actions.reverseObjectEnumerator) { [reversedActions addObject:item]; } actions = reversedActions; } if (actions.count != 0) { [_menuContainerView.menuView setUserInfo:@{@"mid": @(messageId)}]; [_menuContainerView.menuView setButtonsAndActions:actions watcherHandle:_actionHandle]; [_menuContainerView.menuView sizeToFit]; [_menuContainerView showMenuFromRect:[_menuContainerView convertRect:contentFrame fromView:_view]]; highlightedItem = messageItem; [highlightedItem setTemporaryHighlighted:true viewStorage:_viewStorage]; } break; } } for (TGModernCollectionCell *cell in _collectionView.visibleCells) { TGMessageModernConversationItem *messageItem = cell.boundItem; if (messageItem != highlightedItem) [messageItem setTemporaryHighlighted:false viewStorage:_viewStorage]; } } - (void)temporaryHighlightMessage:(int32_t)messageId automatically:(bool)automatically { for (TGMessageModernConversationItem *item in _items) { if (item->_message.mid == messageId) { _temporaryHighlightMessageIdUponDisplay = 0; [item setTemporaryHighlighted:true viewStorage:_viewStorage]; if (automatically) { TGDispatchAfter(0.7, dispatch_get_main_queue(), ^ { [item setTemporaryHighlighted:false viewStorage:_viewStorage]; }); } } } } - (void)showActionsMenuForLink:(NSString *)url { if ([url hasPrefix:@"tel:"]) { TGActionSheet *actionSheet = [[TGActionSheet alloc] initWithTitle:url.length < 70 ? url : [[url substringToIndex:70] stringByAppendingString:@"..."] actions:@[ [[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Conversation.Call") action:@"call"], [[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Conversation.LinkDialogCopy") action:@"copy"], [[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Common.Cancel") action:@"cancel" type:TGActionSheetActionTypeCancel] ] actionBlock:^(__unused TGModernConversationController *controller, NSString *action) { if ([action isEqualToString:@"call"]) { [TGAppDelegateInstance performPhoneCall:[NSURL URLWithString:url]]; } else if ([action isEqualToString:@"copy"]) { UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; if (pasteboard != nil) { NSString *copyString = url; if ([url hasPrefix:@"mailto:"]) copyString = [url substringFromIndex:7]; else if ([url hasPrefix:@"tel:"]) copyString = [url substringFromIndex:4]; [pasteboard setString:copyString]; } } } target:self]; [actionSheet showInView:self.view]; } else { NSString *displayString = url; if ([url hasPrefix:@"hashtag://"]) displayString = [@"#" stringByAppendingString:[url substringFromIndex:@"hashtag://".length]]; else if ([url hasPrefix:@"mention://"]) displayString = [@"@" stringByAppendingString:[url substringFromIndex:@"mention://".length]]; TGActionSheet *actionSheet = [[TGActionSheet alloc] initWithTitle:displayString.length < 70 ? displayString : [[displayString substringToIndex:70] stringByAppendingString:@"..."] actions:@[ [[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Conversation.LinkDialogOpen") action:@"open"], [[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Conversation.LinkDialogCopy") action:@"copy"], [[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Common.Cancel") action:@"cancel" type:TGActionSheetActionTypeCancel] ] actionBlock:^(TGModernConversationController *controller, NSString *action) { if ([action isEqualToString:@"open"]) { [controller openBrowserFromMessage:0 url:url]; } else if ([action isEqualToString:@"copy"]) { UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; if (pasteboard != nil) { NSString *copyString = url; if ([url hasPrefix:@"mailto:"]) copyString = [url substringFromIndex:7]; else if ([url hasPrefix:@"tel:"]) copyString = [url substringFromIndex:4]; else if ([url hasPrefix:@"hashtag://"]) copyString = [@"#" stringByAppendingString:[url substringFromIndex:@"hashtag://".length]]; else if ([url hasPrefix:@"mention://"]) copyString = [@"@" stringByAppendingString:[url substringFromIndex:@"mention://".length]]; [pasteboard setString:copyString]; } } } target:self]; [actionSheet showInView:self.view]; } } - (void)showActionsMenuForContact:(TGUser *)contact isContact:(bool)isContact { NSMutableArray *actions = [[NSMutableArray alloc] init]; if (!isContact) [actions addObject:[[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Conversation.AddContact") action:@"addContact"]]; if (contact.uid > 0) [actions addObject:[[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Conversation.SendMessage") action:@"sendMessage"]]; [actions addObject:[[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Conversation.Call") action:@"call"]]; [actions addObject:[[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Common.Cancel") action:@"cancel" type:TGActionSheetActionTypeCancel]]; TGActionSheet *actionSheet = [[TGActionSheet alloc] initWithTitle:nil actions:actions actionBlock:^(TGModernConversationController *controller, NSString *action) { if ([action isEqualToString:@"addContact"]) [controller showAddContactMenu:contact]; else if ([action isEqualToString:@"sendMessage"]) [controller.companion controllerRequestedNavigationToConversationWithUser:contact.uid]; else if ([action isEqualToString:@"call"]) { NSString *url = [[NSString alloc] initWithFormat:@"tel:%@", [TGPhoneUtils formatPhoneUrl:contact.phoneNumber]]; [TGAppDelegateInstance performPhoneCall:[NSURL URLWithString:url]]; } } target:self]; [actionSheet showInView:self.view]; } - (void)showAddContactMenu:(TGUser *)contact { [[[TGActionSheet alloc] initWithTitle:nil actions:@[ [[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Profile.CreateNewContact") action:@"createNewContact"], [[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Profile.AddToExisting") action:@"addToExisting"], [[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Common.Cancel") action:@"cancel" type:TGActionSheetActionTypeCancel] ] actionBlock:^(TGModernConversationController *controller, NSString *action) { if ([action isEqualToString:@"createNewContact"]) [controller.companion controllerWantsToCreateContact:contact.uid firstName:contact.firstName lastName:contact.lastName phoneNumber:contact.phoneNumber]; else if ([action isEqualToString:@"addToExisting"]) [controller.companion controllerWantsToAddContactToExisting:contact.uid phoneNumber:contact.phoneNumber]; } target:self] showInView:self.view]; } - (void)showCallNumberMenu:(NSArray *)phoneNumbers { NSMutableArray *actions = [[NSMutableArray alloc] init]; for (NSArray *desc in phoneNumbers) { [actions addObject:[[TGActionSheetAction alloc] initWithTitle:((NSString *)desc[0]).length == 0 ? desc[2] : [[NSString alloc] initWithFormat:@"%@: %@", desc[0], desc[2]] action:desc[1]]]; } [actions addObject:[[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Common.Cancel") action:@"cancel" type:TGActionSheetActionTypeCancel]]; [[[TGActionSheet alloc] initWithTitle:nil actions:actions actionBlock:^(__unused id target, NSString *action) { if (![action isEqualToString:@"cancel"]) { NSString *url = [[NSString alloc] initWithFormat:@"tel:%@", [TGPhoneUtils formatPhoneUrl:action]]; [TGAppDelegateInstance performPhoneCall:[NSURL URLWithString:url]]; } } target:self] showInView:self.view]; } - (void)enterEditingMode { [self _enterEditingMode:0]; } - (void)leaveEditingMode { [self _leaveEditingModeAnimated:false]; } - (void)openKeyboard { if (!_editingMode) { _inputTextPanel.inputField.internalTextView.enableFirstResponder = true; [_inputTextPanel.inputField becomeFirstResponder]; } } - (void)hideTitlePanel { [self setCurrentTitlePanel:nil animation:TGModernConversationPanelAnimationSlide]; } - (void)reloadBackground { UIImageView *tempImageView = [[UIImageView alloc] initWithImage:_backgroundView.image]; tempImageView.contentMode = _backgroundView.contentMode; tempImageView.frame = _backgroundView.frame; [_backgroundView.superview insertSubview:tempImageView aboveSubview:_backgroundView]; _backgroundView.image = [[TGWallpaperManager instance] currentWallpaperImage]; [UIView animateWithDuration:0.3 animations:^ { tempImageView.alpha = 0.0f; } completion:^(__unused BOOL finished) { [tempImageView removeFromSuperview]; }]; for (TGMessageModernConversationItem *item in _items) { [item updateAssets]; } [_collectionView updateDecorationAssets]; } - (void)refreshMetrics { for (TGMessageModernConversationItem *item in _items) { [item refreshMetrics]; } [_collectionLayout invalidateLayout]; [_collectionView layoutSubviews]; } - (void)setInputText:(NSString *)inputText replace:(bool)replace { if (_inputTextPanel == nil) _initialInputText = inputText; else if (TGStringCompare(_inputTextPanel.maybeInputField.text, @"") || replace) { [[_inputTextPanel inputField] setText:inputText animated:false]; if (_collectionView != nil) { [_emptyListPlaceholder adjustLayoutForSize:_view.bounds.size contentInsets:UIEdgeInsetsMake(_collectionView.contentInset.bottom - 210.0f - [_collectionView implicitTopInset], 0.0f, _collectionView.contentInset.top, 0.0f) duration:0.0f curve:0]; } else { [_emptyListPlaceholder adjustLayoutForSize:_view.bounds.size contentInsets:UIEdgeInsetsMake(self.controllerInset.top, 0.0f, _currentInputPanel.frame.size.height, 0.0f) duration:0.0f curve:0]; } } } - (NSString *)inputText { return _inputTextPanel == nil ? _initialInputText : _inputTextPanel.maybeInputField.text; } - (void)setReplyMessage:(TGMessage *)replyMessage animated:(bool)animated { TGModenConcersationReplyAssociatedPanel *panel = [[TGModenConcersationReplyAssociatedPanel alloc] initWithMessage:replyMessage]; __weak TGModernConversationController *weakSelf = self; panel.dismiss = ^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf->_inputTextPanel setPrimaryExtendedPanel:nil animated:true]; [strongSelf->_inputTextPanel setSecondaryExtendedPanel:nil animated:true]; } }; [_inputTextPanel setPrimaryExtendedPanel:panel animated:animated]; } - (void)setForwardMessages:(NSArray *)forwardMessages animated:(bool)animated { if (_inputTextPanel == nil) _initialForwardMessages = forwardMessages; else { TGModernConversationForwardInputPanel *panel = [[TGModernConversationForwardInputPanel alloc] initWithMessages:forwardMessages]; __weak TGModernConversationController *weakSelf = self; panel.dismiss = ^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf->_inputTextPanel setPrimaryExtendedPanel:nil animated:true]; [strongSelf->_inputTextPanel setSecondaryExtendedPanel:nil animated:true]; } }; [_inputTextPanel setPrimaryExtendedPanel:panel animated:animated]; } } - (void)setInlineStickerList:(NSArray *)inlineStickerList { if (inlineStickerList.count == 0) { if ([[_inputTextPanel associatedPanel] isKindOfClass:[TGStickerAssociatedInputPanel class]]) [_inputTextPanel setAssociatedPanel:nil animated:true]; } else { __weak TGModernConversationController *weakSelf = self; [_inputTextPanel setAssociatedStickerList:inlineStickerList stickerSelected:^(TGDocumentMediaAttachment *document) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { [[SQueue concurrentDefaultQueue] dispatch:^{ [TGStickersSignals addUseCountForDocumentId:document.documentId]; }]; [strongSelf->_inputTextPanel.maybeInputField setText:@"" animated:true]; [strongSelf->_companion controllerWantsToSendRemoteDocument:document asReplyToMessageId:[strongSelf currentReplyMessageId]]; } }]; } } - (void)setTitle:(NSString *)title { [_titleView setTitle:title]; } - (void)setTitleIcons:(NSArray *)titleIcons { [_titleView setIcons:titleIcons]; } - (void)setTitleModalProgressStatus:(NSString *)titleModalProgressStatus { [_titleView setModalProgressStatus:titleModalProgressStatus]; } - (void)setAvatarConversationId:(int64_t)conversationId title:(NSString *)title icon:(UIImage *)icon { [_avatarButton setAvatarConversationId:conversationId]; [_avatarButton setAvatarTitle:title]; [_avatarButton setAvatarIcon:icon]; } - (void)setAvatarConversationId:(int64_t)conversationId firstName:(NSString *)firstName lastName:(NSString *)lastName { [_avatarButton setAvatarConversationId:conversationId]; [_avatarButton setAvatarFirstName:firstName lastName:lastName]; } - (void)setAvatarUrl:(NSString *)avatarUrl { [_avatarButton setAvatarUrl:avatarUrl]; } - (void)setStatus:(NSString *)status accentColored:(bool)accentColored allowAnimation:(bool)allowAnimation toggleMode:(TGModernConversationControllerTitleToggle)toggleMode { [_titleView setStatus:status animated:self.isViewLoaded && allowAnimation]; [_titleView setStatusHasAccentColor:accentColored]; [_titleView setToggleMode:toggleMode]; } - (void)setAttributedStatus:(NSAttributedString *)status allowAnimation:(bool)allowAnimation { [_titleView setAttributedStatus:status animated:self.isViewLoaded && allowAnimation]; [_titleView setStatusHasAccentColor:false]; } - (void)setTypingStatus:(NSString *)typingStatus activity:(int)activity { [_titleView setTypingStatus:typingStatus activity:(TGModernConversationTitleViewActivity)activity animated:self.isViewLoaded]; } - (void)setGlobalUnreadCount:(int)unreadCount { [_titleView setUnreadCount:unreadCount]; } - (TGModernConversationInputPanel *)defaultInputPanel { if ([_companion _controllerShouldHideInputTextByDefault]) { return nil; } else { return _inputTextPanel; } } - (void)setCustomInputPanel:(TGModernConversationInputPanel *)customInputPanel { if (_customInputPanel != customInputPanel) { _customInputPanel = customInputPanel; if (!_editingMode) { [self setInputPanel:_customInputPanel != nil ? _customInputPanel : [self defaultInputPanel] animated:ABS(CFAbsoluteTimeGetCurrent() - _willAppearTimestamp) > 0.18]; } } } - (TGModernConversationInputPanel *)customInputPanel { return _customInputPanel; } - (void)setPrimaryTitlePanel:(TGModernConversationTitlePanel *)titlePanel { if (_primaryTitlePanel != titlePanel) { bool applyAsCurrent = _currentTitlePanel != nil && _currentTitlePanel == _primaryTitlePanel; _primaryTitlePanel = titlePanel; if (applyAsCurrent) [self setCurrentTitlePanel:titlePanel animation:ABS(CFAbsoluteTimeGetCurrent() - _willAppearTimestamp) > 0.18 ? TGModernConversationPanelAnimationSlide : TGModernConversationPanelAnimationNone]; } } - (TGModernConversationTitlePanel *)primaryTitlePanel { return _primaryTitlePanel; } - (void)setSecondaryTitlePanel:(TGModernConversationTitlePanel *)secondaryTitlePanel { [self setSecondaryTitlePanel:secondaryTitlePanel animated:true]; } - (void)setSecondaryTitlePanel:(TGModernConversationTitlePanel *)secondaryTitlePanel animated:(bool)animated { if (_secondaryTitlePanel != secondaryTitlePanel) { bool applyAsCurrent = _currentTitlePanel == nil || _currentTitlePanel == _secondaryTitlePanel; _secondaryTitlePanel = secondaryTitlePanel; if (applyAsCurrent) { NSTimeInterval appearTime = ABS(CFAbsoluteTimeGetCurrent() - _willAppearTimestamp); [self setCurrentTitlePanel:secondaryTitlePanel animation:(animated && appearTime > 0.1) ? (appearTime > 0.4 ? TGModernConversationPanelAnimationSlide : TGModernConversationPanelAnimationFade) : TGModernConversationPanelAnimationNone]; } } } - (TGModernConversationTitlePanel *)secondaryTitlePanel { return _secondaryTitlePanel; } - (void)setCurrentTitlePanel:(TGModernConversationTitlePanel *)currentTitlePanel animation:(TGModernConversationPanelAnimation)animation { if (_currentTitlePanel != currentTitlePanel) { if (_currentTitlePanel != nil) { if (animation != TGModernConversationPanelAnimationNone) { TGModernConversationTitlePanel *lastPanel = _currentTitlePanel; if (animation == TGModernConversationPanelAnimationSlide) { [UIView animateWithDuration:0.09 delay:0.0 options:iosMajorVersion() < 7 ? 0 : (7 << 16) animations:^ { lastPanel.frame = CGRectOffset(lastPanel.frame, 0.0f, -lastPanel.frame.size.height); } completion:^(__unused BOOL finished) { [lastPanel removeFromSuperview]; }]; } else if (animation == TGModernConversationPanelAnimationSlideFar) { _titlePanelWrappingView.clipsToBounds = false; [UIView animateWithDuration:0.3 delay:0.0 options:0 animations:^ { lastPanel.frame = CGRectOffset(lastPanel.frame, 0.0f, -lastPanel.frame.size.height - lastPanel.superview.frame.origin.y); } completion:^(__unused BOOL finished) { [lastPanel removeFromSuperview]; _titlePanelWrappingView.clipsToBounds = true; }]; } else { [UIView animateWithDuration:0.09 delay:0.0 options:iosMajorVersion() < 7 ? 0 : (7 << 16) animations:^ { lastPanel.alpha = 0.0f; } completion:^(__unused BOOL finished) { [lastPanel removeFromSuperview]; lastPanel.alpha = 1.0f; }]; } } else [_currentTitlePanel removeFromSuperview]; } _currentTitlePanel = currentTitlePanel; if (_currentTitlePanel != nil && [self isViewLoaded]) { if (_titlePanelWrappingView == nil) { _titlePanelWrappingView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, self.controllerInset.top, _view.frame.size.width, 44.0f)]; _titlePanelWrappingView.clipsToBounds = true; [_view addSubview:_titlePanelWrappingView]; } _titlePanelWrappingView.userInteractionEnabled = true; [_titlePanelWrappingView addSubview:_currentTitlePanel]; CGRect titlePanelFrame = CGRectMake(0.0f, 0.0f, _titlePanelWrappingView.frame.size.width, _currentTitlePanel.frame.size.height); if (animation != TGModernConversationPanelAnimationNone) { if (animation == TGModernConversationPanelAnimationSlide) { _currentTitlePanel.frame = CGRectOffset(titlePanelFrame, 0.0f, -titlePanelFrame.size.height); [UIView animateWithDuration:0.09 delay:0.0 options:iosMajorVersion() < 7 ? 0 : (7 << 16) animations:^ { _currentTitlePanel.frame = titlePanelFrame; } completion:nil]; } else { _currentTitlePanel.frame = titlePanelFrame; _currentTitlePanel.alpha = 0.0f; [UIView animateWithDuration:0.09 delay:0.0 options:iosMajorVersion() < 7 ? 0 : (7 << 16) animations:^ { _currentTitlePanel.alpha = 1.0f; } completion:nil]; } } else { _currentTitlePanel.alpha = 1.0f; _currentTitlePanel.frame = titlePanelFrame; } } else _titlePanelWrappingView.userInteractionEnabled = false; } } - (void)setEmptyListPlaceholder:(TGModernConversationEmptyListPlaceholderView *)emptyListPlaceholder { [self setEmptyListPlaceholder:emptyListPlaceholder animated:ABS(CFAbsoluteTimeGetCurrent() - _willAppearTimestamp) > 0.18]; } - (void)setEmptyListPlaceholder:(TGModernConversationEmptyListPlaceholderView *)emptyListPlaceholder animated:(bool)animated { if (_emptyListPlaceholder != emptyListPlaceholder) { if (_emptyListPlaceholder != nil) { if (animated) { UIView *currentView = _emptyListPlaceholder; _emptyListPlaceholder = nil; [UIView animateWithDuration:0.3 * 0.7 animations:^ { currentView.alpha = 0.0f; } completion:^(__unused BOOL finished) { [currentView removeFromSuperview]; }]; } else { [_emptyListPlaceholder removeFromSuperview]; _emptyListPlaceholder = nil; } } _emptyListPlaceholder = emptyListPlaceholder; if (self.isViewLoaded) { [_view insertSubview:_emptyListPlaceholder belowSubview:_currentInputPanel]; if (animated) { _emptyListPlaceholder.alpha = 0.0f; [UIView animateWithDuration:0.3 * 0.7 animations:^ { _emptyListPlaceholder.alpha = 1.0f; }]; } else _emptyListPlaceholder.alpha = 1.0f; if (_collectionView != nil) { [_emptyListPlaceholder adjustLayoutForSize:_view.bounds.size contentInsets:UIEdgeInsetsMake(_collectionView.contentInset.bottom - 210.0f - [_collectionView implicitTopInset], 0.0f, _collectionView.contentInset.top, 0.0f) duration:0.0f curve:0]; } else { [_emptyListPlaceholder adjustLayoutForSize:_view.bounds.size contentInsets:UIEdgeInsetsMake(self.controllerInset.top, 0.0f, 45.0f, 0.0f) duration:0.0f curve:0]; } } } } - (void)setConversationHeader:(UIView *)conversationHeader { _conversationHeader = conversationHeader; if (_collectionView != nil) { _collectionView.headerView = conversationHeader; UIEdgeInsets inset = _collectionView.contentInset; inset.bottom = self.controllerInset.top + 210.0f + [_collectionView implicitTopInset]; _collectionView.contentInset = inset; } } #pragma mark - - (void)setEnableAboveHistoryRequests:(bool)enableAboveHistoryRequests { _enableAboveHistoryRequests = enableAboveHistoryRequests; } - (void)setEnableBelowHistoryRequests:(bool)enableBelowHistoryRequests { _enableBelowHistoryRequests = enableBelowHistoryRequests; } - (void)setEnableSendButton:(bool)enableSendButton { _inputTextPanel.sendButton.userInteractionEnabled = enableSendButton; } - (void)_updateCanReadHistory:(TGModernConversationActivityChange)change { bool canReadHistory = true; if (change == TGModernConversationActivityChangeActive) canReadHistory = true; else if (change == TGModernConversationActivityChangeInactive) canReadHistory = false; else { if (canReadHistory && self.navigationController.topViewController != self) canReadHistory = false; if (canReadHistory && ([UIApplication sharedApplication].applicationState != UIApplicationStateActive && [UIApplication sharedApplication].applicationState != UIApplicationStateInactive)) canReadHistory = false; if ([UIApplication sharedApplication] == nil) canReadHistory = false; } if (_canReadHistory != canReadHistory) { _canReadHistory = canReadHistory; [_companion controllerCanReadHistoryUpdated]; } } - (void)_updateCanRegroupIncomingUnreadMessages { [_companion controllerCanRegroupUnreadIncomingMessages]; } - (bool)canReadHistory { return _canReadHistory || ([UIApplication sharedApplication] != nil && [UIApplication sharedApplication].applicationState == UIApplicationStateActive && self.navigationController.topViewController == self); } - (NSArray *)_items { return _items; } - (int32_t)_currentReplyMessageId { return [self currentReplyMessageId]; } - (NSArray *)_currentForwardMessageDescs { NSMutableArray *messageDescs = [[NSMutableArray alloc] init]; for (TGMessage *message in [self currentForwardMessages]) { [messageDescs addObject:@{@"peerId": @(message.toUid), @"messageId": @(message.mid)}]; } return messageDescs; } - (void)setReplyMarkup:(TGBotReplyMarkup *)replyMarkup { _replyMarkup = replyMarkup; [_inputTextPanel setReplyMarkup:_replyMarkup]; } - (void)setHasBots:(bool)hasBots { _hasBots = hasBots; [_inputTextPanel setHasBots:_hasBots]; } - (void)setCanBroadcast:(bool)canBroadcast { _canBroadcast = canBroadcast; [_inputTextPanel setCanBroadcast:canBroadcast]; } - (void)setIsBroadcasting:(bool)isBroadcasting { _isBroadcasting = isBroadcasting; [_inputTextPanel setIsBroadcasting:isBroadcasting]; } - (void)setIsAlwaysBroadcasting:(bool)isAlwaysBroadcasting { _isAlwaysBroadcasting = isAlwaysBroadcasting; [_inputTextPanel setIsAlwaysBroadcasting:isAlwaysBroadcasting]; } - (void)appendCommand:(NSString *)command { NSString *currentText = self.inputText; NSString *currentNormalizedText = [currentText lowercaseString]; if (currentText.length == 0) currentText = command; else { bool foundSuffix = false; for (NSInteger i = (NSInteger)command.length; i > 0; i--) { if ([currentNormalizedText hasSuffix:[[command lowercaseString] substringToIndex:i]]) { currentText = [currentText stringByReplacingCharactersInRange:NSMakeRange(currentText.length - i, i) withString:command]; //currentText = [currentText stringByAppendingString:[command substringFromIndex:i]]; foundSuffix = true; break; } } if (!foundSuffix) { if ([currentText hasSuffix:@" "]) currentText = [currentText stringByAppendingFormat:@" %@ ", command]; else currentText = [currentText stringByAppendingFormat:@"%@ ", command]; } } [self setInputText:currentText replace:true]; [_inputTextPanel inputField].internalTextView.enableFirstResponder = true; } - (void)setEnableUnloadHistoryRequests:(bool)enableUnloadHistoryRequests { _enableUnloadHistoryRequests = enableUnloadHistoryRequests; } - (void)_updateItemsAnimationsEnabled { for (TGMessageModernConversationItem *item in _items) { [item updateAnimationsEnabled]; } } - (TGModernViewInlineMediaContext *)inlineMediaContext:(int32_t)messageId { if (_currentAudioPlayerMessageId == messageId && _currentAudioPlayer != nil) { return [_currentAudioPlayer inlineMediaContext]; } return nil; } - (void)_maybeUnloadHistory { if (_enableUnloadHistoryRequests && (NSInteger)_items.count >= TGModernConversationControllerUnloadHistoryLimit + TGModernConversationControllerUnloadHistoryThreshold) { NSIndexPath *indexPath = [_collectionView indexPathsForVisibleItems].firstObject; if (indexPath != nil) { if (indexPath.row < (int)(_items.count / 2)) [_companion unloadMessagesAbove]; else [_companion unloadMessagesBelow]; } } } #pragma mark - - (void)titleViewTapped:(TGModernConversationTitleView *)__unused titleView { if (_editingMode) return; if (_titleView.toggleMode != TGModernConversationControllerTitleToggleNone) { [_companion _toggleTitleMode]; } else { if (!_shouldHaveTitlePanelLoaded) { _shouldHaveTitlePanelLoaded = true; [_companion _loadControllerPrimaryTitlePanel]; } if (_primaryTitlePanel != nil) { if (_currentTitlePanel != _primaryTitlePanel) [self setCurrentTitlePanel:_primaryTitlePanel animation:TGModernConversationPanelAnimationSlide]; else [self setCurrentTitlePanel:_secondaryTitlePanel animation:TGModernConversationPanelAnimationSlide]; } } } - (void)editingPanelRequestedDeleteMessages:(TGModernConversationEditingPanel *)__unused editingPanel { [[[TGActionSheet alloc] initWithTitle:nil actions:@[ [[TGActionSheetAction alloc] initWithTitle:[_companion checkedMessageCount] == 1 ? TGLocalized(@"Conversation.DeleteOneMessage") : TGLocalized(@"Conversation.DeleteManyMessages") action:@"delete" type:TGActionSheetActionTypeDestructive], [[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Common.Cancel") action:@"cancel" type:TGActionSheetActionTypeCancel] ] actionBlock:^(TGModernConversationController *controller, NSString *action) { if ([action isEqualToString:@"delete"]) [controller _commitDeleteCheckedMessages]; } target:self] showInView:self.view]; } - (void)editingPanelRequestedForwardMessages:(TGModernConversationEditingPanel *)__unused editingPanel { [_companion controllerWantsToForwardMessages:[_companion checkedMessageIds]]; } - (void)inputTextPanelHasIndicatedTypingActivity:(TGModernConversationInputTextPanel *)__unused inputTextPanel { [_companion controllerDidUpdateTypingActivity]; } - (void)inputPanelTextChanged:(TGModernConversationInputTextPanel *)__unused inputTextPanel text:(NSString *)text { if (iosMajorVersion() >= 8 && _currentActivity != nil) { [_currentActivity addUserInfoEntriesFromDictionary:@{@"text": text == nil ? @"" : text}]; _currentActivity.needsSave = true; } [_companion controllerDidChangeInputText:text]; } - (void)inputPanelMentionEntered:(TGModernConversationInputTextPanel *)__unused inputTextPanel mention:(NSString *)mention { if (mention == nil) { if ([[_inputTextPanel associatedPanel] isKindOfClass:[TGModernConversationMentionsAssociatedPanel class]]) [_inputTextPanel setAssociatedPanel:nil animated:true]; } else { TGModernConversationMentionsAssociatedPanel *panel = nil; if ([[_inputTextPanel associatedPanel] isKindOfClass:[TGModernConversationMentionsAssociatedPanel class]]) panel = (TGModernConversationMentionsAssociatedPanel *)[_inputTextPanel associatedPanel]; else { panel = [[TGModernConversationMentionsAssociatedPanel alloc] init]; __weak TGModernConversationController *weakSelf = self; panel.userSelected = ^(TGUser *user) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { if ([[strongSelf->_inputTextPanel associatedPanel] isKindOfClass:[TGModernConversationMentionsAssociatedPanel class]]) { [strongSelf->_inputTextPanel setAssociatedPanel:nil animated:false]; } [strongSelf->_inputTextPanel replaceMention:user.userName]; } }; [_inputTextPanel setAssociatedPanel:panel animated:true]; } [panel setUserListSignal:[_companion userListForMention:mention]]; } } - (void)inputPanelHashtagEntered:(TGModernConversationInputTextPanel *)__unused inputTextPanel hashtag:(NSString *)hashtag { if (hashtag == nil) { if ([[_inputTextPanel associatedPanel] isKindOfClass:[TGModernConversationHashtagsAssociatedPanel class]]) [_inputTextPanel setAssociatedPanel:nil animated:true]; } else { TGModernConversationHashtagsAssociatedPanel *panel = nil; if ([[_inputTextPanel associatedPanel] isKindOfClass:[TGModernConversationHashtagsAssociatedPanel class]]) panel = (TGModernConversationHashtagsAssociatedPanel *)[_inputTextPanel associatedPanel]; else { panel = [[TGModernConversationHashtagsAssociatedPanel alloc] init]; __weak TGModernConversationController *weakSelf = self; panel.hashtagSelected = ^(NSString *hashtag) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { if ([[strongSelf->_inputTextPanel associatedPanel] isKindOfClass:[TGModernConversationHashtagsAssociatedPanel class]]) { [strongSelf->_inputTextPanel setAssociatedPanel:nil animated:false]; } [strongSelf->_inputTextPanel replaceHashtag:hashtag]; } }; [_inputTextPanel setAssociatedPanel:panel animated:true]; } [panel setHashtagListSignal:[_companion hashtagListForHashtag:hashtag]]; } } - (void)inputPanelCommandEntered:(TGModernConversationInputTextPanel *)__unused inputTextPanel command:(NSString *)command { if (command == nil) { if ([[_inputTextPanel associatedPanel] isKindOfClass:[TGModernConversationCommandsAssociatedPanel class]]) [_inputTextPanel setAssociatedPanel:nil animated:true]; } else { TGModernConversationCommandsAssociatedPanel *panel = nil; if ([[_inputTextPanel associatedPanel] isKindOfClass:[TGModernConversationCommandsAssociatedPanel class]]) panel = ((TGModernConversationCommandsAssociatedPanel *)[_inputTextPanel associatedPanel]); else { panel = [[TGModernConversationCommandsAssociatedPanel alloc] init]; __weak TGModernConversationController *weakSelf = self; panel.commandSelected = ^(TGBotComandInfo *command, TGUser *user) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { if ([[strongSelf->_inputTextPanel associatedPanel] isKindOfClass:[TGModernConversationCommandsAssociatedPanel class]]) { [strongSelf->_inputTextPanel setAssociatedPanel:nil animated:false]; } NSString *commandText = command.command; if (user != nil && ![strongSelf->_companion isASingleBotGroup]) commandText = [commandText stringByAppendingFormat:@"@%@", user.userName]; [strongSelf->_companion controllerWantsToSendTextMessage:[@"/" stringByAppendingString:commandText] asReplyToMessageId:[strongSelf currentReplyMessageId] withAttachedMessages:@[] disableLinkPreviews:false]; //[strongSelf appendCommand:commandText]; } }; [_inputTextPanel setAssociatedPanel:panel animated:true]; } [panel setCommandListSignal:[_companion commandListForCommand:command]]; } } - (void)inputPanelLinkParsed:(TGModernConversationInputTextPanel *)__unused inputTextPanel link:(NSString *)link probablyComplete:(bool)probablyComplete { if (![_companion allowMessageForwarding]) return; if (_currentLinkParseDisposable == nil) _currentLinkParseDisposable = [[SMetaDisposable alloc] init]; if (!TGStringCompare(_currentLinkParseLink, link)) { _disableLinkPreviewsForMessage = false; if (link.length == 0) { _currentLinkParseLink = link; [_currentLinkParseDisposable setDisposable:nil]; if ([[_inputTextPanel secondaryExtendedPanel] isKindOfClass:[TGModernConversationWebPreviewInputPanel class]]) { [_inputTextPanel setSecondaryExtendedPanel:nil animated:true]; } } else { SSignal *parseLinkSignal = [TGUpdateStateRequestBuilder requestWebPageByText:link]; if (!probablyComplete) parseLinkSignal = [parseLinkSignal delay:4.0 onQueue:[SQueue mainQueue]]; __weak TGModernConversationController *weakSelf = self; if ([[_inputTextPanel secondaryExtendedPanel] isKindOfClass:[TGModernConversationWebPreviewInputPanel class]]) { [_inputTextPanel setSecondaryExtendedPanel:nil animated:true]; } _currentLinkParseLink = link; [_currentLinkParseDisposable setDisposable:[[parseLinkSignal deliverOn:[SQueue mainQueue]] startWithNext:^(TGWebPageMediaAttachment *webPage) { //TGLog(@"parsed link %@ to webpage (%@)", link, webPage.url == nil ? @"incomplete" : @"complete"); __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { if (webPage == nil || (webPage.url == nil && webPage.pendingDate == -1)) { if ([[strongSelf->_inputTextPanel secondaryExtendedPanel] isKindOfClass:[TGModernConversationWebPreviewInputPanel class]]) { [strongSelf->_inputTextPanel setSecondaryExtendedPanel:nil animated:true]; } } else { TGModernConversationWebPreviewInputPanel *panel = nil; if ([[strongSelf->_inputTextPanel secondaryExtendedPanel] isKindOfClass:[TGModernConversationWebPreviewInputPanel class]]) { panel = (TGModernConversationWebPreviewInputPanel *)[strongSelf->_inputTextPanel secondaryExtendedPanel]; } else { panel = [[TGModernConversationWebPreviewInputPanel alloc] init]; panel.dismiss = ^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf->_inputTextPanel setSecondaryExtendedPanel:nil animated:true]; strongSelf->_disableLinkPreviewsForMessage = true; } }; [strongSelf->_inputTextPanel setSecondaryExtendedPanel:panel animated:true]; } [panel setLink:link webPage:webPage]; } } }]]; } } } - (bool)isInputPanelTextEnabled:(TGModernConversationInputTextPanel *)__unused inputTextPanel { return [TGApplicationFeatures isTextMessageEnabledForPeerType:[_companion applicationFeaturePeerType] disabledMessage:NULL]; } - (void)inputPanelFocused:(TGModernConversationInputTextPanel *)__unused inputTextPanel { __autoreleasing NSString *disabledMessage = nil; if (![TGApplicationFeatures isTextMessageEnabledForPeerType:[_companion applicationFeaturePeerType] disabledMessage:&disabledMessage]) { [[[TGAlertView alloc] initWithTitle:TGLocalized(@"FeatureDisabled.Oops") message:disabledMessage cancelButtonTitle:TGLocalized(@"Common.OK") okButtonTitle:nil completionBlock:nil] show]; } } - (void)inputTextPanelHasCancelledTypingActivity:(TGModernConversationInputTextPanel *)__unused inputTextPanel { [_companion controllerDidCancelTypingActivity]; } - (int32_t)currentReplyMessageId { int32_t replyMessageId = 0; id extendedPanel = [_inputTextPanel primaryExtendedPanel]; if ([extendedPanel isKindOfClass:[TGModenConcersationReplyAssociatedPanel class]]) replyMessageId = ((TGModenConcersationReplyAssociatedPanel *)extendedPanel).message.mid; return replyMessageId; } - (TGMessage *)currentReplyMessage { id extendedPanel = [_inputTextPanel primaryExtendedPanel]; if ([extendedPanel isKindOfClass:[TGModenConcersationReplyAssociatedPanel class]]) return ((TGModenConcersationReplyAssociatedPanel *)extendedPanel).message; return nil; } - (NSArray *)currentForwardMessages { id extendedPanel = [_inputTextPanel primaryExtendedPanel]; if ([extendedPanel isKindOfClass:[TGModernConversationForwardInputPanel class]]) return ((TGModernConversationForwardInputPanel *)extendedPanel).messages; return nil; } - (void)inputPanelRequestedSendMessage:(TGModernConversationInputTextPanel *)__unused inputTextPanel text:(NSString *)text { [_companion controllerWantsToSendTextMessage:text asReplyToMessageId:[self currentReplyMessageId] withAttachedMessages:[self currentForwardMessages] disableLinkPreviews:_disableLinkPreviewsForMessage]; _disableLinkPreviewsForMessage = false; } - (void)_asyncProcessMediaAssetSignals:(NSArray *)signals forIntent:(TGModernMediaPickerControllerIntent)intent { TGProgressWindow *progressWindow = [[TGProgressWindow alloc] init]; [progressWindow performSelector:@selector(showAnimated) withObject:nil afterDelay:0.5]; SQueue *queue = [[SQueue alloc] init]; __weak TGModernConversationController *weakSelf = self; SSignal *combinedSignal = nil; for (SSignal *signal in signals) { if (combinedSignal == nil) combinedSignal = [signal startOn:queue]; else combinedSignal = [[combinedSignal then:signal] startOn:queue]; } [[[combinedSignal reduceLeft:[[NSMutableArray alloc] init] with:^NSMutableArray *(NSMutableArray *itemDescriptions, id item) { [itemDescriptions addObject:item]; return itemDescriptions; }] deliverOn:[SQueue mainQueue]] startWithNext:^(NSArray *itemDescriptions) { __strong TGModernConversationController *strongSelf = weakSelf; if (intent == TGModernMediaPickerControllerSendPhotoIntent) [strongSelf.companion controllerWantsToSendImagesWithDescriptions:itemDescriptions asReplyToMessageId:[strongSelf currentReplyMessageId]]; else if (intent == TGModernMediaPickerControllerSendFileIntent) [strongSelf.companion controllerWantsToSendDocumentsWithDescriptions:itemDescriptions asReplyToMessageId:[strongSelf currentReplyMessageId]]; [NSObject cancelPreviousPerformRequestsWithTarget:progressWindow selector:@selector(showAnimated) object:nil]; [progressWindow dismiss:true]; } error:^(__unused id error) { [NSObject cancelPreviousPerformRequestsWithTarget:progressWindow selector:@selector(showAnimated) object:nil]; [progressWindow dismiss:true]; } completed:^ { }]; } - (void)inputPanelRequestedAttachmentsMenu:(TGModernConversationInputTextPanel *)__unused inputTextPanel { if (iosMajorVersion() >= 7 && !TGIsPad()) { __weak TGModernConversationController *weakSelf = self; _attachmentSheetWindow = [[TGAttachmentSheetWindow alloc] init]; _attachmentSheetWindow.dismissalBlock = ^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; strongSelf->_attachmentSheetWindow.rootViewController = nil; strongSelf->_attachmentSheetWindow = nil; }; NSMutableArray *items = [[NSMutableArray alloc] init]; TGAttachmentSheetRecentItemView *recentView = [[TGAttachmentSheetRecentItemView alloc] initWithParentController:self mode:TGAttachmentSheetItemViewSendPhotoMode]; __weak TGAttachmentSheetRecentItemView *weakRecentView = recentView; recentView.disallowCaptions = ![_companion allowCaptionedMedia]; recentView.done = ^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { __strong TGAttachmentSheetRecentItemView *strongRecentView = weakRecentView; [strongSelf->_attachmentSheetWindow dismissAnimated:true completion:nil]; if (strongRecentView != nil) { [strongSelf _asyncProcessMediaAssetSignals:[strongRecentView selectedItemSignals:^id (UIImage *image, NSString *caption, NSString *maybeHash) { if (image != nil) { id description = [strongSelf.companion imageDescriptionFromImage:image caption:caption optionalAssetUrl:maybeHash == nil ? nil : [[NSString alloc] initWithFormat:@"image-%@", maybeHash]]; return description; } return nil; }] forIntent:TGModernMediaPickerControllerSendPhotoIntent]; } } }; TGAttachmentSheetRecentControlledButtonItemView *multifunctionButtonView = [[TGAttachmentSheetRecentControlledButtonItemView alloc] initWithTitle:TGLocalized(@"Common.ChoosePhoto") pressed:^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf.view endEditing:true]; [strongSelf->_attachmentSheetWindow dismissAnimated:true completion:nil]; [strongSelf _displayPhotoPicker]; } } alternatePressed:^ { TGProgressWindow *progressWindow = [[TGProgressWindow alloc] init]; [progressWindow performSelector:@selector(showAnimated) withObject:nil afterDelay:0.5]; __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { __strong TGAttachmentSheetRecentItemView *strongRecentView = weakRecentView; [strongSelf->_attachmentSheetWindow dismissAnimated:true completion:nil]; if (strongRecentView != nil) { [strongSelf _asyncProcessMediaAssetSignals:[strongRecentView selectedItemSignals:^id (UIImage *image, NSString *caption, NSString *maybeHash) { if (image != nil) { id description = [strongSelf.companion imageDescriptionFromImage:image caption:caption optionalAssetUrl:maybeHash == nil ? nil : [[NSString alloc] initWithFormat:@"image-%@", maybeHash]]; return description; } return nil; }] forIntent:TGModernMediaPickerControllerSendPhotoIntent]; } } }]; [recentView setMultifunctionButtonView:multifunctionButtonView]; recentView.openCamera = ^(TGAttachmentSheetRecentCameraView *cameraView) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf.view endEditing:true]; [strongSelf _displayCameraWithView:cameraView]; } }; recentView.itemOpened = ^(void) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) [strongSelf.view endEditing:true]; }; recentView.userListSignal = ^SSignal *(NSString *mention) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf == nil) return nil; return [strongSelf->_companion userListForMention:mention]; }; recentView.hashtagListSignal = ^SSignal *(NSString *hashtag) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf == nil) return nil; return [strongSelf->_companion hashtagListForHashtag:hashtag]; }; [items addObject:recentView]; [items addObject:multifunctionButtonView]; [items addObject:[[TGAttachmentSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.ChooseVideo") pressed:^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf.view endEditing:true]; [strongSelf->_attachmentSheetWindow dismissAnimated:true completion:nil]; [strongSelf _displayVideoPicker]; } }]]; [items addObject:[[TGAttachmentSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Conversation.SearchWebImages") pressed:^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf.view endEditing:true]; [strongSelf->_attachmentSheetWindow dismissAnimated:true completion:nil]; [strongSelf _displayWebImagePicker]; } }]]; [items addObject:[[TGAttachmentSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Conversation.Document") pressed:^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf.view endEditing:true]; [strongSelf _displaySendFileMenu]; } }]]; [items addObject:[[TGAttachmentSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Conversation.Location") pressed:^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf.view endEditing:true]; [strongSelf->_attachmentSheetWindow dismissAnimated:true completion:nil]; [strongSelf _displayLocationPicker]; } }]]; if ([_companion allowContactSharing]) { [items addObject:[[TGAttachmentSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Conversation.Contact") pressed:^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf.view endEditing:true]; [strongSelf->_attachmentSheetWindow dismissAnimated:true completion:nil]; [strongSelf _displayContactPicker]; } }]]; } TGAttachmentSheetButtonItemView *cancelItem =[[TGAttachmentSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") pressed:^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) [strongSelf->_attachmentSheetWindow dismissAnimated:true completion:nil]; }]; [cancelItem setBold:true]; [items addObject:cancelItem]; _attachmentSheetWindow.view.items = items; [_attachmentSheetWindow showAnimated:true completion:nil]; } else { NSMutableArray *actions = [[NSMutableArray alloc] initWithArray:@[ [[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Common.ChoosePhoto") action:@"choosePhoto"], [[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Common.ChooseVideo") action:@"chooseVideo"], [[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Conversation.SearchWebImages") action:@"searchWeb"], [[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Conversation.Document") action:@"document"], [[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Conversation.Location") action:@"chooseLocation"] ]]; if ([_companion allowContactSharing]) [actions addObject:[[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Conversation.Contact") action:@"contact"]]; [actions insertObject:[[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Common.TakePhotoOrVideo") action:@"camera"] atIndex:0]; [actions addObject:[[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Common.Cancel") action:@"cancel" type:TGActionSheetActionTypeCancel]]; TGActionSheet *actionSheet = [[TGActionSheet alloc] initWithTitle:nil actions:actions actionBlock:^(TGModernConversationController *controller, NSString *action) { if (![action isEqualToString:@"cancel"]) { [controller.view endEditing:true]; if ([action isEqualToString:@"camera"]) [controller _displayCameraWithView:nil]; if ([action isEqualToString:@"choosePhoto"]) [controller _displayPhotoPicker]; else if ([action isEqualToString:@"searchWeb"]) [controller _displayWebImagePicker]; else if ([action isEqualToString:@"chooseVideo"]) [controller _displayVideoPicker]; else if ([action isEqualToString:@"chooseLocation"]) [controller _displayLocationPicker]; else if ([action isEqualToString:@"document"]) [controller _displaySendFileMenu]; else if ([action isEqualToString:@"contact"]) [controller _displayContactPicker]; } } target:self]; actionSheet.dismissBlock = ^(__unused TGModernConversationController *controller, NSString *action) { return ![action isEqualToString:@"cancel"]; }; if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) [actionSheet showInView:self.view]; else [actionSheet showFromRect:CGRectOffset([self.view convertRect:[_inputTextPanel attachmentButtonFrame] fromView:_inputTextPanel], 0.0f, -6.0f) inView:self.view animated:true]; } } - (void)_displayLocationPicker { __weak TGModernConversationController *weakSelf = self; TGLocationPickerControllerIntent intent = [_companion allowVenueSharing] ? TGLocationPickerControllerDefaultIntent : TGLocationPickerControllerCustomLocationIntent; TGLocationPickerController *controller = [[TGLocationPickerController alloc] initWithIntent:intent]; controller.locationPicked = ^(CLLocationCoordinate2D coordinate, TGVenueAttachment *venue) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf->_companion controllerWantsToSendMapWithLatitude:coordinate.latitude longitude:coordinate.longitude venue:venue asReplyToMessageId:[strongSelf currentReplyMessageId]]; [strongSelf dismissViewControllerAnimated:true completion:nil]; }; TGNavigationController *navigationController = [TGNavigationController navigationControllerWithControllers:@[controller]]; navigationController.restrictLandscape = true; if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { navigationController.presentationStyle = TGNavigationControllerPresentationStyleInFormSheet; navigationController.modalPresentationStyle = UIModalPresentationFormSheet; } [self presentViewController:navigationController animated:true completion:nil]; } - (void)_displayCameraWithView:(TGAttachmentSheetRecentCameraView *)cameraView { __autoreleasing NSString *disabledMessage = nil; if (![TGApplicationFeatures isPhotoUploadEnabledForPeerType:[_companion applicationFeaturePeerType] disabledMessage:&disabledMessage]) { [[[TGAlertView alloc] initWithTitle:TGLocalized(@"FeatureDisabled.Oops") message:disabledMessage cancelButtonTitle:TGLocalized(@"Common.OK") okButtonTitle:nil completionBlock:nil] show]; return; } if (![TGAccessChecker checkCameraAuthorizationStatusWithAlertDismissComlpetion:nil]) return; if (TGAppDelegateInstance.rootController.isSplitView) return; if (iosMajorVersion() < 7 || [UIDevice currentDevice].platformType == UIDevice4iPhone || [UIDevice currentDevice].platformType == UIDevice4GiPod) { [self _displayLegacyCamera]; [self->_attachmentSheetWindow dismissAnimated:true completion:nil]; return; } TGCameraController *controller = nil; CGSize screenSize = TGScreenSize(); if (cameraView.previewView != nil) controller = [[TGCameraController alloc] initWithCamera:cameraView.previewView.camera previewView:cameraView.previewView intent:TGCameraControllerGenericIntent]; else controller = [[TGCameraController alloc] init]; controller.shouldStoreCapturedAssets = [_companion controllerShouldStoreCapturedAssets]; controller.disallowCaptions = ![_companion allowCaptionedMedia]; TGCameraControllerWindow *controllerWindow = [[TGCameraControllerWindow alloc] initWithParentController:self contentController:controller]; if (_attachmentSheetWindow != nil) controllerWindow.windowLevel = _attachmentSheetWindow.windowLevel + 0.0001f; controllerWindow.hidden = false; controllerWindow.clipsToBounds = true; controllerWindow.frame = CGRectMake(0, 0, screenSize.width, screenSize.height); bool standalone = true; CGRect startFrame = CGRectMake(0, screenSize.height, screenSize.width, screenSize.height); if (cameraView != nil) { standalone = false; startFrame = [controller.view convertRect:cameraView.previewView.frame fromView:cameraView]; } [cameraView detachPreviewView]; [controller beginTransitionInFromRect:startFrame]; __weak TGModernConversationController *weakSelf = self; __weak TGCameraController *weakCameraController = controller; __weak TGAttachmentSheetRecentCameraView *weakCameraView = cameraView; controller.beginTransitionOut = ^CGRect { __strong TGCameraController *strongCameraController = weakCameraController; if (strongCameraController == nil) return CGRectZero; if (!standalone) { __strong TGAttachmentSheetRecentCameraView *strongCameraView = weakCameraView; if (strongCameraView != nil) return [strongCameraController.view convertRect:strongCameraView.frame fromView:strongCameraView.superview]; } return CGRectZero; }; controller.finishedTransitionOut = ^ { __strong TGAttachmentSheetRecentCameraView *strongCameraView = weakCameraView; if (strongCameraView == nil) return; [strongCameraView attachPreviewViewAnimated:true]; }; controller.finishedWithPhoto = ^(UIImage *resultImage, NSString *caption) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; __autoreleasing NSString *disabledMessage = nil; if (![TGApplicationFeatures isPhotoUploadEnabledForPeerType:[_companion applicationFeaturePeerType] disabledMessage:&disabledMessage]) { [[[TGAlertView alloc] initWithTitle:TGLocalized(@"FeatureDisabled.Oops") message:disabledMessage cancelButtonTitle:TGLocalized(@"Common.OK") okButtonTitle:nil completionBlock:nil] show]; return; } NSDictionary *imageDescription = [strongSelf->_companion imageDescriptionFromImage:resultImage caption:caption optionalAssetUrl:nil]; NSMutableArray *descriptions = [[NSMutableArray alloc] init]; if (imageDescription != nil) [descriptions addObject:imageDescription]; [strongSelf->_companion controllerWantsToSendImagesWithDescriptions:descriptions asReplyToMessageId:[strongSelf currentReplyMessageId]]; [strongSelf->_attachmentSheetWindow dismissAnimated:false completion:nil]; }; controller.finishedWithVideo = ^(NSString *existingAssetId, NSString *tempFilePath, NSUInteger fileSize, UIImage *previewImage, NSTimeInterval duration, CGSize dimensions, NSString *caption) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; __autoreleasing NSString *disabledMessage = nil; if (![TGApplicationFeatures isFileUploadEnabledForPeerType:[_companion applicationFeaturePeerType] disabledMessage:&disabledMessage]) { [[[TGAlertView alloc] initWithTitle:TGLocalized(@"FeatureDisabled.Oops") message:disabledMessage cancelButtonTitle:TGLocalized(@"Common.OK") okButtonTitle:nil completionBlock:nil] show]; return; } [strongSelf->_companion controllerWantsToSendLocalVideoWithTempFilePath:tempFilePath fileSize:(int32_t)fileSize previewImage:previewImage duration:duration dimensions:dimensions caption:caption assetUrl:existingAssetId liveUploadData:nil asReplyToMessageId:[strongSelf currentReplyMessageId]]; [strongSelf->_attachmentSheetWindow dismissAnimated:false completion:nil]; }; controller.userListSignal = ^SSignal *(NSString *mention) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf == nil) return nil; return [strongSelf->_companion userListForMention:mention]; }; controller.hashtagListSignal = ^SSignal *(NSString *hashtag) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf == nil) return nil; return [strongSelf->_companion hashtagListForHashtag:hashtag]; }; } - (void)_displayLegacyCamera { __autoreleasing NSString *disabledMessage = nil; if (![TGApplicationFeatures isPhotoUploadEnabledForPeerType:[_companion applicationFeaturePeerType] disabledMessage:&disabledMessage]) { [[[TGAlertView alloc] initWithTitle:TGLocalized(@"FeatureDisabled.Oops") message:disabledMessage cancelButtonTitle:TGLocalized(@"Common.OK") okButtonTitle:nil completionBlock:nil] show]; return; } TGLegacyCameraController *legacyCameraController = [[TGLegacyCameraController alloc] init]; legacyCameraController.sourceType = UIImagePickerControllerSourceTypeCamera; legacyCameraController.mediaTypes = [[NSArray alloc] initWithObjects:(__bridge NSString *)kUTTypeImage, (__bridge NSString *)kUTTypeMovie, nil]; legacyCameraController.storeCapturedAssets = [_companion controllerShouldStoreCapturedAssets]; legacyCameraController.completionDelegate = self; legacyCameraController.videoMaximumDuration = 100 * 60 * 60; [legacyCameraController setVideoQuality:UIImagePickerControllerQualityTypeMedium]; legacyCameraController.completionDelegate = self; [self presentViewController:legacyCameraController animated:true completion:nil]; } - (void)_displayVideoPicker { __autoreleasing NSString *disabledMessage = nil; if (![TGApplicationFeatures isFileUploadEnabledForPeerType:[_companion applicationFeaturePeerType] disabledMessage:&disabledMessage]) { [[[TGAlertView alloc] initWithTitle:TGLocalized(@"FeatureDisabled.Oops") message:disabledMessage cancelButtonTitle:TGLocalized(@"Common.OK") okButtonTitle:nil completionBlock:nil] show]; return; } if (![TGAccessChecker checkPhotoAuthorizationStatusForIntent:TGPhotoAccessIntentRead alertDismissCompletion:nil]) return; __weak TGModernConversationController *weakSelf = self; void (^videoPicked)(NSString *, NSString *, CGSize , NSTimeInterval , UIImage *, NSString *, TGLiveUploadActorData *) = ^(NSString *videoAssetId, NSString *tempFilePath, CGSize dimensions, NSTimeInterval duration, UIImage *thumbnail, NSString *caption, TGLiveUploadActorData *liveUploadData) { TGDispatchOnMainThread(^ { __strong TGModernConversationController *strongSelf = weakSelf; TGVideoMediaAttachment *videoAttachment = nil; if (videoAssetId != nil) videoAttachment = [strongSelf.companion serverCachedAssetWithId:videoAssetId]; if (videoAttachment != nil) { videoAttachment.caption = caption; [strongSelf.companion controllerWantsToSendRemoteVideoWithMedia:videoAttachment asReplyToMessageId:[strongSelf currentReplyMessageId]]; } else { int fileSize = [[[NSFileManager defaultManager] attributesOfItemAtPath:tempFilePath error:NULL][NSFileSize] intValue]; if (fileSize != 0) { [strongSelf.companion controllerWantsToSendLocalVideoWithTempFilePath:tempFilePath fileSize:(int32_t)fileSize previewImage:thumbnail duration:duration dimensions:dimensions caption:caption assetUrl:videoAssetId liveUploadData:liveUploadData asReplyToMessageId:[strongSelf currentReplyMessageId]]; } } }); }; void(^dismiss)(void) = ^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf dismissViewControllerAnimated:true completion:nil]; }; void(^showMediaPicker)(TGMediaPickerAssetsGroup *) = ^(TGMediaPickerAssetsGroup *group) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; TGMediaFoldersController *mediaFoldersController = [[TGMediaFoldersController alloc] initWithIntent:TGModernMediaPickerControllerSendVideoIntent]; mediaFoldersController.dismiss = dismiss; mediaFoldersController.videoPicked = videoPicked; mediaFoldersController.liveUpload = [strongSelf->_companion controllerShouldLiveUploadVideo]; mediaFoldersController.enableServerAssetCache = [strongSelf->_companion controllerShouldCacheServerAssets]; mediaFoldersController.disallowCaptions = ![strongSelf->_companion allowCaptionedMedia]; TGModernMediaPickerController *mediaPickerController = [[TGModernMediaPickerController alloc] initWithAssetsGroup:group intent:TGModernMediaPickerControllerSendVideoIntent]; mediaPickerController.dismiss = dismiss; mediaPickerController.videoPicked = videoPicked; mediaPickerController.liveUploadEnabled = [strongSelf->_companion controllerShouldLiveUploadVideo]; mediaPickerController.serverAssetCacheEnabled = [strongSelf->_companion controllerShouldCacheServerAssets]; mediaPickerController.disallowCaptions = ![strongSelf->_companion allowCaptionedMedia]; TGNavigationController *navigationController = [TGNavigationController navigationControllerWithControllers:@[mediaFoldersController, mediaPickerController]]; if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { navigationController.presentationStyle = TGNavigationControllerPresentationStyleInFormSheet; navigationController.modalPresentationStyle = UIModalPresentationFormSheet; } [strongSelf presentViewController:navigationController animated:true completion:nil]; }; if ([[TGMediaPickerAssetsLibrary sharedLibrary] authorizationStatus] == TGMediaPickerAuthorizationStatusNotDetermined) { [[TGMediaPickerAssetsLibrary sharedLibrary] fetchAssetsGroupsWithCompletionBlock:^(NSArray *groups, __unused TGMediaPickerAuthorizationStatus status, __unused NSError *error) { TGDispatchOnMainThread(^ { if (![TGAccessChecker checkPhotoAuthorizationStatusForIntent:TGPhotoAccessIntentRead alertDismissCompletion:nil]) return; TGMediaPickerAssetsGroup *cameraRollGroup = nil; for (TGMediaPickerAssetsGroup *group in groups) { if (group.isCameraRoll) { cameraRollGroup = group; break; } } showMediaPicker(cameraRollGroup); }); }]; } else { showMediaPicker(nil); } } - (void)_displayICloudDrivePicker { NSArray *documentTypes = @[@"public.composite-content", @"public.text", @"public.image", @"public.audio", @"public.video", @"public.movie", @"public.font", @"org.telegram.Telegram.webp", @"com.apple.iwork.pages.pages", @"com.apple.iwork.numbers.numbers", @"com.apple.iwork.keynote.key"]; UIDocumentPickerViewController *controller = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:documentTypes inMode:UIDocumentPickerModeOpen]; controller.view.backgroundColor = [UIColor whiteColor]; controller.delegate = self; if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) controller.modalPresentationStyle = UIModalPresentationFormSheet; [self presentViewController:controller animated:true completion:nil]; } - (void)_displayDropboxPicker { _dropboxProxy = [[TGObserverProxy alloc] initWithTarget:self targetSelector:@selector(dropboxFilesReceived:) name:TGDropboxFilesReceivedNotification]; [TGDropboxHelper openExternalPicker]; } - (void)_displayGoogleDrivePicker { __weak TGModernConversationController *weakSelf = self; TGGoogleDriveController *controller = [[TGGoogleDriveController alloc] init]; controller.dismiss = ^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf dismissViewControllerAnimated:true completion:nil]; }; controller.filePicked = ^(TGGoogleDriveItem *item) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; id description = [strongSelf.companion documentDescriptionFromGoogleDriveItem:item]; [strongSelf.companion controllerWantsToSendCloudDocumentsWithDescriptions:@[description] asReplyToMessageId:[strongSelf currentReplyMessageId]]; }; if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { controller.presentationStyle = TGNavigationControllerPresentationStyleInFormSheet; controller.modalPresentationStyle = UIModalPresentationFormSheet; } [self presentViewController:controller animated:true completion:nil]; } - (void)_displaySendFileMenu { if ((iosMajorVersion() >= 8) || (iosMajorVersion() >= 7 && ([TGDropboxHelper isDropboxInstalled] || [TGGoogleDriveController isGoogleDriveInstalled]))) { __weak TGModernConversationController *weakSelf = self; CGSize screenSize = TGScreenSize(); bool stickToBottom = ABS(screenSize.height - 568.0f) < FLT_EPSILON && UIInterfaceOrientationIsPortrait(self.interfaceOrientation); if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone) { TGAttachmentSheetView *fileSheetView = [[TGAttachmentSheetView alloc] init]; NSMutableArray *items = [[NSMutableArray alloc] init]; __weak TGAttachmentSheetView *weakFileSheetView = fileSheetView; TGAttachmentSheetFileInstructionItemView *instructionItemView = [[TGAttachmentSheetFileInstructionItemView alloc] init]; instructionItemView.folded = [[[NSUserDefaults standardUserDefaults] objectForKey:@"didShowDocumentPickerTip_v2"] boolValue]; if (!instructionItemView.folded) [[NSUserDefaults standardUserDefaults] setObject:@true forKey:@"didShowDocumentPickerTip_v2"]; __weak TGAttachmentSheetFileInstructionItemView *weakInstructionItemView = instructionItemView; instructionItemView.pressed = ^ { __strong TGAttachmentSheetView *strongFileSheetView = weakFileSheetView; if (strongFileSheetView == nil) return; __strong TGAttachmentSheetFileInstructionItemView *strongInstructionItemView = weakInstructionItemView; if (strongInstructionItemView == nil) return; [strongFileSheetView performAnimated:true updates:^ { strongInstructionItemView.folded = false; } stickToBottom:stickToBottom completion:nil]; }; [items addObject:instructionItemView]; [items addObject:[[TGAttachmentSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Conversation.FilePhotoOrVideo") pressed:^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf->_attachmentSheetWindow dismissAnimated:true completion:nil]; [strongSelf _displayPhotoVideoFilePickerFromFileMenu:true]; } }]]; if (iosMajorVersion() >= 8) { [items addObject:[[TGAttachmentSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Conversation.FileICloudDrive") pressed:^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf->_attachmentSheetWindow dismissAnimated:true completion:nil]; [strongSelf _displayICloudDrivePicker]; } }]]; } if ([TGDropboxHelper isDropboxInstalled]) { [items addObject:[[TGAttachmentSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Conversation.FileDropbox") pressed:^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf->_attachmentSheetWindow dismissAnimated:true completion:nil]; [strongSelf _displayDropboxPicker]; } }]]; } if ([TGGoogleDriveController isGoogleDriveInstalled]) { [items addObject:[[TGAttachmentSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Conversation.FileGoogleDrive") pressed:^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf->_attachmentSheetWindow dismissAnimated:true completion:nil]; [strongSelf _displayGoogleDrivePicker]; } }]]; } TGAttachmentSheetButtonItemView *cancelItem = [[TGAttachmentSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") pressed:^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) [strongSelf->_attachmentSheetWindow dismissAnimated:true completion:nil]; }]; [cancelItem setBold:true]; [items addObject:cancelItem]; fileSheetView.items = items; [_attachmentSheetWindow switchToSheetView:fileSheetView stickToBottom:stickToBottom]; } else { NSMutableArray *actions = [[NSMutableArray alloc] init]; [actions addObject:[[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Conversation.FilePhotoOrVideo") action:@"photoOrVideo"]]; if (iosMajorVersion() >= 8) [actions addObject:[[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Conversation.FileICloudDrive") action:@"iCloudDrive"]]; if ([TGDropboxHelper isDropboxInstalled]) [actions addObject:[[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Conversation.FileDropbox") action:@"dropbox"]]; if ([TGGoogleDriveController isGoogleDriveInstalled]) [actions addObject:[[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Conversation.FileGoogleDrive") action:@"googleDrive"]]; [actions addObject:[[TGActionSheetAction alloc] initWithTitle:TGLocalized(@"Common.Cancel") action:@"cancel" type:TGActionSheetActionTypeCancel]]; TGActionSheet *actionSheet = [[TGActionSheet alloc] initWithTitle:nil actions:actions actionBlock:^(TGModernConversationController *controller, NSString *action) { if (![action isEqualToString:@"cancel"]) { [controller.view endEditing:true]; if ([action isEqualToString:@"photoOrVideo"]) [controller _displayPhotoVideoFilePickerFromFileMenu:false]; if ([action isEqualToString:@"iCloudDrive"]) [controller _displayICloudDrivePicker]; else if ([action isEqualToString:@"dropbox"]) [controller _displayDropboxPicker]; else if ([action isEqualToString:@"googleDrive"]) [controller _displayGoogleDrivePicker]; } } target:self]; actionSheet.dismissBlock = ^(__unused TGModernConversationController *controller, NSString *action) { return ![action isEqualToString:@"cancel"]; }; if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) [actionSheet showInView:self.view]; else [actionSheet showFromRect:CGRectOffset([self.view convertRect:[_inputTextPanel attachmentButtonFrame] fromView:_inputTextPanel], 0.0f, -6.0f) inView:self.view animated:true]; } } else { [_attachmentSheetWindow dismissAnimated:true completion:nil]; [self _displayPhotoVideoFilePickerFromFileMenu:false]; } } - (void)dropboxFilesReceived:(NSNotification *)notification { if (notification.object == nil && ![notification.object isKindOfClass:[NSArray class]] && _dropboxProxy != nil) return; NSArray *items = (NSArray *)notification.object; NSMutableArray *descriptions = [[NSMutableArray alloc] init]; for (TGDropboxItem *item in items) { id description = [self.companion documentDescriptionFromDropboxItem:item]; if (description != nil) [descriptions addObject:description]; } [self.companion controllerWantsToSendCloudDocumentsWithDescriptions:descriptions asReplyToMessageId:[self currentReplyMessageId]]; } - (void)documentPicker:(UIDocumentPickerViewController *)__unused controller didPickDocumentAtURL:(NSURL *)url { __weak TGModernConversationController *weakSelf = self; _currentICloudItemRequest = [TGICloudItemRequest requestICloudItemWithUrl:url completion:^(TGICloudItem *item) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; strongSelf->_currentICloudItemRequest = nil; id description = [strongSelf.companion documentDescriptionFromICloudDriveItem:item]; if (description != nil) [strongSelf.companion controllerWantsToSendCloudDocumentsWithDescriptions:@[description] asReplyToMessageId:[strongSelf currentReplyMessageId]]; }]; } - (void)_displayPhotoVideoFilePickerFromFileMenu:(bool)fromFileMenu { if (![TGAccessChecker checkPhotoAuthorizationStatusForIntent:TGPhotoAccessIntentRead alertDismissCompletion:nil]) return; __weak TGModernConversationController *weakSelf = self; void(^photosPicked)(TGModernMediaPickerController *) = ^(TGModernMediaPickerController *sender) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf _asyncProcessMediaAssetSignals:[sender selectedItemSignals:^id(id fileDict, __unused NSString *caption, __unused NSString *maybeHash) { if (fileDict != nil) { id description = [strongSelf.companion documentDescriptionFromFileAtTempUrl:fileDict[@"tempFileUrl"] fileName:fileDict[@"fileName"] mimeType:fileDict[@"mimeType"]]; return description; } return nil; }] forIntent:TGModernMediaPickerControllerSendFileIntent]; }; void(^dismiss)(void) = ^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf dismissViewControllerAnimated:true completion:nil]; }; void (^showMediaPicker)(TGMediaPickerAssetsGroup *) = ^(TGMediaPickerAssetsGroup *group) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; TGMediaFoldersController *mediaFoldersController = [[TGMediaFoldersController alloc] initWithIntent:TGModernMediaPickerControllerSendFileIntent]; mediaFoldersController.photosPicked = photosPicked; mediaFoldersController.dismiss = dismiss; TGModernMediaPickerController *mediaPickerController = [[TGModernMediaPickerController alloc] initWithAssetsGroup:group intent:TGModernMediaPickerControllerSendFileIntent]; mediaPickerController.shouldShowFileTipIfNeeded = !fromFileMenu; mediaPickerController.photosPicked = photosPicked; mediaPickerController.dismiss = dismiss; TGNavigationController *navigationController = [TGNavigationController navigationControllerWithControllers:@[ mediaFoldersController, mediaPickerController ]]; if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { navigationController.presentationStyle = TGNavigationControllerPresentationStyleInFormSheet; navigationController.modalPresentationStyle = UIModalPresentationFormSheet; } [strongSelf presentViewController:navigationController animated:true completion:nil]; }; if ([[TGMediaPickerAssetsLibrary sharedLibrary] authorizationStatus] == TGMediaPickerAuthorizationStatusNotDetermined) { [[TGMediaPickerAssetsLibrary sharedLibrary] fetchAssetsGroupsWithCompletionBlock:^(NSArray *groups, __unused TGMediaPickerAuthorizationStatus status, __unused NSError *error) { TGDispatchOnMainThread(^ { if (![TGAccessChecker checkPhotoAuthorizationStatusForIntent:TGPhotoAccessIntentRead alertDismissCompletion:nil]) return; TGMediaPickerAssetsGroup *cameraRollGroup = nil; for (TGMediaPickerAssetsGroup *group in groups) { if (group.isCameraRoll) { cameraRollGroup = group; break; } } showMediaPicker(cameraRollGroup); }); }]; } else { showMediaPicker(nil); } } - (void)imagePickerController:(TGImagePickerController *)__unused imagePicker didFinishPickingWithAssets:(NSArray *)assets { NSMutableArray *imageDescriptions = [[NSMutableArray alloc] init]; for (id abstractAsset in assets) { if ([abstractAsset isKindOfClass:[UIImage class]]) { @autoreleasepool { NSDictionary *imageDescription = [_companion imageDescriptionFromImage:abstractAsset caption:nil optionalAssetUrl:nil]; if (imageDescription != nil) [imageDescriptions addObject:imageDescription]; } } else if ([abstractAsset isKindOfClass:[NSString class]]) { @autoreleasepool { UIImage *image = [[TGRemoteImageView sharedCache] cachedImage:abstractAsset availability:TGCacheDisk]; if (image != nil) { NSDictionary *imageDescription = [_companion imageDescriptionFromImage:image caption:nil optionalAssetUrl:nil]; if (imageDescription != nil) [imageDescriptions addObject:imageDescription]; } } } } if (imageDescriptions.count != 0) [_companion controllerWantsToSendImagesWithDescriptions:imageDescriptions asReplyToMessageId:[self currentReplyMessageId]]; [self dismissViewControllerAnimated:true completion:nil]; } - (void)legacyCameraControllerCompletedWithDocument:(NSURL *)fileUrl fileName:(NSString *)fileName mimeType:(NSString *)mimeType { [self dismissViewControllerAnimated:true completion:nil]; [_companion controllerWantsToSendDocumentWithTempFileUrl:fileUrl fileName:fileName mimeType:mimeType asReplyToMessageId:[self currentReplyMessageId]]; } - (void)_displayPhotoPicker { if (![TGAccessChecker checkPhotoAuthorizationStatusForIntent:TGPhotoAccessIntentRead alertDismissCompletion:nil]) return; __weak TGModernConversationController *weakSelf = self; void(^photosPicked)(TGModernMediaPickerController *) = ^(TGModernMediaPickerController *sender) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf _asyncProcessMediaAssetSignals:[sender selectedItemSignals:^id (UIImage *image, NSString *caption, NSString *maybeHash) { if (image != nil) { id description = [strongSelf.companion imageDescriptionFromImage:image caption:caption optionalAssetUrl:maybeHash == nil ? nil : [[NSString alloc] initWithFormat:@"image-%@", maybeHash]]; return description; } return nil; }] forIntent:TGModernMediaPickerControllerSendPhotoIntent]; }; void(^dismiss)(void) = ^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf dismissViewControllerAnimated:true completion:nil]; }; SSignal *(^userListSignal)(NSString *) = ^SSignal *(NSString *mention) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf == nil) return nil; return [strongSelf->_companion userListForMention:mention]; }; SSignal *(^hashtagListSignal)(NSString *) = ^SSignal *(NSString *hashtag) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf == nil) return nil; return [strongSelf->_companion hashtagListForHashtag:hashtag]; }; void (^showMediaPicker)(TGMediaPickerAssetsGroup *) = ^(TGMediaPickerAssetsGroup *group) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; TGMediaFoldersController *mediaFoldersController = [[TGMediaFoldersController alloc] initWithIntent:TGModernMediaPickerControllerSendPhotoIntent]; mediaFoldersController.photosPicked = photosPicked; mediaFoldersController.dismiss = dismiss; mediaFoldersController.userListSignal = userListSignal; mediaFoldersController.hashtagListSignal = hashtagListSignal; mediaFoldersController.disallowCaptions = ![strongSelf->_companion allowCaptionedMedia]; TGModernMediaPickerController *mediaPickerController = [[TGModernMediaPickerController alloc] initWithAssetsGroup:group intent:TGModernMediaPickerControllerSendPhotoIntent]; mediaPickerController.photosPicked = photosPicked; mediaPickerController.dismiss = dismiss; mediaPickerController.userListSignal = userListSignal; mediaPickerController.hashtagListSignal = hashtagListSignal; mediaPickerController.disallowCaptions = ![strongSelf->_companion allowCaptionedMedia]; TGNavigationController *navigationController = [TGNavigationController navigationControllerWithControllers:@[ mediaFoldersController, mediaPickerController ]]; if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { navigationController.presentationStyle = TGNavigationControllerPresentationStyleInFormSheet; navigationController.modalPresentationStyle = UIModalPresentationFormSheet; } [strongSelf presentViewController:navigationController animated:true completion:nil]; }; if ([[TGMediaPickerAssetsLibrary sharedLibrary] authorizationStatus] == TGMediaPickerAuthorizationStatusNotDetermined) { [[TGMediaPickerAssetsLibrary sharedLibrary] fetchAssetsGroupsWithCompletionBlock:^(NSArray *groups, __unused TGMediaPickerAuthorizationStatus status, __unused NSError *error) { TGDispatchOnMainThread(^ { if (![TGAccessChecker checkPhotoAuthorizationStatusForIntent:TGPhotoAccessIntentRead alertDismissCompletion:nil]) return; TGMediaPickerAssetsGroup *cameraRollGroup = nil; for (TGMediaPickerAssetsGroup *group in groups) { if (group.isCameraRoll) { cameraRollGroup = group; break; } } showMediaPicker(cameraRollGroup); }); }]; } else { showMediaPicker(nil); } } - (void)_displayWebImagePicker { __weak TGModernConversationController *weakSelf = self; TGWebSearchController *searchController = [[TGWebSearchController alloc] init]; searchController.disallowCaptions = ![_companion allowCaptionedMedia]; searchController.dismiss = ^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf dismissViewControllerAnimated:true completion:nil]; }; searchController.completion = ^(TGWebSearchController *sender) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; [strongSelf _asyncProcessMediaAssetSignals:[sender selectedItemSignals:^id (id item, NSString *caption) { if (item != nil) { if ([item isKindOfClass:[TGBingSearchResultItem class]]) { id description = [strongSelf.companion imageDescriptionFromBingSearchResult:item caption:caption]; return description; } else if ([item isKindOfClass:[TGGiphySearchResultItem class]]) { id description = [strongSelf.companion documentDescriptionFromGiphySearchResult:item]; return description; } else if ([item isKindOfClass:[TGWebSearchInternalImageResult class]]) { id description = [strongSelf.companion imageDescriptionFromInternalSearchImageResult:item caption:caption]; return description; } else if ([item isKindOfClass:[TGWebSearchInternalGifResult class]]) { id description = [strongSelf.companion documentDescriptionFromInternalSearchResult:item]; return description; } else if ([item isKindOfClass:[UIImage class]]) { id description = [strongSelf.companion imageDescriptionFromImage:item caption:caption optionalAssetUrl:nil]; return description; } } return nil; }] forIntent:TGModernMediaPickerControllerSendPhotoIntent]; }; TGNavigationController *navigationController = [TGNavigationController navigationControllerWithControllers:@[ searchController ]]; if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { navigationController.presentationStyle = TGNavigationControllerPresentationStyleInFormSheet; navigationController.modalPresentationStyle = UIModalPresentationFormSheet; } [self presentViewController:navigationController animated:true completion:nil]; } - (void)legacyCameraControllerCapturedVideoWithTempFilePath:(NSString *)tempVideoFilePath fileSize:(int32_t)fileSize previewImage:(UIImage *)previewImage duration:(NSTimeInterval)duration dimensions:(CGSize)dimenstions assetUrl:(NSString *)assetUrl { [self dismissViewControllerAnimated:true completion:nil]; [_companion controllerWantsToSendLocalVideoWithTempFilePath:tempVideoFilePath fileSize:fileSize previewImage:previewImage duration:duration dimensions:dimenstions caption:nil assetUrl:assetUrl liveUploadData:nil asReplyToMessageId:[self currentReplyMessageId]]; } - (void)legacyCameraControllerCompletedWithExistingMedia:(id)media { [self dismissViewControllerAnimated:true completion:nil]; if ([media isKindOfClass:[TGVideoMediaAttachment class]]) [_companion controllerWantsToSendRemoteVideoWithMedia:media asReplyToMessageId:[self currentReplyMessageId]]; } - (void)legacyCameraControllerCompletedWithNoResult { [self dismissViewControllerAnimated:true completion:nil]; } - (void)inputPanelRequestedSendImages:(TGModernConversationInputTextPanel *)__unused inputTextPanel images:(NSArray *)images { __autoreleasing NSString *disabledMessage = nil; if (![TGApplicationFeatures isPhotoUploadEnabledForPeerType:[_companion applicationFeaturePeerType] disabledMessage:&disabledMessage]) { [[[TGAlertView alloc] initWithTitle:TGLocalized(@"FeatureDisabled.Oops") message:disabledMessage cancelButtonTitle:TGLocalized(@"Common.OK") okButtonTitle:nil completionBlock:nil] show]; return; } NSMutableArray *imageDescriptions = [[NSMutableArray alloc] init]; for (id abstractAsset in images) { if ([abstractAsset isKindOfClass:[UIImage class]]) { @autoreleasepool { NSDictionary *imageDescription = [_companion imageDescriptionFromImage:abstractAsset caption:nil optionalAssetUrl:nil]; if (imageDescription != nil) [imageDescriptions addObject:imageDescription]; } } } if (imageDescriptions.count != 0) [_companion controllerWantsToSendImagesWithDescriptions:imageDescriptions asReplyToMessageId:[self currentReplyMessageId]]; } - (void)inputPanelRequestedSendData:(TGModernConversationInputTextPanel *)__unused inputTextPanel data:(NSData *)data { if (data != nil) { NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSString alloc] initWithFormat:@"%x%x", (int)arc4random(), (int)arc4random()]]; [data writeToFile:filePath atomically:true]; [_companion controllerWantsToSendDocumentWithTempFileUrl:[NSURL fileURLWithPath:filePath] fileName:@"animation.gif" mimeType:@"image/gif" asReplyToMessageId:[self currentReplyMessageId]]; } } - (void)inputPanelRequestedSendSticker:(TGModernConversationInputTextPanel *)__unused inputTextPanel sticker:(TGDocumentMediaAttachment *)sticker { [self->_companion controllerWantsToSendRemoteDocument:sticker asReplyToMessageId:[self currentReplyMessageId]]; } - (void)inputPanelRequestedActivateCommand:(TGModernConversationInputTextPanel *)__unused inputTextPanel command:(NSString *)command userId:(int32_t)__unused userId messageId:(int32_t)messageId { int32_t replyMessageId = 0; if (((TGGenericModernConversationCompanion *)_companion).conversationId < 0) replyMessageId = messageId; if (_replyMarkup.hideKeyboardOnActivation && !_replyMarkup.alreadyActivated) { [self setReplyMarkup:[_replyMarkup activatedMarkup]]; [TGDatabaseInstance() storeBotReplyMarkupActivated:_replyMarkup forPeerId:((TGGenericModernConversationCompanion *)_companion).conversationId]; } [self->_companion controllerWantsToSendTextMessage:[[NSString alloc] initWithFormat:@"%@%@", @"", command] asReplyToMessageId:[self currentReplyMessageId] == 0 ? replyMessageId : [self currentReplyMessageId] withAttachedMessages:@[] disableLinkPreviews:false]; } - (void)_displayContactPicker { if (![TGAccessChecker checkAddressBookAuthorizationStatusWithAlertDismissComlpetion:nil]) return; TGForwardContactPickerController *contactPickerController = [[TGForwardContactPickerController alloc] init]; contactPickerController.delegate = self; TGNavigationController *navigationController = [TGNavigationController navigationControllerWithControllers:@[contactPickerController]]; if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { navigationController.presentationStyle = TGNavigationControllerPresentationStyleInFormSheet; navigationController.modalPresentationStyle = UIModalPresentationFormSheet; } [self presentViewController:navigationController animated:true completion:nil]; } - (void)forwardContactPickerController:(TGForwardContactPickerController *)__unused contactPicker didSelectContact:(TGUser *)contactUser { [_companion controllerWantsToSendContact:contactUser asReplyToMessageId:[self currentReplyMessageId]]; } - (NSString *)_dictionaryString:(NSDictionary *)dict { NSMutableString *string = [[NSMutableString alloc] init]; [dict enumerateKeysAndObjectsUsingBlock:^(id key, id value, __unused BOOL *stop) { if ([key isKindOfClass:[NSString class]]) [string appendString:key]; else if ([key isKindOfClass:[NSNumber class]]) [string appendString:[key description]]; [string appendString:@":"]; if ([value isKindOfClass:[NSString class]]) [string appendString:value]; else if ([value isKindOfClass:[NSNumber class]]) [string appendString:[value description]]; else if ([value isKindOfClass:[NSDictionary class]]) { [string appendString:@"{"]; [string appendString:[self _dictionaryString:value]]; [string appendString:@"}"]; } [string appendString:@";"]; }]; return string; } - (bool)inputPanelAudioRecordingEnabled:(TGModernConversationInputTextPanel *)__unused inputTextPanel { __autoreleasing NSString *disabledMessage = nil; if (![TGApplicationFeatures isAudioUploadEnabledForPeerType:[_companion applicationFeaturePeerType] disabledMessage:&disabledMessage]) { [[[TGAlertView alloc] initWithTitle:TGLocalized(@"FeatureDisabled.Oops") message:disabledMessage cancelButtonTitle:TGLocalized(@"Common.OK") okButtonTitle:nil completionBlock:nil] show]; return false; } return true; } - (void)inputPanelAudioRecordingStart:(TGModernConversationInputTextPanel *)__unused inputTextPanel { [self stopAudioRecording]; [self stopInlineMediaIfPlaying]; if (_currentAudioRecorder == nil) { _currentAudioRecorder = [[TGAudioRecorder alloc] initWithFileEncryption:[_companion encryptUploads]]; _currentAudioRecorder.delegate = self; _currentAudioRecorder.activityHolder = [_companion acquireAudioRecordingActivityHolder]; [_currentAudioRecorder start]; [[UIApplication sharedApplication] setIdleTimerDisabled:true]; } } - (void)inputPanelAudioRecordingCancel:(TGModernConversationInputTextPanel *)__unused inputTextPanel { [self stopAudioRecording]; [_inputTextPanel audioRecordingFinished]; [[UIApplication sharedApplication] setIdleTimerDisabled:false]; } - (void)inputPanelAudioRecordingComplete:(TGModernConversationInputTextPanel *)inputTextPanel { if (_currentAudioRecorder != nil) { _currentAudioRecorder.delegate = nil; [_currentAudioRecorder finish:^(TGDataItem *dataItem, NSTimeInterval duration, TGLiveUploadActorData *liveData) { TGDispatchOnMainThread(^ { if (dataItem != nil) { [_companion controllerWantsToSendLocalAudioWithDataItem:dataItem duration:duration liveData:liveData asReplyToMessageId:[self currentReplyMessageId]]; } else [inputTextPanel shakeControls]; }); }]; _currentAudioRecorder = nil; [_inputTextPanel audioRecordingFinished]; if ([self shouldAutorotate]) [TGViewController attemptAutorotation]; [[UIApplication sharedApplication] setIdleTimerDisabled:false]; } } - (NSTimeInterval)inputPanelAudioRecordingDuration:(TGModernConversationInputTextPanel *)__unused inputTextPanel { if (_currentAudioRecorder != nil) return [_currentAudioRecorder currentDuration]; return 0.0; } - (void)audioRecorderDidStartRecording:(TGAudioRecorder *)audioRecorder { TGDispatchOnMainThread(^ { if (audioRecorder == _currentAudioRecorder) [_inputTextPanel audioRecordingStarted]; }); } - (bool)inputPanelSendShouldBeAlwaysEnabled:(TGModernConversationInputTextPanel *)__unused inputTextPanel { return [self currentForwardMessages].count != 0; } - (TGViewController *)inputPanelParentViewController:(TGModernConversationInputTextPanel *)__unused inputTextPanel { return self; } - (void)inputPanelToggleBroadcastMode:(TGModernConversationInputTextPanel *)__unused inputTextPanel { [_companion _toggleBroadcastMode]; } - (void)_enterEditingMode:(int32_t)animateFromMessageId { if (!_editingMode) { [self setCurrentTitlePanel:nil animation:TGModernConversationPanelAnimationSlide]; [_companion clearCheckedMessages]; [_companion setMessageChecked:animateFromMessageId checked:true]; for (TGModernCollectionCell *cell in _collectionView.visibleCells) { TGMessageModernConversationItem *messageItem = cell.boundItem; [messageItem setTemporaryHighlighted:false viewStorage:_viewStorage]; } _editingMode = true; NSArray *visibleCells = [_collectionView.visibleCells sortedArrayUsingComparator:^NSComparisonResult(TGModernCollectionCell *cell1, TGModernCollectionCell *cell2) { return cell1.frame.origin.y > cell2.frame.origin.y ? NSOrderedAscending : NSOrderedDescending; }]; _companion.viewContext.editing = true; NSUInteger animateFromIndex = NSNotFound; for (NSUInteger i = 0; i < visibleCells.count; i++) { TGMessageModernConversationItem * item = ((TGModernCollectionCell *)visibleCells[i]).boundItem; if (item != nil && item->_message.mid == animateFromMessageId) { animateFromIndex = i; break; } } if (false && animateFromIndex != NSNotFound) { [(TGMessageModernConversationItem *)((TGModernCollectionCell *)visibleCells[animateFromIndex]).boundItem updateEditingState:_viewStorage animationDelay:0.0]; NSTimeInterval upDelay = 0.01; for (int i = animateFromIndex + 1; i < (int)visibleCells.count; i++) { TGModernCollectionCell *cell = visibleCells[i]; [(TGMessageModernConversationItem *)cell.boundItem updateEditingState:_viewStorage animationDelay:upDelay]; upDelay += 0.008; } NSTimeInterval downDelay = 0.01; for (int i = animateFromIndex - 1; i >= 0; i--) { TGModernCollectionCell *cell = visibleCells[i]; [(TGMessageModernConversationItem *)cell.boundItem updateEditingState:_viewStorage animationDelay:downDelay]; downDelay += 0.008; } } else { NSTimeInterval delay = 0.0; for (TGModernCollectionCell *cell in visibleCells) { [(TGMessageModernConversationItem *)cell.boundItem updateEditingState:_viewStorage animationDelay:delay]; delay += 0.006; } } [self setLeftBarButtonItem:[[UIBarButtonItem alloc] initWithTitle:[_companion canDeleteAllMessages] ? TGLocalized(@"Conversation.ClearAll") : @"" style:UIBarButtonItemStylePlain target:self action:@selector(clearAllButtonPressed)] animated:true]; [self setRightBarButtonItem:[[UIBarButtonItem alloc] initWithTitle:TGLocalized(@"Common.Cancel") style:UIBarButtonItemStylePlain target:self action:@selector(doneButtonPressed)] animated:true]; TGModernConversationEditingPanel *editPanel = [[TGModernConversationEditingPanel alloc] initWithFrame:CGRectMake(0.0f, 0.0f, _view.frame.size.width, 0.0f)]; editPanel.delegate = self; [editPanel setForwardingEnabled:[_companion allowMessageForwarding]]; [editPanel setDeleteEnabled:[self canDeleteSelectedMessages]]; [self setInputPanel:editPanel animated:true]; [self _updateEditingPanel]; [_titleView setEditingMode:true animated:true]; } } - (bool)canDeleteSelectedMessages { if ([_companion canDeleteMessages]) { return true; } NSArray *checkedMessageIds = [_companion checkedMessageIds]; for (TGMessageModernConversationItem *item in _items) { int32_t mid = item->_message.mid; for (NSNumber *nMid in checkedMessageIds) { if ([nMid intValue] == mid) { if (![_companion canDeleteMessage:item->_message]) { return false; } break; } } } return true; } static UIView *_findBackArrow(UIView *view) { static Class backArrowClass = NSClassFromString(TGEncodeText(@"`VJObwjhbujpoCbsCbdlJoejdbupsWjfx", -1)); if ([view isKindOfClass:backArrowClass]) return view; for (UIView *subview in view.subviews) { UIView *result = _findBackArrow(subview); if (result != nil) return result; } return nil; } - (void)_leaveEditingModeAnimated:(bool)animated { if (_editingMode) { [_companion clearCheckedMessages]; _editingMode = false; NSArray *visibleCells = [_collectionView.visibleCells sortedArrayUsingComparator:^NSComparisonResult(TGModernCollectionCell *cell1, TGModernCollectionCell *cell2) { return cell1.frame.origin.y > cell2.frame.origin.y ? NSOrderedAscending : NSOrderedDescending; }]; _companion.viewContext.editing = false; NSTimeInterval delay = 0.0; for (TGModernCollectionCell *cell in visibleCells) { [(TGMessageModernConversationItem *)cell.boundItem updateEditingState:_viewStorage animationDelay:delay]; delay += 0.006; } [self setLeftBarButtonItem:[self defaultLeftBarButtonItem] animated:animated]; [self setRightBarButtonItem:[self defaultRightBarButtonItem] animated:animated]; [self setInputPanel:_customInputPanel != nil ? _customInputPanel : [self defaultInputPanel] animated:animated]; if (animated && iosMajorVersion() >= 7) { UIView *backArrow = _findBackArrow(self.navigationController.navigationBar); backArrow.alpha = 0.0f; [UIView animateWithDuration:0.3 delay:0.17 options:0 animations:^ { backArrow.alpha = 1.0f; } completion:nil]; } [_titleView setEditingMode:false animated:animated]; } } - (void)clearAllButtonPressed { if ([_companion canDeleteAllMessages]) { ASHandle *actionHandle = _actionHandle; [[[TGAlertView alloc] initWithTitle:nil message:TGLocalized(@"Conversation.ClearAllConfirmation") cancelButtonTitle:TGLocalized(@"Common.Cancel") okButtonTitle:TGLocalized(@"Common.OK") completionBlock:^(bool okButtonPressed) { if (okButtonPressed) [actionHandle requestAction:@"clearAllMessages" options:nil]; }] show]; } } - (void)_commitClearAllMessages { [self _leaveEditingModeAnimated:true]; [self deleteItemsAtIndices:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, _items.count)] animated:true]; [_companion controllerClearedConversation]; } - (void)_commitDeleteCheckedMessages { NSArray *checkedMessageIds = [_companion checkedMessageIds]; std::set messageIds; for (NSNumber *nMid in checkedMessageIds) { messageIds.insert([nMid int32Value]); } NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] init]; int index = -1; for (TGMessageModernConversationItem *messageItem in _items) { index++; if (messageIds.find(messageItem->_message.mid) != messageIds.end()) { [indexSet addIndex:index]; } } //[self _deleteItemsAtIndices:indexSet animated:true animationFactor:1.0f]; //[self _leaveEditingModeAnimated:true]; [_companion _deleteMessages:checkedMessageIds animated:true]; __weak TGModernConversationController *weakSelf = self; [_companion controllerDeletedMessages:checkedMessageIds completion:^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf _leaveEditingModeAnimated:true]; } }]; } - (void)doneButtonPressed { [self _leaveEditingModeAnimated:true]; } - (void)unseenMessagesButtonPressed { if (_scrollBackMessageId != 0) { int32_t messageId = _scrollBackMessageId; _scrollBackMessageId = 0; _hasUnseenMessagesBelow = false; [self setScrollBackButtonVisible:false]; [_companion navigateToMessageId:messageId scrollBackMessageId:0 animated:true]; } else { if (_enableBelowHistoryRequests) [_companion _performFastScrollDown:false]; else { if (_collectionView.contentOffset.y > -_collectionView.contentInset.top) { [_collectionView setContentOffset:CGPointMake(0.0f, -_collectionView.contentInset.top) animated:true]; _scrollingToBottom = @true; } } } } #pragma mark - - (void)inputPanelWillChangeHeight:(TGModernConversationInputPanel *)inputPanel height:(CGFloat)height duration:(NSTimeInterval)duration animationCurve:(int)animationCurve { if (inputPanel == _currentInputPanel) { [_currentInputPanel adjustForSize:_view.bounds.size keyboardHeight:_keyboardHeight duration:duration animationCurve:animationCurve]; [self _adjustCollectionViewForSize:_view.bounds.size keyboardHeight:_keyboardHeight inputContainerHeight:height duration:duration animationCurve:0]; } } - (void)_adjustCollectionViewForSize:(CGSize)size keyboardHeight:(CGFloat)keyboardHeight inputContainerHeight:(CGFloat)inputContainerHeight duration:(NSTimeInterval)duration animationCurve:(int)animationCurve { [self _adjustCollectionViewForSize:size keyboardHeight:keyboardHeight inputContainerHeight:inputContainerHeight scrollToBottom:false duration:duration animationCurve:animationCurve]; } - (void)_adjustCollectionViewForSize:(CGSize)__unused size keyboardHeight:(CGFloat)keyboardHeight inputContainerHeight:(CGFloat)inputContainerHeight scrollToBottom:(bool)scrollToBottom duration:(NSTimeInterval)duration animationCurve:(int)animationCurve { bool stopScrolling = _collectionView.contentOffset.y >= -_collectionView.contentInset.top; CGFloat contentHeight = _collectionView.contentSize.height; UIEdgeInsets originalInset = _collectionView.contentInset; UIEdgeInsets inset = originalInset; inset.top = keyboardHeight + inputContainerHeight; if (_snapshotBackgroundView != nil) { CGRect snapshotBackgroundFrame = _snapshotBackgroundView.frame; snapshotBackgroundFrame.origin.y = -inset.top + 45.0f; _snapshotBackgroundView.frame = snapshotBackgroundFrame; } if (_snapshotImageView != nil) { CGRect snapshotImageFrame = _snapshotImageView.frame; snapshotImageFrame.origin.y = -inset.top + 45.0f; _snapshotImageView.frame = snapshotImageFrame; } CGPoint originalContentOffset = _collectionView.contentOffset; CGPoint contentOffset = originalContentOffset; if (scrollToBottom) contentOffset = CGPointMake(0.0f, -_collectionView.contentInset.top); else { contentOffset.y += originalInset.top - inset.top; contentOffset.y = MIN(contentOffset.y, contentHeight - _collectionView.bounds.size.height + inset.bottom); contentOffset.y = MAX(contentOffset.y, -inset.top); } if (stopScrolling) [_collectionView stopScrollingAnimation]; if (duration > DBL_EPSILON) { [UIView animateWithDuration:duration delay:0 options:(animationCurve << 16) | UIViewAnimationOptionBeginFromCurrentState animations:^ { //[_collectionView setDelayVisibleItemsUpdate:originalInset.top < inset.top && (iosMajorVersion() < 7 || iosMajorVersion() >= 8)]; bool decorationViewUpdatesWereDisabled = [_collectionView disableDecorationViewUpdates]; [_collectionView setDisableDecorationViewUpdates:decorationViewUpdatesWereDisabled || originalInset.top < inset.top]; _collectionView.contentInset = inset; if (!CGPointEqualToPoint(contentOffset, originalContentOffset)) { [_collectionView setBounds:CGRectMake(0, contentOffset.y, _collectionView.frame.size.width, _collectionView.frame.size.height)]; } [self _updateUnseenMessagesButton]; [_collectionView setDelayVisibleItemsUpdate:false]; [_collectionView setDisableDecorationViewUpdates:decorationViewUpdatesWereDisabled]; if (!decorationViewUpdatesWereDisabled) [_collectionView updateHeaderView]; [_emptyListPlaceholder adjustLayoutForSize:_view.bounds.size contentInsets:UIEdgeInsetsMake(_collectionView.contentInset.bottom - 210.0f - [_collectionView implicitTopInset], 0.0f, _collectionView.contentInset.top, 0.0f) duration:0.0 curve:0]; } completion:^(BOOL finished) { if (finished) { [_collectionView updateVisibleItemsNow]; } }]; } else { _collectionView.contentInset = inset; if (!CGPointEqualToPoint(contentOffset, originalContentOffset)) [_collectionView setBounds:CGRectMake(0, contentOffset.y, _collectionView.frame.size.width, _collectionView.frame.size.height)]; [self _updateUnseenMessagesButton]; [_emptyListPlaceholder adjustLayoutForSize:_view.bounds.size contentInsets:UIEdgeInsetsMake(_collectionView == nil ? self.controllerInset.top : (_collectionView.contentInset.bottom - 210.0f - [_collectionView implicitTopInset]), 0.0f, _collectionView == nil ? _currentInputPanel.frame.size.height : _collectionView.contentInset.top, 0.0f) duration:0.0 curve:0]; } } - (void)keyboardWillHide:(NSNotification *)__unused notification { } - (UIView *)keyboardView { UIView *keyboardWindow = [TGHacks applicationKeyboardWindow]; return keyboardWindow; } - (void)keyboardWillChangeFrame:(NSNotification *)notification { if (_collectionView == nil) return; CGSize collectionViewSize = _view.frame.size; NSTimeInterval duration = notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] == nil ? 0.3 : [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; int curve = [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] intValue]; UIView *keyboardTransitionView = nil; if (_inputTextPanel.changingKeyboardMode) { keyboardTransitionView = [self keyboardView]; duration = 0.2; curve = 7; } CGRect screenKeyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; CGRect keyboardFrame = [_view convertRect:screenKeyboardFrame fromView:nil]; CGFloat keyboardHeight = (keyboardFrame.size.height <= FLT_EPSILON || keyboardFrame.size.width <= FLT_EPSILON) ? 0.0f : (collectionViewSize.height - keyboardFrame.origin.y); _halfTransitionKeyboardHeight = keyboardFrame.size.height; if ((freedomUIKitTest3() && freedomUIKitTest3_1()) || [self viewControllerIsAnimatingAppearanceTransition] || [(TGNavigationController *)self.navigationController isInPopTransition] || [(TGNavigationController *)self.navigationController isInControllerTransition]) return; if (_inputTextPanel.changingKeyboardMode && keyboardHeight < FLT_EPSILON) return; keyboardHeight = MAX(keyboardHeight, 0.0f); if (keyboardFrame.origin.y + keyboardFrame.size.height < collectionViewSize.height - FLT_EPSILON) keyboardHeight = 0.0f; if ( ABS(_keyboardHeight - keyboardHeight) < FLT_EPSILON && ABS(collectionViewSize.width - _collectionView.frame.size.width) < FLT_EPSILON) return; if (_inputTextPanel.changingKeyboardMode) { UIView *snapshotView = [keyboardTransitionView snapshotViewAfterScreenUpdates:false]; [[TGHacks applicationKeyboardWindow] addSubview:snapshotView]; CGFloat deltaHeight = keyboardHeight - _keyboardHeight; snapshotView.frame = CGRectOffset(snapshotView.frame, 0.0f, -deltaHeight); UIView *keyboardWindow = [TGHacks applicationKeyboardWindow]; keyboardWindow.frame = CGRectOffset(keyboardWindow.frame, 0.0f, deltaHeight); [UIView animateWithDuration:0.2 delay:0.0 options:curve << 16 animations:^ { snapshotView.alpha = 0.0f; keyboardWindow.frame = CGRectOffset(keyboardWindow.frame, 0.0f, -deltaHeight); } completion:^(__unused BOOL finished) { [snapshotView removeFromSuperview]; }]; if ([_inputTextPanel.maybeInputField.internalTextView.inputView isKindOfClass:[TGCommandKeyboardView class]]) { //[(TGCommandKeyboardView *)_inputTextPanel.maybeInputField.internalTextView.inputView animateTransitionIn]; } } if (ABS(_keyboardHeight - keyboardHeight) > FLT_EPSILON) { _keyboardHeight = keyboardHeight; if (ABS(collectionViewSize.width - _collectionView.frame.size.width) > FLT_EPSILON) { if (iosMajorVersion() >= 9) { [self _performSizeChangesWithDuration:0.3 size:_view.bounds.size]; } else { dispatch_async(dispatch_get_main_queue(), ^ { [self _performSizeChangesWithDuration:0.3 size:_view.bounds.size]; }); } } else { [_currentInputPanel adjustForSize:_view.bounds.size keyboardHeight:keyboardHeight duration:duration animationCurve:curve]; [self _adjustCollectionViewForSize:_view.bounds.size keyboardHeight:keyboardHeight inputContainerHeight:_currentInputPanel.frame.size.height duration:duration animationCurve:curve]; } } } - (void)keyboardDidChangeFrame:(NSNotification *)notification { if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad && notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] == nil) { CGSize collectionViewSize = _view.frame.size; NSTimeInterval duration = 0.3; int curve = [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] intValue]; CGRect screenKeyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; CGRect keyboardFrame = [_view convertRect:screenKeyboardFrame fromView:nil]; CGFloat keyboardHeight = (keyboardFrame.size.height <= FLT_EPSILON || keyboardFrame.size.width <= FLT_EPSILON) ? 0.0f : (collectionViewSize.height - keyboardFrame.origin.y); if (keyboardFrame.origin.y + keyboardFrame.size.height < collectionViewSize.height - FLT_EPSILON) keyboardHeight = 0.0f; _keyboardHeight = keyboardHeight; [_currentInputPanel adjustForSize:_view.bounds.size keyboardHeight:keyboardHeight duration:duration animationCurve:curve]; [self _adjustCollectionViewForSize:_view.bounds.size keyboardHeight:keyboardHeight inputContainerHeight:_currentInputPanel.frame.size.height duration:duration animationCurve:curve]; } } - (void)_performSizeChangesWithDuration:(NSTimeInterval)duration size:(CGSize)size { bool animated = duration > DBL_EPSILON; CGSize collectionViewSize = size; CGFloat keyboardHeight = _keyboardHeight; if (_titlePanelWrappingView != nil) { CGRect titleWrapperFrame = CGRectMake(0.0f, self.controllerInset.top, collectionViewSize.width, _titlePanelWrappingView.frame.size.height); CGRect titlePanelFrame = CGRectMake(0.0f, 0.0f, titleWrapperFrame.size.width, _currentTitlePanel.frame.size.height); if (duration > DBL_EPSILON) { [UIView animateWithDuration:duration animations:^ { _titlePanelWrappingView.frame = titleWrapperFrame; _currentTitlePanel.frame = titlePanelFrame; }]; } else { _titlePanelWrappingView.frame = titleWrapperFrame; _currentTitlePanel.frame = titlePanelFrame; } } [_currentInputPanel changeToSize:size keyboardHeight:keyboardHeight duration:duration]; CGFloat maxOriginY = _collectionView.contentOffset.y + _collectionView.contentInset.top; CGPoint previousContentOffset = _collectionView.contentOffset; CGRect previousCollectionFrame = _collectionView.frame; int anchorItemIndex = -1; CGFloat anchorItemOriginY = 0.0f; CGFloat anchorItemRelativeOffset = 0.0f; CGFloat anchorItemHeight = 0.0f; NSMutableArray *previousItemFrames = [[NSMutableArray alloc] init]; CGRect previousVisibleBounds = CGRectMake(previousContentOffset.x, previousContentOffset.y, _collectionView.frame.size.width, _collectionView.frame.size.height); NSMutableSet *previousVisibleItemIndices = [[NSMutableSet alloc] init]; std::vector previousDecorationAttributes; NSArray *previousLayoutAttributes = [_collectionLayout layoutAttributesForItems:_items containerWidth:previousCollectionFrame.size.width maxHeight:FLT_MAX decorationViewAttributes:&previousDecorationAttributes contentHeight:NULL]; int collectionItemCount = (int)_items.count; for (int i = 0; i < collectionItemCount; i++) { UICollectionViewLayoutAttributes *attributes = previousLayoutAttributes[i]; CGRect itemFrame = attributes.frame; if (itemFrame.origin.y < maxOriginY) { anchorItemHeight = itemFrame.size.height; anchorItemIndex = i; anchorItemOriginY = itemFrame.origin.y; } if (!CGRectIsEmpty(CGRectIntersection(itemFrame, previousVisibleBounds))) [previousVisibleItemIndices addObject:@(i)]; [previousItemFrames addObject:[NSValue valueWithCGRect:itemFrame]]; } if (anchorItemIndex != -1) { if (anchorItemHeight > 1.0f) anchorItemRelativeOffset = (anchorItemOriginY - (_collectionView.contentOffset.y + _collectionView.contentInset.top)) / anchorItemHeight; } _collectionView.frame = CGRectMake(0, -210.0f, collectionViewSize.width, collectionViewSize.height + 210.0f); [_companion _setControllerWidthForItemCalculation:_collectionView.frame.size.width]; [_collectionLayout invalidateLayout]; UIEdgeInsets originalInset = _collectionView.contentInset; UIEdgeInsets inset = originalInset; inset.top = keyboardHeight + _currentInputPanel.frame.size.height; inset.bottom = self.controllerInset.top + 210.0f + [_collectionView implicitTopInset]; _collectionView.contentInset = inset; [self _updateUnseenMessagesButton]; [_emptyListPlaceholder adjustLayoutForSize:size contentInsets:UIEdgeInsetsMake(_collectionView.contentInset.bottom - 210.0f - [_collectionView implicitTopInset], 0.0f, _collectionView.contentInset.top, 0.0f) duration:duration curve:0]; CGFloat newContentHeight = 0.0f; std::vector newDecorationAttributes; NSArray *newLayoutAttributes = [_collectionLayout layoutAttributesForItems:_items containerWidth:_collectionView.frame.size.width maxHeight:FLT_MAX decorationViewAttributes:&newDecorationAttributes contentHeight:&newContentHeight]; CGPoint newContentOffset = _collectionView.contentOffset; newContentOffset.y = - _collectionView.contentInset.top; if (anchorItemIndex >= 0 && anchorItemIndex < (int)newLayoutAttributes.count) { UICollectionViewLayoutAttributes *attributes = newLayoutAttributes[anchorItemIndex]; newContentOffset.y += attributes.frame.origin.y - CGFloor(anchorItemRelativeOffset * attributes.frame.size.height); } if (newContentOffset.y > newContentHeight + _collectionView.contentInset.bottom - _collectionView.frame.size.height) newContentOffset.y = newContentHeight + _collectionView.contentInset.bottom - _collectionView.frame.size.height; if (newContentOffset.y < -_collectionView.contentInset.top) newContentOffset.y = -_collectionView.contentInset.top; NSMutableArray *transitiveItemIndicesWithFrames = [[NSMutableArray alloc] init]; CGRect newVisibleBounds = CGRectMake(newContentOffset.x, newContentOffset.y, _collectionView.frame.size.width, _collectionView.frame.size.height); for (int i = 0; i < collectionItemCount; i++) { UICollectionViewLayoutAttributes *attributes = newLayoutAttributes[i]; CGRect itemFrame = attributes.frame; if (CGRectIsEmpty(CGRectIntersection(itemFrame, newVisibleBounds)) && [previousVisibleItemIndices containsObject:@(i)]) [transitiveItemIndicesWithFrames addObject:@[@(i), [NSValue valueWithCGRect:itemFrame]]]; } NSMutableDictionary *transitiveCells = [[NSMutableDictionary alloc] init]; if (animated && !_collectionView.decelerating && !_collectionView.dragging && !_collectionView.tracking) { for (NSArray *nDesc in transitiveItemIndicesWithFrames) { NSNumber *nIndex = nDesc[0]; TGModernCollectionCell *currentCell = (TGModernCollectionCell *)[_collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:[nIndex intValue] inSection:0]]; if (currentCell != nil) { TGModernCollectionCell *transitiveCell = [[TGModernCollectionCell alloc] initWithFrame:[nDesc[1] CGRectValue]]; [(TGModernConversationItem *)_items[[nIndex intValue]] moveToCell:transitiveCell]; transitiveCells[nIndex] = transitiveCell; } } } _collectionView.contentOffset = newContentOffset; [_collectionView updateVisibleItemsNow]; [_collectionView layoutSubviews]; if (animated) { _collectionView.clipsToBounds = false; CGFloat contentOffsetDifference = newContentOffset.y - previousContentOffset.y + (_collectionView.frame.size.height - previousCollectionFrame.size.height); CGFloat widthDifference = _collectionView.frame.size.width - previousCollectionFrame.size.width; NSMutableArray *itemFramesToRestore = [[NSMutableArray alloc] init]; bool contentUpdatesWereDisabled = _companion.viewContext.contentUpdatesDisabled; _companion.viewContext.contentUpdatesDisabled = true; for (int i = 0; i < collectionItemCount; i++) { TGModernCollectionCell *cell = (TGModernCollectionCell *)[_collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]]; TGModernCollectionCell *transitiveCell = transitiveCells[@(i)]; if (transitiveCell != nil) { if (cell == nil) { cell = transitiveCell; [_collectionView addSubview:transitiveCell]; } else { if ([_items[i] boundCell] == transitiveCell) [_items[i] moveToCell:cell]; [transitiveCells removeObjectForKey:@(i)]; } } if (cell != nil) { [itemFramesToRestore addObject:@[@(i), [NSValue valueWithCGRect:cell.frame]]]; CGRect previousFrame = [previousItemFrames[i] CGRectValue]; cell.frame = CGRectOffset(previousFrame, widthDifference, contentOffsetDifference); TGModernConversationItem *item = _items[i]; [item sizeForContainerSize:CGSizeMake(previousFrame.size.width, 0.0f)]; } } for (auto it = previousDecorationAttributes.begin(); it != previousDecorationAttributes.end(); it++) { UIView *decorationView = [_collectionView viewForDecorationAtIndex:it->index]; decorationView.frame = CGRectOffset(it->frame, widthDifference, contentOffsetDifference); [decorationView layoutSubviews]; } [UIView animateWithDuration:duration animations:^ { for (NSArray *frameDesc in itemFramesToRestore) { UIView *cell = [_collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForRow:[frameDesc[0] intValue] inSection:0]]; if (cell == nil) cell = transitiveCells[frameDesc[0]]; cell.frame = [frameDesc[1] CGRectValue]; TGModernConversationItem *item = _items[[frameDesc[0] intValue]]; [item sizeForContainerSize:CGSizeMake(_collectionView.frame.size.width, 0.0f)]; } for (auto it = newDecorationAttributes.begin(); it != newDecorationAttributes.end(); it++) { UIView *decorationView = [_collectionView viewForDecorationAtIndex:it->index]; decorationView.frame = it->frame; [decorationView layoutSubviews]; } } completion:^(BOOL finished) { if (finished) { _collectionView.clipsToBounds = true; } [transitiveCells enumerateKeysAndObjectsUsingBlock:^(NSNumber *nIndex, TGModernCollectionCell *cell, __unused BOOL *stop) { [(TGModernConversationItem *)_items[[nIndex intValue]] unbindCell:_viewStorage]; [cell removeFromSuperview]; }]; }]; _companion.viewContext.contentUpdatesDisabled = contentUpdatesWereDisabled; [_collectionView updateRelativeBounds]; } } #pragma mark - - (void)actionStageActionRequested:(NSString *)action options:(id)options { if ([action isEqualToString:@"clearAllMessages"]) { [self _commitClearAllMessages]; } else if ([action isEqualToString:@"mapViewFinished"]) { [self dismissViewControllerAnimated:true completion:nil]; if (options[@"latitude"] != nil) { [_companion controllerWantsToSendMapWithLatitude:[options[@"latitude"] doubleValue] longitude:[options[@"longitude"] doubleValue] venue:nil asReplyToMessageId:[self currentReplyMessageId]]; } } else if ([action isEqualToString:@"menuAction"]) { int32_t mid = [options[@"userInfo"][@"mid"] int32Value]; if (mid != 0) { TGMessageModernConversationItem *menuMessageItem = nil; int index = -1; for (TGMessageModernConversationItem *messageItem in _items) { index++; if (messageItem->_message.mid == mid) { menuMessageItem = messageItem; break; } } NSString *menuAction = options[@"action"]; if ([menuAction isEqualToString:@"copy"]) { if (menuMessageItem != nil) { NSString *text = nil; if (menuMessageItem->_message.text.length != 0) { text = menuMessageItem->_message.text; } else { for (TGMediaAttachment *attachment in menuMessageItem->_message.mediaAttachments) { if ([attachment isKindOfClass:[TGImageMediaAttachment class]]) { TGImageMediaAttachment *imageAttachment = (TGImageMediaAttachment *)attachment; if (imageAttachment.caption.length != 0) text = imageAttachment.caption; } } } if (text.length > 0) { UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; [pasteboard setString:text]; } } } else if ([menuAction isEqualToString:@"delete"]) { if (menuMessageItem != nil && index >= 0) { [_companion controllerDeletedMessages:@[@(mid)] completion:nil]; } } else if ([menuAction isEqualToString:@"reply"]) { if (menuMessageItem != nil) { _inputTextPanel.inputField.internalTextView.enableFirstResponder = true; [self setReplyMessage:menuMessageItem->_message animated:true]; if (_currentInputPanel == _inputTextPanel && menuMessageItem->_message.replyMarkup.rows.count == 0) [self openKeyboard]; } } else if ([menuAction isEqualToString:@"forward"]) [_companion controllerWantsToForwardMessages:@[@(mid)]]; else if ([menuAction isEqualToString:@"stickerPackInfo"]) { id packReference = nil; for (id attachment in menuMessageItem->_message.mediaAttachments) { if ([attachment isKindOfClass:[TGDocumentMediaAttachment class]]) { for (id attribute in ((TGDocumentMediaAttachment *)attachment).attributes) { if ([attribute isKindOfClass:[TGDocumentAttributeSticker class]]) { packReference = ((TGDocumentAttributeSticker *)attribute).packReference; } } break; } } if (packReference != nil) { TGProgressWindow *progressWindow = [[TGProgressWindow alloc] init]; [progressWindow show:true]; SSignal *stickerPackInfo = [TGStickersSignals stickerPackInfo:packReference]; SSignal *currentStickerPacks = [[TGStickersSignals stickerPacks] take:1]; SSignal *combinedSignal = [SSignal combineSignals:@[stickerPackInfo, currentStickerPacks]]; [[[combinedSignal deliverOn:[SQueue mainQueue]] onDispose:^ { TGDispatchOnMainThread(^ { [progressWindow dismiss:true]; }); }] startWithNext:^(NSArray *combined) { [TGAppDelegateInstance previewStickerPack:combined[0] currentStickerPacks:combined[1][@"packs"]]; } error:^(__unused id error) { } completed:nil]; } } else if ([menuAction isEqualToString:@"select"]) [self _enterEditingMode:mid]; else if ([menuAction isEqualToString:@"share"]) { if (menuMessageItem != nil) { NSURL *fileUrl = nil; for (id attachment in menuMessageItem->_message.mediaAttachments) { if ([attachment isKindOfClass:[TGDocumentMediaAttachment class]]) { NSString *localFilePath = [[_companion fileUrlForDocumentMedia:attachment] path]; if ([[NSFileManager defaultManager] fileExistsAtPath:localFilePath isDirectory:NULL]) fileUrl = [_companion fileUrlForDocumentMedia:attachment]; break; } } if (fileUrl != nil) { CGRect messageRect = CGRectZero; for (TGModernCollectionCell *cell in _collectionView.visibleCells) { TGMessageModernConversationItem *messageItem = cell.boundItem; if (messageItem != nil && messageItem->_message.mid == mid) { CGRect contentFrame = [[cell contentViewForBinding] convertRect:[messageItem effectiveContentFrame] toView:_view]; if (CGRectIsNull(contentFrame) || CGRectIsEmpty(contentFrame)) break; contentFrame = CGRectIntersection(contentFrame, CGRectMake(0, 0, _view.frame.size.width, _currentInputPanel.frame.origin.y)); if (CGRectIsNull(contentFrame) || CGRectIsEmpty(contentFrame)) break; messageRect = contentFrame; } } _interactionController = [UIDocumentInteractionController interactionControllerWithURL:fileUrl]; _interactionController.delegate = self; _interactionController.UTI = [self utiForFileExtension:[[fileUrl pathExtension] lowercaseString]]; [_interactionController presentOptionsMenuFromRect:messageRect inView:_view animated:true]; } } } } } else if ([action isEqualToString:@"menuWillHide"]) { for (TGModernCollectionCell *cell in _collectionView.visibleCells) { TGMessageModernConversationItem *messageItem = cell.boundItem; [messageItem setTemporaryHighlighted:false viewStorage:_viewStorage]; } } } - (NSString *)utiForFileExtension:(NSString *)extension { if ([extension isEqualToString:@"jpg"] || [extension isEqualToString:@"jpeg"]) return @"public.jpeg"; else if ([extension isEqualToString:@"png"]) return @"public.png"; return @"public.data"; } - (void)userActivityWasContinued:(NSUserActivity *)userActivity { TGDispatchOnMainThread(^ { if (userActivity == _currentActivity) { } }); } - (void)userActivity:(NSUserActivity *)userActivity didReceiveInputStream:(NSInputStream *)__unused inputStream outputStream:(NSOutputStream *)__unused outputStream { TGDispatchOnMainThread(^ { if (userActivity == _currentActivity) { [self setInputText:@"" replace:true]; } }); } - (void)openEmbed:(TGWebPageMediaAttachment *)webPage { if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) { return; } [self.view endEditing:true]; __weak TGModernConversationController *weakSelf = self; _attachmentSheetWindow = [[TGAttachmentSheetWindow alloc] init]; _attachmentSheetWindow.dismissalBlock = ^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf == nil) return; strongSelf->_attachmentSheetWindow.rootViewController = nil; strongSelf->_attachmentSheetWindow = nil; }; NSMutableArray *items = [[NSMutableArray alloc] init]; TGAttachmentSheetEmbedItemView *embedView = [[TGAttachmentSheetEmbedItemView alloc] initWithWebPage:webPage]; [items addObject:embedView]; [items addObject:[[TGAttachmentSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Web.OpenExternal") pressed:^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf->_attachmentSheetWindow dismissAnimated:true completion:nil]; [[UIApplication sharedApplication] openURL:[NSURL URLWithString:webPage.url]]; } }]]; [items addObject:[[TGAttachmentSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Web.CopyLink") pressed:^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf->_attachmentSheetWindow dismissAnimated:true completion:nil]; [[UIPasteboard generalPasteboard] setString:webPage.url]; } }]]; TGAttachmentSheetButtonItemView *cancelItem =[[TGAttachmentSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") pressed:^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) [strongSelf->_attachmentSheetWindow dismissAnimated:true completion:nil]; }]; [cancelItem setBold:true]; [items addObject:cancelItem]; _attachmentSheetWindow.view.items = items; _attachmentSheetWindow.windowLevel = UIWindowLevelNormal; [_attachmentSheetWindow showAnimated:true completion:nil]; /*TGEmbedPreviewController *embedController = [[TGEmbedPreviewController alloc] initWithWebPage:webPage]; TGOverlayControllerWindow *window = [[TGOverlayControllerWindow alloc] initWithParentController:self contentController:embedController]; window.windowLevel = UIWindowLevelNormal; window.hidden = false; [self.view endEditing:true];*/ } - (void)hideKeyboard { [_inputTextPanel.maybeInputField resignFirstResponder]; } - (void)activateSearch { if (_searchBar == nil) { _searchBar = [[TGSearchBar alloc] initWithFrame:CGRectMake(0.0f, 20.0f, _view.frame.size.width, [TGSearchBar searchBarBaseHeight]) style:TGSearchBarStyleLight]; _searchBar.autoresizingMask = UIViewAutoresizingFlexibleWidth; _searchBar.delegate = self; [_searchBar setShowsCancelButton:true animated:false]; [_searchBar setAlwaysExtended:true]; _searchBar.placeholder = TGLocalized(@"Conversation.SearchPlaceholder"); [_searchBar sizeToFit]; _searchBar.delayActivity = false; [_view insertSubview:_searchBar aboveSubview:_collectionView]; __weak TGModernConversationController *weakSelf = self; _searchPanel = [[TGModernConversationSearchInputPanel alloc] init]; _searchPanel.next = ^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { if (strongSelf->_searchResultsOffset + 1 < strongSelf->_searchResultsIds.count) [strongSelf setSearchResultsOffset:strongSelf->_searchResultsOffset + 1]; } }; _searchPanel.previous = ^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { if (strongSelf->_searchResultsIds.count != 0 && strongSelf->_searchResultsOffset > 0) [strongSelf setSearchResultsOffset:strongSelf->_searchResultsOffset - 1]; } }; _searchPanel.done = ^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) [strongSelf searchBarCancelButtonClicked:(UISearchBar *)strongSelf->_searchBar]; }; _searchPanel.delegate = self; } _searchBar.hidden = false; [_searchPanel setInProgress:false]; [_searchPanel setOffset:0 count:0]; [self setCurrentTitlePanel:nil animation:TGModernConversationPanelAnimationSlideFar]; [self setNavigationBarHidden:true withAnimation:TGViewControllerNavigationBarAnimationSlide]; [self setCustomInputPanel:_searchPanel]; [_searchBar becomeFirstResponder]; } - (void)searchBar:(TGSearchBar *)__unused searchBar willChangeHeight:(CGFloat)__unused newHeight { } - (void)searchBarCancelButtonClicked:(UISearchBar *)__unused searchBar { _companion.viewContext.searchText = nil; for (TGMessageModernConversationItem *item in _items) { [item updateSearchText:true]; } [_searchBar resignFirstResponder]; [self setNavigationBarHidden:false withAnimation:TGViewControllerNavigationBarAnimationSlide]; TGDispatchAfter(0.3, dispatch_get_main_queue(), ^ { _searchBar.hidden = true; [_searchBar setText:@""]; }); [_searchPanel setInProgress:false]; [self setCustomInputPanel:nil]; } - (void)beginSearchWithQuery:(NSString *)query { if (_searchDisposable == nil) _searchDisposable = [[SMetaDisposable alloc] init]; _companion.viewContext.searchText = query.length == 0 ? nil : query; for (TGMessageModernConversationItem *item in _items) { [item updateSearchText:false]; } _query = query; if (query.length == 0) { [_searchDisposable setDisposable:nil]; [self setSearchResultsIds:nil]; [_searchPanel setInProgress:false]; } else { __weak TGModernConversationController *weakSelf = self; _searchBar.showActivity = true; [_searchPanel setInProgress:true]; [_searchDisposable setDisposable:[[[[TGGlobalMessageSearchSignals searchMessages:query peerId:((TGGenericModernConversationCompanion *)_companion).conversationId accessHash:[_companion requestAccessHash] itemMapping:^id(id item) { if ([item isKindOfClass:[TGConversation class]]) { TGConversation *conversation = item; return conversation; } return nil; }] deliverOn:[SQueue mainQueue]] onDispose:^ { TGDispatchOnMainThread(^ { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { strongSelf->_searchBar.showActivity = false; [strongSelf->_searchPanel setInProgress:false]; } }); }] startWithNext:^(id next) { __strong TGModernConversationController *strongSelf = weakSelf; if (strongSelf != nil) { NSMutableArray *searchResultsIds = [[NSMutableArray alloc] init]; for (TGConversation *conversation in next) { if (conversation.additionalProperties[@"searchMessageId"] != nil) { [searchResultsIds addObject:conversation.additionalProperties[@"searchMessageId"]]; } } [strongSelf setSearchResultsIds:searchResultsIds]; } } error:^(__unused id error) { } completed:^ { }]]; } _searchPanel.isSearching = query.length != 0; } - (void)searchBar:(UISearchBar *)__unused searchBar textDidChange:(NSString *)searchText { [self beginSearchWithQuery:searchText]; } - (void)setSearchResultsIds:(NSArray *)searchResultIds { bool previousHadResults = _searchResultsIds.count != 0; NSNumber *previousId = nil; if (_searchResultsIds.count != 0) previousId = _searchResultsIds[_searchResultsOffset]; _searchResultsIds = searchResultIds; NSMutableSet *idsSet = [[NSMutableSet alloc] initWithArray:searchResultIds]; NSUInteger offset = 0; if (_searchResultsIds.count != 0) { if (!previousHadResults) offset = 0; else if (previousId != nil && [idsSet containsObject:previousId]) offset = [_searchResultsIds indexOfObject:previousId]; else { NSArray *visibleCells = [_collectionView.visibleCells sortedArrayUsingComparator:^NSComparisonResult(UIView *view1, UIView *view2) { return view1.frame.origin.y > view2.frame.origin.y ? NSOrderedAscending : NSOrderedDescending; }]; for (NSInteger topIndex = visibleCells.count / 2, bottomIndex = visibleCells.count / 2 + 1; topIndex >= 0 || bottomIndex < (NSInteger)visibleCells.count; topIndex--, bottomIndex++) { if (topIndex >= 0) { TGMessageModernConversationItem *item = ((TGMessageModernConversationItem *)((TGModernCollectionCell *)visibleCells[topIndex]).boundItem); if (item != nil) { NSNumber *nMid = @(item->_message.mid); if ([idsSet containsObject:nMid]) { offset = [searchResultIds indexOfObject:nMid]; break; } } } if (bottomIndex < (NSInteger)visibleCells.count) { TGMessageModernConversationItem *item = ((TGMessageModernConversationItem *)((TGModernCollectionCell *)visibleCells[bottomIndex]).boundItem); if (item != nil) { NSNumber *nMid = @(item->_message.mid); if ([idsSet containsObject:nMid]) { offset = [searchResultIds indexOfObject:nMid]; break; } } } } } } [self setSearchResultsOffset:offset]; } - (void)setSearchResultsOffset:(NSUInteger)searchResultsOffset { _searchResultsOffset = searchResultsOffset; if (_searchResultsIds.count != 0 && _searchResultsOffset < _searchResultsIds.count) { [_companion navigateToMessageId:[_searchResultsIds[_searchResultsOffset] intValue] scrollBackMessageId:0 animated:false]; } [_searchPanel setOffset:_searchResultsOffset count:_searchResultsIds.count]; } - (void)setInputDisabled:(bool)inputDisabled { _inputDisabled = inputDisabled; _inputTextPanel.inputDisabled = inputDisabled; } - (void)setIsChannel:(bool)isChannel { _isChannel = isChannel; _inputTextPanel.isChannel = isChannel; } - (void)updateControllerShouldHideInputTextByDefault { if (!_editingMode) { [self setInputPanel:_customInputPanel != nil ? _customInputPanel : [self defaultInputPanel] animated:ABS(CFAbsoluteTimeGetCurrent() - _willAppearTimestamp) > 0.18]; } } - (BOOL)isEditing { return _editingMode; } - (void)check3DTouch { if (iosMajorVersion() >= 9) { if (self.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable) { [self registerForPreviewingWithDelegate:(id)self sourceView:self.view]; } } } - (UIViewController *)previewingContext:(id)__unused previewingContext viewControllerForLocation:(CGPoint)location { if (self.presentedViewController != nil) { return nil; } CGPoint collectionPoint = [self.view convertPoint:location toView:_collectionView]; for (TGModernCollectionCell *cell in _collectionView.visibleCells) { if (CGRectContainsPoint(cell.frame, collectionPoint)) { TGMessageModernConversationItem *item = cell.boundItem; if (item != nil) { NSString *link = [(TGMessageViewModel *)item.viewModel linkAtPoint:[_collectionView convertPoint:collectionPoint toView:[cell contentViewForBinding]]]; if ([link hasPrefix:@"http://"] || [link hasPrefix:@"https://"]) { SFSafariViewController *controller = [[SFSafariViewController alloc] initWithURL:[NSURL URLWithString:link]]; return controller; } } break; } } return nil; } - (void)previewingContext:(id)__unused previewingContext commitViewController:(UIViewController *)viewControllerToCommit { [self presentViewController:viewControllerToCommit animated:true completion:nil]; } @end