From 8f1e024aef06adb3e600f086150994a29f966e01 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 15 Apr 2026 23:12:53 +0200 Subject: [PATCH] Various fixes --- .../Sources/AccountContext.swift | 2 +- submodules/AttachmentTextInputPanelNode/BUILD | 1 + .../AttachmentTextInputPanelNode.swift | 3 + .../Sources/Node/ChatListItem.swift | 67 +- submodules/Display/Source/DeviceMetrics.swift | 2 +- .../TGPhotoCaptionInputMixin.h | 1 - .../TGPhotoPaintStickersContext.h | 5 + .../TGMediaPickerGalleryInterfaceView.m | 11 +- .../Sources/TGPhotoCaptionInputMixin.m | 168 ++-- submodules/SparseItemGrid/BUILD | 1 + .../Sources/SparseDiscreteScrollingArea.swift | 2 + .../Sources/SparseItemGridScrollingArea.swift | 91 +- .../StatisticsUI/Sources/StatsGraphItem.swift | 67 +- .../Sources/ChatTextInputPanelNode.swift | 8 +- .../MessageInlineBlockBackgroundView.swift | 2 + .../ChatEntityKeyboardInputNode/BUILD | 1 - .../Sources/ChatEntityKeyboardInputNode.swift | 575 ++++++------ .../Sources/PollAttachmentScreen.swift | 15 +- .../Sources/GlassBackgroundComponent.swift | 10 +- .../Components/LegacyMessageInputPanel/BUILD | 3 +- .../Sources/LegacyMessageInputPanel.swift | 629 ++++++++++---- ...ionBroadcastNavigationAccessoryPanel.swift | 4 +- .../Sources/MinimizedContainer.swift | 182 ++-- .../Sources/PeerSelectionControllerNode.swift | 401 +++++---- .../Sources/ThemeColorsGridController.swift | 92 +- .../Sources/DataUsageScreen.swift | 2 +- .../Sources/StoryContainerScreen.swift | 62 +- ...StoryItemSetContainerViewSendMessage.swift | 818 +++++++++--------- .../Chat/ChatControllerMediaRecording.swift | 12 +- .../Sources/Chat/ChatControllerPaste.swift | 8 +- .../TelegramUI/Sources/ChatController.swift | 2 +- .../ChatControllerOpenAttachmentMenu.swift | 17 +- .../ChatInterfaceStateContextMenus.swift | 71 +- .../ChatVerifiedPeerTitlePanelNode.swift | 7 +- .../Sources/HorizontalStickerGridItem.swift | 2 +- .../Sources/SharedAccountContext.swift | 7 +- 36 files changed, 1978 insertions(+), 1373 deletions(-) diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 356a456045..369c4d4c03 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1408,7 +1408,7 @@ public protocol SharedAccountContext: AnyObject { func makeSetupTwoFactorAuthController(context: AccountContext) -> ViewController func makeStorageManagementController(context: AccountContext) -> ViewController func makeAttachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, audio: Bool, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, presentDocumentScanner: (() -> Void)?, send: @escaping ([AnyMediaReference], Bool, Int32?, NSAttributedString?) -> Void) -> AttachmentFileController - func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, isScheduledMessages: Bool, isFile: Bool, hasTimer: Bool, customEmojiAvailable: Bool, pushViewController: @escaping (ViewController) -> Void, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject? + func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, isScheduledMessages: Bool, isFile: Bool, hasTimer: Bool, customEmojiAvailable: Bool, pushViewController: @escaping (ViewController) -> Void, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void, getNavigationController: @escaping () -> NavigationController?) -> NSObject? func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, stories: Bool, forceDark: Bool) -> ViewController func makeStorySearchController(context: AccountContext, scope: StorySearchControllerScope, listContext: SearchStoryListContext?) -> ViewController func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController diff --git a/submodules/AttachmentTextInputPanelNode/BUILD b/submodules/AttachmentTextInputPanelNode/BUILD index 25e0d545af..9ed480e640 100644 --- a/submodules/AttachmentTextInputPanelNode/BUILD +++ b/submodules/AttachmentTextInputPanelNode/BUILD @@ -28,6 +28,7 @@ swift_library( "//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState", "//submodules/Pasteboard:Pasteboard", "//submodules/ContextUI:ContextUI", + "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode:ChatEntityKeyboardInputNode", "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", "//submodules/ComponentFlow:ComponentFlow", "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent", diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift index fbc3cb7cce..84cc256aa3 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift @@ -532,6 +532,9 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS public var heightUpdated: ((Bool) -> Void)? public var timerUpdated: ((NSNumber?) -> Void)? public var captionIsAboveUpdated: ((Bool) -> Void)? + public var additionalInputHeight: CGFloat { + return 0.0 + } public func updateLayoutSize(_ size: CGSize, keyboardHeight: CGFloat, sideInset: CGFloat, animated: Bool) -> CGFloat { guard let presentationInterfaceState = self.presentationInterfaceState else { diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index af1a1be48c..e7e05ee43c 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -3248,45 +3248,42 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } if unreadCount.unread { - if !isPeerGroup, let message = messages.last, message.tags.contains(.unseenPersonalMessage), unreadCount.count == 1 { + let badgeTextColor: UIColor + if unreadCount.muted { + if unreadCount.isProvisonal, case .forum = item.chatListLocation { + badgeTextColor = theme.unreadBadgeInactiveBackgroundColor + currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactiveProvisional(item.presentationData.theme, diameter: badgeDiameter) + currentAvatarBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactiveProvisional(item.presentationData.theme, diameter: avatarBadgeDiameter) + } else { + badgeTextColor = theme.unreadBadgeInactiveTextColor + currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.presentationData.theme, diameter: badgeDiameter) + currentAvatarBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.presentationData.theme, diameter: avatarBadgeDiameter) + } } else { - let badgeTextColor: UIColor - if unreadCount.muted { - if unreadCount.isProvisonal, case .forum = item.chatListLocation { - badgeTextColor = theme.unreadBadgeInactiveBackgroundColor - currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactiveProvisional(item.presentationData.theme, diameter: badgeDiameter) - currentAvatarBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactiveProvisional(item.presentationData.theme, diameter: avatarBadgeDiameter) - } else { - badgeTextColor = theme.unreadBadgeInactiveTextColor - currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.presentationData.theme, diameter: badgeDiameter) - currentAvatarBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.presentationData.theme, diameter: avatarBadgeDiameter) - } + if unreadCount.isProvisonal, case .forum = item.chatListLocation { + badgeTextColor = theme.unreadBadgeActiveBackgroundColor + currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActiveProvisional(item.presentationData.theme, diameter: badgeDiameter) + currentAvatarBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActiveProvisional(item.presentationData.theme, diameter: avatarBadgeDiameter) } else { - if unreadCount.isProvisonal, case .forum = item.chatListLocation { - badgeTextColor = theme.unreadBadgeActiveBackgroundColor - currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActiveProvisional(item.presentationData.theme, diameter: badgeDiameter) - currentAvatarBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActiveProvisional(item.presentationData.theme, diameter: avatarBadgeDiameter) - } else { - badgeTextColor = theme.unreadBadgeActiveTextColor - currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.presentationData.theme, diameter: badgeDiameter) - currentAvatarBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.presentationData.theme, diameter: avatarBadgeDiameter) - } - } - let unreadCountText = compactNumericCountString(Int(unreadCount.count), decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) - if unreadCount.count > 0 { - badgeContent = .text(NSAttributedString(string: unreadCountText, font: badgeFont, textColor: badgeTextColor)) - } else if isPeerGroup { - badgeContent = .none - } else { - badgeContent = .blank - } - - if let mutedCount = unreadCount.mutedCount, mutedCount > 0 { - let mutedUnreadCountText = compactNumericCountString(Int(mutedCount), decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) - currentMentionBadgeImage = PresentationResourcesChatList.badgeBackgroundInactive(item.presentationData.theme, diameter: badgeDiameter) - mentionBadgeContent = .text(NSAttributedString(string: mutedUnreadCountText, font: badgeFont, textColor: theme.unreadBadgeInactiveTextColor)) + badgeTextColor = theme.unreadBadgeActiveTextColor + currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.presentationData.theme, diameter: badgeDiameter) + currentAvatarBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.presentationData.theme, diameter: avatarBadgeDiameter) } } + let unreadCountText = compactNumericCountString(Int(unreadCount.count), decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) + if unreadCount.count > 0 { + badgeContent = .text(NSAttributedString(string: unreadCountText, font: badgeFont, textColor: badgeTextColor)) + } else if isPeerGroup { + badgeContent = .none + } else { + badgeContent = .blank + } + + if let mutedCount = unreadCount.mutedCount, mutedCount > 0 { + let mutedUnreadCountText = compactNumericCountString(Int(mutedCount), decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) + currentMentionBadgeImage = PresentationResourcesChatList.badgeBackgroundInactive(item.presentationData.theme, diameter: badgeDiameter) + mentionBadgeContent = .text(NSAttributedString(string: mutedUnreadCountText, font: badgeFont, textColor: theme.unreadBadgeInactiveTextColor)) + } } if !isPeerGroup { diff --git a/submodules/Display/Source/DeviceMetrics.swift b/submodules/Display/Source/DeviceMetrics.swift index c22f8975d5..9380ecba8a 100644 --- a/submodules/Display/Source/DeviceMetrics.swift +++ b/submodules/Display/Source/DeviceMetrics.swift @@ -307,7 +307,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { var keyboardHeight = _keyboardHeight(inLandscape: inLandscape) if #available(iOS 26.0, *) { if !inLandscape { - keyboardHeight += 9.0 + keyboardHeight -= 1.0 } } return keyboardHeight diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoCaptionInputMixin.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoCaptionInputMixin.h index 44d2e574b2..bd58b0f294 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoCaptionInputMixin.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoCaptionInputMixin.h @@ -8,7 +8,6 @@ @interface TGPhotoCaptionInputMixin : NSObject @property (nonatomic, strong) id stickersContext; -@property (nonatomic, readonly) UIView *backgroundView; @property (nonatomic, readonly) id inputPanel; @property (nonatomic, readonly) UIView *inputPanelView; @property (nonatomic, readonly) UIView *dismissView; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h index 05095af3af..4afdca957e 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h @@ -37,6 +37,7 @@ @protocol TGCaptionPanelView @property (nonatomic, readonly) UIView * _Nonnull view; +@property (nonatomic, readonly) CGFloat additionalInputHeight; - (void)setTimeout:(int32_t)timeout isVideo:(bool)isVideo isCaptionAbove:(bool)isCaptionAbove; @@ -55,6 +56,10 @@ @property (nonatomic, copy) void(^ _Nullable timerUpdated)(NSNumber * _Nullable value); @property (nonatomic, copy) void(^ _Nullable captionIsAboveUpdated)(BOOL value); +@optional +- (CGFloat)updateContainerLayoutSize:(CGSize)size safeAreaInset:(UIEdgeInsets)safeAreaInset bottomInset:(CGFloat)bottomInset keyboardHeight:(CGFloat)keyboardHeight animated:(bool)animated; + +@required - (CGFloat)updateLayoutSize:(CGSize)size keyboardHeight:(CGFloat)keyboardHeight sideInset:(CGFloat)sideInset animated:(bool)animated; - (CGFloat)baseHeight; diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m index f8863cee7c..28564775a5 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m @@ -439,7 +439,6 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * }; _captionMixin.stickersContext = stickersContext; - [_captionMixin createInputPanelIfNeeded]; _headerWrapperView = [[TGMediaPickerGalleryWrapperView alloc] init]; [_wrapperView addSubview:_headerWrapperView]; @@ -486,6 +485,8 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * if ([UIDevice currentDevice].userInterfaceIdiom != UIUserInterfaceIdiomPad) [_wrapperView addSubview:_landscapeToolbarView]; + [_captionMixin createInputPanelIfNeeded]; + if (hasCoverButton) { _cancelCoverButton = [[TGModernButton alloc] init]; _cancelCoverButton.hidden = true; @@ -1480,7 +1481,6 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * _portraitToolbarView.alpha = alpha; _landscapeToolbarView.alpha = alpha; _captionMixin.inputPanelView.alpha = alpha; - _captionMixin.backgroundView.alpha = alpha; _captionMixin.livePhotoButtonView.alpha = alpha; } completion:^(BOOL finished) { @@ -1492,7 +1492,6 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * _portraitToolbarView.userInteractionEnabled = !hidden; _landscapeToolbarView.userInteractionEnabled = !hidden; _captionMixin.inputPanelView.userInteractionEnabled = !hidden; - _captionMixin.backgroundView.userInteractionEnabled = !hidden; _captionMixin.livePhotoButtonView.userInteractionEnabled = !hidden; } }]; @@ -1532,9 +1531,6 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * _captionMixin.inputPanelView.alpha = alpha; _captionMixin.inputPanelView.userInteractionEnabled = !hidden; - _captionMixin.backgroundView.alpha = alpha; - _captionMixin.backgroundView.userInteractionEnabled = !hidden; - _captionMixin.livePhotoButtonView.alpha = alpha; _captionMixin.livePhotoButtonView.userInteractionEnabled = !hidden; } @@ -1722,7 +1718,6 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * - (void)immediateEditorTransitionIn { [self setSelectionInterfaceHidden:true animated:false]; _captionMixin.inputPanelView.alpha = 0.0f; - _captionMixin.backgroundView.alpha = 0.0f; _portraitToolbarView.doneButton.alpha = 0.0f; _landscapeToolbarView.doneButton.alpha = 0.0f; @@ -1743,7 +1738,6 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * [UIView animateWithDuration:0.2 animations:^ { _captionMixin.inputPanelView.alpha = 0.0f; - _captionMixin.backgroundView.alpha = 0.0f; _portraitToolbarView.doneButton.alpha = 0.0f; _landscapeToolbarView.doneButton.alpha = 0.0f; }]; @@ -1756,7 +1750,6 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * [UIView animateWithDuration:0.3 animations:^ { _captionMixin.inputPanelView.alpha = 1.0f; - _captionMixin.backgroundView.alpha = 1.0f; _portraitToolbarView.doneButton.alpha = 1.0f; _landscapeToolbarView.doneButton.alpha = 1.0f; }]; diff --git a/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m b/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m index 7c5c7528f8..a0bb1df327 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m +++ b/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m @@ -10,13 +10,14 @@ { TGObserverProxy *_keyboardWillChangeFrameProxy; bool _editing; - + UIGestureRecognizer *_dismissTapRecognizer; - + CGRect _currentFrame; UIEdgeInsets _currentEdgeInsets; - + bool _currentIsCaptionAbove; + CGFloat _effectiveKeyboardHeight; } @end @@ -48,7 +49,7 @@ { if (_inputPanel != nil) return; - + UIView *parentView = [self _parentView]; id inputPanel = nil; @@ -63,7 +64,7 @@ [TGViewController enableAutorotation]; strongSelf->_editing = false; - + if (strongSelf.finishedWithCaption != nil) strongSelf.finishedWithCaption(string); }; @@ -104,10 +105,9 @@ }; _inputPanelView = inputPanel.view; + _inputPanelView.frame = parentView.bounds; + _inputPanelView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - _backgroundView = [[UIView alloc] init]; - _backgroundView.backgroundColor = [TGPhotoEditorInterfaceAssets toolbarTransparentBackgroundColor]; - //[parentView addSubview:_backgroundView]; [parentView addSubview:_inputPanelView]; if (_stickersContext && _stickersContext.livePhotoButton != nil) { @@ -207,6 +207,25 @@ _dismissTapRecognizer.enabled = true; } +- (CGFloat)currentEffectiveKeyboardHeight +{ + return MAX(_keyboardHeight, _inputPanel.additionalInputHeight); +} + +- (bool)usesContainerLayout +{ + return [_inputPanel respondsToSelector:@selector(updateContainerLayoutSize:safeAreaInset:bottomInset:keyboardHeight:animated:)]; +} + +- (CGFloat)updateInputPanelLayoutForFrame:(CGRect)frame edgeInsets:(UIEdgeInsets)edgeInsets keyboardHeight:(CGFloat)keyboardHeight animated:(bool)animated +{ + if ([self usesContainerLayout]) { + return [_inputPanel updateContainerLayoutSize:frame.size safeAreaInset:_safeAreaInset bottomInset:edgeInsets.bottom keyboardHeight:keyboardHeight animated:animated]; + } else { + return [_inputPanel updateLayoutSize:frame.size keyboardHeight:keyboardHeight sideInset:0.0 animated:animated]; + } +} + #pragma mark - - (void)finishEditing { @@ -261,9 +280,19 @@ } _keyboardHeight = keyboardHeight; - + if (!UIInterfaceOrientationIsPortrait([[LegacyComponentsGlobals provider] applicationStatusBarOrientation]) && !TGIsPad()) + return; + + CGRect frame = _currentFrame; + UIEdgeInsets edgeInsets = _currentEdgeInsets; + bool usesContainerLayout = [self usesContainerLayout]; + CGRect containerFrame = CGRectMake(edgeInsets.left, 0.0, frame.size.width, frame.size.height); + _inputPanelView.frame = containerFrame; + CGFloat panelHeight = [self updateInputPanelLayoutForFrame:frame edgeInsets:edgeInsets keyboardHeight:keyboardHeight animated:usesContainerLayout]; + CGFloat effectiveKeyboardHeight = [self currentEffectiveKeyboardHeight]; + _effectiveKeyboardHeight = effectiveKeyboardHeight; CGFloat fadeAlpha = 1.0; - if (keyboardHeight < FLT_EPSILON) { + if (effectiveKeyboardHeight < FLT_EPSILON) { fadeAlpha = 0.0; } @@ -275,87 +304,90 @@ }]; } - if (!UIInterfaceOrientationIsPortrait([[LegacyComponentsGlobals provider] applicationStatusBarOrientation]) && !TGIsPad()) - return; - - CGRect frame = _currentFrame; - UIEdgeInsets edgeInsets = _currentEdgeInsets; - CGFloat panelHeight = [_inputPanel updateLayoutSize:frame.size keyboardHeight:keyboardHeight sideInset:0.0 animated:false]; - - CGFloat panelY = 0.0; - if (frame.size.width > frame.size.height && !TGIsPad()) { - panelY = edgeInsets.top + frame.size.height; - } else { - if (_currentIsCaptionAbove) { - if (_keyboardHeight > 0.0) { - panelY = _safeAreaInset.top + 8.0; - } else { - panelY = _safeAreaInset.top + 8.0 + 40.0; - } + if (!usesContainerLayout) { + CGFloat panelY = 0.0; + if (frame.size.width > frame.size.height && !TGIsPad()) { + panelY = edgeInsets.top + frame.size.height; } else { - panelY = edgeInsets.top + frame.size.height - panelHeight - MAX(edgeInsets.bottom, _keyboardHeight); + if (_currentIsCaptionAbove) { + if (effectiveKeyboardHeight > 0.0) { + panelY = _safeAreaInset.top + 8.0; + } else { + panelY = _safeAreaInset.top + 8.0 + 40.0; + } + } else { + panelY = edgeInsets.top + frame.size.height - panelHeight - MAX(edgeInsets.bottom, _keyboardHeight); + } } + + [UIView animateWithDuration:duration delay:0.0f options:(curve << 16) animations:^{ + _inputPanelView.frame = CGRectMake(edgeInsets.left, panelY, frame.size.width, panelHeight); + } completion:nil]; } - - CGFloat backgroundHeight = panelHeight; - if (_keyboardHeight > 0.0) { - backgroundHeight += _keyboardHeight - edgeInsets.bottom; - } - - [UIView animateWithDuration:duration delay:0.0f options:(curve << 16) animations:^{ - _inputPanelView.frame = CGRectMake(edgeInsets.left, panelY, frame.size.width, panelHeight); - _backgroundView.frame = CGRectMake(edgeInsets.left, panelY, frame.size.width, backgroundHeight); - } completion:nil]; if (self.keyboardHeightChanged != nil) - self.keyboardHeightChanged(keyboardHeight, duration, curve); + self.keyboardHeightChanged(effectiveKeyboardHeight, duration, curve); } - (void)updateLayoutWithFrame:(CGRect)frame edgeInsets:(UIEdgeInsets)edgeInsets animated:(bool)animated { _currentFrame = frame; _currentEdgeInsets = edgeInsets; - - CGFloat panelHeight = [_inputPanel updateLayoutSize:frame.size keyboardHeight:_keyboardHeight sideInset:0.0 animated:animated]; - - CGFloat panelY = 0.0; - if (frame.size.width > frame.size.height && !TGIsPad()) { - panelY = edgeInsets.top + frame.size.height; - } else { - if (_currentIsCaptionAbove) { - if (_keyboardHeight > 0.0) { - panelY = _safeAreaInset.top + 8.0; - } else { - panelY = _safeAreaInset.top + 8.0 + 40.0; - } + + bool usesContainerLayout = [self usesContainerLayout]; + CGRect containerFrame = CGRectMake(edgeInsets.left, 0.0, frame.size.width, frame.size.height); + CGFloat panelHeight = [self updateInputPanelLayoutForFrame:frame edgeInsets:edgeInsets keyboardHeight:_keyboardHeight animated:animated]; + CGFloat effectiveKeyboardHeight = [self currentEffectiveKeyboardHeight]; + CGFloat fadeAlpha = effectiveKeyboardHeight < FLT_EPSILON ? 0.0 : 1.0; + if (ABS(_dismissView.alpha - fadeAlpha) > FLT_EPSILON) { + if (animated) { + [UIView animateWithDuration:0.3 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ + _dismissView.alpha = fadeAlpha; + } completion:nil]; } else { - panelY = edgeInsets.top + frame.size.height - panelHeight - MAX(edgeInsets.bottom, _keyboardHeight); + _dismissView.alpha = fadeAlpha; } } - CGFloat backgroundHeight = panelHeight; - if (_keyboardHeight > 0.0) { - backgroundHeight += _keyboardHeight - edgeInsets.bottom; - } - CGFloat livePhotoY = 0.0; if (frame.size.width > frame.size.height && !TGIsPad()) { livePhotoY = 48.0; } else { - livePhotoY = edgeInsets.top + 145.0 - _keyboardHeight; + livePhotoY = edgeInsets.top + 145.0 - effectiveKeyboardHeight; } CGRect livePhotoButtonFrame = CGRectMake(_safeAreaInset.left + edgeInsets.left + 16.0, livePhotoY, 160.0, 18.0); _livePhotoButtonView.frame = livePhotoButtonFrame; - - CGRect panelFrame = CGRectMake(edgeInsets.left, panelY, frame.size.width, panelHeight); - CGRect backgroundFrame = CGRectMake(edgeInsets.left, panelY, frame.size.width, backgroundHeight); - - if (animated) { - [_inputPanel animateView:_inputPanelView frame:panelFrame]; - [_inputPanel animateView:_backgroundView frame:backgroundFrame]; + + if (usesContainerLayout) { + _inputPanelView.frame = containerFrame; } else { - _inputPanelView.frame = panelFrame; - _backgroundView.frame = backgroundFrame; + CGFloat panelY = 0.0; + if (frame.size.width > frame.size.height && !TGIsPad()) { + panelY = edgeInsets.top + frame.size.height; + } else { + if (_currentIsCaptionAbove) { + if (effectiveKeyboardHeight > 0.0) { + panelY = _safeAreaInset.top + 8.0; + } else { + panelY = _safeAreaInset.top + 8.0 + 40.0; + } + } else { + panelY = edgeInsets.top + frame.size.height - panelHeight - MAX(edgeInsets.bottom, _keyboardHeight); + } + } + + CGRect panelFrame = CGRectMake(edgeInsets.left, panelY, frame.size.width, panelHeight); + if (animated) { + [_inputPanel animateView:_inputPanelView frame:panelFrame]; + } else { + _inputPanelView.frame = panelFrame; + } + } + if (ABS(_effectiveKeyboardHeight - effectiveKeyboardHeight) > FLT_EPSILON) { + _effectiveKeyboardHeight = effectiveKeyboardHeight; + if (self.keyboardHeightChanged != nil) { + self.keyboardHeightChanged(effectiveKeyboardHeight, animated ? 0.3 : 0.0, UIViewAnimationCurveEaseInOut); + } } } diff --git a/submodules/SparseItemGrid/BUILD b/submodules/SparseItemGrid/BUILD index 302566dbbb..75ad547e78 100644 --- a/submodules/SparseItemGrid/BUILD +++ b/submodules/SparseItemGrid/BUILD @@ -17,6 +17,7 @@ swift_library( "//submodules/AnimationUI:AnimationUI", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/SparseItemGrid/Sources/SparseDiscreteScrollingArea.swift b/submodules/SparseItemGrid/Sources/SparseDiscreteScrollingArea.swift index 311fdf4d1d..b40d79b9b1 100644 --- a/submodules/SparseItemGrid/Sources/SparseDiscreteScrollingArea.swift +++ b/submodules/SparseItemGrid/Sources/SparseDiscreteScrollingArea.swift @@ -351,6 +351,8 @@ public final class SparseDiscreteScrollingArea: ASDisplayNode { let indicatorSize = self.dateIndicator.update( transition: .immediate, component: AnyComponent(SparseItemGridScrollingIndicatorComponent( + isDark: theme.overallDarkAppearance, + isVisible: true, backgroundColor: theme.list.itemBlocksBackgroundColor, shadowColor: .black, foregroundColor: theme.list.itemPrimaryTextColor, diff --git a/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift b/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift index 4b5eba4437..e7709d56fc 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift @@ -6,6 +6,7 @@ import ComponentFlow import SwiftSignalKit import AnimationUI import TelegramPresentationData +import GlassBackgroundComponent public final class MultilineText: Component { public let text: String @@ -709,6 +710,8 @@ public final class RollingText: Component { final class SparseItemGridScrollingIndicatorComponent: CombinedComponent { + let isDark: Bool + let isVisible: Bool let backgroundColor: UIColor let shadowColor: UIColor let foregroundColor: UIColor @@ -716,12 +719,16 @@ final class SparseItemGridScrollingIndicatorComponent: CombinedComponent { let previousDate: (String, Int32)? init( + isDark: Bool, + isVisible: Bool, backgroundColor: UIColor, shadowColor: UIColor, foregroundColor: UIColor, date: (String, Int32), previousDate: (String, Int32)? ) { + self.isDark = isDark + self.isVisible = isVisible self.backgroundColor = backgroundColor self.shadowColor = shadowColor self.foregroundColor = foregroundColor @@ -730,6 +737,12 @@ final class SparseItemGridScrollingIndicatorComponent: CombinedComponent { } static func ==(lhs: SparseItemGridScrollingIndicatorComponent, rhs: SparseItemGridScrollingIndicatorComponent) -> Bool { + if lhs.isDark != rhs.isDark { + return false + } + if lhs.isVisible != rhs.isVisible { + return false + } if lhs.backgroundColor != rhs.backgroundColor { return false } @@ -749,13 +762,11 @@ final class SparseItemGridScrollingIndicatorComponent: CombinedComponent { } static var body: Body { - let rect = Child(ShadowRoundedRectangle.self) + let rect = Child(GlassBackgroundComponent.self) let textMonth = Child(RollingText.self) let textYear = Child(RollingText.self) return { context in - context.view.clipsToBounds = true - let date = context.component.date let components = date.0.components(separatedBy: " ") @@ -804,11 +815,17 @@ final class SparseItemGridScrollingIndicatorComponent: CombinedComponent { transition: .immediate ) + let rectSize = CGSize(width: textMonth.size.width + 3.0 + textYear.size.width + 26.0, height: 32.0) let rect = rect.update( - component: ShadowRoundedRectangle( - color: context.component.backgroundColor + component: GlassBackgroundComponent( + size: rectSize, + cornerRadius: rectSize.height * 0.5, + isDark: context.component.isDark, + tintColor: .init(kind: .panel), + isInteractive: false, + isVisible: context.component.isVisible ), - availableSize: CGSize(width: textMonth.size.width + 3.0 + textYear.size.width + 26.0, height: 32.0), + availableSize: rectSize, transition: .easeInOut(duration: 0.2) ) @@ -818,7 +835,7 @@ final class SparseItemGridScrollingIndicatorComponent: CombinedComponent { )) context.add(rect - .position(CGPoint(x: rectFrame.midX + shadowInset, y: rectFrame.midY + shadowInset)) + .position(CGPoint(x: rectFrame.midX, y: rectFrame.midY)) ) let offset = CGSize(width: textMonth.size.width + 3.0 + textYear.size.width, height: textMonth.size.height).centered(in: rectFrame) @@ -827,9 +844,11 @@ final class SparseItemGridScrollingIndicatorComponent: CombinedComponent { let yearTextFrame = textYear.size.leftCentered(in: rectFrame).offsetBy(dx: offset.minX + monthTextFrame.width + 3.0, dy: 0.0) context.add(textMonth .position(CGPoint(x: monthTextFrame.midX, y: monthTextFrame.midY)) + .opacity(context.component.isVisible ? 1.0 : 0.0) ) context.add(textYear .position(CGPoint(x: yearTextFrame.midX, y: yearTextFrame.midY)) + .opacity(context.component.isVisible ? 1.0 : 0.0) ) return rect.size @@ -1029,7 +1048,6 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { self.dateIndicator = ComponentHostView() self.lineIndicator = ComponentHostView() - self.dateIndicator.alpha = 0.0 self.lineIndicator.alpha = 0.0 super.init() @@ -1159,6 +1177,36 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { private var currentDate: (String, Int32)? private var isScrolling: Bool = false + private var isDateIndicatorVisible = false + + private func requestUpdate(animated: Bool) { + guard let containerSize = self.containerSize else { + return + } + let _ = self.updateIndicator(containerSize: containerSize, animated: animated) + } + + private func updateIndicator(containerSize: CGSize, previousDate: (String, Int32)? = nil, animated: Bool) -> CGSize { + guard let theme = self.theme, let date = self.currentDate else { + return .zero + } + let indicatorSize = self.dateIndicator.update( + transition: animated ? .easeInOut(duration: 0.2) : .immediate, + component: AnyComponent(SparseItemGridScrollingIndicatorComponent( + isDark: theme.overallDarkAppearance, + isVisible: self.isDateIndicatorVisible, + backgroundColor: theme.list.itemBlocksBackgroundColor, + shadowColor: .black, + foregroundColor: theme.list.itemPrimaryTextColor, + date: date, + previousDate: previousDate + )), + environment: {}, + containerSize: containerSize + ) + return indicatorSize + } + public func update( containerSize: CGSize, containerInsets: UIEdgeInsets, @@ -1186,18 +1234,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { } let animateIndicatorFrame = previousDate != nil && previousDate?.1 != date.1 - let indicatorSize = self.dateIndicator.update( - transition: animateIndicatorFrame ? .easeInOut(duration: 0.2) : .immediate, - component: AnyComponent(SparseItemGridScrollingIndicatorComponent( - backgroundColor: theme.list.itemBlocksBackgroundColor, - shadowColor: .black, - foregroundColor: theme.list.itemPrimaryTextColor, - date: date, - previousDate: previousDate - )), - environment: {}, - containerSize: containerSize - ) + let indicatorSize = self.updateIndicator(containerSize: containerSize, previousDate: previousDate, animated: animateIndicatorFrame) let scrollIndicatorHeightFraction = min(1.0, max(0.0, (containerSize.height - containerInsets.top - containerInsets.bottom) / contentHeight)) if scrollIndicatorHeightFraction >= 0.55 - .ulpOfOne { @@ -1244,7 +1281,6 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { if isScrolling { let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut) - transition.updateAlpha(layer: self.dateIndicator.layer, alpha: 1.0) transition.updateAlpha(layer: self.lineIndicator.layer, alpha: 1.0) } @@ -1284,10 +1320,13 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { private func updateActivityTimer(isScrolling: Bool) { self.activityTimer?.invalidate() + + if isScrolling || self.isDragging { + self.isDateIndicatorVisible = true + } if self.isDragging { let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut) - transition.updateAlpha(layer: self.dateIndicator.layer, alpha: 1.0) transition.updateAlpha(layer: self.lineIndicator.layer, alpha: 1.0) } else { self.activityTimer = SwiftSignalKit.Timer(timeout: 2.0, repeat: false, completion: { [weak self] in @@ -1295,9 +1334,11 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { return } let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut) - transition.updateAlpha(layer: strongSelf.dateIndicator.layer, alpha: 0.0) transition.updateAlpha(layer: strongSelf.lineIndicator.layer, alpha: 0.0) + strongSelf.isDateIndicatorVisible = false + strongSelf.requestUpdate(animated: true) + strongSelf.dismissLineTooltip() }, queue: .mainQueue()) self.activityTimer?.start() @@ -1391,9 +1432,11 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { public func hideScroller() { let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut) - transition.updateAlpha(layer: self.dateIndicator.layer, alpha: 0.0) transition.updateAlpha(layer: self.lineIndicator.layer, alpha: 0.0) + self.isDateIndicatorVisible = false + self.requestUpdate(animated: true) + self.dismissLineTooltip() } } diff --git a/submodules/StatisticsUI/Sources/StatsGraphItem.swift b/submodules/StatisticsUI/Sources/StatsGraphItem.swift index 8fb221d919..ce10ccaeea 100644 --- a/submodules/StatisticsUI/Sources/StatsGraphItem.swift +++ b/submodules/StatisticsUI/Sources/StatsGraphItem.swift @@ -109,6 +109,7 @@ public final class StatsGraphItemNode: ListViewItemNode { let chartNode: ChartNode private let activityIndicator: ActivityIndicator + private let errorTextNode: TextNode private var item: StatsGraphItem? private var visibilityHeight: CGFloat? @@ -134,11 +135,15 @@ public final class StatsGraphItemNode: ListViewItemNode { self.chartNode = ChartNode() self.activityIndicator = ActivityIndicator(type: ActivityIndicatorType.custom(.black, 16.0, 2.0, false)) self.activityIndicator.isHidden = true + self.errorTextNode = TextNode() + self.errorTextNode.isUserInteractionEnabled = false + self.errorTextNode.isHidden = true super.init(layerBacked: false) self.chartContainerNode.addSubnode(self.chartNode) self.chartContainerNode.addSubnode(self.activityIndicator) + self.chartContainerNode.addSubnode(self.errorTextNode) } public override func didLoad() { @@ -154,12 +159,14 @@ public final class StatsGraphItemNode: ListViewItemNode { } func asyncLayout() -> (_ item: StatsGraphItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let makeErrorTextLayout = TextNode.asyncLayout(self.errorTextNode) let currentItem = self.item let currentVisibilityHeight = self.visibilityHeight return { item, params, neighbors in let leftInset = params.leftInset let rightInset: CGFloat = params.rightInset + let errorTextFont = Font.regular(item.presentationData.fontSize.itemListBaseLabelFontSize / 14.0 * 16.0) var updatedTheme: PresentationTheme? var updatedGraph: StatsGraph? var updatedController: BaseChartController? @@ -197,6 +204,27 @@ public final class StatsGraphItemNode: ListViewItemNode { contentSize = CGSize(width: params.width, height: 361.0) insets = itemListNeighborsGroupedInsets(neighbors, params) } + + let errorText: String + if case let .Failed(text) = item.graph { + errorText = text + } else { + errorText = "" + } + let (errorTextLayout, errorTextApply) = makeErrorTextLayout(TextNodeLayoutArguments( + attributedString: NSAttributedString( + string: errorText, + font: errorTextFont, + textColor: item.presentationData.theme.list.itemSecondaryTextColor + ), + backgroundColor: nil, + maximumNumberOfLines: 0, + truncationType: .end, + constrainedSize: CGSize(width: max(1.0, params.width - leftInset - rightInset - 32.0), height: contentSize.height), + alignment: .center, + cutout: nil, + insets: UIEdgeInsets() + )) var visibilityHeight = currentVisibilityHeight if let updatedController = updatedController { @@ -293,8 +321,16 @@ public final class StatsGraphItemNode: ListViewItemNode { strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) strongSelf.activityIndicator.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - 16.0) / 2.0), y: floor((layout.size.height - 16.0) / 2.0)), size: CGSize(width: 16.0, height: 16.0)) + strongSelf.errorTextNode.frame = CGRect( + origin: CGPoint( + x: floorToScreenPixels((strongSelf.chartContainerNode.bounds.width - errorTextLayout.size.width) / 2.0), + y: floorToScreenPixels((strongSelf.chartContainerNode.bounds.height - errorTextLayout.size.height) / 2.0) + ), + size: errorTextLayout.size + ) } + let _ = errorTextApply() strongSelf.activityIndicator.type = .custom(item.presentationData.theme.list.itemSecondaryTextColor, 16.0, 2.0, false) if let updatedTheme = updatedTheme { @@ -310,15 +346,27 @@ public final class StatsGraphItemNode: ListViewItemNode { ) } - if let updatedGraph = updatedGraph { - if case .Loaded = updatedGraph, let updatedController = updatedController { - strongSelf.chartNode.setup(controller: updatedController, noInitialZoom: item.noInitialZoom) - strongSelf.activityIndicator.isHidden = true - strongSelf.chartNode.isHidden = false - } else if case .OnDemand = updatedGraph { - strongSelf.activityIndicator.isHidden = false - strongSelf.chartNode.isHidden = true - } + if let updatedGraph = updatedGraph, case .Loaded = updatedGraph, let updatedController = updatedController { + strongSelf.chartNode.setup(controller: updatedController, noInitialZoom: item.noInitialZoom) + } + + switch item.graph { + case .Loaded: + strongSelf.activityIndicator.isHidden = true + strongSelf.chartNode.isHidden = false + strongSelf.errorTextNode.isHidden = true + case .OnDemand: + strongSelf.activityIndicator.isHidden = false + strongSelf.chartNode.isHidden = true + strongSelf.errorTextNode.isHidden = true + case let .Failed(error): + strongSelf.activityIndicator.isHidden = true + strongSelf.chartNode.isHidden = true + strongSelf.errorTextNode.isHidden = error.isEmpty + case .Empty: + strongSelf.activityIndicator.isHidden = true + strongSelf.chartNode.isHidden = true + strongSelf.errorTextNode.isHidden = true } } }) @@ -337,4 +385,3 @@ public final class StatsGraphItemNode: ListViewItemNode { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } } - diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift index f0465c0afe..f349894a89 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift @@ -5600,12 +5600,8 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg } public func frameForInputActionButton() -> CGRect? { - if !self.mediaActionButtons.alpha.isZero { - if self.mediaActionButtons.micButton.alpha.isZero { - return self.mediaActionButtons.frame.insetBy(dx: 0.0, dy: -4.0).offsetBy(dx: -3.0, dy: 0.0) - } else { - return self.mediaActionButtons.frame.insetBy(dx: 0.0, dy: -4.0).offsetBy(dx: -3.0, dy: 0.0) - } + if !self.mediaActionButtons.alpha.isZero && self.mediaActionButtons.frame.minX < self.bounds.width { + return self.mediaActionButtons.frame.insetBy(dx: 0.0, dy: -4.0).offsetBy(dx: -3.0, dy: 0.0) } return nil } diff --git a/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/Sources/MessageInlineBlockBackgroundView.swift b/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/Sources/MessageInlineBlockBackgroundView.swift index 7408d13b04..743a04a2b4 100644 --- a/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/Sources/MessageInlineBlockBackgroundView.swift +++ b/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/Sources/MessageInlineBlockBackgroundView.swift @@ -506,6 +506,8 @@ public final class MessageInlineBlockBackgroundView: UIView { thirdColor: params.thirdColor, backgroundColor: params.backgroundColor, pattern: params.pattern, + patternTopRightPosition: params.patternTopRightPosition, + patternAlpha: params.patternAlpha, animation: .None ) } diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD index 18aedceb49..6035b65862 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD @@ -39,7 +39,6 @@ swift_library( "//submodules/FeaturedStickersScreen:FeaturedStickersScreen", "//submodules/TelegramUI/Components/EntityKeyboardGifContent:EntityKeyboardGifContent", "//submodules/TelegramUI/Components/LegacyMessageInputPanelInputView:LegacyMessageInputPanelInputView", - "//submodules/AttachmentTextInputPanelNode", "//submodules/TelegramUI/Components/BatchVideoRendering", "//submodules/TelegramUI/Components/GlassBackgroundComponent", "//submodules/TelegramUI/Components/SubcodecAnimationCacheImpl:SubcodecAnimationCacheImpl", diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 2b3544686b..3b4e28b601 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -30,7 +30,6 @@ import FeaturedStickersScreen import Pasteboard import EntityKeyboardGifContent import LegacyMessageInputPanelInputView -import AttachmentTextInputPanelNode import GlassBackgroundComponent private let keyboardCornerRadius: CGFloat = 30.0 @@ -64,7 +63,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { let getNavigationController: () -> NavigationController? let requestLayout: (ContainedViewLayoutTransition) -> Void public var forceTheme: PresentationTheme? - + public init( sendSticker: @escaping (FileMediaReference, Bool, Bool, String?, Bool, UIView?, CGRect?, CALayer?, [ItemCollectionId]) -> Bool, sendEmoji: @escaping (String, ChatTextInputTextCustomEmojiAttribute, Bool) -> Void, @@ -98,7 +97,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { self.getNavigationController = getNavigationController self.requestLayout = requestLayout } - + public init(chatControllerInteraction: ChatControllerInteraction, panelInteraction: ChatPanelInterfaceInteraction) { self.sendSticker = chatControllerInteraction.sendSticker self.sendEmoji = chatControllerInteraction.sendEmoji @@ -121,13 +120,13 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { self.requestLayout = panelInteraction.requestLayout } } - + public struct InputData: Equatable { public var emoji: EmojiPagerContentComponent? public var stickers: EmojiPagerContentComponent? public var gifs: EntityKeyboardGifContent? public var availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji] - + public init( emoji: EmojiPagerContentComponent?, stickers: EmojiPagerContentComponent?, @@ -140,14 +139,14 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { self.availableGifSearchEmojies = availableGifSearchEmojies } } - + public final class StateContext { let emojiState = EmojiPagerContentComponent.StateContext() - + public init() { } } - + public static func hasPremium(context: AccountContext, chatPeerId: EnginePeer.Id?, premiumIfSavedMessages: Bool) -> Signal { let hasPremium: Signal if premiumIfSavedMessages, let chatPeerId = chatPeerId, chatPeerId == context.account.peerId { @@ -164,7 +163,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } return hasPremium } - + public static func inputData( context: AccountContext, chatPeerId: PeerId?, @@ -181,7 +180,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { ) -> Signal { let animationCache = context.animationCache let animationRenderer = context.animationRenderer - + let emojiItems = EmojiPagerContentComponent.emojiInputData( context: context, animationCache: animationCache, @@ -198,12 +197,12 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { hideBackground: hideBackground, maskEdge: maskEdge ) - + let stickerNamespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks] let stickerOrderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers] - + let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings - + let stickerItems: Signal if hasStickers { stickerItems = EmojiPagerContentComponent.stickerInputData( @@ -226,20 +225,20 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } else { stickerItems = .single(nil) } - + let reactions: Signal<[String], NoError> = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.App()) |> map { appConfiguration -> [String] in let defaultReactions: [String] = ["👍", "👎", "😍", "😂", "😯", "😕", "😢", "😡", "💪", "👏", "🙈", "😒"] - + guard let data = appConfiguration.data, let emojis = data["gif_search_emojies"] as? [String] else { return defaultReactions } return emojis } |> distinctUntilChanged - + let animatedEmojiStickers: Signal<[String: [StickerPackItem]], NoError> - + if hasGifs { animatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false) |> map { animatedEmoji -> [String: [StickerPackItem]] in @@ -263,7 +262,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } else { animatedEmojiStickers = .single([:]) } - + let gifInputInteraction = GifPagerContentComponent.InputInteraction( performItemAction: { item, view, rect in if let sendGif { @@ -281,7 +280,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { hideBackground: hideBackground, hasSearch: hasSearch ) - + // We are going to subscribe to the actual data when the view is loaded let gifItems: Signal if hasGifs { @@ -304,7 +303,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } else { gifItems = .single(nil) } - + return combineLatest(queue: .mainQueue(), emojiItems, stickerItems, @@ -341,15 +340,15 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { default: break } - + guard let title = title else { continue } - + availableGifSearchEmojies.append(EntityKeyboardComponent.GifSearchEmoji(emoji: reaction, file: file._parse(), title: title)) } } - + return InputData( emoji: emoji, stickers: stickers, @@ -358,11 +357,11 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { ) } } - + private let context: AccountContext private let stateContext: StateContext? private let entityKeyboardView: ComponentView - + private let defaultToEmojiTab: Bool private var stableReorderableGroupOrder: [EntityKeyboardComponent.ReorderCategory: [ItemCollectionId]] = [:] private var currentInputData: InputData @@ -371,24 +370,24 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { private let opaqueTopPanelBackground: Bool private let useOpaqueTheme: Bool private let displayBottomPanel: Bool - + private struct EmojiSearchResult { var groups: [EmojiPagerContentComponent.ItemGroup] var id: AnyHashable var version: Int var isPreset: Bool } - + private struct EmojiSearchState { var result: EmojiSearchResult? var isSearching: Bool - + init(result: EmojiSearchResult?, isSearching: Bool) { self.result = result self.isSearching = isSearching } } - + private let emojiSearchDisposable = MetaDisposable() private let emojiSearchState = Promise(EmojiSearchState(result: nil, isSearching: false)) private var emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) { @@ -396,7 +395,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { self.emojiSearchState.set(.single(self.emojiSearchStateValue)) } } - + private let stickerSearchDisposable = MetaDisposable() private let stickerSearchState = Promise(EmojiSearchState(result: nil, isSearching: false)) private var stickerSearchStateValue = EmojiSearchState(result: nil, isSearching: false) { @@ -404,27 +403,27 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { self.stickerSearchState.set(.single(self.stickerSearchStateValue)) } } - + private let interaction: ChatEntityKeyboardInputNode.Interaction? private var inputNodeInteraction: ChatMediaInputNodeInteraction? - + private let trendingGifsPromise = Promise(nil) - + private var isMarkInputCollapsed: Bool = false - + private var isEmojiSearchActive: Bool = false { didSet { self.followsDefaultHeight = !self.isEmojiSearchActive } } - + public var clipContentToTopPanel: Bool = false - + public var externalTopPanelContainerImpl: PagerExternalTopPanelContainer? public override var externalTopPanelContainer: UIView? { return self.externalTopPanelContainerImpl } - + private let clippingView: UIView private var backgroundView: BlurredBackgroundView? private var backgroundTintView: UIImageView? @@ -432,14 +431,14 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { private var backgroundTintMaskView: UIView? private var backgroundTintMaskContentView: UIView? private var externalBackground: EmojiPagerContentComponent.ExternalBackground? - + public var switchToTextInput: (() -> Void)? - + private var currentState: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, interfaceState: ChatPresentationInterfaceState, layoutMetrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool)? - + private var scheduledContentAnimationHint: EmojiPagerContentComponent.ContentAnimation? private var scheduledInnerTransition: ComponentTransition? - + private var gifMode: GifPagerContentComponent.Subject? { didSet { if let gifMode = self.gifMode, gifMode != oldValue { @@ -447,7 +446,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } } } - + public var canSwitchToTextInputAutomatically: Bool { if let pagerView = self.entityKeyboardView.view as? EntityKeyboardComponent.View, let centralId = pagerView.centralId { if centralId == AnyHashable("emoji") { @@ -456,9 +455,9 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } return true } - + public var useExternalSearchContainer: Bool = false - + private var gifContext: GifContext? { didSet { if let gifContext = self.gifContext { @@ -468,12 +467,12 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } private let gifComponent = Promise() private var gifInputInteraction: GifPagerContentComponent.InputInteraction? - + fileprivate var emojiInputInteraction: EmojiPagerContentComponent.InputInteraction? private var stickerInputInteraction: EmojiPagerContentComponent.InputInteraction? - + private weak var currentUndoOverlayController: UndoOverlayController? - + private var choosingStickerDisposable: Disposable? private var scrollingStickersGridPromise = Promise(false) private var previewingStickersPromise = ValuePromise(false) @@ -484,7 +483,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } |> distinctUntilChanged } - + public init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal, defaultToEmojiTab: Bool, opaqueTopPanelBackground: Bool = false, useOpaqueTheme: Bool = false, interaction: ChatEntityKeyboardInputNode.Interaction?, chatPeerId: PeerId?, stateContext: StateContext?, forceHasPremium: Bool = false, displayBottomPanel: Bool = true) { self.context = context self.currentInputData = currentInputData @@ -493,31 +492,31 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { self.useOpaqueTheme = useOpaqueTheme self.stateContext = stateContext self.displayBottomPanel = displayBottomPanel - + self.interaction = interaction - + self.clippingView = SparseContainerView() self.clippingView.clipsToBounds = true self.clippingView.layer.cornerRadius = keyboardCornerRadius - + self.entityKeyboardView = ComponentView() - + super.init() - + self.currentInputData = self.processInputData(inputData: self.currentInputData) - + self.topBackgroundExtension = 34.0 self.followsDefaultHeight = true - + if "".isEmpty { let backgroundView = BlurredBackgroundView(color: .black, enableBlur: true) self.backgroundView = backgroundView self.view.addSubview(backgroundView) - + let backgroundTintView = UIImageView() self.backgroundTintView = backgroundTintView self.view.addSubview(backgroundTintView) - + let backgroundTintMaskView = UIView() backgroundTintMaskView.backgroundColor = .white self.backgroundTintMaskView = backgroundTintMaskView @@ -525,31 +524,31 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { backgroundTintMaskView.layer.filters = [filter] } backgroundTintView.mask = backgroundTintMaskView - + let backgroundTintMaskContentView = UIView() backgroundTintMaskView.addSubview(backgroundTintMaskContentView) self.backgroundTintMaskContentView = backgroundTintMaskContentView - + let backgroundChromeView = UIImageView() self.backgroundChromeView = backgroundChromeView - + self.externalBackground = EmojiPagerContentComponent.ExternalBackground( effectContainerView: backgroundTintMaskContentView ) } - + self.view.addSubview(self.clippingView) - + if let backgroundChromeView = self.backgroundChromeView { self.view.addSubview(backgroundChromeView) } - + self.externalTopPanelContainerImpl = PagerExternalTopPanelContainer() - + var stickerPeekBehavior: EmojiContentPeekBehaviorImpl? if let interaction { let context = self.context - + stickerPeekBehavior = EmojiContentPeekBehaviorImpl( context: self.context, interaction: EmojiContentPeekBehaviorImpl.Interaction( @@ -561,7 +560,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { switch attribute { case let .CustomEmoji(_, _, displayText, stickerPackReference): text = displayText - + var packId: ItemCollectionId? if case let .id(id, _) = stickerPackReference { packId = ItemCollectionId(namespace: Namespaces.ItemCollection.CloudEmojiPacks, id: id) @@ -572,7 +571,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { break } } - + if let emojiAttribute { interaction.sendEmoji(text, emojiAttribute, true) } @@ -582,16 +581,16 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { return } let _ = strongSelf.context.engine.accountData.setEmojiStatus(file: file, expirationDate: nil).start() - + var animateInAsReplacement = false if let currentUndoOverlayController = strongSelf.currentUndoOverlayController { currentUndoOverlayController.dismissWithCommitActionAndReplacementAnimation() strongSelf.currentUndoOverlayController = nil animateInAsReplacement = true } - + let presentationData = context.sharedContext.currentPresentationData.with { $0 } - + let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.EmojiStatus_AppliedText, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false }) strongSelf.currentUndoOverlayController = controller interaction.presentController(controller, nil) @@ -600,33 +599,33 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { guard let strongSelf = self else { return } - + var text = "." var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? loop: for attribute in file.attributes { switch attribute { case let .CustomEmoji(_, _, displayText, _): text = displayText - + emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file) break loop default: break } } - + if let _ = emojiAttribute { storeMessageTextInPasteboard(text, entities: [MessageTextEntity(range: 0 ..< (text as NSString).length, type: .CustomEmoji(stickerPack: nil, fileId: file.fileId.id))]) - + var animateInAsReplacement = false if let currentUndoOverlayController = strongSelf.currentUndoOverlayController { currentUndoOverlayController.dismissWithCommitActionAndReplacementAnimation() strongSelf.currentUndoOverlayController = nil animateInAsReplacement = true } - + let presentationData = context.sharedContext.currentPresentationData.with { $0 } - + let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.Conversation_EmojiCopied, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false }) strongSelf.currentUndoOverlayController = controller interaction.presentController(controller, nil) @@ -645,7 +644,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } ) } - + var premiumToastCounter = 0 self.emojiInputInteraction = EmojiPagerContentComponent.InputInteraction( performItemAction: { [weak self, weak interaction] groupId, item, _, _, _, _ in @@ -659,7 +658,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { guard let strongSelf = self, let interaction else { return } - + if groupId == AnyHashable("featuredTop"), let file = item.itemFile { let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks) let _ = (combineLatest( @@ -677,18 +676,18 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { guard let self else { return } - + let _ = interaction - + var installedCollectionIds = Set() for (id, _, _) in emojiPacksView.collectionInfos { installedCollectionIds.insert(id) } - + let stickerPacks = view.items.map({ $0.contents.get(FeaturedStickerPackItem.self)! }).filter({ !installedCollectionIds.contains($0.info.id) }) - + for featuredStickerPack in stickerPacks { if featuredStickerPack.topItems.contains(where: { $0.file.fileId == file.fileId }) { if let pagerView = self.entityKeyboardView.view as? EntityKeyboardComponent.View, let emojiInputInteraction = self.emojiInputInteraction { @@ -713,7 +712,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { switch attribute { case let .CustomEmoji(_, _, displayText, _): text = displayText - + var packId: ItemCollectionId? if let id = groupId.base as? ItemCollectionId { packId = id @@ -724,7 +723,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { break } } - + if file.isPremiumEmoji && !hasPremium && groupId != AnyHashable("peerSpecific") && !forceHasPremium { var animateInAsReplacement = false if let currentUndoOverlayController = strongSelf.currentUndoOverlayController { @@ -732,9 +731,9 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { strongSelf.currentUndoOverlayController = nil animateInAsReplacement = true } - + let presentationData = context.sharedContext.currentPresentationData.with { $0 } - + premiumToastCounter += 1 var suggestSavedMessages = premiumToastCounter % 2 == 0 if chatPeerId == nil { @@ -749,19 +748,19 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { text = presentationData.strings.EmojiInput_PremiumEmojiToast_Text actionTitle = presentationData.strings.EmojiInput_PremiumEmojiToast_Action } - + let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: text, undoText: actionTitle, customAction: { [weak interaction] in guard let interaction else { return } - + if suggestSavedMessages { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> deliverOnMainQueue).start(next: { peer in guard let peer = peer, let navigationController = interaction.getNavigationController() else { return } - + context.sharedContext.navigateToChatController(NavigateToChatControllerParams( navigationController: navigationController, chatController: nil, @@ -791,7 +790,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { interaction.presentController(controller, nil) return } - + if let emojiAttribute = emojiAttribute { AudioServicesPlaySystemSound(0x450) interaction.insertText(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute])) @@ -817,7 +816,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { guard let interaction, let collectionId = groupId.base as? ItemCollectionId else { return } - + if isPremiumLocked { var replaceImpl: ((ViewController) -> Void)? let controller = PremiumDemoScreen(context: context, subject: .animatedEmoji, action: { @@ -828,10 +827,10 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { controller?.replace(with: c) } interaction.getNavigationController()?.pushViewController(controller) - + return } - + let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks) let _ = (context.account.postbox.combinedView(keys: [viewKey]) |> take(1) @@ -845,7 +844,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { strongSelf.scheduledContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupInstalled(id: collectionId, scrollToGroup: scrollToGroup)) } let _ = context.engine.stickers.addStickerPackInteractively(info: featuredEmojiPack.info._parse(), items: featuredEmojiPack.topItems).start() - + break } } @@ -923,20 +922,20 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { guard let self = self else { return } - + switch query { case .none: self.emojiSearchDisposable.set(nil) self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) case let .text(rawQuery, languageCode): let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) - + if query.isEmpty { self.emojiSearchDisposable.set(nil) self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) } else { let context = self.context - + var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false) if !languageCode.lowercased().hasPrefix("en") { signal = signal @@ -950,7 +949,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { ) } } - + let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> map { peer -> Bool in guard case let .user(user) = peer else { @@ -959,7 +958,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { return user.isPremium } |> distinctUntilChanged - + let resultSignal = combineLatest( signal, hasPremium @@ -995,7 +994,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { return .complete() } var items: [EmojiPagerContentComponent.Item] = [] - + let appendUnicodeEmoji = { for (_, list) in EmojiPagerContentComponent.staticEmojiMapping { for emojiString in list { @@ -1013,11 +1012,11 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } } } - + if !hasPremium { appendUnicodeEmoji() } - + var existingIds = Set() for itemFile in foundEmoji.items { if existingIds.contains(itemFile.fileId) { @@ -1038,11 +1037,11 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { ) items.append(item) } - + if hasPremium { appendUnicodeEmoji() } - + var resultGroups: [EmojiPagerContentComponent.ItemGroup] = [] resultGroups.append(EmojiPagerContentComponent.ItemGroup( supergroupId: "search", @@ -1062,7 +1061,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { fillWithLoadingPlaceholders: false, items: items )) - + for (collectionId, info, _, _) in foundPacks.sets.infos { if let info = info as? StickerPackCollectionInfo { var topItems: [StickerPackItem] = [] @@ -1073,14 +1072,14 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } } } - + var groupItems: [EmojiPagerContentComponent.Item] = [] for item in topItems { var tintMode: EmojiPagerContentComponent.Item.TintMode = .none if item.file.isCustomTemplateEmoji { tintMode = .primary } - + let animationData = EntityKeyboardAnimationData(file: item.file) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, @@ -1090,10 +1089,10 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { icon: .none, tintMode: tintMode ) - + groupItems.append(resultItem) } - + resultGroups.append(EmojiPagerContentComponent.ItemGroup( supergroupId: AnyHashable(info.id), groupId: AnyHashable(info.id), @@ -1114,11 +1113,11 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { )) } } - + return .single(resultGroups) } } - + var version = 0 self.emojiSearchStateValue.isSearching = true self.emojiSearchDisposable.set((resultSignal @@ -1127,7 +1126,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { guard let self else { return } - + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(query), version: version, isPreset: false), isSearching: false) version += 1 })) @@ -1136,7 +1135,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { let resultSignal = self.context.engine.stickers.searchEmoji(category: value) |> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in var items: [EmojiPagerContentComponent.Item] = [] - + var existingIds = Set() for itemFile in files { if existingIds.contains(itemFile.fileId) { @@ -1154,7 +1153,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { ) items.append(item) } - + return .single(([EmojiPagerContentComponent.ItemGroup( supergroupId: "search", groupId: "search", @@ -1174,14 +1173,14 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { items: items )], isFinalResult)) } - + var version = 0 self.emojiSearchDisposable.set((resultSignal |> deliverOnMainQueue).start(next: { [weak self] result in guard let self else { return } - + guard let group = result.items.first else { return } @@ -1227,7 +1226,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { stateContext: self.stateContext?.emojiState, addImage: nil ) - + self.stickerInputInteraction = EmojiPagerContentComponent.InputInteraction( performItemAction: { [weak interaction] groupId, item, view, rect, layer, _ in let _ = (ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: false) |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in @@ -1240,7 +1239,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } return } - + if groupId == AnyHashable("featuredTop") { let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks) let _ = (context.account.postbox.combinedView(keys: [viewKey]) @@ -1265,7 +1264,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { return interaction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, []) } )) - + break } } @@ -1274,14 +1273,14 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { if file.isPremiumSticker && !hasPremium { let controller = PremiumIntroScreen(context: context, source: .stickers) interaction.getNavigationController()?.pushViewController(controller) - + return } var bubbleUpEmojiOrStickersets: [ItemCollectionId] = [] if let id = groupId.base as? ItemCollectionId, context.sharedContext.currentStickerSettings.with({ $0 }).dynamicPackOrder { bubbleUpEmojiOrStickersets.append(id) } - + let reference: FileMediaReference if groupId == AnyHashable("saved") { reference = .savedSticker(media: file) @@ -1333,14 +1332,14 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { guard let interaction, let collectionId = groupId.base as? ItemCollectionId else { return } - + if isPremiumLocked { let controller = PremiumIntroScreen(context: context, source: .stickers) interaction.getNavigationController()?.pushViewController(controller) - + return } - + let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks) let _ = (context.account.postbox.combinedView(keys: [viewKey]) |> take(1) @@ -1368,7 +1367,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } |> deliverOnMainQueue).start(completed: { }) - + break } } @@ -1470,7 +1469,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { guard let strongSelf = self else { return } - + switch query { case .none: strongSelf.stickerSearchDisposable.set(nil) @@ -1482,7 +1481,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { let resultSignal = strongSelf.context.engine.stickers.searchStickers(category: value, scope: [.installed, .remote]) |> mapToSignal { files -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in var items: [EmojiPagerContentComponent.Item] = [] - + var existingIds = Set() for item in files.items { let itemFile = item.file @@ -1501,7 +1500,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { ) items.append(item) } - + return .single(([EmojiPagerContentComponent.ItemGroup( supergroupId: "search", groupId: "search", @@ -1521,7 +1520,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { items: items )], files.isFinalResult)) } - + var version = 0 strongSelf.stickerSearchDisposable.set((resultSignal |> deliverOnMainQueue).start(next: { result in @@ -1574,7 +1573,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { stateContext: nil, addImage: nil ) - + self.inputDataDisposable = (combineLatest(queue: .mainQueue(), updatedInputData, .single(self.currentInputData.gifs) |> then(self.gifComponent.get() |> map(Optional.init)), @@ -1587,9 +1586,9 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } var inputData = inputData inputData.gifs = gifs - + let presentationData = context.sharedContext.currentPresentationData.with { $0 } - + if let emojiSearchResult = emojiSearchState.result { var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults? if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) { @@ -1607,7 +1606,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { inputData.emoji = emoji.withUpdatedItemGroups(panelItemGroups: emoji.panelItemGroups, contentItemGroups: emoji.contentItemGroups, itemContentUniqueId: emoji.itemContentUniqueId, emptySearchResults: emoji.emptySearchResults, searchState: .searching) } } - + if let stickerSearchResult = stickerSearchState.result { var stickerSearchResults: EmojiPagerContentComponent.EmptySearchResults? if !stickerSearchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) { @@ -1625,10 +1624,10 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { inputData.stickers = stickers.withUpdatedItemGroups(panelItemGroups: stickers.panelItemGroups, contentItemGroups: stickers.contentItemGroups, itemContentUniqueId: stickers.itemContentUniqueId, emptySearchResults: stickers.emptySearchResults, searchState: .searching) } } - + var transition: ComponentTransition = .immediate var useAnimation = false - + if let pagerView = strongSelf.entityKeyboardView.view as? EntityKeyboardComponent.View, let centralId = pagerView.centralId { if centralId == AnyHashable("emoji") { useAnimation = strongSelf.currentInputData.emoji != inputData.emoji @@ -1636,7 +1635,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { useAnimation = strongSelf.currentInputData.stickers != inputData.stickers } } - + if useAnimation { let contentAnimation: EmojiPagerContentComponent.ContentAnimation if let scheduledContentAnimationHint = strongSelf.scheduledContentAnimationHint { @@ -1650,7 +1649,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { strongSelf.currentInputData = strongSelf.processInputData(inputData: inputData) strongSelf.performLayout(transition: transition) }) - + self.inputNodeInteraction = ChatMediaInputNodeInteraction( navigateToCollectionId: { _ in }, @@ -1673,7 +1672,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { clearRecentlyUsedStickers: { } ) - + self.trendingGifsPromise.set(.single(nil)) self.trendingGifsPromise.set(paneGifSearchForQuery(context: context, query: "", offset: nil, incompleteResults: true, delayRequest: false, updateActivity: nil) |> map { items -> ChatMediaInputGifPaneTrendingState? in @@ -1683,13 +1682,13 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { return nil } }) - + self.gifInputInteraction = GifPagerContentComponent.InputInteraction( performItemAction: { [weak interaction] item, view, rect in guard let interaction else { return } - + if let (collection, result) = item.contextResult { let _ = interaction.sendBotContextResultAsGif(collection, result, view, rect, false, false) } else { @@ -1726,25 +1725,25 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { hideBackground: currentInputData.gifs?.component.hideBackground ?? false, hasSearch: currentInputData.gifs?.component.inputInteraction.hasSearch ?? false ) - + self.switchToTextInput = { [weak self] in if let self { self.interaction?.switchToTextInput() } } - + if self.currentInputData.gifs != nil { let hasRecentGifs = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs)) |> map { savedGifs -> Bool in return !savedGifs.isEmpty } - + self.hasRecentGifsDisposable = (hasRecentGifs |> deliverOnMainQueue).start(next: { [weak self] hasRecentGifs in guard let strongSelf = self else { return } - + if let gifMode = strongSelf.gifMode { if !hasRecentGifs, case .recent = gifMode { strongSelf.gifMode = .trending @@ -1754,7 +1753,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } }) } - + self.choosingStickerDisposable = (self.choosingSticker |> deliverOnMainQueue).start(next: { [weak self] value in if let self { @@ -1762,7 +1761,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } }) } - + deinit { self.inputDataDisposable?.dispose() self.hasRecentGifsDisposable?.dispose() @@ -1770,17 +1769,17 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { self.stickerSearchDisposable.dispose() self.choosingStickerDisposable?.dispose() } - + private func reloadGifContext() { if let gifInputInteraction = self.gifInputInteraction, let gifMode = self.gifMode { self.gifContext = GifContext(context: self.context, subject: gifMode, gifInputInteraction: gifInputInteraction, trendingGifs: self.trendingGifsPromise.get()) } } - + public func markInputCollapsed() { self.isMarkInputCollapsed = true } - + private func performLayout(transition: ComponentTransition) { guard let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, layoutMetrics, deviceMetrics, isVisible, isExpanded) = self.currentState else { return @@ -1788,14 +1787,14 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { self.scheduledInnerTransition = transition let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .immediate, interfaceState: interfaceState, layoutMetrics: layoutMetrics, deviceMetrics: deviceMetrics, isVisible: isVisible, isExpanded: isExpanded) } - + public func simulateUpdateLayout(isVisible: Bool) { guard let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, layoutMetrics, deviceMetrics, _, isExpanded) = self.currentState else { return } let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .immediate, interfaceState: interfaceState, layoutMetrics: layoutMetrics, deviceMetrics: deviceMetrics, isVisible: isVisible, isExpanded: isExpanded) } - + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.alpha.isZero || !self.view.isUserInteractionEnabled { return nil @@ -1805,7 +1804,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { return result } } - + if let backgroundView = self.backgroundView, backgroundView.frame.contains(point) { for subview in self.view.subviews.reversed() { if let result = subview.hitTest(self.view.convert(point, to: subview), with: event) { @@ -1813,13 +1812,13 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } } } - + return nil } - + public override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, layoutMetrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) { self.currentState = (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, layoutMetrics, deviceMetrics, isVisible, isExpanded) - + let innerTransition: ComponentTransition if let scheduledInnerTransition = self.scheduledInnerTransition { self.scheduledInnerTransition = nil @@ -1827,40 +1826,40 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } else { innerTransition = ComponentTransition(transition) } - + let wasMarkedInputCollapsed = self.isMarkInputCollapsed self.isMarkInputCollapsed = false - + var expandedHeight = standardInputHeight if self.isEmojiSearchActive && !isExpanded { expandedHeight += 118.0 } - + var hiddenInputHeight: CGFloat = 0.0 if self.hideInput && !self.adjustLayoutForHiddenInput { hiddenInputHeight = inputPanelHeight } - + let context = self.context let interaction = self.interaction let inputNodeInteraction = self.inputNodeInteraction! let trendingGifsPromise = self.trendingGifsPromise - + var mappedTransition = innerTransition - + if wasMarkedInputCollapsed || !isExpanded { mappedTransition = mappedTransition.withUserData(EntityKeyboardComponent.MarkInputCollapsed()) } - + var emojiContent: EmojiPagerContentComponent? = self.currentInputData.emoji var stickerContent: EmojiPagerContentComponent? = self.currentInputData.stickers var gifContent: EntityKeyboardGifContent? = self.currentInputData.gifs - + var stickersEnabled = true var emojiEnabled = true if let peer = interfaceState.renderedPeer?.peer as? TelegramChannel { if let boostsToUnrestrict = interfaceState.boostsToUnrestrict, boostsToUnrestrict > 0 { - + } else { if peer.hasBannedPermission(.banSendStickers) != nil { stickersEnabled = false @@ -1877,7 +1876,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { emojiEnabled = false } } - + if !stickersEnabled || interfaceState.interfaceState.editMessage != nil { stickerContent = nil gifContent = nil @@ -1896,16 +1895,16 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { gifContent = nil } } - + stickerContent?.inputInteractionHolder.inputInteraction = self.stickerInputInteraction self.currentInputData.emoji?.inputInteractionHolder.inputInteraction = self.emojiInputInteraction - + if let stickerInputInteraction = self.stickerInputInteraction { self.scrollingStickersGridPromise.set(stickerInputInteraction.scrollingStickersGridPromise.get()) } - + let startTime = CFAbsoluteTimeGetCurrent() - + var keyboardBottomInset = bottomInset if case .regular = layoutMetrics.widthClass, inputHeight > 0.0 && inputHeight < 100.0 { keyboardBottomInset = inputHeight + 15.0 @@ -1983,7 +1982,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { case .gifs: mappedMode = .gif } - + let searchContainerNode = PaneSearchContainerNode( context: context, theme: interfaceState.theme, @@ -2003,7 +2002,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } self.openGifContextMenu(file: item.file, contextResult: item.contextResult, sourceView: sourceNode.view, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved) } - + return searchContainerNode }, contentIdUpdated: { _ in }, @@ -2018,26 +2017,26 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { environment: {}, containerSize: CGSize(width: width, height: expandedHeight) ) - + var clippingFrame = CGRect(origin: CGPoint(), size: entityKeyboardSize) clippingFrame.size.height += 32.0 - + var entityKeyboardSizeFrame = CGRect(origin: CGPoint(), size: entityKeyboardSize) if self.hideInput { clippingFrame.size.height += self.topBackgroundExtension clippingFrame.origin.y -= self.topBackgroundExtension entityKeyboardSizeFrame.origin.y += self.topBackgroundExtension } - + if let entityKeyboardComponentView = self.entityKeyboardView.view { if entityKeyboardComponentView.superview == nil { self.clippingView.addSubview(entityKeyboardComponentView) } transition.updateFrame(view: entityKeyboardComponentView, frame: entityKeyboardSizeFrame) } - + transition.updateFrame(view: self.clippingView, frame: clippingFrame) - + if let backgroundView = self.backgroundView, let backgroundTintView = self.backgroundTintView, let backgroundTintMaskView = self.backgroundTintMaskView, let backgroundTintMaskContentView = self.backgroundTintMaskContentView, let backgroundChromeView = self.backgroundChromeView { var backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: entityKeyboardSize) if self.hideInput { @@ -2045,7 +2044,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { backgroundFrame.origin.y -= self.topBackgroundExtension } backgroundFrame.size.height += 32.0 - + if backgroundChromeView.image == nil { backgroundChromeView.image = GlassBackgroundView.generateForegroundImage(size: CGSize(width: keyboardCornerRadius * 2.0, height: keyboardCornerRadius * 2.0), isDark: interfaceState.theme.overallDarkAppearance, fillColor: .clear) } @@ -2053,33 +2052,33 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { backgroundTintView.image = generateStretchableFilledCircleImage(diameter: keyboardCornerRadius * 2.0, color: .white)?.withRenderingMode(.alwaysTemplate) } backgroundTintView.tintColor = interfaceState.theme.chat.inputMediaPanel.backgroundColor - + transition.updateFrame(view: backgroundView, frame: backgroundFrame) backgroundView.updateColor(color: .clear, forceKeepBlur: true, transition: .immediate) backgroundView.update(size: backgroundFrame.size, cornerRadius: keyboardCornerRadius, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition) - + transition.updateFrame(view: backgroundChromeView, frame: backgroundFrame.insetBy(dx: -1.0, dy: 0.0)) - + var backgroundTintMaskContentFrame = CGRect(origin: CGPoint(), size: backgroundFrame.size) if self.hideInput { backgroundTintMaskContentFrame.origin.y += self.topBackgroundExtension } transition.updateFrame(view: backgroundTintView, frame: backgroundFrame) - + transition.updateFrame(view: backgroundTintMaskView, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) transition.updateFrame(view: backgroundTintMaskContentView, frame: backgroundTintMaskContentFrame) } - + let layoutTime = CFAbsoluteTimeGetCurrent() - startTime if layoutTime > 0.1 { #if DEBUG print("EntityKeyboard layout in \(layoutTime * 1000.0) ms") #endif } - + return (expandedHeight, 0.0) } - + private func processStableItemGroupList(category: EntityKeyboardComponent.ReorderCategory, itemGroups: [EmojiPagerContentComponent.ItemGroup]) -> [EmojiPagerContentComponent.ItemGroup] { let nextIds: [ItemCollectionId] = itemGroups.compactMap { group -> ItemCollectionId? in if group.isEmbedded { @@ -2094,11 +2093,11 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { return nil } } - + let stableOrder = self.stableReorderableGroupOrder[category] ?? nextIds - + var updatedGroups: [EmojiPagerContentComponent.ItemGroup] = [] - + var staticIsFirst = false let topStaticGroups: [String] = [ "static", @@ -2120,7 +2119,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { break } } - + for group in itemGroups { if !(group.groupId.base is ItemCollectionId) { if group.groupId != AnyHashable("static") || staticIsFirst { @@ -2148,7 +2147,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { updatedGroups.append(group) } } - + let updatedIds = updatedGroups.compactMap { group -> ItemCollectionId? in if group.isEmbedded { return nil @@ -2164,10 +2163,10 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } self.stableReorderableGroupOrder[category] = updatedIds - + return updatedGroups } - + private func processInputData(inputData: InputData) -> InputData { return InputData( emoji: inputData.emoji.flatMap { emoji in @@ -2180,7 +2179,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { availableGifSearchEmojies: inputData.availableGifSearchEmojies ) } - + private func reorderItems(category: EntityKeyboardComponent.ReorderCategory, items: [EntityKeyboardTopPanelComponent.Item]) { var currentIds: [ItemCollectionId] = [] for item in items { @@ -2198,9 +2197,9 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { case .masks: namespace = Namespaces.ItemCollection.CloudMaskPacks } - + self.stableReorderableGroupOrder.removeValue(forKey: category) - + let _ = (self.context.engine.stickers.reorderStickerPacks(namespace: namespace, itemIds: currentIds) |> deliverOnMainQueue).start(completed: { [weak self] in guard let strongSelf = self else { @@ -2208,19 +2207,19 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } strongSelf.performLayout(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) }) - + if self.context.sharedContext.currentStickerSettings.with({ $0 }).dynamicPackOrder { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } self.interaction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_reorder", scale: 0.05, colors: [:], title: presentationData.strings.StickerPacksSettings_DynamicOrderOff, text: presentationData.strings.StickerPacksSettings_DynamicOrderOffInfo, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in return false }), nil) - + let _ = updateStickerSettingsInteractively(accountManager: self.context.sharedContext.accountManager, { return $0.withUpdatedDynamicPackOrder(false) }).start() } } - + private func openGifContextMenu(file: FileMediaReference, contextResult: (ChatContextResultCollection, ChatContextResult)?, sourceView: UIView, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) { let canSaveGif: Bool if file.media.fileId.namespace == Namespaces.Media.CloudFile { @@ -2228,7 +2227,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } else { canSaveGif = false } - + let _ = (self.context.engine.stickers.isGifSaved(id: file.media.fileId) |> deliverOnMainQueue).start(next: { [weak self] isGifSaved in guard let strongSelf = self else { @@ -2239,11 +2238,11 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { isGifSaved = false } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - + let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: PeerId(0), namespace: Namespaces.Message.Local, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file.media], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - + let gallery = strongSelf.context.sharedContext.makeGalleryController(context: strongSelf.context, source: .standaloneMessage(message, nil), streamSingleVideo: true, isPreview: true) - + var items: [ContextMenuItem] = [] items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_Send, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.actionSheet.primaryTextColor) @@ -2257,10 +2256,10 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } } }))) - + if let currentState = strongSelf.currentState { let interfaceState = currentState.interfaceState - + var isScheduledMessages = false if case .scheduledMessages = interfaceState.subject { isScheduledMessages = true @@ -2281,7 +2280,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } }))) } - + if isSaved && interfaceState.sendPaidMessageStars == nil { items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor) @@ -2295,7 +2294,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } } } - + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Preview_Gif_AddCaption, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddCaption"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in @@ -2304,7 +2303,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { let _ = self.interaction?.editGif(file, true) } }))) - + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Preview_Gif_Edit, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Draw"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in @@ -2313,13 +2312,13 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { let _ = self.interaction?.editGif(file, false) } }))) - + if isSaved || isGifSaved { items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor) }, action: { [weak self] _, f in f(.dismissWithoutContent) - + if let self { let _ = removeSavedGif(postbox: self.context.account.postbox, mediaId: file.media.fileId).start() } @@ -2329,11 +2328,11 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in f(.dismissWithoutContent) - + guard let strongSelf = self else { return } - + let context = strongSelf.context let presentationData = context.sharedContext.currentPresentationData.with { $0 } let _ = (toggleGifSaved(account: context.account, fileReference: file, saved: true) @@ -2356,7 +2355,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { guard let strongSelf = self else { return false } - + if case .info = action { let controller = PremiumIntroScreen(context: context, source: .savedGifs) strongSelf.interaction?.getNavigationController()?.pushViewController(controller) @@ -2368,12 +2367,12 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { }) }))) } - + let contextController = makeContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceView: sourceView, sourceRect: sourceRect)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) strongSelf.interaction?.presentGlobalOverlayController(contextController, nil) }) } - + public func scrollToGroupEmoji() { if let pagerView = self.entityKeyboardView.view as? EntityKeyboardComponent.View { pagerView.scrollToItemGroup(contentId: "emoji", groupId: "peerSpecific", subgroupId: nil) @@ -2385,17 +2384,17 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent let controller: ViewController weak var sourceView: UIView? let sourceRect: CGRect - + let navigationController: NavigationController? = nil - + let passthroughTouches: Bool = false - + init(controller: ViewController, sourceView: UIView?, sourceRect: CGRect) { self.controller = controller self.sourceView = sourceView self.sourceRect = sourceRect } - + func transitionInfo() -> ContextControllerTakeControllerInfo? { let sourceView = self.sourceView let sourceRect = self.sourceRect @@ -2407,7 +2406,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent } }) } - + func animatedIn() { if let controller = self.controller as? GalleryControllerProtocol { controller.viewDidAppear(false) @@ -2415,19 +2414,19 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent } } -public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputView, LegacyMessageInputPanelInputView, UIInputViewAudioFeedback { +public final class EntityInputView: UIInputView, LegacyMessageInputPanelInputView, UIInputViewAudioFeedback { private let context: AccountContext - + public var insertText: ((NSAttributedString) -> Void)? public var deleteBackwards: (() -> Void)? public var switchToKeyboard: (() -> Void)? public var presentController: ((ViewController) -> Void)? - + private var presentationData: PresentationData private var inputNode: ChatEntityKeyboardInputNode? private let animationCache: AnimationCache private let animationRenderer: MultiAnimationRenderer - + public init( context: AccountContext, isDark: Bool, @@ -2436,20 +2435,20 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi forceHasPremium: Bool = false ) { self.context = context - + self.animationCache = context.animationCache self.animationRenderer = context.animationRenderer - + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } if isDark { self.presentationData = self.presentationData.withUpdated(theme: defaultDarkPresentationTheme) } - + super.init(frame: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)), inputViewStyle: .default) - + self.autoresizingMask = [.flexibleWidth, .flexibleHeight] self.clipsToBounds = true - + let inputInteraction = EmojiPagerContentComponent.InputInteraction( performItemAction: { [weak self] groupId, item, _, _, _, _ in let hasPremium: Signal @@ -2462,7 +2461,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi guard let strongSelf = self else { return } - + if groupId == AnyHashable("featuredTop") { } else { if let file = item.itemFile?._parse() { @@ -2482,14 +2481,14 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi break } } - + if file.isPremiumEmoji && !hasPremium { let presentationData = context.sharedContext.currentPresentationData.with { $0 } strongSelf.presentController?(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.EmojiInput_PremiumEmojiToast_Text, undoText: presentationData.strings.EmojiInput_PremiumEmojiToast_Action, customAction: { guard let strongSelf = self else { return } - + var replaceImpl: ((ViewController) -> Void)? let controller = PremiumDemoScreen(context: strongSelf.context, subject: .animatedEmoji, action: { let controller = PremiumIntroScreen(context: strongSelf.context, source: .animatedEmoji) @@ -2503,7 +2502,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi controller.replace(with: c) } else { controller.dismiss() - + if let self { self.presentController?(c) } @@ -2513,7 +2512,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi }), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false })) return } - + if let emojiAttribute = emojiAttribute { AudioServicesPlaySystemSound(0x450) strongSelf.insertText?(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute])) @@ -2588,7 +2587,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi stateContext: nil, addImage: nil ) - + let semaphore = DispatchSemaphore(value: 0) var emojiComponent: EmojiPagerContentComponent? let _ = EmojiPagerContentComponent.emojiInputData( @@ -2597,7 +2596,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi animationRenderer: self.animationRenderer, isStandalone: true, subject: .generic, - hasTrending: false, + hasTrending: false, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, @@ -2608,7 +2607,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi semaphore.signal() }) semaphore.wait() - + if let emojiComponent = emojiComponent { let inputNode = ChatEntityKeyboardInputNode( context: self.context, @@ -2625,7 +2624,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi isStandalone: true, subject: .generic, hasTrending: false, - topReactionItems: [], + topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil, @@ -2658,31 +2657,31 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi self.addSubnode(inputNode) } } - + required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + public override func layoutSubviews() { super.layoutSubviews() - + guard let inputNode = self.inputNode else { return } - + for view in self.subviews { if view !== inputNode.view { view.isHidden = true } } - + let bottomInset: CGFloat if #available(iOS 11.0, *) { bottomInset = max(0.0, UIScreen.main.bounds.height - (self.window?.safeAreaLayoutGuide.layoutFrame.maxY ?? 10000.0)) } else { bottomInset = 0.0 } - + let presentationInterfaceState = ChatPresentationInterfaceState( chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, @@ -2739,7 +2738,7 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { public let presentGlobalOverlayController: (ViewController, Any?) -> Void public let navigationController: () -> NavigationController? public let updateIsPreviewing: (Bool) -> Void - + public init(sendSticker: @escaping (FileMediaReference, Bool, Bool, String?, Bool, UIView?, CGRect?, CALayer?, [ItemCollectionId]) -> Bool, sendEmoji: @escaping (TelegramMediaFile) -> Void, setStatus: @escaping (TelegramMediaFile) -> Void, copyEmoji: @escaping (TelegramMediaFile) -> Void, presentController: @escaping (ViewController, Any?) -> Void, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, updateIsPreviewing: @escaping (Bool) -> Void) { self.sendSticker = sendSticker self.sendEmoji = sendEmoji @@ -2751,26 +2750,26 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { self.updateIsPreviewing = updateIsPreviewing } } - + private final class ViewRecord { weak var view: UIView? let peekRecognizer: PeekControllerGestureRecognizer - + init(view: UIView, peekRecognizer: PeekControllerGestureRecognizer) { self.view = view self.peekRecognizer = peekRecognizer } } - + private let context: AccountContext private let forceTheme: PresentationTheme? private let interaction: Interaction? private let chatPeerId: EnginePeer.Id? private let present: (ViewController, Any?) -> Void - + private var viewRecords: [ViewRecord] = [] private weak var peekController: PeekController? - + public init(context: AccountContext, forceTheme: PresentationTheme? = nil, interaction: Interaction?, chatPeerId: EnginePeer.Id?, present: @escaping (ViewController, Any?) -> Void) { self.context = context self.forceTheme = forceTheme @@ -2778,12 +2777,12 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { self.chatPeerId = chatPeerId self.present = present } - + public func setGestureRecognizerEnabled(view: UIView, isEnabled: Bool, itemAtPoint: @escaping (CGPoint) -> (AnyHashable, CALayer, TelegramMediaFile)?) { self.viewRecords = self.viewRecords.filter({ $0.view != nil }) - + let viewRecord = self.viewRecords.first(where: { $0.view === view }) - + if let viewRecord = viewRecord { viewRecord.peekRecognizer.isEnabled = isEnabled } else { @@ -2794,19 +2793,19 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { guard let (groupId, itemLayer, file) = itemAtPoint(point) else { return nil } - + let context = strongSelf.context - + var bubbleUpEmojiOrStickersets: [ItemCollectionId] = [] if let id = groupId.base as? ItemCollectionId { if file.isCustomEmoji || context.sharedContext.currentStickerSettings.with({ $0 }).dynamicPackOrder { bubbleUpEmojiOrStickersets.append(id) } } - + let accountPeerId = context.account.peerId let chatPeerId = strongSelf.chatPeerId - + if file.isCustomEmoji { return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: accountPeerId)) |> map { peer -> Bool in var hasPremium = false @@ -2820,9 +2819,9 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { guard let strongSelf = self, let itemLayer = itemLayer else { return nil } - + var menuItems: [ContextMenuItem] = [] - + let presentationData = context.sharedContext.currentPresentationData.with { $0 } var isLocked = false if !hasPremium { @@ -2831,7 +2830,7 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { isLocked = false } } - + if let interaction = strongSelf.interaction { let sendEmoji: (TelegramMediaFile) -> Void = { file in interaction.sendEmoji(file) @@ -2842,13 +2841,13 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { let copyEmoji: (TelegramMediaFile) -> Void = { file in interaction.copyEmoji(file) } - + if let _ = strongSelf.chatPeerId { menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.EmojiPreview_SendEmoji, icon: { theme in if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) { return generateImage(image.size, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - + if let cgImage = image.cgImage { context.draw(cgImage, in: CGRect(origin: CGPoint(), size: size)) } @@ -2860,16 +2859,16 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { sendEmoji(file) f(.default) }))) - + menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.EmojiPreview_SetAsStatus, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Smile"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in f(.default) - + guard let strongSelf = self else { return } - + if hasPremium { setStatus(file) } else { @@ -2884,7 +2883,7 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { strongSelf.interaction?.navigationController()?.pushViewController(controller) } }))) - + menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.EmojiPreview_CopyEmoji, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in @@ -2893,14 +2892,14 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { }))) } } - + if menuItems.isEmpty { return nil } guard let view = view else { return nil } - + return (view, itemLayer.convert(itemLayer.bounds, to: view.layer), StickerPreviewPeekContent(context: context, theme: presentationData.theme, strings: presentationData.strings, item: .pack(file), isLocked: isLocked, menu: menuItems, openPremiumIntro: { guard let strongSelf = self, let interaction = strongSelf.interaction else { return @@ -2916,7 +2915,7 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { } else { sendPaidMessageStars = .single(nil) } - + return combineLatest( context.engine.stickers.isStickerSaved(id: file.fileId), context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: accountPeerId)) |> map { peer -> Bool in @@ -2934,15 +2933,15 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { return nil } var menuItems: [ContextMenuItem] = [] - + let presentationData = context.sharedContext.currentPresentationData.with { $0 } let isLocked = file.isPremiumSticker && !hasPremium - + if let interaction = strongSelf.interaction { let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView?, CGRect?, CALayer?) -> Void = { fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer in let _ = interaction.sendSticker(fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer, bubbleUpEmojiOrStickersets) } - + if let chatPeerId = strongSelf.chatPeerId, !isLocked { if chatPeerId != strongSelf.context.account.peerId && chatPeerId.namespace != Namespaces.Peer.SecretChat { menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendSilently, icon: { theme in @@ -2958,7 +2957,7 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { f(.default) }))) } - + if sendPaidMessageStars == nil { menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor) @@ -2974,11 +2973,11 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { }))) } } - + menuItems.append( .action(ContextMenuActionItem(text: isStarred ? presentationData.strings.Stickers_RemoveFromFavorites : presentationData.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unfave") : UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.default) - + let presentationData = context.sharedContext.currentPresentationData.with { $0 } let _ = (context.engine.stickers.toggleStickerSaved(file: file, saved: !isStarred) |> deliverOnMainQueue).start(next: { result in @@ -3005,18 +3004,18 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { }) })) ) - + if !file.isPremiumSticker { menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Stickers_EditSticker, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Draw"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) - + var emoji: [String] = [] for attribute in file.attributes { if case let .Sticker(displayText, _, _) = attribute { emoji = [displayText] } } - + let controller = context.sharedContext.makeStickerEditorScreen( context: context, source: (file, emoji), @@ -3031,7 +3030,7 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { interaction.navigationController()?.pushViewController(controller) }))) } - + loop: for attribute in file.attributes { switch attribute { case let .CustomEmoji(_, _, _, packReference), let .Sticker(_, packReference, _): @@ -3041,16 +3040,16 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in f(.default) - + guard let strongSelf = self else { return } - + let controller = strongSelf.context.sharedContext.makeStickerPackScreen(context: context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], actionTitle: nil, isEditing: false, expandIfNeeded: false, parentNavigationController: interaction.navigationController(), sendSticker: { file, sourceView, sourceRect in sendSticker(file, false, false, nil, false, sourceView, sourceRect, nil) return true }, actionPerformed: nil) - + interaction.navigationController()?.view.window?.endEditing(true) interaction.presentController(controller, nil) })) @@ -3061,31 +3060,31 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { break } } - + if groupId == AnyHashable("recent") { menuItems.append( .action(ContextMenuActionItem(text: presentationData.strings.Stickers_RemoveFromRecent, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor) }, action: { _, f in f(.default) - + guard let strongSelf = self else { return } - + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } interaction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.Conversation_StickerRemovedFromRecent, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), nil) - + strongSelf.context.engine.stickers.removeRecentlyUsedSticker(fileReference: .recentSticker(media: file)) })) ) } } - + guard let view = view else { return nil } - + return (view, itemLayer.convert(itemLayer.bounds, to: view.layer), StickerPreviewPeekContent(context: context, theme: presentationData.theme, strings: presentationData.strings, item: .pack(file), isLocked: isLocked && !isStarred, menu: menuItems, openPremiumIntro: { guard let strongSelf = self, let interaction = strongSelf.interaction else { return @@ -3099,7 +3098,7 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { guard let strongSelf = self else { return nil } - + var presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } if let forceTheme = strongSelf.forceTheme { presentationData = presentationData.withUpdated(theme: forceTheme) @@ -3120,7 +3119,7 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { guard let strongSelf = self else { return } - + let _ = strongSelf }) self.viewRecords.append(ViewRecord(view: view, peekRecognizer: peekRecognizer)) diff --git a/submodules/TelegramUI/Components/ComposePollScreen/Sources/PollAttachmentScreen.swift b/submodules/TelegramUI/Components/ComposePollScreen/Sources/PollAttachmentScreen.swift index 876b0964dc..c38c24e3aa 100644 --- a/submodules/TelegramUI/Components/ComposePollScreen/Sources/PollAttachmentScreen.swift +++ b/submodules/TelegramUI/Components/ComposePollScreen/Sources/PollAttachmentScreen.swift @@ -39,10 +39,7 @@ public func presentPollAttachmentScreen( chatLocation: nil, isScheduledMessages: false, buttons: availableButtons, - initialButton: .gallery, - makeEntityInputView: { - return nil - } + initialButton: .gallery ) let inputMediaNodeDataPromise = Promise(nil) if let inputMediaNodeData { @@ -64,7 +61,7 @@ public func presentPollAttachmentScreen( |> map(Optional.init) )) } - + attachmentController.requestController = { [weak attachmentController] type, controllerCompletion in let mediaPickerPollSubject: MediaPickerScreenImpl.Subject.AssetsMode.PollMode let filePickerPollSubject: AttachmentFileControllerSource.PollMode @@ -87,7 +84,7 @@ public func presentPollAttachmentScreen( locationPickerPollSubject = .option stickerPickerPollSubject = .option } - + switch type { case .gallery: let controller = MediaPickerScreenImpl( @@ -122,7 +119,7 @@ public func presentPollAttachmentScreen( bannedSendMedia: nil, presentGallery: { [weak attachmentController] in attachmentController?.dismiss(animated: true) - + let controller = MediaPickerScreenImpl( context: context, updatedPresentationData: updatedPresentationData, @@ -150,7 +147,7 @@ public func presentPollAttachmentScreen( }, presentFiles: { [weak attachmentController] in attachmentController?.dismiss(animated: true) - + let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } let controller = legacyICloudFilePicker(theme: presentationData.theme, documentTypes: ["public.item"], completion: { urls in guard let url = urls.first else { @@ -172,7 +169,7 @@ public func presentPollAttachmentScreen( if let audioMetadata = item.audioMetadata { attributes.append(.Audio(isVoice: false, duration: audioMetadata.duration, title: audioMetadata.title, performer: audioMetadata.performer, waveform: nil)) } - + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(item.fileSize), attributes: attributes, alternativeRepresentations: []) completion(.standalone(media: file)) }) diff --git a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift index c87d7d3985..040cc0c9de 100644 --- a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift +++ b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift @@ -1165,19 +1165,22 @@ public final class GlassBackgroundComponent: Component { private let isDark: Bool private let tintColor: GlassBackgroundView.TintColor private let isInteractive: Bool + private let isVisible: Bool public init( size: CGSize, cornerRadius: CGFloat, isDark: Bool, tintColor: GlassBackgroundView.TintColor, - isInteractive: Bool = false + isInteractive: Bool = false, + isVisible: Bool = true ) { self.size = size self.cornerRadius = cornerRadius self.isDark = isDark self.tintColor = tintColor self.isInteractive = isInteractive + self.isVisible = isVisible } public static func == (lhs: GlassBackgroundComponent, rhs: GlassBackgroundComponent) -> Bool { @@ -1196,12 +1199,15 @@ public final class GlassBackgroundComponent: Component { if lhs.isInteractive != rhs.isInteractive { return false } + if lhs.isVisible != rhs.isVisible { + return false + } return true } public final class View: GlassBackgroundView { func update(component: GlassBackgroundComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - self.update(size: component.size, cornerRadius: component.cornerRadius, isDark: component.isDark, tintColor: component.tintColor, isInteractive: component.isInteractive, transition: transition) + self.update(size: component.size, cornerRadius: component.cornerRadius, isDark: component.isDark, tintColor: component.tintColor, isInteractive: component.isInteractive, isVisible: component.isVisible, transition: transition) return component.size } diff --git a/submodules/TelegramUI/Components/LegacyMessageInputPanel/BUILD b/submodules/TelegramUI/Components/LegacyMessageInputPanel/BUILD index 97f4042d3c..fe0a6b4c34 100644 --- a/submodules/TelegramUI/Components/LegacyMessageInputPanel/BUILD +++ b/submodules/TelegramUI/Components/LegacyMessageInputPanel/BUILD @@ -17,13 +17,14 @@ swift_library( "//submodules/Postbox", "//submodules/AccountContext", "//submodules/LegacyComponents", + "//submodules/ChatPresentationInterfaceState", "//submodules/ComponentFlow", "//submodules/TelegramPresentationData", "//submodules/ContextUI", "//submodules/TooltipUI", "//submodules/UndoUI", + "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", "//submodules/TelegramUI/Components/MessageInputPanelComponent", - "//submodules/TelegramUI/Components/LegacyMessageInputPanelInputView", "//submodules/TelegramNotices", "//submodules/TextFormat", "//submodules/TelegramUIPreferences", diff --git a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift index 4801c4627f..0d4bad62c6 100644 --- a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift +++ b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift @@ -7,18 +7,18 @@ import TelegramCore import Postbox import SwiftSignalKit import AccountContext -import LegacyComponents import ComponentFlow import MessageInputPanelComponent import TelegramPresentationData import ContextUI import TooltipUI -import LegacyMessageInputPanelInputView import UndoUI import TelegramNotices import TextFormat import TelegramUIPreferences import Pasteboard +import ChatEntityKeyboardInputNode +import ChatPresentationInterfaceState public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { private let context: AccountContext @@ -26,78 +26,181 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { private let isScheduledMessages: Bool private let isFile: Bool private let hasTimer: Bool + private let customEmojiAvailable: Bool private let pushViewController: (ViewController) -> Void private let present: (ViewController) -> Void - private let presentInGlobalOverlay: (ViewController) -> Void - private let makeEntityInputView: () -> LegacyMessageInputPanelInputView? - + private let presentInGlobalOverlay: (ViewController) -> Void + private let getNavigationController: () -> NavigationController? + private let state = ComponentState() private let inputPanelExternalState = MessageInputPanelComponent.ExternalState() private let inputPanel = ComponentView() - + private var currentTimeout: Int32? private var currentIsEditing = false private var currentHeight: CGFloat? private var currentIsVideo = false private var currentIsCaptionAbove = false - + private var currentInputMode: MessageInputPanelComponent.InputMode = .text + private var currentAdditionalInputHeight: CGFloat = 0.0 + private var currentSafeAreaInset: UIEdgeInsets = .zero + private var currentContainerBottomInset: CGFloat = 0.0 + private var usesContainerLayout = false + private let hapticFeedback = HapticFeedback() - - private var inputView: LegacyMessageInputPanelInputView? - private var isEmojiKeyboardActive = false - + + private let inputMediaNodeDataPromise = Promise() + private var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData? + private var inputMediaNodeDataDisposable: Disposable? + private var inputMediaNodeStateContext = ChatEntityKeyboardInputNode.StateContext() + private var inputMediaInteraction: ChatEntityKeyboardInputNode.Interaction? + private var inputMediaNode: ChatEntityKeyboardInputNode? + public var sendPressed: ((NSAttributedString?) -> Void)? public var focusUpdated: ((Bool) -> Void)? public var heightUpdated: ((Bool) -> Void)? public var timerUpdated: ((NSNumber?) -> Void)? public var captionIsAboveUpdated: ((Bool) -> Void)? - + + public var additionalInputHeight: CGFloat { + return self.currentAdditionalInputHeight + } + private weak var undoController: UndoOverlayController? private weak var tooltipController: TooltipScreen? - + private var isAIEnabled: Bool = false - + private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, keyboardHeight: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, metrics: LayoutMetrics)? - + public init( context: AccountContext, chatLocation: ChatLocation, isScheduledMessages: Bool, isFile: Bool, hasTimer: Bool, + customEmojiAvailable: Bool, pushViewController: @escaping (ViewController) -> Void, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void, - makeEntityInputView: @escaping () -> LegacyMessageInputPanelInputView? + getNavigationController: @escaping () -> NavigationController? ) { self.context = context self.chatLocation = chatLocation self.isScheduledMessages = isScheduledMessages self.isFile = isFile self.hasTimer = hasTimer + self.customEmojiAvailable = customEmojiAvailable self.pushViewController = pushViewController self.present = present self.presentInGlobalOverlay = presentInGlobalOverlay - self.makeEntityInputView = makeEntityInputView - + self.getNavigationController = getNavigationController + super.init() - + if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["ios_disable_ai_attach"] as? Double, value == 1.0 { } else if let peerId = chatLocation.peerId, peerId.namespace != Namespaces.Peer.SecretChat { self.isAIEnabled = true } - + + self.inputMediaNodeDataPromise.set( + ChatEntityKeyboardInputNode.inputData( + context: context, + chatPeerId: nil, + areCustomEmojiEnabled: customEmojiAvailable, + hasTrending: false, + hasStickers: false, + hasGifs: false, + sendGif: nil + ) + ) + self.inputMediaNodeDataDisposable = (self.inputMediaNodeDataPromise.get() + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let self else { + return + } + self.inputMediaNodeData = value + if case .emoji = self.currentInputMode { + self.update(transition: .immediate) + } + }) + + self.inputMediaInteraction = ChatEntityKeyboardInputNode.Interaction( + sendSticker: { _, _, _, _, _, _, _, _, _ in + return false + }, + sendEmoji: { _, _, _ in + }, + sendGif: { _, _, _, _, _ in + return false + }, + sendBotContextResultAsGif: { _, _, _, _, _, _ in + return false + }, + editGif: { _, _ in + }, + updateChoosingSticker: { _ in + }, + switchToTextInput: { [weak self] in + self?.activateInput() + }, + dismissTextInput: { + }, + insertText: { [weak self] text in + self?.inputPanelExternalState.insertText(text) + }, + backwardsDeleteText: { [weak self] in + self?.inputPanelExternalState.deleteBackward() + }, + openStickerEditor: { + }, + presentController: { [weak self] controller, _ in + guard let self else { + return + } + self.prepareForPresentedController(controller) + self.present(controller) + }, + presentGlobalOverlayController: { [weak self] controller, _ in + guard let self else { + return + } + self.prepareForPresentedController(controller) + self.presentInGlobalOverlay(controller) + }, + getNavigationController: getNavigationController, + requestLayout: { [weak self] transition in + self?.update(transition: transition) + } + ) + self.inputMediaInteraction?.forceTheme = defaultDarkColorPresentationTheme + self.state._updated = { [weak self] transition, _ in if let self { self.update(transition: transition.containedViewLayoutTransition) } } } - - public func updateLayoutSize(_ size: CGSize, keyboardHeight: CGFloat, sideInset: CGFloat, animated: Bool) -> CGFloat { - return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, keyboardHeight: keyboardHeight, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), isMediaInputExpanded: false) + + deinit { + self.inputMediaNodeDataDisposable?.dispose() } - + + public func updateLayoutSize(_ size: CGSize, keyboardHeight: CGFloat, sideInset: CGFloat, animated: Bool) -> CGFloat { + self.currentSafeAreaInset = .zero + self.currentContainerBottomInset = 0.0 + self.usesContainerLayout = false + return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, keyboardHeight: keyboardHeight, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), isMediaInputExpanded: false) + } + + @objc(updateContainerLayoutSize:safeAreaInset:bottomInset:keyboardHeight:animated:) + public func updateContainerLayoutSize(_ size: CGSize, safeAreaInset: UIEdgeInsets, bottomInset: CGFloat, keyboardHeight: CGFloat, animated: Bool) -> CGFloat { + self.currentSafeAreaInset = safeAreaInset + self.currentContainerBottomInset = bottomInset + self.usesContainerLayout = true + return self.updateLayout(width: size.width, leftInset: 0.0, rightInset: 0.0, bottomInset: 0.0, keyboardHeight: keyboardHeight, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), isMediaInputExpanded: false) + } + public func caption() -> NSAttributedString { if let view = self.inputPanel.view as? MessageInputPanelComponent.View, case let .text(caption) = view.getSendMessageInput() { return caption @@ -105,7 +208,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { return NSAttributedString() } } - + private var scheduledMessageInput: MessageInputPanelComponent.SendMessageInput? public func setCaption(_ caption: NSAttributedString?) { let sendMessageInput = MessageInputPanelComponent.SendMessageInput.text(caption ?? NSAttributedString()) @@ -115,12 +218,12 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { self.scheduledMessageInput = sendMessageInput } } - + public func animate(_ view: UIView, frame: CGRect) { let transition = ComponentTransition.spring(duration: 0.4) transition.setFrame(view: view, frame: frame) } - + public func setTimeout(_ timeout: Int32, isVideo: Bool, isCaptionAbove: Bool) { self.dismissAllTooltips() var timeout: Int32? = timeout @@ -131,33 +234,56 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { self.currentIsVideo = isVideo self.currentIsCaptionAbove = isCaptionAbove } - + public func activateInput() { + let transition: ContainedViewLayoutTransition + if self.currentInputMode != .text { + transition = .animated(duration: 0.4, curve: .spring) + } else { + transition = .immediate + } + self.currentInputMode = .text + self.update(transition: transition) if let view = self.inputPanel.view as? MessageInputPanelComponent.View { view.activateInput() } } - + public func dismissInput() -> Bool { if let view = self.inputPanel.view as? MessageInputPanelComponent.View { if view.canDeactivateInput() { - self.isEmojiKeyboardActive = false - self.inputView = nil - view.deactivateInput(force: true) + let inputModeTransition: ContainedViewLayoutTransition + if self.currentInputMode != .text { + self.currentInputMode = .text + inputModeTransition = .animated(duration: 0.4, curve: .spring) + } else { + inputModeTransition = .immediate + } + if view.isActive { + view.deactivateInput(force: true) + } + if !inputModeTransition.isAnimated { + return true + } + self.update(transition: inputModeTransition) return true } else { view.animateError() return false } } else { + if self.currentInputMode != .text { + self.currentInputMode = .text + self.update(transition: .animated(duration: 0.4, curve: .spring)) + } return true } } - + public func onAnimateOut() { self.dismissAllTooltips() } - + public func baseHeight() -> CGFloat { return 52.0 } @@ -165,25 +291,41 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + private func update(transition: ContainedViewLayoutTransition) { if let (width, leftInset, rightInset, bottomInset, keyboardHeight, additionalSideInsets, maxHeight, isSecondary, metrics) = self.validLayout { let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, keyboardHeight: keyboardHeight, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: transition, metrics: metrics, isMediaInputExpanded: false) } } - - public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, keyboardHeight: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { + + public func updateLayout( + width: CGFloat, + leftInset: CGFloat, + rightInset: CGFloat, + bottomInset: CGFloat, + keyboardHeight: CGFloat, + additionalSideInsets: UIEdgeInsets, + maxHeight: CGFloat, + isSecondary: Bool, + transition: ContainedViewLayoutTransition, + metrics: LayoutMetrics, + isMediaInputExpanded: Bool + ) -> CGFloat { let previousLayout = self.validLayout self.validLayout = (width, leftInset, rightInset, bottomInset, keyboardHeight, additionalSideInsets, maxHeight, isSecondary, metrics) - + var transition = transition if keyboardHeight.isZero, let previousKeyboardHeight = previousLayout?.keyboardHeight, previousKeyboardHeight > 0.0, !transition.isAnimated { transition = .animated(duration: 0.4, curve: .spring) } - + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let theme = defaultDarkColorPresentationTheme - + let isLandscape = width > maxHeight + let deviceMetrics = DeviceMetrics(screenSize: CGSize(width: width, height: maxHeight), scale: UIScreen.main.scale, statusBarHeight: 0.0, onScreenNavigationHeight: nil) + let standardInputHeight = deviceMetrics.standardInputHeight(inLandscape: isLandscape) + let keyboardWasHidden = self.inputPanelExternalState.isKeyboardHidden + var timeoutValue: String? var timeoutSelected = false if self.isFile { @@ -200,25 +342,34 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { timeoutValue = "1" } } - - var maxInputPanelHeight = maxHeight - if keyboardHeight.isZero { + + let reservedKeyboardHeight: CGFloat + if case .emoji = self.currentInputMode { + reservedKeyboardHeight = max(keyboardHeight, standardInputHeight) + } else if self.inputPanelExternalState.isEditing && keyboardHeight.isZero && keyboardWasHidden { + reservedKeyboardHeight = standardInputHeight + } else { + reservedKeyboardHeight = keyboardHeight + } + + let maxInputPanelHeight: CGFloat + if keyboardHeight.isZero, case .text = self.currentInputMode, !keyboardWasHidden { maxInputPanelHeight = 60.0 } else { - maxInputPanelHeight = maxHeight - keyboardHeight - 100.0 + maxInputPanelHeight = max(60.0, maxHeight - reservedKeyboardHeight - 100.0) } - + var resetInputContents: MessageInputPanelComponent.SendMessageInput? if let scheduledMessageInput = self.scheduledMessageInput { resetInputContents = scheduledMessageInput self.scheduledMessageInput = nil } - + var hasTimer = self.hasTimer && self.chatLocation.peerId?.namespace == Namespaces.Peer.CloudUser && !self.isScheduledMessages if self.chatLocation.peerId?.isRepliesOrSavedMessages(accountPeerId: self.context.account.peerId) == true { hasTimer = false } - + self.inputPanel.parentState = self.state let inputPanelSize = self.inputPanel.update( transition: ComponentTransition(transition), @@ -236,9 +387,15 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { alwaysDarkWhenHasText: false, resetInputContents: resetInputContents, nextInputMode: { [weak self] _ in - if self?.isEmojiKeyboardActive == true { + guard let self else { + return .emoji + } + switch self.currentInputMode { + case .text: + return .emoji + case .emoji: return .text - } else { + default: return .emoji } }, @@ -258,31 +415,23 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { stopAndPreviewMediaRecording: nil, discardMediaRecordingPreview: nil, attachmentAction: { [weak self] in - if let self { - self.toggleIsCaptionAbove() - } + self?.toggleIsCaptionAbove() }, attachmentButtonMode: self.currentIsCaptionAbove ? .captionDown : .captionUp, myReaction: nil, likeAction: nil, likeOptionsAction: nil, inputModeAction: { [weak self] in - if let self { - self.toggleInputMode() - } + self?.toggleInputMode() }, timeoutAction: hasTimer ? { [weak self] sourceView, gesture in - if let self { - self.presentTimeoutSetup(sourceView: sourceView, gesture: gesture) - } + self?.presentTimeoutSetup(sourceView: sourceView, gesture: gesture) } : nil, forwardAction: nil, paidMessageAction: nil, moreAction: nil, presentCaptionPositionTooltip: { [weak self] sourceView in - if let self { - self.presentCaptionPositionTooltip(sourceView: sourceView) - } + self?.presentCaptionPositionTooltip(sourceView: sourceView) }, presentVoiceMessagesUnavailableTooltip: nil, presentTextLengthLimitTooltip: nil, @@ -300,63 +449,247 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { displayGradient: false, bottomInset: 0.0, isFormattingLocked: false, - hideKeyboard: false, - customInputView: self.inputView, - forceIsEditing: false, + hideKeyboard: self.currentInputMode == .emoji, + customInputView: nil, + forceIsEditing: self.currentInputMode == .emoji, disabledPlaceholder: nil, header: nil, isChannel: false, storyItem: nil, chatLocation: self.chatLocation, aiCompose: self.isAIEnabled ? { [weak self] in - guard let self else { - return - } - self.openAICompose() + self?.openAICompose() } : nil ) ), environment: {}, containerSize: CGSize(width: width, height: maxInputPanelHeight) ) + let inputPanelHeight = inputPanelSize.height - 8.0 + var totalHeight = inputPanelHeight + var inputMediaHeight: CGFloat = 0.0 + self.currentAdditionalInputHeight = 0.0 + var inputMediaNodeForLayout: ChatEntityKeyboardInputNode? + var isNewInputMediaNode = false + var retainedInputHeight = keyboardHeight + var shouldRetainHiddenInputHeight = false + + if case .emoji = self.currentInputMode, let inputData = self.inputMediaNodeData { + let inputMediaNode: ChatEntityKeyboardInputNode + if let current = self.inputMediaNode { + inputMediaNode = current + } else { + isNewInputMediaNode = true + inputMediaNode = ChatEntityKeyboardInputNode( + context: self.context, + currentInputData: inputData, + updatedInputData: self.inputMediaNodeDataPromise.get(), + defaultToEmojiTab: true, + opaqueTopPanelBackground: false, + useOpaqueTheme: false, + interaction: self.inputMediaInteraction, + chatPeerId: nil, + stateContext: self.inputMediaNodeStateContext + ) + inputMediaNode.clipsToBounds = true + inputMediaNode.externalTopPanelContainerImpl = nil + inputMediaNode.useExternalSearchContainer = true + self.inputMediaNode = inputMediaNode + } + + if inputMediaNode.view.superview == nil { + if let inputPanelView = self.inputPanel.view { + self.view.insertSubview(inputMediaNode.view, belowSubview: inputPanelView) + } else { + self.view.addSubview(inputMediaNode.view) + } + } + inputMediaNodeForLayout = inputMediaNode + + let inputPresentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) + let presentationInterfaceState = ChatPresentationInterfaceState( + chatWallpaper: .builtin(WallpaperSettings()), + theme: inputPresentationData.theme, + preferredGlassType: .default, + strings: inputPresentationData.strings, + dateTimeFormat: inputPresentationData.dateTimeFormat, + nameDisplayOrder: inputPresentationData.nameDisplayOrder, + limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, + fontSize: inputPresentationData.chatFontSize, + bubbleCorners: inputPresentationData.chatBubbleCorners, + accountPeerId: self.context.account.peerId, + mode: .standard(.default), + chatLocation: .peer(id: self.context.account.peerId), + subject: nil, + peerNearbyData: nil, + greetingData: nil, + pendingUnpinnedAllMessages: false, + activeGroupCallInfo: nil, + hasActiveGroupCall: false, + threadData: nil, + isGeneralThreadClosed: nil, + replyMessage: nil, + accountPeerColor: nil, + businessIntro: nil + ) + + let heightAndOverflow = inputMediaNode.updateLayout( + width: width, + leftInset: 0.0, + rightInset: 0.0, + bottomInset: 0.0, + standardInputHeight: standardInputHeight, + inputHeight: 0.0, + maximumHeight: maxHeight, + inputPanelHeight: 0.0, + transition: .immediate, + interfaceState: presentationInterfaceState, + layoutMetrics: metrics, + deviceMetrics: deviceMetrics, + isVisible: true, + isExpanded: false + ) + inputMediaHeight = heightAndOverflow.0 + self.currentAdditionalInputHeight = inputMediaHeight + totalHeight += inputMediaHeight + } else if let inputMediaNode = self.inputMediaNode { + self.inputMediaNode = nil + + if transition.isAnimated { + var dismissingInputHeight = keyboardHeight + if self.inputPanelExternalState.isEditing && (dismissingInputHeight.isZero && keyboardWasHidden) { + dismissingInputHeight = max(dismissingInputHeight, standardInputHeight) + } + let targetOriginY: CGFloat + if self.usesContainerLayout { + if dismissingInputHeight > 0.0 { + targetOriginY = maxHeight - dismissingInputHeight + } else { + targetOriginY = maxHeight + } + } else { + if dismissingInputHeight > 0.0 { + targetOriginY = inputPanelHeight + } else { + targetOriginY = inputPanelHeight + inputMediaNode.frame.height + } + } + let targetFrame = CGRect( + origin: CGPoint(x: inputMediaNode.frame.minX, y: targetOriginY), + size: inputMediaNode.frame.size + ) + transition.updateFrame(view: inputMediaNode.view, frame: targetFrame) + inputMediaNode.view.layer.animateAlpha(from: inputMediaNode.view.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak inputMediaNode] _ in + inputMediaNode?.view.removeFromSuperview() + }) + } else { + inputMediaNode.view.removeFromSuperview() + } + } + + if self.inputPanelExternalState.isEditing { + if case .emoji = self.currentInputMode { + retainedInputHeight = max(retainedInputHeight, standardInputHeight) + shouldRetainHiddenInputHeight = true + } else if retainedInputHeight.isZero && keyboardWasHidden { + retainedInputHeight = max(retainedInputHeight, standardInputHeight) + shouldRetainHiddenInputHeight = true + } + } + if self.currentAdditionalInputHeight.isZero && retainedInputHeight > 0.0 && shouldRetainHiddenInputHeight { + self.currentAdditionalInputHeight = retainedInputHeight + totalHeight += retainedInputHeight + } + + let isLandscapePhone = width > maxHeight && UIDevice.current.userInterfaceIdiom != .pad + let collapsedCaptionTopInset = self.currentSafeAreaInset.top + 48.0 + let expandedCaptionTopInset = self.currentSafeAreaInset.top + 8.0 + + var inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: -8.0), size: inputPanelSize) + var inputMediaFrame = CGRect(origin: CGPoint(x: 0.0, y: inputPanelHeight), size: CGSize(width: width, height: inputMediaHeight)) + + if self.usesContainerLayout { + if isLandscapePhone { + inputPanelFrame.origin.y = maxHeight + 16.0 - 8.0 + inputMediaFrame.origin.y = maxHeight + 16.0 + } else if case .emoji = self.currentInputMode { + inputMediaFrame.origin.y = maxHeight - inputMediaHeight + if self.currentIsCaptionAbove { + inputPanelFrame.origin.y = expandedCaptionTopInset - 8.0 + } else { + inputPanelFrame.origin.y = inputMediaFrame.minY - inputPanelHeight - 8.0 + } + } else { + if self.currentIsCaptionAbove { + inputPanelFrame.origin.y = (retainedInputHeight > 0.0 ? expandedCaptionTopInset : collapsedCaptionTopInset) - 8.0 + } else { + let bottomOffset = max(self.currentContainerBottomInset, retainedInputHeight) + inputPanelFrame.origin.y = maxHeight - inputPanelHeight - bottomOffset - 8.0 + } + inputMediaFrame.origin.y = maxHeight + } + } + if let view = self.inputPanel.view { if view.superview == nil { self.view.addSubview(view) } - let inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: -8.0), size: inputPanelSize) transition.updateFrame(view: view, frame: inputPanelFrame) } - + + if let inputMediaNode = inputMediaNodeForLayout { + if isNewInputMediaNode && transition.isAnimated { + inputMediaNode.view.frame = inputMediaFrame.offsetBy(dx: 0.0, dy: inputMediaHeight) + inputMediaNode.view.alpha = 0.0 + inputMediaNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + inputMediaNode.view.alpha = 1.0 + transition.updateFrame(view: inputMediaNode.view, frame: inputMediaFrame) + } + if self.currentIsEditing != self.inputPanelExternalState.isEditing { self.currentIsEditing = self.inputPanelExternalState.isEditing self.focusUpdated?(self.currentIsEditing) } - - if self.currentHeight != inputPanelSize.height { - self.currentHeight = inputPanelSize.height + + if self.currentHeight != totalHeight { + self.currentHeight = totalHeight self.heightUpdated?(transition.isAnimated) } - - return inputPanelSize.height - 8.0 + + return totalHeight } - + + private func prepareForPresentedController(_ controller: ViewController) { + if controller is UndoOverlayController { + return + } + if let view = self.inputPanel.view as? MessageInputPanelComponent.View, view.isActive { + view.deactivateInput(force: true) + } + if self.currentInputMode != .text { + self.currentInputMode = .text + self.update(transition: .immediate) + } + } + private func openAICompose() { Task { @MainActor [weak self] in guard let self else { return } - + let effectiveInputText: NSAttributedString = self.caption() if effectiveInputText.length == 0 { return } - + let inputText = trimChatInputText(effectiveInputText) var entities: [MessageTextEntity] = [] if inputText.length != 0 { entities = generateTextEntities(inputText.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(inputText, maxAnimatedEmojisInText: 0)) } - + self.pushViewController(await self.context.sharedContext.makeTextProcessingScreen( context: self.context, theme: defaultDarkColorPresentationTheme, @@ -372,86 +705,57 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { sendContextActions: nil ), inputText: TextWithEntities(text: inputText.string, entities: entities), - copyResult: { [weak self] text in - guard let self else { - return - } - let _ = self + copyResult: { text in storeMessageTextInPasteboard(text.text, entities: text.entities) }, translateChat: nil )) } } - + private func toggleInputMode() { - self.isEmojiKeyboardActive = !self.isEmojiKeyboardActive - - if self.isEmojiKeyboardActive { - let inputView = self.makeEntityInputView() - inputView?.insertText = { [weak self] text in - if let self { - self.inputPanelExternalState.insertText(text) - } + switch self.currentInputMode { + case .text: + self.currentInputMode = .emoji + self.update(transition: .animated(duration: 0.4, curve: .spring)) + if let view = self.inputPanel.view as? MessageInputPanelComponent.View, !view.isActive { + view.activateInput() } - inputView?.deleteBackwards = { [weak self] in - if let self { - self.inputPanelExternalState.deleteBackward() - } - } - inputView?.switchToKeyboard = { [weak self] in - if let self { - self.isEmojiKeyboardActive = false - self.inputView = nil - self.update(transition: .immediate) - } - } - inputView?.presentController = { [weak self] c in - if let self { - if !(c is UndoOverlayController) { - self.isEmojiKeyboardActive = false - if let view = self.inputPanel.view as? MessageInputPanelComponent.View { - view.deactivateInput(force: true) - } - } - self.present(c) - } - } - self.inputView = inputView - self.update(transition: .immediate) - } else { - self.inputView = nil - self.update(transition: .immediate) + case .emoji: + self.activateInput() + default: + self.currentInputMode = .emoji + self.update(transition: .animated(duration: 0.4, curve: .spring)) } } - + private func toggleIsCaptionAbove() { self.currentIsCaptionAbove = !self.currentIsCaptionAbove self.captionIsAboveUpdated?(self.currentIsCaptionAbove) self.update(transition: .animated(duration: 0.3, curve: .spring)) - + self.dismissAllTooltips() - + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - + let title = self.currentIsCaptionAbove ? presentationData.strings.MediaPicker_InvertCaption_Updated_Up_Title : presentationData.strings.MediaPicker_InvertCaption_Updated_Down_Title let text = self.currentIsCaptionAbove ? presentationData.strings.MediaPicker_InvertCaption_Updated_Up_Text : presentationData.strings.MediaPicker_InvertCaption_Updated_Down_Text let animationName = self.currentIsCaptionAbove ? "message_preview_sort_above" : "message_preview_sort_below" - + let controller = UndoOverlayController( presentationData: presentationData, content: .universal(animation: animationName, scale: 1.0, colors: ["__allcolors__": UIColor.white], title: title, text: text, customUndoText: nil, timeout: 2.0), elevatedLayout: false, position: self.currentIsCaptionAbove ? .bottom : .top, - action: { _ in return false } + action: { _ in return false } ) self.present(controller) self.undoController = controller } - + private func presentTimeoutSetup(sourceView: UIView, gesture: ContextGesture?) { self.hapticFeedback.impact(.light) - + var items: [ContextMenuItem] = [] let updateTimeout: (Int32?) -> Void = { [weak self] timeout in @@ -465,46 +769,46 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { } } } - + let currentValue = self.currentTimeout let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme) let title = presentationData.strings.MediaPicker_Timer_Description let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil - - items.append(.action(ContextMenuActionItem(text: title, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction))) + + items.append(.action(ContextMenuActionItem(text: title, textLayout: .multiline, textFont: .small, icon: { _ in nil }, action: emptyAction))) items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_Timer_ViewOnce, icon: { theme in return currentValue == viewOnceTimeout ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : UIImage() - }, action: { _, a in - a(.default) - + }, action: { _, action in + action(.default) + updateTimeout(viewOnceTimeout) }))) - + let values: [Int32] = [3, 10, 30] - + for value in values { items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_Timer_Seconds(value), icon: { theme in return currentValue == value ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : UIImage() - }, action: { _, a in - a(.default) - + }, action: { _, action in + action(.default) + updateTimeout(value) }))) } - + items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_Timer_DoNotDelete, icon: { theme in return currentValue == nil ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : UIImage() - }, action: { _, a in - a(.default) - + }, action: { _, action in + action(.default) + updateTimeout(nil) }))) - + let contextController = makeContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(sourceView: sourceView, position: self.currentIsCaptionAbove ? .bottom : .top)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) self.present(contextController) } - + private func dismissAllTooltips() { if let undoController = self.undoController { self.undoController = nil @@ -515,17 +819,17 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { tooltipController.dismiss() } } - + private func presentTimeoutTooltip(sourceView: UIView, timeout: Int32?) { guard let superview = self.view.superview?.superview else { return } self.dismissAllTooltips() - + let parentFrame = superview.convert(superview.bounds, to: nil) let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0) let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 2.0), size: CGSize()) - + let isVideo = self.currentIsVideo let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let text: String @@ -540,7 +844,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { text = isVideo ? presentationData.strings.MediaPicker_Timer_Video_KeepTooltip : presentationData.strings.MediaPicker_Timer_Photo_KeepTooltip iconName = "anim_autoremove_off" } - + let tooltipController = TooltipScreen( account: self.context.account, sharedContext: self.context.sharedContext, @@ -559,14 +863,13 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { self.tooltipController = tooltipController self.present(tooltipController) } - + private func presentCaptionPositionTooltip(sourceView: UIView) { guard let superview = self.view.superview?.superview else { return } self.dismissAllTooltips() - - + let _ = (ApplicationSpecificNotice.getCaptionAboveMediaTooltip(accountManager: self.context.sharedContext.accountManager) |> deliverOnMainQueue).start(next: { [weak self] count in guard let self else { @@ -575,13 +878,13 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { if count > 2 { return } - + let parentFrame = superview.convert(superview.bounds, to: nil) let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0) let location = CGRect(origin: CGPoint(x: absoluteFrame.midX + 2.0, y: absoluteFrame.minY + 6.0), size: CGSize()) - + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - + let tooltipController = TooltipScreen( account: self.context.account, sharedContext: self.context.sharedContext, @@ -600,16 +903,22 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { ) self.tooltipController = tooltipController self.present(tooltipController) - + let _ = ApplicationSpecificNotice.incrementCaptionAboveMediaTooltip(accountManager: self.context.sharedContext.accountManager).start() }) } - + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - let result = super.hitTest(point, with: event) if let view = self.inputPanel.view, let panelResult = view.hitTest(self.view.convert(point, to: view), with: event) { return panelResult } + if let inputMediaNode = self.inputMediaNode, let inputMediaResult = inputMediaNode.view.hitTest(self.view.convert(point, to: inputMediaNode.view), with: event) { + return inputMediaResult + } + let result = super.hitTest(point, with: event) + if result === self.view { + return nil + } return result } } @@ -619,7 +928,7 @@ private final class HeaderContextReferenceContentSource: ContextReferenceContent var keepInPlace: Bool { return true } - + let position: ContextControllerReferenceViewInfo.ActionsPosition init(sourceView: UIView, position: ContextControllerReferenceViewInfo.ActionsPosition) { diff --git a/submodules/TelegramUI/Components/LiveLocationHeaderPanelComponent/Sources/LocationBroadcastNavigationAccessoryPanel.swift b/submodules/TelegramUI/Components/LiveLocationHeaderPanelComponent/Sources/LocationBroadcastNavigationAccessoryPanel.swift index c0f86a677d..6bc4d95cee 100644 --- a/submodules/TelegramUI/Components/LiveLocationHeaderPanelComponent/Sources/LocationBroadcastNavigationAccessoryPanel.swift +++ b/submodules/TelegramUI/Components/LiveLocationHeaderPanelComponent/Sources/LocationBroadcastNavigationAccessoryPanel.swift @@ -178,8 +178,8 @@ final class LocationBroadcastNavigationAccessoryPanel: ASDisplayNode { let minimizedTitleOffset: CGFloat = subtitleString == nil ? 6.0 : 0.0 - let minimizedTitleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleLayout.size.width) / 2.0), y: 4.0 + minimizedTitleOffset), size: titleLayout.size) - let minimizedSubtitleFrame = CGRect(origin: CGPoint(x: floor((size.width - subtitleLayout.size.width) / 2.0), y: 20.0), size: subtitleLayout.size) + let minimizedTitleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleLayout.size.width) / 2.0), y: 6.0 + minimizedTitleOffset), size: titleLayout.size) + let minimizedSubtitleFrame = CGRect(origin: CGPoint(x: floor((size.width - subtitleLayout.size.width) / 2.0), y: 22.0), size: subtitleLayout.size) if let image = self.iconNode.image { transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: 7.0 + leftInset, y: 9.0), size: image.size)) diff --git a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift index 71f461ab79..5395153fb8 100644 --- a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift +++ b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift @@ -330,6 +330,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll private var expandedTapGestureRecoginzer: UITapGestureRecognizer? private var currentTransition: Transition? + private var pendingAction: PendingAction? private var isApplyingTransition = false private var validLayout: ContainerViewLayout? @@ -446,6 +447,9 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll return } if let result = self.scrollView.hitTest(gestureRecognizer.location(in: self.scrollView), with: nil), result === self.scrollView { + guard self.canStartMutatingTransition() else { + return + } self.collapse() } } @@ -596,6 +600,62 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll } } + private enum PendingAction: Equatable { + case maximize(itemId: AnyHashable) + } + + private func canStartMutatingTransition() -> Bool { + return self.currentTransition == nil && !self.isApplyingTransition + } + + private func performMaximize(for item: Item) { + guard let navigationController = self.navigationController else { + return + } + item.beforeMaximize(navigationController, { [weak self] in + self?.navigationController?.maximizeViewController(item.controller, animated: true) + }) + } + + private func requestOrQueueMaximize(for item: Item) { + if self.canStartMutatingTransition() { + self.performMaximize(for: item) + return + } + if case let .dismiss(dismissingItemId)? = self.currentTransition, dismissingItemId != item.id { + self.pendingAction = .maximize(itemId: item.id) + } + } + + private func drainPendingAction() { + guard self.canStartMutatingTransition(), let pendingAction = self.pendingAction else { + return + } + self.pendingAction = nil + + switch pendingAction { + case let .maximize(itemId): + guard let item = self.items.first(where: { $0.id == itemId }) else { + return + } + self.performMaximize(for: item) + } + } + + private func completeTransition( + _ completedTransition: Transition, + completion: @escaping (Transition) -> Void, + cleanup: () -> Void = {} + ) { + self.isApplyingTransition = false + if self.currentTransition == completedTransition { + self.currentTransition = nil + } + cleanup() + completion(completedTransition) + self.drainPendingAction() + } + public func maximizeController(_ viewController: MinimizableController, animated: Bool, completion: @escaping (Bool) -> Void) { guard let item = self.items.first(where: { $0.controller === viewController }) else { completion(self.items.count == 0) @@ -634,11 +694,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll } if self.items.count == 1, let item = self.items.first { - if let navigationController = self.navigationController { - item.beforeMaximize(navigationController, { [weak self] in - self?.navigationController?.maximizeViewController(item.controller, animated: true) - }) - } + self.performMaximize(for: item) } else { let contentOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.bounds.height) self.scrollView.contentOffset = CGPoint(x: 0.0, y: contentOffset) @@ -662,6 +718,9 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll return } self.requestUpdate(transition: .immediate) + guard self.canStartMutatingTransition() else { + return + } let contentOffset = scrollView.contentOffset if scrollView.contentOffset.y < -64.0, let lastItemId = self.items.last?.id, let itemNode = self.itemNodes[lastItemId] { @@ -798,6 +857,9 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll return } if self.isExpanded { + guard self.canStartMutatingTransition() else { + return + } let proceed = { [weak self] in guard let self else { return @@ -864,13 +926,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll return } if self.isExpanded, let itemNode { - if let navigationController = self.navigationController { - itemNode.item.beforeMaximize(navigationController, { [weak self, weak itemNode] in - if let item = itemNode?.item { - self?.navigationController?.maximizeViewController(item.controller, animated: true) - } - }) - } + self.requestOrQueueMaximize(for: itemNode.item) } else { self.expand() } @@ -1032,11 +1088,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll } transition.animatePosition(node: itemNode, from: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + initialOffset), completion: { _ in - self.isApplyingTransition = false - if self.currentTransition == currentTransition { - self.currentTransition = nil - } - completion(currentTransition) + self.completeTransition(currentTransition, completion: completion) }) case let .maximize(itemId): let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) @@ -1075,31 +1127,26 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll } transition.updatePosition(node: itemNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + maximizeTopInset + self.scrollView.contentOffset.y), completion: { _ in - self.isApplyingTransition = false - if self.currentTransition == currentTransition { - self.currentTransition = nil - } - - completion(currentTransition) - - if let _ = itemNode.snapshotView { - let snapshotContainerView = itemNode.snapshotContainerView - snapshotContainerView.isUserInteractionEnabled = true - snapshotContainerView.layer.allowsGroupOpacity = true - snapshotContainerView.center = CGPoint(x: itemNode.item.controller.displayNode.view.bounds.width / 2.0, y: snapshotContainerView.bounds.height / 2.0) - itemNode.item.controller.displayNode.view.addSubview(snapshotContainerView) - Queue.mainQueue().after(0.35, { - snapshotContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in - snapshotContainerView.removeFromSuperview() + self.completeTransition(currentTransition, completion: completion, cleanup: { + if let _ = itemNode.snapshotView { + let snapshotContainerView = itemNode.snapshotContainerView + snapshotContainerView.isUserInteractionEnabled = true + snapshotContainerView.layer.allowsGroupOpacity = true + snapshotContainerView.center = CGPoint(x: itemNode.item.controller.displayNode.view.bounds.width / 2.0, y: snapshotContainerView.bounds.height / 2.0) + itemNode.item.controller.displayNode.view.addSubview(snapshotContainerView) + Queue.mainQueue().after(0.35, { + snapshotContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + snapshotContainerView.removeFromSuperview() + }) }) - }) - } - - self.itemNodes[itemId] = nil - itemNode.removeFromSupernode() - dimView.removeFromSuperview() - - self.requestUpdate(transition: .immediate) + } + + self.itemNodes[itemId] = nil + itemNode.removeFromSupernode() + dimView.removeFromSuperview() + + self.requestUpdate(transition: .immediate) + }) }) case let .dismiss(itemId): let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) @@ -1129,18 +1176,15 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll } transition.updatePosition(node: itemNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + topInset + self.scrollView.contentOffset.y), completion: { _ in - self.isApplyingTransition = false - if self.currentTransition == currentTransition { - self.currentTransition = nil - } - completion(currentTransition) - self.itemNodes[itemId] = nil - itemNode.removeFromSupernode() - dimView.removeFromSuperview() - - self.navigationController?.maximizeViewController(itemNode.item.controller, animated: false) - - self.requestUpdate(transition: .immediate) + self.completeTransition(currentTransition, completion: completion, cleanup: { + self.itemNodes[itemId] = nil + itemNode.removeFromSupernode() + dimView.removeFromSuperview() + + self.navigationController?.maximizeViewController(itemNode.item.controller, animated: false) + + self.requestUpdate(transition: .immediate) + }) }) }) } @@ -1148,18 +1192,14 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll } else { let isLast = self.items.isEmpty transition.updatePosition(node: dismissedItemNode, position: CGPoint(x: -layout.size.width, y: dismissedItemNode.position.y), completion: { _ in - self.isApplyingTransition = false - if self.currentTransition == currentTransition { - self.currentTransition = nil - } - completion(currentTransition) - - self.itemNodes[itemId] = nil - dismissedItemNode.removeFromSupernode() - - if isLast { - self.didDismiss?(self) - } + self.completeTransition(currentTransition, completion: completion, cleanup: { + self.itemNodes[itemId] = nil + dismissedItemNode.removeFromSupernode() + + if isLast { + self.didDismiss?(self) + } + }) }) if isLast { let dismissOffset = collapsedHeight(layout: layout) @@ -1169,20 +1209,12 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll case .dismissAll: let dismissOffset = collapsedHeight(layout: layout) transition.updatePosition(layer: self.bottomEdgeView.layer, position: self.bottomEdgeView.layer.position.offsetBy(dx: 0.0, dy: dismissOffset), completion: { _ in - self.isApplyingTransition = false - if self.currentTransition == currentTransition { - self.currentTransition = nil - } - completion(currentTransition) + self.completeTransition(currentTransition, completion: completion) }) transition.updatePosition(layer: self.scrollView.layer, position: self.scrollView.center.offsetBy(dx: 0.0, dy: dismissOffset)) case .collapse: transition.updateBounds(layer: self.scrollView.layer, bounds: CGRect(origin: .zero, size: self.scrollView.bounds.size), completion: { _ in - self.isApplyingTransition = false - if self.currentTransition == currentTransition { - self.currentTransition = nil - } - completion(currentTransition) + self.completeTransition(currentTransition, completion: completion) }) default: break diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index abc5f036a2..6ae0f17047 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -42,16 +42,16 @@ final class PeerSelectionControllerNode: ASDisplayNode { private let hasTypeHeaders: Bool private let requestPeerType: [ReplyMarkupButtonRequestPeerType]? private let suggestedPeers: [EnginePeer] - + private var presentationInterfaceState: ChatPresentationInterfaceState private let presentationInterfaceStatePromise = ValuePromise() - + private var interfaceInteraction: ChatPanelInterfaceInteraction? - + var inProgress: Bool = false - + var navigationBar: NavigationBar? - + private let requirementsBackgroundNode: NavigationBackgroundNode? private let requirementsSeparatorNode: ASDisplayNode? private let requirementsTextNode: ImmediateTextNode? @@ -61,28 +61,28 @@ final class PeerSelectionControllerNode: ASDisplayNode { private let emptyTitleNode: ImmediateTextNode private let emptyTextNode: ImmediateTextNode private let emptyButtonNode: SolidRoundedButtonNode - + private let bottomEdgeEffectView: EdgeEffectView? private let segmentedControl: ComponentView? private var segmentedControlItems: [String]? private var segmentedControlSelectedIndex: Int = 0 - + private var textInputPanelNode: AttachmentTextInputPanelNode? private var forwardAccessoryPanelNode: ForwardAccessoryPanelNode? - + var contactListNode: ContactListNode? let chatListNode: ChatListNode? let mainContainerNode: ChatListContainerNode? - + private var contactListActive = false - + private var searchDisplayController: SearchDisplayController? - + private var containerLayout: (ContainerViewLayout, CGFloat, CGFloat)? - + var contentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)? var contentScrollingEnded: ((ListView) -> Bool)? - + var requestActivateSearch: (() -> Void)? var requestDeactivateSearch: (() -> Void)? var requestOpenPeer: ((EnginePeer, Int64?) -> Void)? @@ -90,32 +90,32 @@ final class PeerSelectionControllerNode: ASDisplayNode { var requestOpenPeerFromSearch: ((EnginePeer, Int64?) -> Void)? var requestOpenMessageFromSearch: ((EnginePeer, Int64?, EngineMessage.Id) -> Void)? var requestSend: (([EnginePeer], [EnginePeer.Id: EnginePeer], NSAttributedString, AttachmentTextInputPanelSendMode, ChatInterfaceForwardOptionsState?, ChatSendMessageActionSheetController.SendParameters?) -> Void)? - + private var presentationData: PresentationData { didSet { self.presentationDataPromise.set(.single(self.presentationData)) } } private var presentationDataPromise = Promise() - + private let animationCache: AnimationCache private let animationRenderer: MultiAnimationRenderer - + private var countPanelNode: PeersCountPanelNode? - + weak var pushedController: ViewController? - + private var readyValue = Promise() var ready: Signal { return self.readyValue.get() } - + private var isEmpty = false - + private var updatedPresentationData: (initial: PresentationData, signal: Signal) { return (self.presentationData, self.presentationDataPromise.get()) } - + init(context: AccountContext, controller: PeerSelectionControllerImpl, presentationData: PresentationData, filter: ChatListNodePeersFilter, forumPeerId: (id: EnginePeer.Id, isMonoforum: Bool)?, hasFilters: Bool, hasChatListSelector: Bool, hasContactSelector: Bool, hasGlobalSearch: Bool, forwardedMessageIds: [EngineMessage.Id], hasTypeHeaders: Bool, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, hasCreation: Bool, createNewGroup: (() -> Void)?, suggestedPeers: [EnginePeer], present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) { self.context = context self.controller = controller @@ -129,17 +129,17 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.hasTypeHeaders = hasTypeHeaders self.requestPeerType = requestPeerType self.suggestedPeers = suggestedPeers - + self.presentationData = presentationData - + self.animationCache = context.animationCache self.animationRenderer = context.animationRenderer - + self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, preferredGlassType: .default, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(.default), chatLocation: .peer(id: PeerId(0)), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil, businessIntro: nil) - + self.presentationInterfaceState = self.presentationInterfaceState.updatedInterfaceState { $0.withUpdatedForwardMessageIds(forwardedMessageIds) } self.presentationInterfaceStatePromise.set(self.presentationInterfaceState) - + if let _ = self.requestPeerType { self.requirementsBackgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor) self.requirementsSeparatorNode = ASDisplayNode() @@ -152,34 +152,34 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.requirementsSeparatorNode = nil self.requirementsTextNode = nil } - + self.emptyTitleNode = ImmediateTextNode() self.emptyTitleNode.displaysAsynchronously = false self.emptyTitleNode.maximumNumberOfLines = 0 self.emptyTitleNode.isHidden = true self.emptyTitleNode.textAlignment = .center self.emptyTitleNode.lineSpacing = 0.25 - + self.emptyTextNode = ImmediateTextNode() self.emptyTextNode.displaysAsynchronously = false self.emptyTextNode.maximumNumberOfLines = 0 self.emptyTextNode.isHidden = true self.emptyTextNode.lineSpacing = 0.25 - + self.emptyAnimationNode = DefaultAnimatedStickerNodeImpl() self.emptyAnimationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "ChatListNoResults"), width: 256, height: 256, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) self.emptyAnimationNode.isHidden = true self.emptyAnimationSize = CGSize(width: 120.0, height: 120.0) - + self.emptyButtonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), cornerRadius: 11.0, isShimmering: true) self.emptyButtonNode.isHidden = true self.emptyButtonNode.pressed = { createNewGroup?() } - + if hasChatListSelector && hasContactSelector { self.bottomEdgeEffectView = EdgeEffectView() - + self.segmentedControl = ComponentView() self.segmentedControlItems = [ self.presentationData.strings.DialogList_TabTitle, @@ -189,13 +189,13 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.bottomEdgeEffectView = nil self.segmentedControl = nil } - + var chatListCategories: [ChatListNodeAdditionalCategory] = [] - + if let _ = createNewGroup { chatListCategories.append(ChatListNodeAdditionalCategory(id: 0, icon: PresentationResourcesItemList.createGroupIcon(self.presentationData.theme), smallIcon: nil, title: self.presentationData.strings.PeerSelection_ImportIntoNewGroup, appearance: .action)) } - + let chatListLocation: ChatListControllerLocation if let (forumPeerId, isMonoforum) = self.forumPeerId { if isMonoforum { @@ -206,14 +206,14 @@ final class PeerSelectionControllerNode: ASDisplayNode { } else { chatListLocation = .chatList(groupId: .root) } - + let chatListMode: ChatListNodeMode if let requestPeerType = self.requestPeerType { chatListMode = .peerType(type: requestPeerType, hasCreate: hasCreation) } else { chatListMode = .peers(filter: filter, isSelecting: false, additionalCategories: chatListCategories, topPeers: self.suggestedPeers, chatListFilters: nil, displayAutoremoveTimeout: false, displayPresence: false) } - + if hasFilters { self.mainContainerNode = ChatListContainerNode(context: context, controller: nil, location: chatListLocation, chatListMode: chatListMode, previewing: false, controlsHistoryPreload: false, isInlineMode: false, presentationData: presentationData, animationCache: self.animationCache, animationRenderer: self.animationRenderer, filterBecameEmpty: { _ in }, filterEmptyAction: { _ in @@ -228,38 +228,38 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.chatListNode?.selectionLimit = multipleSelectionLimit } } - + super.init() - + self.setViewBlock({ return UITracingLayerView() }) - + self.chatListNode?.additionalCategorySelected = { _ in createNewGroup?() } - + self.backgroundColor = self.presentationData.theme.chatList.backgroundColor - + self.chatListNode?.selectionCountChanged = { [weak self] count in self?.textInputPanelNode?.updateSendButtonEnabled(count > 0, animated: true) } self.chatListNode?.accessibilityPageScrolledString = { row, count in return presentationData.strings.VoiceOver_ScrollStatus(row, count).string } - + self.chatListNode?.activateSearch = { [weak self] in self?.requestActivateSearch?() } self.mainContainerNode?.activateSearch = { [weak self] in self?.requestActivateSearch?() } - + self.chatListNode?.peerSelected = { [weak self] peer, threadId, _, _, _ in guard let self else { return } - + if let (peerId, isMonoforum) = self.forumPeerId, isMonoforum { let _ = (self.context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) @@ -280,7 +280,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { guard let self else { return } - + if let (peerId, isMonoforum) = self.forumPeerId, isMonoforum { let _ = (self.context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) @@ -299,14 +299,14 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.requestOpenPeer?(peer, threadId) } } - + self.chatListNode?.disabledPeerSelected = { [weak self] peer, threadId, reason in self?.requestOpenDisabledPeer?(peer, threadId, reason) } self.mainContainerNode?.disabledPeerSelected = { [weak self] peer, threadId, reason in self?.requestOpenDisabledPeer?(peer, threadId, reason) } - + self.chatListNode?.contentOffsetChanged = { [weak self] offset in guard let strongSelf = self else { return @@ -315,7 +315,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { strongSelf.contentOffsetChanged?(offset) } } - + self.mainContainerNode?.contentOffsetChanged = { [weak self] offset, _ in guard let strongSelf = self else { return @@ -324,11 +324,11 @@ final class PeerSelectionControllerNode: ASDisplayNode { strongSelf.contentOffsetChanged?(offset) } } - + self.chatListNode?.contentScrollingEnded = { [weak self] listView in return self?.contentScrollingEnded?(listView) ?? false } - + self.chatListNode?.isEmptyUpdated = { [weak self] state, _, _ in guard let strongSelf = self else { return @@ -339,7 +339,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .immediate) } } - + if let mainContainerNode = self.mainContainerNode { mainContainerNode.displayFilterLimit = { [weak self] in guard let strongSelf = self else { @@ -362,28 +362,28 @@ final class PeerSelectionControllerNode: ASDisplayNode { if let chatListNode = self.chatListNode { self.addSubnode(chatListNode) } - + if hasChatListSelector && hasContactSelector { if let bottomEdgeEffectView = self.bottomEdgeEffectView { self.view.addSubview(bottomEdgeEffectView) } } - + if let requirementsBackgroundNode = self.requirementsBackgroundNode, let requirementsSeparatorNode = self.requirementsSeparatorNode, let requirementsTextNode = self.requirementsTextNode { self.chatListNode?.addSubnode(requirementsBackgroundNode) self.chatListNode?.addSubnode(requirementsSeparatorNode) self.chatListNode?.addSubnode(requirementsTextNode) - + self.addSubnode(self.emptyAnimationNode) self.addSubnode(self.emptyTitleNode) self.addSubnode(self.emptyTextNode) self.addSubnode(self.emptyButtonNode) } - + if !hasChatListSelector && hasContactSelector { self.indexChanged(1) } - + self.interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { _, _, _ in }, setupEditMessage: { _, _ in }, beginMessageSelection: { _, _ in @@ -407,16 +407,16 @@ final class PeerSelectionControllerNode: ASDisplayNode { } let presentationData = strongSelf.presentationData - + let peerIds = strongSelf.selectedPeers.0.map { $0.id } - + let forwardOptions: Signal forwardOptions = strongSelf.presentationInterfaceStatePromise.get() |> map { state -> ChatControllerSubject.ForwardOptions in return ChatControllerSubject.ForwardOptions(hideNames: state.interfaceState.forwardOptionsState?.hideNames ?? false, hideCaptions: state.interfaceState.forwardOptionsState?.hideCaptions ?? false) } |> distinctUntilChanged - + let chatController = strongSelf.context.sharedContext.makeChatController( context: strongSelf.context, chatLocation: .peer(id: strongSelf.context.account.peerId), @@ -426,7 +426,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { params: nil ) chatController.canReadHistory.set(false) - + let messageIds = strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [] let messagesCount: Signal if messageIds.count > 1 { @@ -443,15 +443,15 @@ final class PeerSelectionControllerNode: ASDisplayNode { } else { messagesCount = .single(1) } - + let accountPeerId = strongSelf.context.account.peerId let items = combineLatest(forwardOptions, strongSelf.context.account.postbox.messagesAtIds(messageIds), messagesCount) |> map { forwardOptions, messages, messagesCount -> [ContextMenuItem] in var items: [ContextMenuItem] = [] - + var hasCaptions = false var uniquePeerIds = Set() - + var hasOther = false var hasNotOwnMessages = false for message in messages { @@ -464,7 +464,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { hasNotOwnMessages = true } } - + var isDice = false var isMusic = false for media in message.media { @@ -484,12 +484,12 @@ final class PeerSelectionControllerNode: ASDisplayNode { hasOther = true } } - + let canHideNames = hasNotOwnMessages && hasOther - + let hideNames = forwardOptions.hideNames let hideCaptions = forwardOptions.hideCaptions - + if !"".isEmpty { // check if seecret chat } else { if canHideNames { @@ -508,7 +508,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { return updated }) }))) - + items.append(.action(ContextMenuActionItem(text: uniquePeerIds.count == 1 ? presentationData.strings.Conversation_ForwardOptions_HideSendersName : presentationData.strings.Conversation_ForwardOptions_HideSendersNames, icon: { theme in if hideNames { return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) @@ -523,10 +523,10 @@ final class PeerSelectionControllerNode: ASDisplayNode { return updated }) }))) - + items.append(.separator) } - + if hasCaptions { items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ForwardOptions_ShowCaption, icon: { theme in if hideCaptions { @@ -545,7 +545,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { return updated }) }))) - + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ForwardOptions_HideCaption, icon: { theme in if hideCaptions { return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) @@ -563,11 +563,11 @@ final class PeerSelectionControllerNode: ASDisplayNode { return updated }) }))) - + items.append(.separator) } } - + items.append(.action(ContextMenuActionItem(text: messagesCount == 1 ? presentationData.strings.Conversation_ForwardOptions_SendMessage : presentationData.strings.Conversation_ForwardOptions_SendMessages, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { [weak self, weak chatController] c, f in guard let strongSelf = self else { return @@ -581,7 +581,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { f(.default) }))) - + return items } @@ -685,7 +685,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { var selectionRange: Range? var text: NSAttributedString? var inputMode: ChatInputMode? - + strongSelf.updateChatPresentationInterfaceState(animated: true, { state in selectionRange = state.interfaceState.effectiveInputState.selectionRange if let selectionRange = selectionRange { @@ -694,7 +694,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { inputMode = state.inputMode return state }) - + var link: String? if let text { text.enumerateAttributes(in: NSMakeRange(0, text.length)) { attributes, _, _ in @@ -703,7 +703,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { } } } - + let controller = chatTextLinkEditController(context: context, updatedPresentationData: (presentationData, .never()), text: text?.string ?? "", link: link, apply: { [weak self] link in if let strongSelf = self, let inputMode = inputMode, let selectionRange = selectionRange { if let link = link { @@ -725,13 +725,13 @@ final class PeerSelectionControllerNode: ASDisplayNode { }) strongSelf.present(controller, nil) } - }, openDateEditing: { + }, openDateEditing: { }, displaySlowmodeTooltip: { _, _ in }, displaySendMessageOptions: { [weak self] node, gesture in guard let strongSelf = self else { return } - + let _ = (ChatSendMessageContextScreen.initialData(context: strongSelf.context, currentMessageEffectId: nil) |> deliverOnMainQueue).start(next: { initialData in guard let strongSelf = self, let textInputPanelNode = strongSelf.textInputPanelNode else { @@ -741,12 +741,12 @@ final class PeerSelectionControllerNode: ASDisplayNode { guard let textInputNode = textInputPanelNode.textInputNode else { return } - + var hasEntityKeyboard = false if case .media = strongSelf.presentationInterfaceState.inputMode { hasEntityKeyboard = true } - + let controller = makeChatSendMessageActionSheetController( initialData: initialData, context: strongSelf.context, @@ -847,15 +847,15 @@ final class PeerSelectionControllerNode: ASDisplayNode { }, chatController: { return nil }, statuses: nil) - + if let chatListNode = self.chatListNode { self.readyValue.set(chatListNode.ready) } } - + override func didLoad() { super.didLoad() - + if let controller = self.controller, controller.immediatelySwitchToContacts { self.segmentedControlSelectedIndex = 1 if let (layout, navigationBarHeight, actualNavigationBarHeight) = self.containerLayout { @@ -864,33 +864,33 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.indexChanged(1) } } - + func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData self.updateThemeAndStrings() self.mainContainerNode?.updatePresentationData(presentationData) } - + private func updateChatPresentationInterfaceState(animated: Bool = true, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) { self.updateChatPresentationInterfaceState(transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate, f, completion: completion) } - + private func updateChatPresentationInterfaceState(transition: ContainedViewLayoutTransition, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion externalCompletion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) { let presentationInterfaceState = f(self.presentationInterfaceState) let updateInputTextState = self.presentationInterfaceState.interfaceState.effectiveInputState != presentationInterfaceState.interfaceState.effectiveInputState - + self.presentationInterfaceState = presentationInterfaceState self.presentationInterfaceStatePromise.set(presentationInterfaceState) - + if let textInputPanelNode = self.textInputPanelNode, updateInputTextState { textInputPanelNode.updateInputTextState(presentationInterfaceState.interfaceState.effectiveInputState, animated: transition.isAnimated) } - + if let (layout, navigationBarHeight, actualNavigationBarHeight) = self.containerLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: transition) } } - + private var selectedPeers: ([EnginePeer], [EnginePeer.Id: EnginePeer]) { if self.contactListActive { let selectedContactPeers = self.contactListNode?.selectedPeers ?? [] @@ -930,7 +930,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { return (selectedPeers, selectedPeerMap) } } - + func beginSelection() { guard let controller = self.controller else { return @@ -947,7 +947,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { }) self.addSubnode(countPanelNode) self.countPanelNode = countPanelNode - + if let (layout, navigationBarHeight, actualNavigationBarHeight) = self.containerLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .immediate) } @@ -958,33 +958,50 @@ final class PeerSelectionControllerNode: ASDisplayNode { forwardAccessoryPanelNode.interfaceInteraction = self.interfaceInteraction self.addSubnode(forwardAccessoryPanelNode) self.forwardAccessoryPanelNode = forwardAccessoryPanelNode - - let textInputPanelNode = AttachmentTextInputPanelNode(context: self.context, presentationInterfaceState: self.presentationInterfaceState, presentController: { [weak self] c in self?.present(c, nil) }, makeEntityInputView: { - return nil - }) + + let textInputPanelNode = AttachmentTextInputPanelNode( + context: self.context, + presentationInterfaceState: self.presentationInterfaceState, + customEmojiAvailable: self.presentationInterfaceState.customEmojiAvailable, + presentController: { [weak self] c in + self?.present(c, nil) + }, + presentInGlobalOverlay: { [weak self] c in + self?.presentInGlobalOverlay(c, nil) + }, + getNavigationController: { [weak self] in + return self?.controller?.navigationController as? NavigationController + } + ) textInputPanelNode.interfaceInteraction = self.interfaceInteraction textInputPanelNode.sendMessage = { [weak self] mode, messageEffect in guard let strongSelf = self else { return } - + let effectiveInputText = strongSelf.presentationInterfaceState.interfaceState.composeInputState.inputText let forwardOptionsState = strongSelf.presentationInterfaceState.interfaceState.forwardOptionsState - + let (selectedPeers, selectedPeerMap) = strongSelf.selectedPeers if !selectedPeers.isEmpty { strongSelf.requestSend?(selectedPeers, selectedPeerMap, effectiveInputText, mode, forwardOptionsState, messageEffect) } } + textInputPanelNode.updateHeight = { [weak self] _ in + guard let self, let (layout, navigationBarHeight, actualNavigationBarHeight) = self.containerLayout else { + return + } + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .animated(duration: 0.3, curve: .spring)) + } self.addSubnode(textInputPanelNode) self.textInputPanelNode = textInputPanelNode - + if let (layout, navigationBarHeight, actualNavigationBarHeight) = self.containerLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .animated(duration: 0.3, curve: .spring)) } } } - + if self.contactListActive { self.contactListNode?.multipleSelection = true self.contactListNode?.updateSelectionState({ _ in @@ -1009,11 +1026,11 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.countPanelNode?.buttonTitle = self.presentationData.strings.ShareMenu_Send } self.countPanelNode?.count = count - + if let titleView = self.controller?.titleView, let maxCount = self.controller?.multipleSelectionLimit { titleView.title = CounterControllerTitle(title: titleView.title.title, counter: "\(count)/\(maxCount)") } - + if let (layout, navigationBarHeight, actualNavigationBarHeight) = self.containerLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .animated(duration: 0.3, curve: .spring)) } @@ -1027,49 +1044,49 @@ final class PeerSelectionControllerNode: ASDisplayNode { } } } - + private func updateThemeAndStrings() { self.backgroundColor = self.presentationData.theme.chatList.backgroundColor self.searchDisplayController?.updatePresentationData(self.presentationData) self.chatListNode?.updateThemeAndStrings(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) - + self.updateChatPresentationInterfaceState({ $0.updatedTheme(self.presentationData.theme) }) - + self.requirementsBackgroundNode?.updateColor(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) - + if let (layout, navigationBarHeight, actualNavigationBarHeight) = self.containerLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .immediate) } } - + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, actualNavigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.containerLayout = (layout, navigationBarHeight, actualNavigationBarHeight) - + let cleanInsets = layout.insets(options: []) var insets = layout.insets(options: [.input]) - + var toolbarHeight: CGFloat = cleanInsets.bottom var textPanelHeight: CGFloat? var accessoryHeight: CGFloat = 0.0 - + if let forwardAccessoryPanelNode = self.forwardAccessoryPanelNode { let size = forwardAccessoryPanelNode.calculateSizeThatFits(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: layout.size.height)) accessoryHeight = size.height } - + if let textInputPanelNode = self.textInputPanelNode { var panelTransition = transition if textInputPanelNode.frame.width.isZero { panelTransition = .immediate } - var panelHeight = textInputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.intrinsicInsets.bottom, additionalSideInsets: UIEdgeInsets(), maxHeight: layout.size.height / 2.0, isSecondary: false, transition: panelTransition, interfaceState: self.presentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: false) + var panelHeight = textInputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.intrinsicInsets.bottom, keyboardHeight: layout.inputHeight ?? 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: layout.size.height / 2.0, isSecondary: false, transition: panelTransition, interfaceState: self.presentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: false) if self.searchDisplayController == nil { panelHeight += insets.bottom } else { panelHeight += cleanInsets.bottom } textPanelHeight = panelHeight - + let panelFrame = CGRect(x: 0.0, y: layout.size.height - panelHeight, width: layout.size.width, height: panelHeight) if textInputPanelNode.frame.width.isZero { var initialPanelFrame = panelFrame @@ -1078,13 +1095,13 @@ final class PeerSelectionControllerNode: ASDisplayNode { } transition.updateFrame(node: textInputPanelNode, frame: panelFrame) } - + if let forwardAccessoryPanelNode = self.forwardAccessoryPanelNode { let size = forwardAccessoryPanelNode.calculateSizeThatFits(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: layout.size.height)) forwardAccessoryPanelNode.updateState(size: size, inset: layout.safeInsets.left, interfaceState: self.presentationInterfaceState) forwardAccessoryPanelNode.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings, forwardOptionsState: self.presentationInterfaceState.interfaceState.forwardOptionsState) let panelFrame = CGRect(x: 0.0, y: layout.size.height - (textPanelHeight ?? 0.0) - size.height, width: size.width, height: size.height) - + accessoryHeight = size.height if forwardAccessoryPanelNode.frame.width.isZero { var initialPanelFrame = panelFrame @@ -1093,7 +1110,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { } transition.updateFrame(node: forwardAccessoryPanelNode, frame: panelFrame) } - + if let countPanelNode = self.countPanelNode { let countPanelHeight = countPanelNode.updateLayout(width: layout.size.width, sideInset: layout.safeInsets.left, bottomInset: layout.intrinsicInsets.bottom, transition: transition) if countPanelNode.count == 0 { @@ -1108,11 +1125,11 @@ final class PeerSelectionControllerNode: ASDisplayNode { } else { toolbarHeight += 44.0 } - + let edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarHeight - 10.0), size: CGSize(width: layout.size.width, height: toolbarHeight + 10.0)) transition.updateFrame(view: bottomEdgeEffectView, frame: edgeEffectFrame) bottomEdgeEffectView.update(content: self.presentationData.theme.list.plainBackgroundColor, blur: true, alpha: 0.3, rect: edgeEffectFrame, edge: .bottom, edgeSize: min(30.0, edgeEffectFrame.height), transition: ComponentTransition(transition)) - + let controlSize = segmentedControl.update( transition: ComponentTransition(transition), component: AnyComponent(SegmentControlComponent( @@ -1132,7 +1149,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { if let (layout, navigationBarHeight, actualNavigationBarHeight) = self.containerLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .animated(duration: 0.4, curve: .spring)) } - + self.indexChanged(index) } )), @@ -1148,28 +1165,28 @@ final class PeerSelectionControllerNode: ASDisplayNode { transition.updateFrame(view: segmentedControlView, frame: segmentedControlFrame) } } - + insets.top += navigationBarHeight insets.bottom = max(insets.bottom, toolbarHeight) insets.left += layout.safeInsets.left insets.right += layout.safeInsets.right - + var headerInsets = layout.insets(options: [.input]) headerInsets.top += actualNavigationBarHeight headerInsets.bottom = max(headerInsets.bottom, cleanInsets.bottom) headerInsets.left += layout.safeInsets.left headerInsets.right += layout.safeInsets.right - + if let chatListNode = self.chatListNode { chatListNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) chatListNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) } - + if let mainContainerNode = self.mainContainerNode { transition.updateFrame(node: mainContainerNode, frame: CGRect(origin: CGPoint(), size: layout.size)) mainContainerNode.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: actualNavigationBarHeight, originalNavigationHeight: navigationBarHeight, cleanNavigationBarHeight: navigationBarHeight, insets: insets, isReorderingFilters: false, isEditing: false, inlineNavigationLocation: nil, inlineNavigationTransitionFraction: 0.0, storiesInset: 0.0, transition: transition) } - + if let requestPeerTypes = self.requestPeerType, let requestPeerType = requestPeerTypes.first { if self.isEmpty { self.chatListNode?.isHidden = true @@ -1177,7 +1194,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.requirementsTextNode?.isHidden = true self.requirementsSeparatorNode?.isHidden = true self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) - + var emptyTitle: String var emptyText: String var emptyButtonText: String @@ -1219,14 +1236,14 @@ final class PeerSelectionControllerNode: ASDisplayNode { emptyText = "" emptyButtonText = "" } - + self.emptyTitleNode.attributedText = NSAttributedString(string: emptyTitle, font: Font.semibold(15.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) self.emptyTextNode.attributedText = NSAttributedString(string: emptyText, font: Font.regular(15.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) - + let padding: CGFloat = 44.0 let emptyTitleSize = self.emptyTitleNode.updateLayout(CGSize(width: layout.size.width - insets.left * 2.0 - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) let emptyTextSize = self.emptyTextNode.updateLayout(CGSize(width: layout.size.width - insets.left * 2.0 - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) - + let emptyAnimationHeight = self.emptyAnimationSize.height let emptyAnimationSpacing: CGFloat = 12.0 let emptyTextSpacing: CGFloat = 17.0 @@ -1238,7 +1255,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { } let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing + emptyButtonSpacing + emptyButtonHeight let emptyAnimationY = floorToScreenPixels((layout.size.height - emptyTotalHeight) / 2.0) - + if !emptyButtonText.isEmpty { let buttonPadding: CGFloat = 30.0 self.emptyButtonNode.title = emptyButtonText @@ -1249,13 +1266,13 @@ final class PeerSelectionControllerNode: ASDisplayNode { } else { self.emptyButtonNode.isHidden = true } - + let textTransition = ContainedViewLayoutTransition.immediate textTransition.updateFrame(node: self.emptyAnimationNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - self.emptyAnimationSize.width) / 2.0), y: emptyAnimationY), size: self.emptyAnimationSize)) textTransition.updateFrame(node: self.emptyTitleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - emptyTitleSize.width) / 2.0), y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing), size: emptyTitleSize)) textTransition.updateFrame(node: self.emptyTextNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - emptyTextSize.width) / 2.0), y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize)) self.emptyAnimationNode.updateLayout(size: self.emptyAnimationSize) - + self.emptyAnimationNode.isHidden = false self.emptyTitleNode.isHidden = false self.emptyTextNode.isHidden = false @@ -1263,54 +1280,54 @@ final class PeerSelectionControllerNode: ASDisplayNode { } else if let requirementsBackgroundNode = self.requirementsBackgroundNode, let requirementsSeparatorNode = self.requirementsSeparatorNode, let requirementsTextNode = self.requirementsTextNode, let requirementsText = stringForRequestPeerType(strings: self.presentationData.strings, peerType: requestPeerType, offset: true) { let requirements = NSMutableAttributedString(string: self.presentationData.strings.RequestPeer_Requirements + "\n", font: Font.semibold(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) requirements.append(NSAttributedString(string: requirementsText, font: Font.regular(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor)) - + requirementsTextNode.attributedText = requirements let sideInset: CGFloat = 16.0 let verticalInset: CGFloat = 11.0 let requirementsSize = requirementsTextNode.updateLayout(CGSize(width: layout.size.width - insets.left - insets.right - sideInset * 2.0, height: .greatestFiniteMagnitude)) - + let requirementsBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: actualNavigationBarHeight), size: CGSize(width: layout.size.width, height: requirementsSize.height + verticalInset * 2.0)) insets.top += requirementsBackgroundFrame.height - + requirementsBackgroundNode.update(size: requirementsBackgroundFrame.size, transition: transition) transition.updateFrame(node: requirementsBackgroundNode, frame: requirementsBackgroundFrame) - + transition.updateFrame(node: requirementsSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: requirementsBackgroundFrame.maxY - UIScreenPixel), size: CGSize(width: layout.size.width, height: UIScreenPixel))) - + requirementsTextNode.frame = CGRect(origin: CGPoint(x: insets.left + sideInset, y: requirementsBackgroundFrame.minY + verticalInset), size: requirementsSize) } } - + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, headerInsets: headerInsets, duration: duration, curve: curve) - + if let chatListNode = self.chatListNode { chatListNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, visibleTopInset: updateSizeAndInsets.insets.top, originalTopInset: updateSizeAndInsets.insets.top, storiesInset: 0.0, inlineNavigationLocation: nil, inlineNavigationTransitionFraction: 0.0) } - + if let contactListNode = self.contactListNode { contactListNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) contactListNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) - + contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), headerInsets: headerInsets, storiesInset: 0.0, transition: transition) } - + if let searchDisplayController = self.searchDisplayController { searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) } } - + func activateSearch(placeholderNode: SearchBarPlaceholderNode) { guard let (containerLayout, navigationBarHeight, _) = self.containerLayout, let navigationBar = self.navigationBar else { return } - + self.navigationBar?.setSecondaryContentNode(nil, animated: true) - + if self.chatListNode?.supernode != nil || self.mainContainerNode?.supernode != nil { self.chatListNode?.accessibilityElementsHidden = true self.mainContainerNode?.accessibilityElementsHidden = true - + let chatListLocation: ChatListControllerLocation if let (forumPeerId, isMonoforum) = self.forumPeerId { if isMonoforum { @@ -1321,7 +1338,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { } else { chatListLocation = .chatList(groupId: EngineChatList.Group(.root)) } - + self.searchDisplayController = SearchDisplayController( presentationData: self.presentationData, contentNode: ChatListSearchContainerNode( @@ -1341,14 +1358,14 @@ final class PeerSelectionControllerNode: ASDisplayNode { } var updated = false var count = 0 - + let chatListNode: ChatListNode? if let mainContainerNode = strongSelf.mainContainerNode { chatListNode = mainContainerNode.currentItemNode } else { chatListNode = strongSelf.chatListNode } - + chatListNode?.updateState { state in if state.editing { updated = true @@ -1419,7 +1436,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { } }, fieldStyle: placeholderNode.fieldStyle ) - + self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate) self.searchDisplayController?.activate(insertSubnode: { [weak self, weak placeholderNode] subnode, isSearchBar in if let strongSelf = self, let strongPlaceholderNode = placeholderNode { @@ -1430,10 +1447,10 @@ final class PeerSelectionControllerNode: ASDisplayNode { } } }, placeholder: placeholderNode) - + } else if let contactListNode = self.contactListNode, contactListNode.supernode != nil { contactListNode.accessibilityElementsHidden = true - + var categories: ContactsSearchCategories = [.cloudContacts] if self.hasGlobalSearch { categories.insert(.global) @@ -1465,7 +1482,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { return nil } } - + if updated { strongSelf.textInputPanelNode?.updateSendButtonEnabled(count > 0, animated: true) strongSelf.requestDeactivateSearch?() @@ -1493,7 +1510,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { requestDeactivateSearch() } }, fieldStyle: placeholderNode.fieldStyle) - + self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate) self.searchDisplayController?.activate(insertSubnode: { [weak self, weak placeholderNode] subnode, isSearchBar in if let strongSelf = self, let strongPlaceholderNode = placeholderNode { @@ -1506,30 +1523,30 @@ final class PeerSelectionControllerNode: ASDisplayNode { }, placeholder: placeholderNode) } } - + func deactivateSearch(placeholderNode: SearchBarPlaceholderNode) { if let searchDisplayController = self.searchDisplayController { if self.chatListNode?.supernode != nil || self.mainContainerNode?.supernode != nil { self.chatListNode?.accessibilityElementsHidden = false self.mainContainerNode?.accessibilityElementsHidden = false - + if let controller = self.controller, controller.chatListFiltersNonEmpty { self.navigationBar?.setSecondaryContentNode(self.controller?.tabContainerNode, animated: true) } self.controller?.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring)) - + searchDisplayController.deactivate(placeholder: placeholderNode) self.searchDisplayController = nil } else if let contactListNode = self.contactListNode, contactListNode.supernode != nil { contactListNode.accessibilityElementsHidden = false - + self.controller?.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring)) searchDisplayController.deactivate(placeholder: placeholderNode) self.searchDisplayController = nil } } } - + func scrollToTop() { if self.mainContainerNode?.supernode != nil { self.mainContainerNode?.scrollToTop(animated: true, adjustForTempInset: false) @@ -1539,7 +1556,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { //contactListNode.scrollToTop() } } - + private func indexChanged(_ index: Int) { let contactListActive = index == 1 if contactListActive != self.contactListActive { @@ -1556,7 +1573,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { } self.recursivelyEnsureDisplaySynchronously(true) contactListNode.enableUpdates = true - + if let (layout, _, _) = self.containerLayout { self.controller?.containerLayoutUpdated(layout, transition: .immediate) } @@ -1599,14 +1616,14 @@ final class PeerSelectionControllerNode: ASDisplayNode { strongSelf.contentOffsetChanged?(offset) } } - + contactListNode.contentScrollingEnded = { [weak self] listView in return self?.contentScrollingEnded?(listView) ?? false } - + if let (layout, navigationHeight, actualNavigationHeight) = self.containerLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, actualNavigationBarHeight: actualNavigationHeight, transition: .immediate) - + let _ = (contactListNode.ready |> deliverOnMainQueue).start(next: { [weak self] _ in if let strongSelf = self { strongSelf.navigationBar?.setSecondaryContentNode(nil, animated: false) @@ -1620,7 +1637,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { } } strongSelf.recursivelyEnsureDisplaySynchronously(true) - + if let (layout, _, _) = strongSelf.containerLayout { strongSelf.controller?.containerLayoutUpdated(layout, transition: .immediate) } @@ -1636,7 +1653,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { mainContainerNode.removeFromSupernode() } self.recursivelyEnsureDisplaySynchronously(true) - + if let (layout, _, _) = self.containerLayout { self.controller?.containerLayoutUpdated(layout, transition: .immediate) } @@ -1647,7 +1664,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.navigationBar?.setSecondaryContentNode(self.controller?.tabContainerNode, animated: false) } contactListNode.enableUpdates = false - + if let mainContainerNode = self.mainContainerNode { self.insertSubnode(mainContainerNode, aboveSubnode: contactListNode) } @@ -1655,7 +1672,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.insertSubnode(chatListNode, aboveSubnode: contactListNode) } contactListNode.removeFromSupernode() - + if let (layout, _, _) = self.containerLayout { self.controller?.containerLayoutUpdated(layout, transition: .immediate) } @@ -1669,7 +1686,7 @@ public func stringForAdminRights(strings: PresentationStrings, adminRights: Tele func append(_ string: String) { rights.append("• \(string)") } - + if isChannel { if adminRights.rights.contains(.canChangeInfo) { append(strings.RequestPeer_Requirement_Channel_Rights_Info) @@ -1745,7 +1762,7 @@ public func stringForAdminRights(strings: PresentationStrings, adminRights: Tele private func stringForRequestPeerType(strings: PresentationStrings, peerType: ReplyMarkupButtonRequestPeerType, offset: Bool) -> String? { var lines: [String] = [] - + func append(_ string: String) { if offset { lines.append(" • \(string)") @@ -1753,7 +1770,7 @@ private func stringForRequestPeerType(strings: PresentationStrings, peerType: Re lines.append("• \(string)") } } - + switch peerType { case let .user(user): if let isPremium = user.isPremium { @@ -1886,25 +1903,25 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent let controller: ViewController weak var sourceView: UIView? let sourceRect: CGRect? - + let navigationController: NavigationController? = nil - + let passthroughTouches: Bool - + init(controller: ViewController, sourceNode: ASDisplayNode?, sourceRect: CGRect? = nil, passthroughTouches: Bool) { self.controller = controller self.sourceView = sourceNode?.view self.sourceRect = sourceRect self.passthroughTouches = passthroughTouches } - + init(controller: ViewController, sourceView: UIView?, sourceRect: CGRect? = nil, passthroughTouches: Bool) { self.controller = controller self.sourceView = sourceView self.sourceRect = sourceRect self.passthroughTouches = passthroughTouches } - + func transitionInfo() -> ContextControllerTakeControllerInfo? { let sourceView = self.sourceView let sourceRect = self.sourceRect @@ -1916,7 +1933,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent } }) } - + func animatedIn() { } } @@ -1924,60 +1941,60 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent private final class PeersCountPanelNode: ASDisplayNode { private let theme: PresentationTheme private let strings: PresentationStrings - + private let separatorNode: ASDisplayNode private let button: SolidRoundedButtonNode - + private var validLayout: (CGFloat, CGFloat, CGFloat)? - + var buttonTitle: String = "" var count: Int = 0 { didSet { if self.count != oldValue && self.count > 0 { self.button.title = self.buttonTitle self.button.badge = "\(self.count)" - + if let (width, sideInset, bottomInset) = self.validLayout { let _ = self.updateLayout(width: width, sideInset: sideInset, bottomInset: bottomInset, transition: .immediate) } } } } - + init(theme: PresentationTheme, strings: PresentationStrings, action: @escaping () -> Void) { self.theme = theme self.strings = strings self.separatorNode = ASDisplayNode() self.separatorNode.backgroundColor = theme.rootController.navigationBar.separatorColor - + self.button = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: theme), height: 48.0, cornerRadius: 10.0) - + super.init() - + self.backgroundColor = theme.rootController.navigationBar.opaqueBackgroundColor - + self.addSubnode(self.button) self.addSubnode(self.separatorNode) - + self.button.pressed = { action() } } - + func updateLayout(width: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { self.validLayout = (width, sideInset, bottomInset) let topInset: CGFloat = 9.0 var bottomInset = bottomInset bottomInset += topInset - (bottomInset.isZero ? 0.0 : 4.0) - + let buttonInset: CGFloat = 16.0 + sideInset let buttonWidth = width - buttonInset * 2.0 let buttonHeight = self.button.updateLayout(width: buttonWidth, transition: transition) transition.updateFrame(node: self.button, frame: CGRect(x: buttonInset, y: topInset, width: buttonWidth, height: buttonHeight)) - + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) - + return topInset + buttonHeight + bottomInset } } diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridController.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridController.swift index 0871ba3e2e..6f7d3abdad 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridController.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridController.swift @@ -108,7 +108,7 @@ public final class ThemeColorsGridController: ViewController, AttachmentContaina public enum Mode { case `default` case peer(EnginePeer) - + var galleryMode: WallpaperGalleryController.Mode { switch self { case .default: @@ -117,7 +117,7 @@ public final class ThemeColorsGridController: ViewController, AttachmentContaina return .peer(peer, false) } } - + var colorPickerMode: ThemeAccentColorController.ResultMode { switch self { case .default: @@ -127,61 +127,61 @@ public final class ThemeColorsGridController: ViewController, AttachmentContaina } } } - + private var controllerNode: ThemeColorsGridControllerNode { return self.displayNode as! ThemeColorsGridControllerNode } - + private let _ready = Promise() public override var ready: Promise { return self._ready } - + private let context: AccountContext let mode: Mode - + private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - + private var validLayout: ContainerViewLayout? - + private var previousContentOffset: GridNodeVisibleContentOffset? - + fileprivate var mainButtonState: AttachmentMainButtonState? - + var pushController: (ViewController) -> Void = { _ in } var dismissControllers: (() -> Void)? var openGallery: (() -> Void)? - + public init(context: AccountContext, mode: Mode = .default, canDelete: Bool = false) { self.context = context self.mode = mode self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, style: .glass)) - + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style - + self.scrollToTop = { [weak self] in if let strongSelf = self { strongSelf.controllerNode.scrollToTop() } } - + self.presentationDataDisposable = (context.sharedContext.presentationData |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { let previousTheme = strongSelf.presentationData.theme let previousStrings = strongSelf.presentationData.strings - + strongSelf.presentationData = presentationData - + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { strongSelf.updateThemeAndStrings() } } }) - + if case .peer = mode { self.title = self.presentationData.strings.Conversation_Theme_ChooseColorTitle self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) @@ -189,40 +189,40 @@ public final class ThemeColorsGridController: ViewController, AttachmentContaina } else { self.title = self.presentationData.strings.WallpaperColors_Title } - + self.pushController = { [weak self] controller in self?.push(controller) } - + self.mainButtonState = AttachmentMainButtonState(text: self.presentationData.strings.Conversation_Theme_SetPhotoWallpaper, font: .regular, background: .color(.clear), textColor: self.presentationData.theme.actionSheet.controlAccentColor, isVisible: true, progress: .none, isEnabled: true, hasShimmer: false) } - + required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + deinit { self.presentationDataDisposable?.dispose() } - + @objc private func cancelPressed() { self.dismiss() } - + @objc private func customPressed() { self.presentColorPicker() } - + private func updateThemeAndStrings() { self.title = self.presentationData.strings.WallpaperColors_Title self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData, style: .glass), transition: .immediate) - + if self.isNodeLoaded { self.controllerNode.updatePresentationData(self.presentationData) } } - + private func presentColorPicker() { let _ = (self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings]) |> take(1) @@ -231,7 +231,7 @@ public final class ThemeColorsGridController: ViewController, AttachmentContaina return } let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings]?.get(PresentationThemeSettings.self) ?? PresentationThemeSettings.defaultSettings - + let autoNightModeTriggered = strongSelf.presentationData.autoNightModeTriggered let themeReference: PresentationThemeReference if autoNightModeTriggered { @@ -239,7 +239,7 @@ public final class ThemeColorsGridController: ViewController, AttachmentContaina } else { themeReference = settings.theme } - + let controller = ThemeAccentColorController(context: strongSelf.context, mode: .background(themeReference: themeReference), resultMode: strongSelf.mode.colorPickerMode) controller.completion = { [weak self, weak controller] in if let strongSelf = self { @@ -268,7 +268,7 @@ public final class ThemeColorsGridController: ViewController, AttachmentContaina strongSelf.pushController(controller) }) } - + public override func loadDisplayNode() { var dark = false if case .default = self.mode { @@ -285,7 +285,7 @@ public final class ThemeColorsGridController: ViewController, AttachmentContaina strongSelf.presentColorPicker() } }) - + let transitionOffset: CGFloat switch self.mode { case .default: @@ -293,7 +293,7 @@ public final class ThemeColorsGridController: ViewController, AttachmentContaina case .peer: transitionOffset = 2.0 } - + self.controllerNode.gridNode.visibleContentOffsetChanged = { [weak self] offset in if let strongSelf = self { var previousContentOffsetValue: CGFloat? @@ -312,29 +312,29 @@ public final class ThemeColorsGridController: ViewController, AttachmentContaina case .unknown, .none: strongSelf.navigationBar?.updateBackgroundAlpha(1.0, transition: .immediate) } - + strongSelf.previousContentOffset = offset } } - + self._ready.set(self.controllerNode.ready.get()) - + self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) - + self.displayNodeDidLoad() } - + public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } - + @objc fileprivate func mainButtonPressed() { self.dismiss(animated: true) self.openGallery?() } - + public var requestAttachmentMenuExpansion: () -> Void = {} public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } public var parentController: () -> ViewController? = { @@ -346,7 +346,7 @@ public final class ThemeColorsGridController: ViewController, AttachmentContaina public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } public var isMinimized: Bool = false - + public var mediaPickerContext: AttachmentMediaPickerContext? { return ThemeColorsGridContext(controller: self) } @@ -354,15 +354,15 @@ public final class ThemeColorsGridController: ViewController, AttachmentContaina private final class ThemeColorsGridContext: AttachmentMediaPickerContext { private weak var controller: ThemeColorsGridController? - + public var mainButtonState: Signal { return .single(self.controller?.mainButtonState) } - + init(controller: ThemeColorsGridController) { self.controller = controller } - + func mainButtonAction() { self.controller?.mainButtonPressed() } @@ -376,9 +376,7 @@ public func standaloneColorPickerController( push: @escaping (ViewController) -> Void, openGallery: @escaping () -> Void ) -> ViewController { - let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: { - return nil - }) + let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false) controller.requestController = { _, present in let colorPickerController = ThemeColorsGridController(context: context, mode: .peer(peer)) colorPickerController.pushController = { controller in diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift index 5598017901..c6cf49f953 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift @@ -911,7 +911,7 @@ final class DataUsageScreenComponent: Component { self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(value: .modeChanged))) })), environment: {}, - containerSize: CGSize(width: availableSize.width - (environment.safeInsets.left + 48.0) * 2.0, height: 100.0) + containerSize: CGSize(width: availableSize.width - (environment.safeInsets.left + 36.0) * 2.0, height: 100.0) ) let segmentedControlFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - segmentedSize.width) * 0.5), y: contentHeight), size: segmentedSize) if let segmentedControlComponentView = self.segmentedControlView.view { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index e48abd47d2..8b09a8053c 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -982,6 +982,9 @@ private final class StoryContainerScreenComponent: Component { if let environment = self.environment, case .regular = environment.metrics.widthClass { if result.isDescendant(of: self.backgroundEffectView) { if let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id] { + if point.x < itemSetView.frame.minX || point.x > itemSetView.frame.maxX { + return result + } return itemSetView.view.view } } @@ -1245,6 +1248,13 @@ private final class StoryContainerScreenComponent: Component { } } } + + fileprivate func navigateWithKeyShortcut(direction: StoryItemSetContainerComponent.NavigationDirection) { + guard !hasFirstResponder(self) else { + return + } + self.navigate(direction: direction) + } func presentExternalTooltip(_ tooltipScreen: UndoOverlayController) { guard let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else { @@ -2012,7 +2022,7 @@ private final class StoryContainerScreenComponent: Component { } } -public class StoryContainerScreen: ViewControllerComponentContainer { +public class StoryContainerScreen: ViewControllerComponentContainer, KeyShortcutResponder { public struct TransitionState: Equatable { public var sourceSize: CGSize public var destinationSize: CGSize @@ -2152,6 +2162,56 @@ public class StoryContainerScreen: ViewControllerComponentContainer { } } } + + public var keyShortcuts: [KeyShortcut] { + if self.isViewLoaded, hasFirstResponder(self.view) { + return [] + } + var keyShortcuts: [KeyShortcut] = [] + keyShortcuts.append( + KeyShortcut( + title: "", + input: UIKeyCommand.inputUpArrow, + modifiers: [.command], + action: { [weak self] in + self?.dismiss() + } + ) + ) + keyShortcuts.append( + KeyShortcut( + title: "", + input: "W", + modifiers: [.command], + action: { [weak self] in + self?.dismiss() + } + ) + ) + keyShortcuts.append( + KeyShortcut( + input: UIKeyCommand.inputLeftArrow, + modifiers: [], + action: { [weak self] in + if let componentView = self?.node.hostView.componentView as? StoryContainerScreenComponent.View { + componentView.navigateWithKeyShortcut(direction: .previous) + } + } + ) + ) + keyShortcuts.append( + KeyShortcut( + input: UIKeyCommand.inputRightArrow, + modifiers: [], + action: { [weak self] in + if let componentView = self?.node.hostView.componentView as? StoryContainerScreenComponent.View { + componentView.navigateWithKeyShortcut(direction: .next) + } + } + ) + ) + return keyShortcuts + } public func presentExternalTooltip(_ tooltipScreen: UndoOverlayController) { if let componentView = self.node.hostView.componentView as? StoryContainerScreenComponent.View { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index ed20ffc4e6..17f064a0ab 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -62,11 +62,11 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { case text case media } - + private var context: AccountContext? private weak var view: StoryItemSetContainerComponent.View? private var inputPanelExternalState: MessageInputPanelComponent.ExternalState? - + weak var attachmentController: AttachmentController? weak var shareController: ViewController? weak var tooltipScreen: ViewController? @@ -76,59 +76,59 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { weak var menuController: ViewController? var progressPauseContext = StoryProgressPauseContext() var isViewingAttachedStickers = false - + var currentTooltipUpdateTimer: Foundation.Timer? - + var currentInputMode: InputMode = .text private var needsInputActivation = false - + var audioRecorderValue: ManagedAudioRecorder? var audioRecorder = Promise() var recordedAudioPreview: ChatRecordedMediaPreview? - + var videoRecorderValue: InstantVideoController? var videoRecorder = Promise() var hasRecordedVideoPreview = false - + var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData? var inputMediaNodeDataDisposable: Disposable? var inputMediaNodeStateContext = ChatEntityKeyboardInputNode.StateContext() var inputMediaInteraction: ChatEntityKeyboardInputNode.Interaction? var inputMediaNode: ChatEntityKeyboardInputNode? - + let controllerNavigationDisposable = MetaDisposable() let enqueueMediaMessageDisposable = MetaDisposable() let navigationActionDisposable = MetaDisposable() let resolvePeerByNameDisposable = MetaDisposable() - + var currentSpeechHolder: SpeechSynthesizerHolder? - + var currentLiveStreamMessageStars: StarsAmount? weak var currentSendStarsUndoController: UndoOverlayController? var currentLiveStreamStarsIsActive: Bool = false var currentLiveStreamStarsIsActiveTimer: Foundation.Timer? - + struct SendAsData: Equatable { var isPremium: Bool var availablePeers: [SendAsPeer] - + init(isPremium: Bool, availablePeers: [SendAsPeer]) { self.isPremium = isPremium self.availablePeers = availablePeers } } - + var sendAsData: SendAsData? var currentSendAsPeer: SendAsPeer? var isSelectingSendAsPeer: Bool = false var sendAsDisposable: Disposable? - + private(set) var isMediaRecordingLocked: Bool = false var wasRecordingDismissed: Bool = false - + init() { } - + deinit { self.controllerNavigationDisposable.dispose() self.enqueueMediaMessageDisposable.dispose() @@ -138,12 +138,12 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.currentTooltipUpdateTimer?.invalidate() self.sendAsDisposable?.dispose() } - + func setup(component: StoryItemSetContainerComponent, view: StoryItemSetContainerComponent.View, inputPanelExternalState: MessageInputPanelComponent.ExternalState, keyboardInputData: Signal) { self.context = component.context self.inputPanelExternalState = inputPanelExternalState self.view = view - + if self.inputMediaNodeDataDisposable == nil { self.inputMediaNodeDataDisposable = (keyboardInputData |> deliverOnMainQueue).start(next: { [weak self] value in @@ -153,7 +153,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.inputMediaNodeData = value }) } - + self.inputMediaInteraction = ChatEntityKeyboardInputNode.Interaction( sendSticker: { [weak self] fileReference, _, _, _, _, _, _, _, _ in if let self, let view = self.view { @@ -192,7 +192,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } }, dismissTextInput: { - + }, insertText: { [weak self] text in if let self { @@ -229,7 +229,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } ) self.inputMediaInteraction?.forceTheme = defaultDarkColorPresentationTheme - + if let peerId = component.slice.item.peerId { self.sendAsDisposable = combineLatest( queue: Queue.mainQueue(), @@ -241,9 +241,9 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let self, let view, let accountPeer else { return } - + let isPremium = accountPeer.isPremium - + var availablePeers: [SendAsPeer] = [] availablePeers.append(SendAsPeer( peer: accountPeer._asPeer(), @@ -256,7 +256,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } availablePeers.append(peer) } - + let sendAsData = SendAsData( isPremium: isPremium, availablePeers: availablePeers @@ -270,7 +270,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) } } - + func toggleInputMode() { guard let view = self.view else { return @@ -284,12 +284,12 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.currentInputMode = .text } } - + func updateInputMediaNode(view: StoryItemSetContainerComponent.View, availableSize: CGSize, bottomInset: CGFloat, bottomContainerInset: CGFloat, inputHeight: CGFloat, effectiveInputHeight: CGFloat, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, transition: ComponentTransition) -> CGFloat { guard let context = self.context, let inputPanelView = view.inputPanel.view as? MessageInputPanelComponent.View else { return 0.0 } - + var height: CGFloat = 0.0 if let component = self.view?.component, case .media = self.currentInputMode, let inputData = self.inputMediaNodeData { var updatedInputData = inputData @@ -297,7 +297,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { if case .liveStream = component.slice.item.storyItem.media { isLiveStream = true } - + if isLiveStream { updatedInputData = ChatEntityKeyboardInputNode.InputData( emoji: updatedInputData.emoji, @@ -306,7 +306,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { availableGifSearchEmojies: [] ) } - + var animateIn = false let inputMediaNode: ChatEntityKeyboardInputNode if let current = self.inputMediaNode { @@ -341,7 +341,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } self.inputMediaNode = inputMediaNode } - + let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) let presentationInterfaceState = ChatPresentationInterfaceState( chatWallpaper: .builtin(WallpaperSettings()), @@ -368,22 +368,22 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { accountPeerColor: nil, businessIntro: nil ) - + let heightAndOverflow = inputMediaNode.updateLayout(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, bottomInset: bottomInset, standardInputHeight: deviceMetrics.standardInputHeight(inLandscape: false), inputHeight: inputHeight < 100.0 ? inputHeight - bottomContainerInset : inputHeight, maximumHeight: availableSize.height, inputPanelHeight: 0.0, transition: .immediate, interfaceState: presentationInterfaceState, layoutMetrics: metrics, deviceMetrics: deviceMetrics, isVisible: true, isExpanded: false) let inputNodeHeight = heightAndOverflow.0 let inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputNodeHeight), size: CGSize(width: availableSize.width, height: inputNodeHeight)) - + if animateIn { let inputNodeFrame = inputNodeFrame.offsetBy(dx: 0.0, dy: inputNodeHeight) ComponentTransition.immediate.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) } - + transition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) - + height = heightAndOverflow.0 } else if let inputMediaNode = self.inputMediaNode { self.inputMediaNode = nil - + var targetFrame = inputMediaNode.frame targetFrame.origin.y = availableSize.height transition.setFrame(view: inputMediaNode.view, frame: targetFrame, completion: { [weak inputMediaNode] _ in @@ -396,17 +396,17 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } }) } - + if self.needsInputActivation { self.needsInputActivation = false Queue.mainQueue().justDispatch { inputPanelView.activateInput() } } - + return height } - + func animateOut(bounds: CGRect) { if let inputMediaNode = self.inputMediaNode { inputMediaNode.layer.animatePosition( @@ -420,20 +420,20 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { inputMediaNode.layer.animateAlpha(from: inputMediaNode.alpha, to: 0.0, duration: 0.3, removeOnCompletion: false) } } - + private func presentMessageSentTooltip(view: StoryItemSetContainerComponent.View, peer: EnginePeer, messageId: EngineMessage.Id?, isScheduled: Bool = false) { guard let component = view.component, let controller = component.controller() as? StoryContainerScreen else { return } - + if let tooltipScreen = self.tooltipScreen { tooltipScreen.dismiss(animated: true) } - + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - + let text = isScheduled ? presentationData.strings.Story_TooltipMessageScheduled : presentationData.strings.Story_TooltipMessageSent - + let tooltipScreen = UndoOverlayController( presentationData: presentationData, content: .actionSucceeded(title: "", text: text, cancel: messageId != nil ? presentationData.strings.Story_ToastViewInChat : "", destructive: false), @@ -451,31 +451,31 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { controller.present(tooltipScreen, in: .current) self.tooltipScreen = tooltipScreen view.updateIsProgressPaused() - + HapticFeedback().success() } - + func presentSendMessageOptions(view: StoryItemSetContainerComponent.View, sourceView: UIView, gesture: ContextGesture?) { guard let component = view.component, let controller = component.controller() as? StoryContainerScreen else { return } - + view.dismissAllTooltips() - + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) var items: [ContextMenuItem] = [] - + if case .liveStream = component.slice.item.storyItem.media { items.append(.action(ContextMenuActionItem(text: self.currentLiveStreamMessageStars != nil ? presentationData.strings.LiveStream_InputContextMenuEditStars : presentationData.strings.LiveStream_InputContextMenuAddStars, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconSuggestPost"), color: theme.contextMenu.primaryColor) }, action: { [weak self, weak view] _, a in a(.default) - + guard let self, let view else { return } self.performPaidMessageAction(view: view) }))) - + var canRemoveStars = self.currentLiveStreamMessageStars != nil if let visibleItemView = view.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View { if let liveChatStateValue = visibleItemView.liveChatState { @@ -484,12 +484,12 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } } - + if canRemoveStars { items.append(.action(ContextMenuActionItem(text: presentationData.strings.LiveStream_InputContextMenuRemoveStars, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/RemovePrice"), color: theme.contextMenu.primaryColor) }, action: { [weak self, weak view] _, a in a(.default) - + guard let self, let view else { return } @@ -502,34 +502,34 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { if let presence = component.slice.additionalPeerData.presence, case .present = presence.status { sendWhenOnlineAvailable = true } - + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendSilently, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.contextMenu.primaryColor) }, action: { [weak self, weak view] _, a in a(.default) - + guard let self, let view else { return } self.performSendMessageAction(view: view, silentPosting: true) }))) - + if sendWhenOnlineAvailable { items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendWhenOnline, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/WhenOnlineIcon"), color: theme.contextMenu.primaryColor) }, action: { [weak self, weak view] _, a in a(.default) - + guard let self, let view else { return } self.performSendMessageAction(view: view, scheduleTime: scheduleWhenOnlineTimestamp) }))) } - + if component.slice.additionalPeerData.sendPaidMessageStars == nil { items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.contextMenu.primaryColor) }, action: { [weak self, weak view] _, a in a(.default) - + guard let self, let view else { return } @@ -537,9 +537,9 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }))) } } - + let contextItems = ContextController.Items(content: .list(items)) - + let contextController = makeContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, position: .top)), items: .single(contextItems), gesture: gesture) contextController.dismissed = { [weak view] in guard let view else { @@ -552,7 +552,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { view.updateIsProgressPaused() controller.present(contextController, in: .window(.root)) } - + func presentScheduleTimePicker( view: StoryItemSetContainerComponent.View ) { @@ -565,12 +565,12 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } let _ = peerId let controller = component.controller() as? StoryContainerScreen - + var sendWhenOnlineAvailable = false if let presence = component.slice.additionalPeerData.presence, case .present = presence.status { sendWhenOnlineAvailable = true } - + let timeController = ChatScheduleTimeController(context: component.context, updatedPresentationData: nil, mode: .scheduledMessages(sendWhenOnlineAvailable: sendWhenOnlineAvailable), style: .media, currentTime: nil, minimalTime: nil, dismissByTapOutside: true, completion: { [weak self, weak view] time in guard let self, let view else { return @@ -586,18 +586,18 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } view.endEditing(true) controller?.present(timeController, in: .window(.root)) - + self.actionSheet = timeController view.updateIsProgressPaused() } - + func presentPaidMessageAlertIfNeeded(view: StoryItemSetContainerComponent.View, completion: @escaping () -> Void) { guard let component = view.component, let sendPaidMessageStars = component.slice.additionalPeerData.sendPaidMessageStars else { completion() return } let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme) - + let controller = chatMessagePaymentAlertController( context: component.context, presentationData: presentationData, @@ -614,13 +614,13 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { ) component.controller()?.present(controller, in: .window(.root)) } - + func performWithPossibleStealthModeConfirmation(view: StoryItemSetContainerComponent.View, action: @escaping () -> Void) { guard let component = view.component, component.stealthModeTimeout != nil else { action() return } - + let _ = (combineLatest( component.context.engine.data.get( TelegramEngine.EngineData.Item.Configuration.StoryConfigurationState() @@ -629,16 +629,16 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { ) |> deliverOnMainQueue).start(next: { [weak self, weak view] data, noticeCount in let config = data - + guard let self, let view, let component = view.component else { return } - + let timestamp = Int32(Date().timeIntervalSince1970) if noticeCount < 1, let activeUntilTimestamp = config.stealthModeState.actualizedNow().activeUntilTimestamp, activeUntilTimestamp > timestamp { let theme = component.theme let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) - + let alertController = AlertScreen( title: component.strings.Story_AlertStealthModeActiveTitle, text: component.strings.Story_AlertStealthModeActiveText, @@ -659,9 +659,9 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } self.actionSheet = alertController view.updateIsProgressPaused() - + component.controller()?.presentInGlobalOverlay(alertController) - + #if DEBUG #else let _ = ApplicationSpecificNotice.incrementStoryStealthModeReplyCount(accountManager: component.context.sharedContext.accountManager).start() @@ -671,7 +671,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } }) } - + func performSendMessageAction( view: StoryItemSetContainerComponent.View, silentPosting: Bool = false, @@ -682,7 +682,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } if case .liveStream = component.slice.item.storyItem.media { if let visibleItem = view.visibleItems[component.slice.item.id], let itemView = visibleItem.view.view as? StoryItemContentComponent.View { - + var sendPaidMessageStars = self.currentLiveStreamMessageStars var isAdmin = false var sendAsPeer: SendAsPeer? @@ -697,7 +697,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { sendPaidMessageStars = StarsAmount(value: minMessagePrice, nanos: 0) } } - + if let currentSendAsPeer = self.currentSendAsPeer { sendAsPeer = currentSendAsPeer } else { @@ -705,7 +705,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { return self.sendAsData?.availablePeers.first(where: { $0.peer.id == defaultSendAs }) } } - + if liveChatStateValue.isAdmin { if let sendAsPeer { isAdmin = sendAsPeer.peer.id == component.context.account.peerId @@ -715,7 +715,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } } - + if let call = itemView.mediaStreamCall { let focusedItem = component.slice.item guard let peerId = focusedItem.peerId else { @@ -724,7 +724,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let inputPanelView = view.inputPanel.view as? MessageInputPanelComponent.View else { return } - + switch inputPanelView.getSendMessageInput() { case let .text(text): if !text.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { @@ -734,7 +734,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { let params = GroupCallMessagesContext.getStarAmountParamMapping(params: paramSets, value: isAdmin ? 1000000000 : sendPaidMessageStars?.value ?? 0) maxInputLength = params.maxLength maxEmojiCount = params.emojiCount - + var sendDisabledMinStars: Int64? if let maxInputLength, text.string.count > maxInputLength { sendDisabledMinStars = paramSets.paramSets.sorted(by: { $0.minStars < $1.minStars }).first(where: { $0.maxMessageLength >= text.string.count })?.minStars ?? 1000000000 @@ -763,36 +763,36 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { if emojiCount > absMaxEmojiCount { let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) view.component?.controller()?.present(textAlertController(context: component.context, updatedPresentationData: (presentationData, .single(presentationData)), title: nil, text: presentationData.strings.LiveStream_ErrorMaxAllowedEmoji_Text(Int32(absMaxEmojiCount)), actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - + return } - + sendDisabledMinStars = paramSets.paramSets.sorted(by: { $0.minStars < $1.minStars }).first(where: { $0.maxEmojiCount >= emojiCount })?.minStars ?? 1000000000 } } - + if let sendDisabledMinStars { self.performPaidMessageAction(view: view, minStars: Int(sendDisabledMinStars)) return } - + if let visibleItemView = view.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View { if !(visibleItemView.liveChatState?.isExpanded ?? true) { visibleItemView.toggleLiveChatExpanded() } visibleItemView.scheduleScrollLiveChatToBottom() } - + let entities = generateChatInputTextEntities(text) - + call.sendMessage(fromId: sendAsPeer?.peer.id, isAdmin: isAdmin, text: text.string, entities: entities, paidStars: sendPaidMessageStars?.value) - + component.storyItemSharedState.replyDrafts.removeValue(forKey: StoryId(peerId: peerId, id: focusedItem.storyItem.id)) inputPanelView.clearSendMessageInput(updateState: true) - + self.currentLiveStreamMessageStars = nil view.state?.updated(transition: .spring(duration: 0.4)) - + let controller = component.controller() as? StoryContainerScreen controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring)) } @@ -801,7 +801,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } return } - + self.performWithPossibleStealthModeConfirmation(view: view, action: { [weak self, weak view] in guard let self, let view else { return @@ -809,7 +809,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let component = view.component else { return } - + let focusedItem = component.slice.item guard let peerId = focusedItem.peerId else { return @@ -819,27 +819,27 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { return } let peer = component.slice.effectivePeer - + let controller = component.controller() as? StoryContainerScreen - + self.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self, weak view] in guard let self, let view else { return } if let recordedAudioPreview = self.recordedAudioPreview, case let .audio(audio) = recordedAudioPreview { self.recordedAudioPreview = nil - + let waveformBuffer = audio.waveform.makeBitstream() - + var messageAttributes: [MessageAttribute] = [] if let sendPaidMessageStars = component.slice.additionalPeerData.sendPaidMessageStars { messageAttributes.append(PaidStarsMessageAttribute(stars: sendPaidMessageStars, postponeSending: false)) } - + let messages: [EnqueueMessage] = [.message(text: "", attributes: messageAttributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: audio.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(audio.fileSize), attributes: [.Audio(isVoice: true, duration: Int(audio.duration), title: nil, performer: nil, waveform: waveformBuffer)], alternativeRepresentations: [])), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] - + let _ = enqueueMessages(account: component.context.account, peerId: peerId, messages: messages).start() - + view.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) } else if self.hasRecordedVideoPreview, let videoRecorderValue = self.videoRecorderValue { videoRecorderValue.send() @@ -868,7 +868,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) component.storyItemSharedState.replyDrafts.removeValue(forKey: StoryId(peerId: peerId, id: focusedItem.storyItem.id)) inputPanelView.clearSendMessageInput(updateState: true) - + self.currentInputMode = .text if hasFirstResponder(view) { view.endEditing(true) @@ -882,7 +882,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) }) } - + func performSendStickerAction(view: StoryItemSetContainerComponent.View, fileReference: FileMediaReference) { self.performWithPossibleStealthModeConfirmation(view: view, action: { [weak self, weak view] in guard let self, let view else { @@ -897,9 +897,9 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } let focusedStoryId = StoryId(peerId: peerId, id: focusedItem.storyItem.id) let peer = component.slice.effectivePeer - + let controller = component.controller() as? StoryContainerScreen - + if let navigationController = controller?.navigationController as? NavigationController { var controllers = navigationController.viewControllers for controller in controllers.reversed() { @@ -910,14 +910,14 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } navigationController.setViewControllers(controllers, animated: true) - + controller?.window?.forEachController({ controller in if let controller = controller as? StickerPackScreenImpl { controller.dismiss() } }) } - + self.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self, weak view] in guard let self, let view else { return @@ -935,7 +935,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } }) - + self.currentInputMode = .text if hasFirstResponder(view) { view.endEditing(true) @@ -946,7 +946,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) }) } - + func performSendContextResultAction(view: StoryItemSetContainerComponent.View, results: ChatContextResultCollection, result: ChatContextResult) { guard let component = view.component else { return @@ -957,9 +957,9 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } let focusedStoryId = StoryId(peerId: peerId, id: focusedItem.storyItem.id) let peer = component.slice.effectivePeer - + let controller = component.controller() as? StoryContainerScreen - + if let navigationController = controller?.navigationController as? NavigationController { var controllers = navigationController.viewControllers for controller in controllers.reversed() { @@ -970,14 +970,14 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } navigationController.setViewControllers(controllers, animated: true) - + controller?.window?.forEachController({ controller in if let controller = controller as? StickerPackScreenImpl { controller.dismiss() } }) } - + self.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self, weak view] in guard let self, let view else { return @@ -995,7 +995,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } }) - + self.currentInputMode = .text if hasFirstResponder(view) { view.endEditing(true) @@ -1005,7 +1005,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring)) }) } - + func enqueueGifData(view: StoryItemSetContainerComponent.View, data: Data) { guard let component = view.component else { return @@ -1022,15 +1022,15 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) }) } - + func enqueueStickerImage(view: StoryItemSetContainerComponent.View, image: UIImage, isMemoji: Bool) { guard let component = view.component else { return } let peer = component.slice.effectivePeer - + let size = image.size.aspectFitted(CGSize(width: 512.0, height: 512.0)) - + func scaleImage(_ image: UIImage, size: CGSize, boundiingSize: CGSize) -> UIImage? { let format = UIGraphicsImageRendererFormat() format.scale = 1.0 @@ -1045,13 +1045,13 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { if let targetSize = targetSize, let scaledImage = scaleImage(image, size: targetSize, boundiingSize: targetSize) { image = scaledImage } - + return Signal { subscriber in if let data = try? WebP.convert(toWebP: image, quality: quality * 100.0) { subscriber.putNext(data) } subscriber.putCompletion() - + return EmptyDisposable } |> runOn(Queue.concurrentDefaultQueue()) } @@ -1060,15 +1060,15 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { if let self, let view, !data.isEmpty { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) component.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data) - + var fileAttributes: [TelegramMediaFileAttribute] = [] fileAttributes.append(.FileName(fileName: "sticker.webp")) fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil)) fileAttributes.append(.ImageSize(size: PixelDimensions(size))) - + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: Int64(data.count), attributes: fileAttributes, alternativeRepresentations: []) let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) - + self.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self] in guard let self else { return @@ -1078,7 +1078,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } }) } - + func setMediaRecordingActive( view: StoryItemSetContainerComponent.View, isActive: Bool, @@ -1086,7 +1086,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { sendAction: Bool ) { self.isMediaRecordingLocked = false - + guard let component = view.component else { return } @@ -1102,7 +1102,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let view, let component = view.component, let peer else { return } - + if isActive { if isVideo { if self.videoRecorderValue == nil { @@ -1159,24 +1159,24 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let self, let view, let component = view.component else { return } - + self.wasRecordingDismissed = !sendAction self.audioRecorder.set(.single(nil)) - + guard let data else { return } - + if data.duration < 0.5 || !sendAction { HapticFeedback().error() } else { let randomId = Int64.random(in: Int64.min ... Int64.max) - + let resource = LocalFileMediaResource(fileId: randomId) component.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data.compressedData) - + let waveformBuffer: Data? = data.waveform - + self.performWithPossibleStealthModeConfirmation(view: view, action: { [weak self, weak view] in guard let self, let view else { return @@ -1186,7 +1186,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { return } self.sendMessages(view: view, peer: peer, messages: [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)], alternativeRepresentations: [])), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) - + HapticFeedback().tap() }) }) @@ -1194,24 +1194,24 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) } else if let videoRecorderValue = self.videoRecorderValue { self.wasRecordingDismissed = !sendAction - + if sendAction { videoRecorderValue.completeVideo() } else { self.videoRecorder.set(.single(nil)) } self.hasRecordedVideoPreview = false - + view.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) } } }) } - + func lockMediaRecording() { self.isMediaRecordingLocked = true } - + func stopMediaRecording(view: StoryItemSetContainerComponent.View) { if let audioRecorderValue = self.audioRecorderValue { let _ = (audioRecorderValue.takenRecordedData() |> deliverOnMainQueue).start(next: { [weak self, weak view] data in @@ -1219,7 +1219,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { return } self.audioRecorder.set(.single(nil)) - + guard let data else { return } @@ -1227,7 +1227,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { HapticFeedback().error() } else if let waveform = data.waveform { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max), size: Int64(data.compressedData.count)) - + component.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data.compressedData) self.recordedAudioPreview = .audio(ChatRecordedMediaPreview.Audio(resource: resource, fileSize: Int32(data.compressedData.count), duration: Int32(data.duration), waveform: AudioWaveform(bitstream: waveform, bitsPerSample: 5))) view.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) @@ -1242,7 +1242,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } } - + func discardMediaRecordingPreview(view: StoryItemSetContainerComponent.View) { if self.recordedAudioPreview != nil { self.recordedAudioPreview = nil @@ -1255,7 +1255,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { view.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) } } - + func performShareAction(view: StoryItemSetContainerComponent.View) { guard let component = view.component else { return @@ -1267,16 +1267,16 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let peerId = focusedItem.peerId else { return } - + if focusedItem.storyItem.isForwardingDisabled { let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) let actionSheet = ActionSheetController(presentationData: presentationData) - + actionSheet.setItemGroups([ ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Story_Context_CopyLink, color: .accent, action: { [weak self, weak view, weak actionSheet] in actionSheet?.dismissAnimated() - + guard let self, let view else { return } @@ -1289,7 +1289,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) ]) ]) - + actionSheet.dismissed = { [weak self, weak view] _ in guard let self, let view else { return @@ -1299,7 +1299,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } self.actionSheet = actionSheet view.updateIsProgressPaused() - + component.presentController(actionSheet, nil) } else { var preferredAction: ShareControllerPreferredAction? @@ -1309,7 +1309,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { |> deliverOnMainQueue).start(next: { link in if let link { UIPasteboard.general.string = link - + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) component.presentController(UndoOverlayController( presentationData: presentationData, @@ -1322,7 +1322,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) })) } - + let shareController = component.context.sharedContext.makeShareController(context: component.context, params: ShareControllerParams( subject: .media(AnyMediaReference.standalone(media: TelegramMediaStory(storyId: StoryId(peerId: peerId, id: focusedItem.storyItem.id), isMention: false)), nil), preferredAction: preferredAction ?? .default, @@ -1419,7 +1419,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { controller.present(shareController, in: .window(.root)) } } - + func performPaidMessageAction(view: StoryItemSetContainerComponent.View, minStars: Int? = nil) { Task { @MainActor [weak view] in guard let view else { @@ -1438,22 +1438,22 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let peerId = focusedItem.peerId else { return } - + var inputText = NSAttributedString(string: "") switch inputPanelView.getSendMessageInput() { case let .text(text): inputText = text } - + guard let visibleItemView = view.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View else { return } - + var minAmount: Int64 = 1 if let minMessagePrice = visibleItemView.liveChatState?.minMessagePrice { minAmount = minMessagePrice } - + var currentAmount: Int? = (self.currentLiveStreamMessageStars?.value).flatMap { Int($0) } if let minStars { if let currentAmountValue = currentAmount { @@ -1462,7 +1462,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { currentAmount = minStars } } - + let initialData = await ChatSendStarsScreen.initialDataLiveStreamMessage( context: component.context, peerId: peerId, @@ -1473,7 +1473,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let self, let view else { return } - + self.currentLiveStreamMessageStars = StarsAmount(value: amount, nanos: 0) view.state?.updated(transition: .spring(duration: 0.4)) } @@ -1487,7 +1487,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } } - + func performShareTextAction(view: StoryItemSetContainerComponent.View, text: String) { guard let component = view.component else { return @@ -1495,10 +1495,10 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let controller = component.controller() else { return } - + let theme = component.theme let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) - + let shareController = component.context.sharedContext.makeShareController(context: component.context, params: ShareControllerParams(subject: .text(text), externalShare: true, immediateExternalShare: false, updatedPresentationData: updatedPresentationData, dismissed: { [weak self, weak view] _ in guard let self, let view else { return @@ -1512,12 +1512,12 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { controller.present(shareController, in: .window(.root)) } - + func performTranslateTextAction(view: StoryItemSetContainerComponent.View, text: String, entities: [MessageTextEntity]) { guard let component = view.component else { return } - + let _ = (component.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings]) |> take(1) |> deliverOnMainQueue).start(next: { [weak self, weak view] sharedData in @@ -1525,25 +1525,25 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { return } let peer = component.slice.effectivePeer - + let _ = self - + let translationSettings: TranslationSettings if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) { translationSettings = current } else { translationSettings = TranslationSettings.defaultSettings } - + var showTranslateIfTopical = false if case let .channel(channel) = peer, !(channel.addressName ?? "").isEmpty { showTranslateIfTopical = true } - + let (_, language) = canTranslateText(context: component.context, text: text, showTranslate: translationSettings.showTranslate, showTranslateIfTopical: showTranslateIfTopical, ignoredLanguages: translationSettings.ignoredLanguages) - + let _ = ApplicationSpecificNotice.incrementTranslationSuggestion(accountManager: component.context.sharedContext.accountManager, timestamp: Int32(Date().timeIntervalSince1970)).start() - + let translateController = TranslateScreen(context: component.context, forceTheme: defaultDarkPresentationTheme, text: text, entities: entities, canCopy: true, fromLanguage: language, ignoredLanguages: translationSettings.ignoredLanguages) translateController.pushController = { [weak view] c in guard let view, let component = view.component else { @@ -1557,10 +1557,10 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } component.controller()?.present(c, in: .window(.root)) } - + self.actionSheet = translateController view.updateIsProgressPaused() - + translateController.wasDismissed = { [weak self, weak view] in guard let self, let view else { return @@ -1568,11 +1568,11 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.actionSheet = nil view.updateIsProgressPaused() } - + component.controller()?.present(translateController, in: .window(.root)) }) } - + func performLookupTextAction(view: StoryItemSetContainerComponent.View, text: String) { guard let component = view.component else { return @@ -1582,38 +1582,38 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { controller.popoverPresentationController?.sourceView = window controller.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0)) window.rootViewController?.present(controller, animated: true) - + final class DeinitWatcher: NSObject { let f: () -> Void - + init(_ f: @escaping () -> Void) { self.f = f } - + deinit { f() } } - + self.lookupController = controller view.updateIsProgressPaused() - + objc_setAssociatedObject(controller, &ObjCKey_DeinitWatcher, DeinitWatcher { [weak self, weak view] in guard let self, let view else { return } - + self.lookupController = nil view.updateIsProgressPaused() }, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } - + func performCopyLinkAction(view: StoryItemSetContainerComponent.View) { guard let component = view.component else { return } - + let _ = (component.context.engine.messages.exportStoryLink(peerId: component.slice.effectivePeer.id, id: component.slice.item.storyItem.id) |> deliverOnMainQueue).start(next: { [weak view] link in guard let view, let component = view.component else { @@ -1621,7 +1621,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } if let link { UIPasteboard.general.string = link - + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) component.presentController(UndoOverlayController( presentationData: presentationData, @@ -1633,13 +1633,13 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } }) } - + private func clearInputText(view: StoryItemSetContainerComponent.View) { guard let inputPanelView = view.inputPanel.view as? MessageInputPanelComponent.View else { return } inputPanelView.clearSendMessageInput(updateState: true) - + guard let component = view.component else { return } @@ -1649,11 +1649,11 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } component.storyItemSharedState.replyDrafts.removeValue(forKey: StoryId(peerId: peerId, id: focusedItem.storyItem.id)) } - + enum AttachMenuSubject { case `default` } - + func presentAttachmentMenu( view: StoryItemSetContainerComponent.View, subject: AttachMenuSubject @@ -1669,13 +1669,13 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let inputPanelView = view.inputPanel.view as? MessageInputPanelComponent.View else { return } - + var inputText = NSAttributedString(string: "") switch inputPanelView.getSendMessageInput() { case let .text(text): inputText = text } - + let _ = (component.context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) ) @@ -1686,19 +1686,19 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let peer else { return } - + let inputIsActive = !"".isEmpty - + self.currentInputMode = .text view.endEditing(true) - + var banSendText: (Int32, Bool)? var bannedSendPhotos: (Int32, Bool)? var bannedSendVideos: (Int32, Bool)? var bannedSendFiles: (Int32, Bool)? - + let _ = bannedSendFiles - + var canSendPolls = true if case let .user(peer) = peer, peer.botInfo == nil { canSendPolls = false @@ -1737,7 +1737,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { canSendPolls = false } } - + var availableButtons: [AttachmentButtonType] = [.gallery, .file] if banSendText == nil { availableButtons.append(.location) @@ -1746,9 +1746,9 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { if canSendPolls { availableButtons.insert(.poll, at: max(0, availableButtons.count - 1)) } - + let isScheduledMessages = !"".isEmpty - + var peerType: AttachMenuBots.Bot.PeerFlags = [] if case let .user(user) = peer { if let _ = user.botInfo { @@ -1765,7 +1765,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { peerType = .group } } - + let buttons: Signal<([AttachmentButtonType], [AttachmentButtonType], AttachmentButtonType?), NoError> if !isScheduledMessages { buttons = component.context.engine.messages.attachMenuBots() @@ -1781,7 +1781,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { case .gift: initialButton = .gift*/ } - + if !"".isEmpty { for bot in attachMenuBots.reversed() { var peerType = peerType @@ -1792,7 +1792,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { let button: AttachmentButtonType = .app(bot) if !bot.peerTypes.intersection(peerType).isEmpty { buttons.insert(button, at: 1) - + /*if case let .bot(botId, _, _) = subject { if initialButton == nil && bot.peer.id == botId { initialButton = button @@ -1802,18 +1802,18 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { allButtons.insert(button, at: 1) } } - + return (buttons, allButtons, initialButton) } } else { buttons = .single((availableButtons, availableButtons, .gallery)) } - + let dataSettings = component.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) return entry ?? GeneratedMediaStoreSettings.defaultSettings } - + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) let premiumGiftOptions: [CachedPremiumGiftOption] if !premiumConfiguration.isPremiumDisabled && premiumConfiguration.showPremiumGiftInAttachMenu, case let .user(user) = peer, !user.isPremium && !user.isDeleted && user.botInfo == nil && !user.flags.contains(.isSupport) { @@ -1822,45 +1822,36 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } else { premiumGiftOptions = [] } - + let _ = combineLatest(queue: Queue.mainQueue(), buttons, dataSettings).start(next: { [weak self, weak view] buttonsAndInitialButton, dataSettings in guard let self, let view, let component = view.component else { return } - + var (buttons, allButtons, initialButton) = buttonsAndInitialButton if !premiumGiftOptions.isEmpty { buttons.insert(.gift, at: 1) } let _ = allButtons - + guard let initialButton = initialButton else { return } - + let currentMediaController = Atomic(value: nil) let currentFilesController = Atomic(value: nil) let currentLocationController = Atomic(value: nil) - + let theme = component.theme let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) - + let attachmentController = AttachmentController( context: component.context, updatedPresentationData: updatedPresentationData, chatLocation: .peer(id: peer.id), buttons: buttons, initialButton: initialButton, - makeEntityInputView: { [weak view] in - guard let view, let component = view.component else { - return nil - } - return EntityInputView( - context: component.context, - isDark: true, - areCustomEmojiEnabled: true //TODO:check custom emoji - ) - } + customEmojiAvailable: true ) attachmentController.didDismiss = { [weak self, weak view] in guard let self, let view else { @@ -1931,7 +1922,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } let theme = component.theme let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) - + let controller = component.context.sharedContext.makeAttachmentFileController(context: component.context, updatedPresentationData: updatedPresentationData, audio: false, bannedSendMedia: bannedSendFiles, presentGallery: { [weak self, weak view, weak attachmentController] in guard let self, let view else { return @@ -2004,7 +1995,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) }) completion(controller, controller.mediaPickerContext) - + let _ = currentLocationController.swap(controller) }) return true @@ -2027,9 +2018,9 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let self, let view, let (peers, _, silent, scheduleTime, text, _) = peers else { return } - + let targetPeer = peer - + var textEnqueueMessage: EnqueueMessage? if let text = text, text.length > 0 { var attributes: [EngineMessage.Attribute] = [] @@ -2052,7 +2043,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { continue } let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") - + let phone = contactData.basicData.phoneNumbers[0].value media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: contact.id, vCardData: nil) case let .deviceContact(_, basicData): @@ -2060,17 +2051,17 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { continue } let contactData = DeviceContactExtendedData(basicData: basicData, middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") - + let phone = contactData.basicData.phoneNumbers[0].value media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: nil, vCardData: nil) } - + if let media = media { let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) enqueueMessages.append(message) } } - + self.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self] in guard let self else { return @@ -2099,7 +2090,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } } - + if let stableId = stableId { return (context.sharedContext.contactDataManager?.extendedData(stableId: stableId) ?? .single(nil)) |> take(1) @@ -2130,7 +2121,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { enqueueMessages.append(textEnqueueMessage) } enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) - + self.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self] in guard let self else { return @@ -2148,13 +2139,13 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { let phone = contactData.basicData.phoneNumbers[0].value if let vCardData = contactData.serializedVCard() { let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peer?.id, vCardData: vCardData) - + var enqueueMessages: [EnqueueMessage] = [] if let textEnqueueMessage = textEnqueueMessage { enqueueMessages.append(textEnqueueMessage) } enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) - + self.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self] in guard let self else { return @@ -2184,7 +2175,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) completion(controller, controller.mediaPickerContext) strongSelf.controllerNavigationDisposable.set(nil) - + let _ = ApplicationSpecificNotice.incrementDismissedPremiumGiftSuggestion(accountManager: context.sharedContext.accountManager, peerId: peer.id).start() }*/ //TODO:gift controller @@ -2236,7 +2227,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.attachmentController = attachmentController view.updateIsProgressPaused() } - + if inputIsActive { Queue.mainQueue().after(0.15, { present() @@ -2247,7 +2238,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) }) } - + private func presentMediaPicker( view: StoryItemSetContainerComponent.View, peer: EnginePeer, @@ -2325,7 +2316,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } present(controller, mediaPickerContext) } - + private func presentOldMediaPicker(view: StoryItemSetContainerComponent.View, peer: EnginePeer, replyMessageId: EngineMessage.Id?, replyToStoryId: StoryId?, fileMode: Bool, editingMedia: Bool, push: @escaping (ViewController) -> Void, completion: @escaping ([Any], Bool, Int32) -> Void) { guard let component = view.component else { return @@ -2338,11 +2329,11 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { case let .text(text): inputText = text } - + let engine = component.context.engine let _ = (component.context.sharedContext.accountManager.transaction { transaction -> Signal<(GeneratedMediaStoreSettings, EngineConfiguration.SearchBots), NoError> in let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) - + return engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots()) |> map { configuration -> (GeneratedMediaStoreSettings, EngineConfiguration.SearchBots) in return (entry ?? GeneratedMediaStoreSettings.defaultSettings, configuration) @@ -2359,13 +2350,13 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { selectionLimit = 10 slowModeEnabled = true } - + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - + let _ = legacyAssetPicker(context: component.context, presentationData: presentationData, editingMedia: editingMedia, fileMode: fileMode, peer: peer._asPeer(), threadTitle: nil, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, selectionLimit: selectionLimit).start(next: { [weak self, weak view] generator in if let view, let component = view.component, let controller = component.controller() { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - + let legacyController = LegacyController(presentation: fileMode ? .navigation : .custom, theme: presentationData.theme, initialLayout: controller.currentlyAppliedLayout) legacyController.navigationPresentation = .modal legacyController.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style @@ -2374,10 +2365,10 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { legacyController?.view.disablesInteractiveModalDismiss = true } let controller = generator(legacyController.context) - + legacyController.bind(controller: controller) legacyController.deferScreenEdgeGestures = [.top] - + configureLegacyAssetPicker(controller, context: component.context, peer: peer._asPeer(), chatLocation: .peer(id: peer.id), initialCaption: inputText, hasSchedule: peer.id.namespace != Namespaces.Peer.SecretChat, presentWebSearch: editingMedia ? nil : { [weak view, weak legacyController] in if let view, let component = view.component { let theme = component.theme @@ -2458,7 +2449,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) }) } - + private func presentFileGallery(view: StoryItemSetContainerComponent.View, peer: EnginePeer, replyMessageId: EngineMessage.Id?, replyToStoryId: StoryId?, editingMessage: Bool = false) { self.presentOldMediaPicker(view: view, peer: peer, replyMessageId: replyMessageId, replyToStoryId: replyToStoryId, fileMode: true, editingMedia: editingMessage, push: { [weak view] c in view?.component?.controller()?.push(c) @@ -2473,7 +2464,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } }) } - + private func presentICloudFileGallery(view: StoryItemSetContainerComponent.View, peer: EnginePeer, replyMessageId: EngineMessage.Id?, replyToStoryId: StoryId?) { guard let component = view.component else { return @@ -2489,9 +2480,9 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } let (accountPeer, limits, premiumLimits) = result let isPremium = accountPeer?.isPremium ?? false - + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - + component.controller()?.present(legacyICloudFilePicker(theme: presentationData.theme, hasMultiselection: true, completion: { [weak self, weak view] urls in if let strongSelf = self, let view, !urls.isEmpty { var signals: [Signal] = [] @@ -2524,7 +2515,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } } - + var groupingKey: Int64? var fileTypes: (music: Bool, other: Bool) = (false, false) if results.count > 1 { @@ -2542,7 +2533,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { if fileTypes.music != fileTypes.other { groupingKey = Int64.random(in: Int64.min ... Int64.max) } - + var messages: [EnqueueMessage] = [] for item in results { if let item = item { @@ -2557,7 +2548,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { if let audioMetadata = item.audioMetadata { attributes.append(.Audio(isVoice: false, duration: audioMetadata.duration, title: audioMetadata.title, performer: audioMetadata.performer, waveform: nil)) } - + let file = TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(item.fileSize), attributes: attributes, alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: replyMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, innerSubject: nil) }, replyToStoryId: replyToStoryId, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []) messages.append(message) @@ -2566,7 +2557,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { groupingKey = Int64.random(in: Int64.min ... Int64.max) } } - + if !messages.isEmpty { strongSelf.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self] in guard let strongSelf = self else { @@ -2581,7 +2572,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }), in: .window(.root)) }) } - + func presentMediaPasteboard(view: StoryItemSetContainerComponent.View, subjects: [MediaPickerScreenImpl.Subject.Media]) { // guard let component = view.component else { // return @@ -2594,13 +2585,13 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { // guard let inputPanelView = view.inputPanel.view as? MessageInputPanelComponent.View else { // return // } -// +// // var inputText = NSAttributedString(string: "") // switch inputPanelView.getSendMessageInput() { // case let .text(text): // inputText = text // } -// +// // let peer = component.slice.effectivePeer // let theme = defaultDarkPresentationTheme // let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) @@ -2649,12 +2640,12 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { // controller.navigationPresentation = .flatModal // component.controller()?.push(controller) } - + private func enqueueChatContextResult(view: StoryItemSetContainerComponent.View, peer: EnginePeer, replyMessageId: EngineMessage.Id?, storyId: StoryId?, results: ChatContextResultCollection, result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false, silentPosting: Bool = false, resetTextInputState: Bool = true) { if !canSendMessagesToPeer(peer._asPeer()) { return } - + let sendMessage: (Int32?) -> Void = { [weak self, weak view] scheduleTime in guard let self, let view, let component = view.component else { return @@ -2673,15 +2664,15 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { postpone: false ) { } - + if let attachmentController = self.attachmentController { attachmentController.dismiss(animated: true) } } - + sendMessage(nil) } - + private func presentWebSearch(view: StoryItemSetContainerComponent.View, activateOnDisplay: Bool = true, present: @escaping (ViewController, Any?) -> Void) { guard let component = view.component else { return @@ -2689,10 +2680,10 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { let context = component.context let peer = component.slice.effectivePeer let storyId = component.slice.item.storyItem.id - + let theme = component.theme let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) - + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots()) |> deliverOnMainQueue).start(next: { [weak self, weak view] configuration in if let self { @@ -2718,7 +2709,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } }) } - + private func getCaptionPanelView(view: StoryItemSetContainerComponent.View, peer: EnginePeer, mediaPicker: MediaPickerScreenImpl? = nil) -> TGCaptionPanelView? { guard let component = view.component else { return nil @@ -2754,14 +2745,19 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { view.updateIsProgressPaused() } view.component?.controller()?.push(c) - + view.updateIsProgressPaused() } else { view.component?.controller()?.presentInGlobalOverlay(c) } + }, getNavigationController: { [weak view] in + guard let controller = view?.component?.controller() else { + return nil + } + return controller.navigationController as? NavigationController }) as? TGCaptionPanelView } - + private func openCamera(view: StoryItemSetContainerComponent.View, peer: EnginePeer, replyToMessageId: EngineMessage.Id?, replyToStoryId: StoryId?, cameraView: TGAttachmentCameraView? = nil) { guard let component = view.component else { return @@ -2769,13 +2765,13 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let inputPanelView = view.inputPanel.view as? MessageInputPanelComponent.View else { return } - + var inputText = NSAttributedString(string: "") switch inputPanelView.getSendMessageInput() { case let .text(text): inputText = text } - + let _ = (component.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) return entry ?? GeneratedMediaStoreSettings.defaultSettings @@ -2784,17 +2780,17 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let self, let view, let component = view.component, let parentController = component.controller() else { return } - + var enablePhoto = true var enableVideo = true - + if let callManager = component.context.sharedContext.callManager, callManager.hasActiveCall { enableVideo = false } - + var bannedSendPhotos: (Int32, Bool)? var bannedSendVideos: (Int32, Bool)? - + if case let .channel(channel) = peer { if let value = channel.hasBannedPermission(.banSendPhotos) { bannedSendPhotos = value @@ -2810,16 +2806,16 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { bannedSendVideos = (Int32.max, false) } } - + if bannedSendPhotos != nil { enablePhoto = false } if bannedSendVideos != nil { enableVideo = false } - + let storeCapturedMedia = peer.id.namespace != Namespaces.Peer.SecretChat - + presentedLegacyCamera(context: component.context, peer: peer._asPeer(), chatLocation: .peer(id: peer.id), cameraView: cameraView, menuController: nil, parentController: parentController, attachmentController: self.attachmentController, editingMedia: false, saveCapturedPhotos: storeCapturedMedia, mediaGrouping: true, initialCaption: inputText, hasSchedule: peer.id.namespace != Namespaces.Peer.SecretChat, enablePhoto: enablePhoto, enableVideo: enableVideo, sendMessagesWithSignals: { [weak self, weak view] signals, silentPosting, scheduleTime, parameters in guard let self, let view else { return @@ -2866,7 +2862,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) }) } - + private func presentScheduleTimePicker( view: StoryItemSetContainerComponent.View, peer: EnginePeer, @@ -2885,7 +2881,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let view, let component = view.component else { return } - + var sendWhenOnlineAvailable = false if let presence, case .present = presence.status { sendWhenOnlineAvailable = true @@ -2893,7 +2889,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id.id._internalGetInt64Value() == 777000 { sendWhenOnlineAvailable = false } - + let mode: ChatScheduleTimeControllerMode if peer.id == component.context.account.peerId { mode = .reminders @@ -2908,7 +2904,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { view.component?.controller()?.present(controller, in: .window(.root)) }) } - + private func presentTimerPicker(view: StoryItemSetContainerComponent.View, peer: EnginePeer, style: ChatTimerScreenStyle = .default, selectedTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) { guard let component = view.component else { return @@ -2920,16 +2916,16 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { view.endEditing(true) component.controller()?.present(controller, in: .window(.root)) } - + private func transformEnqueueMessages(view: StoryItemSetContainerComponent.View, messages: [EnqueueMessage], silentPosting: Bool, scheduleTime: Int32? = nil) -> [EnqueueMessage] { var focusedStoryId: StoryId? if let component = view.component, let peerId = component.slice.item.peerId { focusedStoryId = StoryId(peerId: peerId, id: component.slice.item.storyItem.id) } - + return messages.map { message in var message = message - + if let focusedStoryId { switch message { case let .message(text, attributes, inlineStickers, mediaReference, threadId, replyToMessageId, _, localGroupingKey, correlationId, bubbleUpEmojiOrStickersets): @@ -2940,7 +2936,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { break } } - + return message.withUpdatedAttributes { attributes in var attributes = attributes if silentPosting || scheduleTime != nil { @@ -2966,7 +2962,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } } - + private func sendMessages(view: StoryItemSetContainerComponent.View, peer: EnginePeer, messages: [EnqueueMessage], silentPosting: Bool = false, scheduleTime: Int32? = nil) { guard let component = view.component else { return @@ -2979,29 +2975,29 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } }) - + donateSendMessageIntent(account: component.context.account, sharedContext: component.context.sharedContext, intentContext: .chat, peerIds: [peer.id]) - + if let attachmentController = self.attachmentController { attachmentController.dismiss(animated: true) } } - + private func enqueueMediaMessages(view: StoryItemSetContainerComponent.View, peer: EnginePeer, replyToMessageId: EngineMessage.Id?, replyToStoryId: StoryId?, signals: [Any]?, silentPosting: Bool, scheduleTime: Int32? = nil, parameters: ChatSendMessageActionSheetController.SendParameters? = nil, getAnimatedTransitionSource: ((String) -> UIView?)? = nil, completion: @escaping () -> Void = {}) { guard let component = view.component else { return } - + self.enqueueMediaMessageDisposable.set((legacyAssetPickerEnqueueMessages(context: component.context, account: component.context.account, signals: signals!) |> deliverOnMainQueue).start(next: { [weak self, weak view] items in if let strongSelf = self, let view { var mappedMessages: [EnqueueMessage] = [] var addedTransitions: [(Int64, [String], () -> Void)] = [] - + var groupedCorrelationIds: [Int64: Int64] = [:] - + var skipAddingTransitions = false - + for item in items { var message = item.message if message.groupingKey != nil { @@ -3011,7 +3007,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } else if items.count > 3 { skipAddingTransitions = true } - + if let uniqueId = item.uniqueId, !item.isFile && !skipAddingTransitions { let correlationId: Int64 var addTransition = scheduleTime == nil @@ -3058,12 +3054,12 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } strongSelf.sendMessages(view: view, peer: peer, messages: mappedMessages.map { $0.withUpdatedReplyToMessageId(replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, innerSubject: nil) }).withUpdatedReplyToStoryId(replyToStoryId) }, silentPosting: silentPosting, scheduleTime: scheduleTime) - + completion() } })) } - + private func editMessageMediaWithLegacySignals(view: StoryItemSetContainerComponent.View, signals: [Any]) { guard let component = view.component else { return @@ -3071,12 +3067,12 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { let _ = (legacyAssetPickerEnqueueMessages(context: component.context, account: component.context.account, signals: signals) |> deliverOnMainQueue).start() } - + func openResolved(view: StoryItemSetContainerComponent.View, result: ResolvedUrl, forceExternal: Bool = false, concealed: Bool = false, completion: (() -> Void)? = nil) { guard let component = view.component, let navigationController = component.controller()?.navigationController as? NavigationController else { return } - + self.progressPauseContext.update = { [weak self, weak view] controller in guard let self, let view else { return @@ -3084,7 +3080,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.progressPauseContext.externalController = controller view.updateIsProgressPaused() } - + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) let updatedPresentationData: (PresentationData, Signal) = (presentationData, .single(presentationData)) let peerId = component.slice.effectivePeer.id @@ -3142,7 +3138,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { return } controller.present(c, in: .window(.root), with: a) - }, + }, dismissInput: { [weak view] in guard let view else { return @@ -3154,7 +3150,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { completion: completion ) } - + func navigateToMessage(view: StoryItemSetContainerComponent.View, messageId: EngineMessage.Id, completion: (() -> Void)?) { guard let component = view.component else { return @@ -3170,14 +3166,14 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { completion?() }) } - + func openPeerMention(view: StoryItemSetContainerComponent.View, name: String, sourceMessageId: MessageId? = nil) { guard let component = view.component, let parentController = component.controller() else { return } let disposable = self.resolvePeerByNameDisposable var resolveSignal = component.context.engine.peers.resolvePeerByName(name: name, referrer: nil, ageLimit: 10) - + var cancelImpl: (() -> Void)? let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let progressSignal = Signal { [weak self, weak view, weak parentController] subscriber in @@ -3185,14 +3181,14 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { cancelImpl?() })) parentController?.present(controller, in: .window(.root)) - + self?.statusController = controller view?.updateIsProgressPaused() - + return ActionDisposable { [weak controller] in Queue.mainQueue().async() { controller?.dismiss() - + self?.statusController = nil view?.updateIsProgressPaused() } @@ -3201,7 +3197,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { |> runOn(Queue.mainQueue()) |> delay(0.15, queue: Queue.mainQueue()) let progressDisposable = progressSignal.start() - + resolveSignal = resolveSignal |> afterDisposed { Queue.mainQueue().async { @@ -3242,14 +3238,14 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } })) } - + func openHashtag(view: StoryItemSetContainerComponent.View, hashtag: String, peerName: String?) { guard let component = view.component, let parentController = component.controller() else { return } - + let peerId = component.slice.effectivePeer.id - + var resolveSignal: Signal if let peerName = peerName { resolveSignal = component.context.engine.peers.resolvePeerByName(name: peerName, referrer: nil) @@ -3277,14 +3273,14 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { cancelImpl?() })) parentController?.present(controller, in: .window(.root)) - + self?.statusController = controller view?.updateIsProgressPaused() - + return ActionDisposable { [weak controller] in Queue.mainQueue().async() { controller?.dismiss() - + self?.statusController = nil view?.updateIsProgressPaused() } @@ -3293,7 +3289,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { |> runOn(Queue.mainQueue()) |> delay(0.15, queue: Queue.mainQueue()) let progressDisposable = progressSignal.start() - + resolveSignal = resolveSignal |> afterDisposed { Queue.mainQueue().async { @@ -3325,7 +3321,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } })) } - + func openPeerMention(view: StoryItemSetContainerComponent.View, peerId: EnginePeer.Id) { guard let component = view.component else { return @@ -3338,12 +3334,12 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.openPeer(view: view, peer: peer) }) } - + func openPeer(view: StoryItemSetContainerComponent.View, peer: EnginePeer, expandAvatar: Bool = false, peerTypes: ReplyMarkupButtonAction.PeerTypes? = nil) { guard let component = view.component else { return } - + let peerSignal: Signal = component.context.account.postbox.loadedPeerWithId(peer.id) |> map(Optional.init) self.navigationActionDisposable.set((peerSignal |> take(1) |> deliverOnMainQueue).start(next: { [weak view] peer in guard let view, let component = view.component, let peer else { @@ -3362,16 +3358,16 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } })) } - + func presentTextEntityActions(view: StoryItemSetContainerComponent.View, action: StoryContentCaptionComponent.Action, openUrl: @escaping (String, Bool) -> Void) { guard let component = view.component else { return } - + let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: component.theme, fontSize: .regular), allowInputInset: false) - + var canOpenIn = false - + let title: String let value: String var openAction: String? = component.strings.Conversation_LinkDialogOpen @@ -3401,10 +3397,10 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { case .customEmoji: return } - + var items: [ActionSheetItem] = [] items.append(ActionSheetTextItem(title: title)) - + if let openAction { items.append(ActionSheetButtonItem(title: openAction, color: .accent, action: { [weak self, weak view, weak actionSheet] in actionSheet?.dismissAnimated() @@ -3434,19 +3430,19 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } })) } - + items.append(ActionSheetButtonItem(title: copyAction, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() UIPasteboard.general.string = value })) - + if case let .url(url, _) = action, let link = URL(string: url) { items.append(ActionSheetButtonItem(title: component.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) })) } - + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: component.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() @@ -3459,18 +3455,18 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.actionSheet = nil view.updateIsProgressPaused() } - + component.controller()?.present(actionSheet, in: .window(.root)) - + self.actionSheet = actionSheet view.updateIsProgressPaused() } - + func openAttachedStickers(view: StoryItemSetContainerComponent.View, packs: Signal<[StickerPackReference], NoError>) { guard let component = view.component else { return } - + guard let parentController = component.controller() as? StoryContainerScreen else { return } @@ -3479,14 +3475,14 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { let progressSignal = Signal { [weak parentController, weak self, weak view] subscriber in let progressController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) parentController?.present(progressController, in: .window(.root), with: nil) - + self?.statusController = progressController view?.updateIsProgressPaused() - + return ActionDisposable { [weak progressController] in Queue.mainQueue().async() { progressController?.dismiss() - + self?.statusController = nil view?.updateIsProgressPaused() } @@ -3495,7 +3491,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { |> runOn(Queue.mainQueue()) |> delay(0.15, queue: Queue.mainQueue()) let progressDisposable = progressSignal.start() - + let signal = packs |> afterDisposed { Queue.mainQueue().async { @@ -3533,16 +3529,16 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) parentController?.present(controller, in: .window(.root), with: nil) }) - + self.isViewingAttachedStickers = true view.updateIsProgressPaused() } - + func requestStealthMode(view: StoryItemSetContainerComponent.View) { guard let component = view.component else { return } - + let _ = (component.context.engine.data.get( TelegramEngine.EngineData.Item.Configuration.StoryConfigurationState(), TelegramEngine.EngineData.Item.Configuration.App() @@ -3551,11 +3547,11 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let self, let view, let component = view.component, let controller = component.controller() else { return } - + let timestamp = Int32(Date().timeIntervalSince1970) if let activeUntilTimestamp = config.stealthModeState.actualizedNow().activeUntilTimestamp, activeUntilTimestamp > timestamp { let remainingActiveSeconds = activeUntilTimestamp - timestamp - + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) let text = component.strings.Story_ToastStealthModeActiveText(timeIntervalString(strings: presentationData.strings, value: remainingActiveSeconds)).string let tooltipScreen = UndoOverlayController( @@ -3579,24 +3575,24 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.currentTooltipUpdateTimer = nil return } - + let timestamp = Int32(Date().timeIntervalSince1970) let remainingActiveSeconds = max(1, activeUntilTimestamp - timestamp) - + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) let text = component.strings.Story_ToastStealthModeActiveText(timeIntervalString(strings: presentationData.strings, value: remainingActiveSeconds)).string tooltipScreenValue.content = .actionSucceeded(title: component.strings.Story_ToastStealthModeActiveTitle, text: text, cancel: "", destructive: false) }) - + self.tooltipScreen?.dismiss(animated: true) self.tooltipScreen = tooltipScreen controller.present(tooltipScreen, in: .current) - + view.updateIsProgressPaused() - + return } - + let pastPeriod: Int32 let futurePeriod: Int32 if let data = appConfig.data, let futurePeriodF = data["stories_stealth_future_period"] as? Double, let pastPeriodF = data["stories_stealth_past_period"] as? Double { @@ -3606,7 +3602,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { pastPeriod = 5 * 60 futurePeriod = 25 * 60 } - + let sheet = StoryStealthModeSheetScreen( context: component.context, mode: .control(external: false, cooldownUntilTimestamp: config.stealthModeState.actualizedNow().cooldownUntilTimestamp), @@ -3617,13 +3613,13 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let self, let view, let component = view.component else { return } - + let _ = (component.context.engine.messages.enableStoryStealthMode() |> deliverOnMainQueue).start(completed: { [weak self, weak view] in guard let self, let view, let component = view.component, let controller = component.controller() else { return } - + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) let text = component.strings.Story_ToastStealthModeActivatedText(timeIntervalString(strings: presentationData.strings, value: pastPeriod), timeIntervalString(strings: presentationData.strings, value: futurePeriod)).string let tooltipScreen = UndoOverlayController( @@ -3638,9 +3634,9 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.tooltipScreen?.dismiss(animated: true) self.tooltipScreen = tooltipScreen controller.present(tooltipScreen, in: .current) - + view.updateIsProgressPaused() - + HapticFeedback().success() }) } @@ -3657,12 +3653,12 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { controller.push(sheet) }) } - + func presentStealthModeUpgrade(view: StoryItemSetContainerComponent.View, action: @escaping () -> Void) { guard let component = view.component else { return } - + let _ = (component.context.engine.data.get( TelegramEngine.EngineData.Item.Configuration.StoryConfigurationState(), TelegramEngine.EngineData.Item.Configuration.App() @@ -3671,7 +3667,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let self, let view, let component = view.component, let controller = component.controller() else { return } - + let pastPeriod: Int32 let futurePeriod: Int32 if let data = appConfig.data, let futurePeriodF = data["stories_stealth_future_period"] as? Double, let pastPeriodF = data["stories_stealth_past_period"] as? Double { @@ -3681,7 +3677,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { pastPeriod = 5 * 60 futurePeriod = 25 * 60 } - + let sheet = StoryStealthModeSheetScreen( context: component.context, mode: .upgrade, @@ -3704,12 +3700,12 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { controller.push(sheet) }) } - + func presentQualityUpgrade(view: StoryItemSetContainerComponent.View, action: @escaping () -> Void) { guard let component = view.component, let controller = component.controller() else { return } - + let sheet = StoryQualityUpgradeSheetScreen( context: component.context, buttonAction: { @@ -3727,20 +3723,20 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { view.updateIsProgressPaused() controller.push(sheet) } - + private var selectedMediaArea: MediaArea? func activateMediaArea(view: StoryItemSetContainerComponent.View, mediaArea: MediaArea, position: CGPoint? = nil, immediate: Bool = false) { guard let component = view.component, let controller = component.controller() else { return } - + let theme = defaultDarkColorPresentationTheme let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) - + let context = component.context - + var useGesturePosition = false - + var actions: [ContextMenuAction] = [] switch mediaArea { case let .venue(coordinates, venue): @@ -3859,18 +3855,18 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { action() })) } - + self.selectedMediaArea = mediaArea - + let referenceSize = view.controlsContainerView.frame.size let size = CGSize(width: 16.0, height: mediaArea.coordinates.height / 100.0 * referenceSize.height * 1.1) var frame = CGRect(x: mediaArea.coordinates.x / 100.0 * referenceSize.width - size.width / 2.0, y: mediaArea.coordinates.y / 100.0 * referenceSize.height - size.height / 2.0, width: size.width, height: size.height) frame = view.controlsContainerView.convert(frame, to: nil) - + if useGesturePosition, let position { frame = CGRect(origin: position.offsetBy(dx: 0.0, dy: 44.0), size: .zero) } - + let node = controller.displayNode let menuController = makeContextMenuController(actions: actions, blurred: true) menuController.centerHorizontally = true @@ -3901,17 +3897,17 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.menuController = menuController view.updateIsProgressPaused() } - + func activateInlineReaction(view: StoryItemSetContainerComponent.View, reactionView: UIView, reaction: MessageReaction.Reaction) { guard let component = view.component else { return } - + let animateWithReactionItem: (ReactionItem) -> Void = { [weak self, weak view] reactionItem in guard let self, let view else { return } - + self.performWithPossibleStealthModeConfirmation(view: view, action: { [weak view] in guard let view, let component = view.component else { return @@ -3919,26 +3915,26 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { if component.slice.effectivePeer.id != component.context.account.peerId { let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.effectivePeer.id, id: component.slice.item.storyItem.id, reaction: reaction).start() } - + let targetFrame = reactionView.convert(reactionView.bounds, to: view) - + let targetView = UIView(frame: targetFrame) targetView.isUserInteractionEnabled = false view.addSubview(targetView) - + let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: nil, useDirectRendering: false) view.componentContainerView.addSubview(standaloneReactionAnimation.view) - + if let standaloneReactionAnimation = view.standaloneReactionAnimation { view.standaloneReactionAnimation = nil - + let standaloneReactionAnimationView = standaloneReactionAnimation.view standaloneReactionAnimation.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak standaloneReactionAnimationView] _ in standaloneReactionAnimationView?.removeFromSuperview() }) } view.standaloneReactionAnimation = standaloneReactionAnimation - + standaloneReactionAnimation.frame = view.bounds standaloneReactionAnimation.animateReactionSelection( context: component.context, @@ -3954,13 +3950,13 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let view else { return } - + if let standaloneReactionAnimation = view.standaloneReactionAnimation { view.standaloneReactionAnimation = nil standaloneReactionAnimation.view.removeFromSuperview() } view.standaloneReactionAnimation = standaloneReactionAnimation - + standaloneReactionAnimation.frame = view.bounds view.componentContainerView.addSubview(standaloneReactionAnimation.view) }, @@ -3971,7 +3967,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { ) }) } - + switch reaction { case .builtin: if let availableReactions = component.availableReactions { @@ -4011,7 +4007,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } } - + func openSendStars(view: StoryItemSetContainerComponent.View) { Task { @MainActor [weak view] in guard let view else { @@ -4027,7 +4023,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let peerId = focusedItem.peerId else { return } - + var topPeers: [ReactionsMessageAttribute.TopPeer] = [] var minAmount: Int64 = 1 var sendAsPeer: SendAsPeer? @@ -4043,11 +4039,11 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { ) } } - + if let minMessagePrice = visibleItemView.liveChatState?.minMessagePrice { minAmount = minMessagePrice } - + if let currentSendAsPeer = self.currentSendAsPeer { sendAsPeer = currentSendAsPeer } else { @@ -4056,7 +4052,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } } - + let initialData = await ChatSendStarsScreen.initialData( context: component.context, peerId: peerId, @@ -4086,30 +4082,30 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } } - + func performSendStars(view: StoryItemSetContainerComponent.View, buttonView: UIView?, count: Int, isFromExpandedView: Bool) { Task { @MainActor [weak self, weak view] in guard let self, let view, let component = view.component else { return } - + guard let visibleItemView = view.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View else { return } - + if isFromExpandedView { if let currentSendStarsUndoController = self.currentSendStarsUndoController { self.currentSendStarsUndoController = nil currentSendStarsUndoController.dismiss() } - + self.commitSendStars(view: view, count: count, delay: false) } else { let starsContextState = await component.context.starsContext?.state.get() guard let balance = starsContextState?.balance else { return } - + var totalExpectedStars = count if let pendingMyStars = visibleItemView.liveChatState?.starStats?.pendingMyStars, pendingMyStars > 0 { totalExpectedStars += Int(pendingMyStars) @@ -4121,7 +4117,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let targetPeerId = component.slice.item.peerId else { return } - + let customTheme = component.theme let options = await component.context.engine.payments.starsTopUpOptions().get() let controller = component.context.sharedContext.makeStarsPurchaseScreen( @@ -4134,10 +4130,10 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { completion: { _ in } ) navigationController.pushViewController(controller) - + return } - + var reactionItem: ReactionItem? if let availableReactions = await component.context.availableReactions.get() { for item in availableReactions.reactions { @@ -4148,7 +4144,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let aroundAnimation = item.aroundAnimation else { continue } - + reactionItem = ReactionItem( reaction: ReactionItem.Reaction(rawValue: item.value), appearAnimation: item.appearAnimation, @@ -4163,27 +4159,27 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } } - + if let reactionItem, let buttonView { let targetFrame = buttonView.convert(buttonView.bounds, to: view) - + let targetView = UIView(frame: targetFrame) targetView.isUserInteractionEnabled = false view.addSubview(targetView) - + let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: nil, useDirectRendering: false) view.componentContainerView.addSubview(standaloneReactionAnimation.view) - + if let standaloneReactionAnimation = view.standaloneReactionAnimation { view.standaloneReactionAnimation = nil - + let standaloneReactionAnimationView = standaloneReactionAnimation.view standaloneReactionAnimation.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak standaloneReactionAnimationView] _ in standaloneReactionAnimationView?.removeFromSuperview() }) } view.standaloneReactionAnimation = standaloneReactionAnimation - + standaloneReactionAnimation.frame = view.bounds standaloneReactionAnimation.animateReactionSelection( context: component.context, @@ -4199,13 +4195,13 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let view else { return } - + if let standaloneReactionAnimation = view.standaloneReactionAnimation { view.standaloneReactionAnimation = nil standaloneReactionAnimation.view.removeFromSuperview() } view.standaloneReactionAnimation = standaloneReactionAnimation - + standaloneReactionAnimation.frame = view.bounds view.componentContainerView.addSubview(standaloneReactionAnimation.view) }, @@ -4215,7 +4211,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } ) } - + self.currentLiveStreamStarsIsActive = true self.currentLiveStreamStarsIsActiveTimer?.invalidate() self.currentLiveStreamStarsIsActiveTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false, block: { [weak self, weak view] _ in @@ -4225,7 +4221,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.currentLiveStreamStarsIsActive = false view.state?.updated(transition: .spring(duration: 0.4)) }) - + var totalStars = 0 if let pendingMyStars = visibleItemView.liveChatState?.starStats?.pendingMyStars, pendingMyStars > 0 { totalStars += count @@ -4236,10 +4232,10 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { var count = count count = max(Int(minAmount), count) totalStars += count - + self.commitSendStars(view: view, count: count, delay: true) } - + let title: String /*if case .anonymous = privacy { title = self.presentationData.strings.Chat_ToastStarsSent_AnonymousTitle(Int32(self.currentSendStarsUndoCount)) @@ -4249,12 +4245,12 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } else*/ do { title = component.strings.Chat_ToastStarsSent_Title(Int32(totalStars)) } - + let textItems = AnimatedTextComponent.extractAnimatedTextString(string: component.strings.Chat_ToastStarsSent_Text("", ""), id: "text", mapping: [ 0: .number(totalStars, minDigits: 1), 1: .text(component.strings.Chat_ToastStarsSent_TextStarAmount(Int32(totalStars))) ]) - + if let current = self.currentSendStarsUndoController { current.content = .starsSent(context: component.context, title: title, text: textItems, hasUndo: true) } else { @@ -4286,7 +4282,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } } - + private func commitSendStars(view: StoryItemSetContainerComponent.View, count: Int, delay: Bool) { guard let component = view.component else { return @@ -4310,7 +4306,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { return self.sendAsData?.availablePeers.first(where: { $0.peer.id == defaultSendAs }) } } - + if liveChatStateValue.isAdmin { if let sendAsPeer { isAdmin = sendAsPeer.peer.id == component.context.account.peerId @@ -4319,10 +4315,10 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } } - + call.sendStars(fromId: sendAsPeer?.peer.id, isAdmin: isAdmin, amount: Int64(count), delay: delay) } - + func openSendAsSelection(view: StoryItemSetContainerComponent.View, sourceView: UIView, gesture: ContextGesture?) { guard let component = view.component, let sendAsData = self.sendAsData, let controller = component.controller() else { return @@ -4330,7 +4326,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let visibleItemView = view.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View else { return } - + var currentSendAsPeer: SendAsPeer? if let currentSendAsPeerValue = self.currentSendAsPeer { currentSendAsPeer = currentSendAsPeerValue @@ -4339,13 +4335,13 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { return self.sendAsData?.availablePeers.first(where: { $0.peer.id == defaultSendAs }) } } - + let focusedItem = component.slice.item guard let peerId = focusedItem.peerId else { return } let isPremium = sendAsData.isPremium - + var items: [ContextMenuItem] = [] items.append(.custom(ChatSendAsPeerTitleContextItem(text: component.strings.Conversation_SendMesageAs.uppercased()), false)) items.append(.custom(ChatSendAsPeerListContextItem( @@ -4368,7 +4364,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { return } HapticFeedback().impact() - + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) controller.present(UndoOverlayController(presentationData: presentationData, content: .invitedToVoiceChat(context: component.context, peer: peer, title: nil, text: presentationData.strings.Conversation_SendMesageAsPremiumInfo, action: presentationData.strings.EmojiInput_PremiumEmojiToast_Action, duration: 3), elevatedLayout: false, action: { [weak view] action in guard let view, let component = view.component, let controller = component.controller() else { @@ -4376,7 +4372,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } if case .undo = action { view.endEditing(true) - + let introScreen = PremiumIntroScreen(context: component.context, source: .settings) controller.push(introScreen) } @@ -4385,25 +4381,25 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }), false )) - + final class ReferenceSource: ContextReferenceContentSource { let controller: ViewController let sourceView: UIView let insets: UIEdgeInsets let contentInsets: UIEdgeInsets - + init(controller: ViewController, sourceView: UIView, insets: UIEdgeInsets, contentInsets: UIEdgeInsets = UIEdgeInsets()) { self.controller = controller self.sourceView = sourceView self.insets = insets self.contentInsets = contentInsets } - + func transitionInfo() -> ContextControllerReferenceViewInfo? { return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds.inset(by: self.insets), insets: self.contentInsets, actionsPosition: .top) } } - + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) let contextController = makeContextController(presentationData: presentationData, source: .reference(ReferenceSource(controller: controller, sourceView: sourceView, insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0), contentInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0))), items: .single(ContextController.Items(content: .list(items))), gesture: gesture, workaroundUseLegacyImplementation: false) contextController.dismissed = { [weak self, weak view] in @@ -4414,19 +4410,19 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { view.state?.updated(transition: .spring(duration: 0.4)) } controller.presentInGlobalOverlay(contextController) - + self.isSelectingSendAsPeer = true view.state?.updated(transition: .spring(duration: 0.4)) } - + func displayLiveStreamSettings(view: StoryItemSetContainerComponent.View) { Task { @MainActor [weak self, weak view] in guard let self, let view, let component = view.component, let controller = component.controller(), let mediaStreamCall = view.mediaStreamCall else { return } - + let callState = await (mediaStreamCall.state |> take(1)).get() - + let stateContext = LiveStreamSettingsScreen.StateContext( context: component.context, mode: .edit( @@ -4463,7 +4459,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { public class StoryProgressPauseContext { fileprivate weak var externalController: ViewController? public fileprivate(set) var update: (ViewController?) -> Void = { _ in } - + var hasExternalController: Bool { return self.externalController != nil } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift index 56b4cc2aba..af2bb3adf2 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift @@ -477,6 +477,9 @@ extension ChatControllerImpl { if let _ = self.presentationInterfaceState.inputTextPanelState.mediaRecordingState { self.dismissMediaRecorder(pause ? .pause : .preview) } else { + if case .video = self.presentationInterfaceState.interfaceState.mediaDraftState { + return + } self.videoRecorder.set(.single(nil)) } } @@ -495,6 +498,10 @@ extension ChatControllerImpl { }.updatedInterfaceState { $0.withUpdatedMediaDraftState(nil) } }) } else { + if case .video = self.presentationInterfaceState.interfaceState.mediaDraftState { + return + } + let proceed = { self.withAudioRecorder(resuming: true, { audioRecorder in audioRecorder.resume() @@ -773,7 +780,10 @@ extension ChatControllerImpl { donateSendMessageIntent(account: self.context.account, sharedContext: self.context.sharedContext, intentContext: .chat, peerIds: [peerId]) case .video: - self.videoRecorderValue?.sendVideoRecording(silentPosting: silentPosting, scheduleTime: scheduleTime, messageEffect: messageEffect) + guard let videoRecorderValue = self.videoRecorderValue else { + return + } + videoRecorderValue.sendVideoRecording(silentPosting: silentPosting, scheduleTime: scheduleTime, messageEffect: messageEffect) } } } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift index 5d489d7069..31aea1e533 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift @@ -10,7 +10,6 @@ import MediaPickerUI import MediaPasteboardUI import LegacyMediaPickerUI import MediaEditor -import ChatEntityKeyboardInputNode extension ChatControllerImpl { func displayPasteMenu(_ subjects: [MediaPickerScreenImpl.Subject.Media]) { @@ -39,12 +38,7 @@ extension ChatControllerImpl { } }, getSourceRect: nil, - makeEntityInputView: { [weak self] in - guard let self else { - return nil - } - return EntityInputView(context: self.context, isDark: false, areCustomEmojiEnabled: self.presentationInterfaceState.customEmojiAvailable) - } + customEmojiAvailable: strongSelf.presentationInterfaceState.customEmojiAvailable ) controller.navigationPresentation = .flatModal strongSelf.push(controller) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 87c6a95c98..fe135f6300 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -9653,7 +9653,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let tooltipController = self.mediaRecordingModeTooltipController { tooltipController.updateContent(.text(text), animated: true, extendTimer: true) - } else if let rect = rect { + } else if let rect { let tooltipController = TooltipController(content: .text(text), baseFontSize: self.presentationData.listsFontSize.baseDisplaySize, padding: 2.0) self.mediaRecordingModeTooltipController = tooltipController tooltipController.dismissed = { [weak self, weak tooltipController] _ in diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index e8ba9b0947..65d20bc263 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -302,22 +302,17 @@ extension ChatControllerImpl { } return } - + let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText - + let currentMediaController = Atomic(value: nil) let currentFilesController = Atomic(value: nil) let currentAudioController = Atomic(value: nil) let currentLocationController = Atomic(value: nil) - + strongSelf.canReadHistory.set(false) - - let attachmentController = AttachmentController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, style: .glass, chatLocation: strongSelf.chatLocation, isScheduledMessages: isScheduledMessages, buttons: buttons, initialButton: initialButton, makeEntityInputView: { [weak self] in - guard let strongSelf = self else { - return nil - } - return EntityInputView(context: strongSelf.context, isDark: false, areCustomEmojiEnabled: strongSelf.presentationInterfaceState.customEmojiAvailable) - }) + + let attachmentController = AttachmentController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, style: .glass, chatLocation: strongSelf.chatLocation, isScheduledMessages: isScheduledMessages, buttons: buttons, initialButton: initialButton, customEmojiAvailable: strongSelf.presentationInterfaceState.customEmojiAvailable) attachmentController.attachmentButton = strongSelf.chatDisplayNode.getAttachmentButton() attachmentController.shouldMinimizeOnSwipe = { [weak attachmentController] button in if case .app = button { @@ -1899,6 +1894,8 @@ extension ChatControllerImpl { return } self.presentInGlobalOverlay(c) + }, getNavigationController: { [weak self] in + return self?.navigationController as? NavigationController }) as? TGCaptionPanelView } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 0d3099e97e..63f0c1331b 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -1660,7 +1660,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } - if let activePoll = activePoll, messages[0].forwardInfo == nil { + if let activePoll, messages[0].forwardInfo == nil { var canStopPoll = false if !messages[0].flags.contains(.Incoming) { canStopPoll = true @@ -1867,6 +1867,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } var clearCacheAsDelete = false + var hasViewStats = false if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info, !isMigrated { var views: Int = 0 var forwards: Int = 0 @@ -1887,11 +1888,28 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState controllerInteraction.openMessageStats(messages[0].id) }) }))) + hasViewStats = true } clearCacheAsDelete = true } + if !hasViewStats, messages[0].forwardInfo == nil { + for media in message.media { + if let poll = media as? TelegramMediaPoll, message.id.namespace == Namespaces.Message.Cloud, poll.pollId.namespace == Namespaces.Media.CloudPoll { + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ViewPollStats, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Statistics"), color: theme.actionSheet.primaryTextColor) + }, action: { c, _ in + c?.dismiss(completion: { + let controller = context.sharedContext.makePollStatsScreen(context: context, messageId: messages[0].id) + controllerInteraction.navigationController()?.pushViewController(controller) + }) + }))) + break + } + } + } + if message.id.namespace == Namespaces.Message.Cloud, let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info, canEditFactCheck(appConfig: appConfig) { var canAddFactCheck = true if message.media.contains(where: { $0 is TelegramMediaAction || $0 is TelegramMediaGiveaway }) { @@ -2266,6 +2284,57 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } + + + // + for media in message.media { + if let poll = media as? TelegramMediaPoll, message.id.namespace == Namespaces.Message.Cloud, poll.pollId.namespace == Namespaces.Media.CloudPoll { + var restrictionText: String = "" + let peerName: String = chatPresentationInterfaceState.renderedPeer?.peer.flatMap(EnginePeer.init)?.compactDisplayTitle ?? "" + + if !poll.countries.isEmpty { + let locale = localeWithStrings(chatPresentationInterfaceState.strings) + let countryNames = poll.countries.map { id in + if let countryName = locale.localizedString(forRegionCode: id) { + return countryName + //return "\(flagEmoji(countryCode: id))\u{feff}\(countryName)" + } else { + return id + } + } + var countries: String = "" + if countryNames.count == 1, let country = countryNames.first { + countries = "**\(country)**" + } else { + for i in 0 ..< countryNames.count { + countries.append("**\(countryNames[i])**") + if i == countryNames.count - 2 { + countries.append(chatPresentationInterfaceState.strings.Chat_Poll_Restriction_Country_CountriesLastDelimiter) + } else if i < countryNames.count - 2 { + countries.append(chatPresentationInterfaceState.strings.Chat_Poll_Restriction_Country_CountriesDelimiter) + } + } + } + if poll.restrictToSubscribers { + restrictionText = chatPresentationInterfaceState.strings.Chat_Poll_Restriction_SubscribersCountry(peerName, countries).string + } else { + restrictionText = chatPresentationInterfaceState.strings.Chat_Poll_Restriction_Country(countries).string + } + } else if poll.restrictToSubscribers { + restrictionText = chatPresentationInterfaceState.strings.Chat_Poll_Restriction_Subscribers(peerName).string + } + + if !restrictionText.isEmpty { + actions.append(.separator) + let noAction: ((ContextMenuActionItem.Action) -> Void)? = nil + actions.append( + .action(ContextMenuActionItem(text: restrictionText, textLayout: .multiline, textFont: .small, parseMarkdown: true, icon: { _ in return nil }, action: noAction)) + ) + } + break + } + } + return ContextController.Items(content: .list(actions), tip: nil) } } diff --git a/submodules/TelegramUI/Sources/ChatVerifiedPeerTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatVerifiedPeerTitlePanelNode.swift index 5cd2fb706d..4490cf5c0d 100644 --- a/submodules/TelegramUI/Sources/ChatVerifiedPeerTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatVerifiedPeerTitlePanelNode.swift @@ -78,7 +78,7 @@ final class ChatVerifiedPeerTitlePanelNode: ChatTitleAccessoryPanelNode { self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor } - var panelHeight: CGFloat = 8.0 + var panelHeight: CGFloat = 12.0 if let peer = interfaceState.renderedPeer?.peer, let verification = interfaceState.peerVerification { if isFirstTime { @@ -94,6 +94,7 @@ final class ChatVerifiedPeerTitlePanelNode: ChatTitleAccessoryPanelNode { let attributedText = NSMutableAttributedString(attributedString: NSAttributedString(string: plainText, font: Font.regular(12.0), textColor: interfaceState.theme.rootController.navigationBar.secondaryTextColor, paragraphAlignment: .center)) attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: emojiStatus.fileId, file: nil), range: NSMakeRange(0, 1)) + attributedText.addAttribute(.baselineOffset, value: 1.0, range: NSMakeRange(0, 1)) if let entity = entities.first { let range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound) attributedText.addAttribute(NSAttributedString.Key.foregroundColor, value: interfaceState.theme.rootController.navigationBar.accentTextColor, range: range) @@ -125,8 +126,8 @@ final class ChatVerifiedPeerTitlePanelNode: ChatTitleAccessoryPanelNode { placeholderColor: interfaceState.theme.list.mediaPlaceholderColor, attemptSynchronous: false )) - transition.updateFrame(node: emojiStatusTextNode.textNode, frame: CGRect(origin: CGPoint(x: floor((width - emojiStatusLayout.size.width) / 2.0), y: panelHeight), size: emojiStatusLayout.size)) - panelHeight += emojiStatusLayout.size.height + 8.0 + transition.updateFrame(node: emojiStatusTextNode.textNode, frame: CGRect(origin: CGPoint(x: floor((width - emojiStatusLayout.size.width) / 2.0), y: panelHeight + 1.0), size: emojiStatusLayout.size)) + panelHeight += emojiStatusLayout.size.height + 12.0 emojiStatusTextNode.visibilityRect = .infinite } diff --git a/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift b/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift index 3d15ecc22a..9faa2ba98a 100755 --- a/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift +++ b/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift @@ -256,7 +256,7 @@ final class HorizontalStickerGridItemNode: GridItemNode { placeholderNode.frame = bounds if let context = self.currentState?.0, let theme = self.currentState?.1.theme, let file = self.currentState?.1.file { - placeholderNode.update(backgroundColor: theme.list.plainBackgroundColor, foregroundColor: theme.list.mediaPlaceholderColor.mixedWith(theme.list.plainBackgroundColor, alpha: 0.4), shimmeringColor: theme.list.mediaPlaceholderColor.withAlphaComponent(0.3), data: file.immediateThumbnailData, size: bounds.size, enableEffect: context.sharedContext.energyUsageSettings.fullTranslucency) + placeholderNode.update(backgroundColor: nil, foregroundColor: theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.05), shimmeringColor: theme.list.mediaPlaceholderColor.withAlphaComponent(0.2), data: file.immediateThumbnailData, size: bounds.size, enableEffect: context.sharedContext.energyUsageSettings.fullTranslucency) } } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index dd5d28ed28..e87cb748ed 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2756,19 +2756,18 @@ public final class SharedAccountContextImpl: SharedAccountContext { return makeAttachmentFileControllerImpl(context: context, updatedPresentationData: updatedPresentationData, mode: audio ? .audio(.chat) : .recent, bannedSendMedia: bannedSendMedia, presentGallery: presentGallery, presentFiles: presentFiles, presentDocumentScanner: presentDocumentScanner, send: send) } - public func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, isScheduledMessages: Bool, isFile: Bool, hasTimer: Bool, customEmojiAvailable: Bool, pushViewController: @escaping (ViewController) -> Void, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject? { + public func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, isScheduledMessages: Bool, isFile: Bool, hasTimer: Bool, customEmojiAvailable: Bool, pushViewController: @escaping (ViewController) -> Void, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void, getNavigationController: @escaping () -> NavigationController?) -> NSObject? { let inputPanelNode = LegacyMessageInputPanelNode( context: context, chatLocation: chatLocation, isScheduledMessages: isScheduledMessages, isFile: isFile, hasTimer: hasTimer, + customEmojiAvailable: customEmojiAvailable, pushViewController: pushViewController, present: present, presentInGlobalOverlay: presentInGlobalOverlay, - makeEntityInputView: { - return EntityInputView(context: context, isDark: true, areCustomEmojiEnabled: customEmojiAvailable) - } + getNavigationController: getNavigationController ) return inputPanelNode }