diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 0c3d34a763..f208f82f9f 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7883,8 +7883,13 @@ Sorry for the inconvenience."; "EmojiPackActionInfo.AddedTitle" = "Emoji Added"; "EmojiPackActionInfo.AddedText" = "%@ has been added to your emoji."; "EmojiPackActionInfo.RemovedTitle" = "Emoji Removed"; +"EmojiPackActionInfo.ArchivedTitle" = "Emoji Archived"; "EmojiPackActionInfo.RemovedText" = "%@ is no longer in your emoji."; +"MaskPackActionInfo.RemovedTitle" = "Masks Removed"; +"MaskPackActionInfo.ArchivedTitle" = "Masks Archived"; +"MaskPackActionInfo.RemovedText" = "%@ is no longer in your masks."; + "WebApp.ShareMyPhoneNumber" = "Share My Phone Number"; "WebApp.ShareMyPhoneNumberConfirmation" = "Are you sure you want to share your phone number **%1$@** with **%2$@**?"; @@ -7893,3 +7898,15 @@ Sorry for the inconvenience."; "Emoji.ClearRecent" = "Clear Recent Emoji"; "Premium.AnimatedEmoji.Proceed" = "Unlock Animated Emoji"; + +"EmojiPacksSettings.Title" = "Emoji"; + +"EmojiStickerSettings.Info" = "Artists are welcome to add their own emoji sets using our @stickers bot.\n\nTap on a message to view and add the whole set."; + +"StickerPack.MaskCount_1" = "1 mask"; +"StickerPack.MaskCount_any" = "%@ masks"; + +"StickerPack.EmojiCount_1" = "1 emoji"; +"StickerPack.EmojiCount_any" = "%@ emoji"; + +"StickerSettings.EmojiContextInfo" = "If you archive an emoji set, you can quickly restore it later from the Archived Emoji section."; diff --git a/submodules/ArchivedStickerPacksNotice/Sources/ArchivedStickerPacksNoticeController.swift b/submodules/ArchivedStickerPacksNotice/Sources/ArchivedStickerPacksNoticeController.swift index bacd63fd78..8e81d93b98 100644 --- a/submodules/ArchivedStickerPacksNotice/Sources/ArchivedStickerPacksNoticeController.swift +++ b/submodules/ArchivedStickerPacksNotice/Sources/ArchivedStickerPacksNoticeController.swift @@ -127,7 +127,15 @@ private final class ArchivedStickersNoticeAlertContentNode: AlertContentNode { var index: Int = 0 var entries: [ArchivedStickersNoticeEntry] = [] for pack in archivedStickerPacks { - entries.append(ArchivedStickersNoticeEntry(index: index, info: pack.0, topItem: pack.1, count: presentationData.strings.StickerPack_StickerCount(pack.0.count))) + let countTitle: String + if pack.0.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks { + countTitle = presentationData.strings.StickerPack_EmojiCount(pack.0.count) + } else if pack.0.id.namespace == Namespaces.ItemCollection.CloudMaskPacks { + countTitle = presentationData.strings.StickerPack_MaskCount(pack.0.count) + } else { + countTitle = presentationData.strings.StickerPack_StickerCount(pack.0.count) + } + entries.append(ArchivedStickersNoticeEntry(index: index, info: pack.0, topItem: pack.1, count: countTitle)) index += 1 } diff --git a/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm b/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm index 7bfee153fa..bba12025a8 100644 --- a/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm +++ b/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm @@ -172,8 +172,10 @@ // If our text-view's width is already the constrained width, we can use our existing TextKit stack for this sizing calculation. // Otherwise, we create a temporary stack to size for `constrainedWidth`. + UIEdgeInsets additionalInsets = UIEdgeInsetsZero; if (CGRectGetWidth(components.textView.threadSafeBounds) != constrainedWidth) { - components = [ASTextKitComponents componentsWithAttributedSeedString:components.textStorage textContainerSize:CGSizeMake(constrainedWidth, CGFLOAT_MAX)]; + additionalInsets = self.textView.textContainerInset; + components = [ASTextKitComponents componentsWithAttributedSeedString:components.textStorage textContainerSize:CGSizeMake(constrainedWidth - additionalInsets.left - additionalInsets.right, CGFLOAT_MAX)]; } // Force glyph generation and layout, which may not have happened yet (and isn't triggered by -usedRectForTextContainer:). diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift index ae4ea05956..ab164cbba7 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift @@ -405,6 +405,17 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS strongSelf.ensureFocused() } } + recognizer.waitForTouchUp = { [weak self] in + guard let strongSelf = self, let textInputNode = strongSelf.textInputNode else { + return true + } + + if textInputNode.textView.isFirstResponder { + return true + } else { + return false + } + } self.textInputBackgroundNode.view.addGestureRecognizer(recognizer) self.emojiViewProvider = { [weak self] emoji in @@ -525,6 +536,17 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS strongSelf.ensureFocused() } } + recognizer.waitForTouchUp = { [weak self] in + guard let strongSelf = self, let textInputNode = strongSelf.textInputNode else { + return true + } + + if textInputNode.textView.isFirstResponder { + return true + } else { + return false + } + } textInputNode.view.addGestureRecognizer(recognizer) textInputNode.textView.accessibilityHint = self.textPlaceholderNode.attributedText?.string diff --git a/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift b/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift index ea7b60c4dd..c584daf0e8 100644 --- a/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift +++ b/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift @@ -141,6 +141,7 @@ public final class LottieAnimationComponent: Component { } if let animationView = self.animationView, animationView.isAnimationPlaying { + updateComponent = false self.currentCompletion = { [weak self] in guard let strongSelf = self else { return @@ -154,6 +155,7 @@ public final class LottieAnimationComponent: Component { self.animationView?.removeFromSuperview() self.didPlayToCompletion = false + self.currentCompletion = nil if let url = getAppBundle().url(forResource: component.animation.name, withExtension: "json"), let animation = Animation.filepath(url.path) { let view = AnimationView(animation: animation, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable)) diff --git a/submodules/Components/PagerComponent/Sources/PagerComponent.swift b/submodules/Components/PagerComponent/Sources/PagerComponent.swift index a020e249f8..f5d31dfa3a 100644 --- a/submodules/Components/PagerComponent/Sources/PagerComponent.swift +++ b/submodules/Components/PagerComponent/Sources/PagerComponent.swift @@ -430,6 +430,7 @@ public final class PagerComponent Double?, proposedWidth: Int, proposedHeight: Int) { + if proposedWidth == 0 || proposedHeight == 0 { + self.isFailed = true + return + } if self.isFailed || self.isFinished { return } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index eb5f007d65..f36990b98a 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -1205,7 +1205,11 @@ private final class GroupExpandActionButton: UIButton { self.currentTextLayout = (title, color, textConstrainedWidth, textSize) } - let size = CGSize(width: textSize.width + 10.0 * 2.0, height: 28.0) + var sideInset: CGFloat = 10.0 + if textSize.width > 24.0 { + sideInset = 6.0 + } + let size = CGSize(width: textSize.width + sideInset * 2.0, height: 28.0) let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: floor((size.height - textSize.height) / 2.0)), size: textSize) self.textLayer.frame = textFrame @@ -2834,6 +2838,7 @@ public final class EmojiPagerContentComponent: Component { let groupHeaderPoint = self.scrollView.convert(locationInScrollView, to: groupHeader) if let clearIconLayer = groupHeader.clearIconLayer, clearIconLayer.frame.insetBy(dx: -4.0, dy: -4.0).contains(groupHeaderPoint) { component.inputInteractionHolder.inputInteraction?.clearGroup(id) + return } else { if groupHeader.tapGesture(recognizer) { return @@ -3019,8 +3024,16 @@ public final class EmojiPagerContentComponent: Component { let contentAnimation = transition.userData(ContentAnimation.self) var transitionHintInstalledGroupId: AnyHashable? - if let contentAnimation = contentAnimation, case let .groupInstalled(groupId) = contentAnimation.type { - transitionHintInstalledGroupId = groupId + var transitionHintExpandedGroupId: AnyHashable? + if let contentAnimation = contentAnimation { + switch contentAnimation.type { + case let .groupInstalled(groupId): + transitionHintInstalledGroupId = groupId + case let .groupExpanded(groupId): + transitionHintExpandedGroupId = groupId + default: + break + } } for groupItems in itemLayout.visibleItems(for: effectiveVisibleBounds) { @@ -3504,8 +3517,19 @@ public final class EmojiPagerContentComponent: Component { for (id, button) in self.visibleGroupExpandActionButtons { if !validGroupExpandActionButtons.contains(id) { removedGroupExpandActionButtonIds.append(id) - button.removeFromSuperview() - button.tintContainerLayer.removeFromSuperlayer() + + if !transition.animation.isImmediate && transitionHintExpandedGroupId == id { + button.alpha = 0.0 + button.layer.animateScale(from: 1.0, to: 0.5, duration: 0.2) + let tintContainerLayer = button.tintContainerLayer + button.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak button, weak tintContainerLayer] _ in + button?.removeFromSuperview() + tintContainerLayer?.removeFromSuperlayer() + }) + } else { + button.removeFromSuperview() + button.tintContainerLayer.removeFromSuperlayer() + } } } for id in removedGroupExpandActionButtonIds { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index ed42e4b1cd..738ed544e1 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -285,6 +285,7 @@ public final class EntityKeyboardComponent: Component { contentTopPanels.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(EntityKeyboardTopPanelComponent( theme: component.theme, items: topGifItems, + containerSideInset: component.containerInsets.left, forceActiveItemId: defaultActiveGifItemId, activeContentItemIdUpdated: gifsContentItemIdUpdated, reorderItems: { _ in @@ -352,7 +353,7 @@ public final class EntityKeyboardComponent: Component { if let file = itemGroup.items[0].file { topStickerItems.append(EntityKeyboardTopPanelComponent.Item( id: itemGroup.supergroupId, - isReorderable: true, + isReorderable: !itemGroup.isFeatured, content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( context: stickerContent.context, file: file, @@ -375,6 +376,7 @@ public final class EntityKeyboardComponent: Component { contentTopPanels.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(EntityKeyboardTopPanelComponent( theme: component.theme, items: topStickerItems, + containerSideInset: component.containerInsets.left, defaultActiveItemId: stickerContent.itemGroups.first?.groupId, activeContentItemIdUpdated: stickersContentItemIdUpdated, reorderItems: { [weak self] items in @@ -478,6 +480,7 @@ public final class EntityKeyboardComponent: Component { contentTopPanels.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(EntityKeyboardTopPanelComponent( theme: component.theme, items: topEmojiItems, + containerSideInset: component.containerInsets.left, activeContentItemIdUpdated: emojiContentItemIdUpdated, reorderItems: { [weak self] items in guard let strongSelf = self else { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift index 7286fe13ab..694a8b0956 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift @@ -700,7 +700,7 @@ final class EntityKeyboardStaticStickersPanelComponent: Component { for i in 0 ..< items.count { if AnyHashable(items[i].rawValue) == scrollToItem { let itemFrame = itemLayout.frame(at: i) - self.scrollView.scrollRectToVisible(itemFrame.insetBy(dx: -itemLayout.sideInset, dy: 0.0), animated: true) + self.scrollView.scrollRectToVisible(itemFrame.insetBy(dx: -itemLayout.sideInset - (itemLayout.itemSpacing + itemFrame.width) * 2.0, dy: 0.0), animated: true) break } } @@ -1010,6 +1010,7 @@ final class EntityKeyboardTopPanelComponent: Component { let theme: PresentationTheme let items: [Item] + let containerSideInset: CGFloat let defaultActiveItemId: AnyHashable? let forceActiveItemId: AnyHashable? let activeContentItemIdUpdated: ActionSlot<(AnyHashable, AnyHashable?, Transition)> @@ -1018,6 +1019,7 @@ final class EntityKeyboardTopPanelComponent: Component { init( theme: PresentationTheme, items: [Item], + containerSideInset: CGFloat, defaultActiveItemId: AnyHashable? = nil, forceActiveItemId: AnyHashable? = nil, activeContentItemIdUpdated: ActionSlot<(AnyHashable, AnyHashable?, Transition)>, @@ -1025,6 +1027,7 @@ final class EntityKeyboardTopPanelComponent: Component { ) { self.theme = theme self.items = items + self.containerSideInset = containerSideInset self.defaultActiveItemId = defaultActiveItemId self.forceActiveItemId = forceActiveItemId self.activeContentItemIdUpdated = activeContentItemIdUpdated @@ -1038,6 +1041,9 @@ final class EntityKeyboardTopPanelComponent: Component { if lhs.items != rhs.items { return false } + if lhs.containerSideInset != rhs.containerSideInset { + return false + } if lhs.defaultActiveItemId != rhs.defaultActiveItemId { return false } @@ -1064,7 +1070,7 @@ final class EntityKeyboardTopPanelComponent: Component { } let topInset: CGFloat = -3.0 - let sideInset: CGFloat = 7.0 + let sideInset: CGFloat let itemSize: CGSize let staticItemSize: CGSize let staticExpandedItemSize: CGSize @@ -1074,7 +1080,9 @@ final class EntityKeyboardTopPanelComponent: Component { let isExpanded: Bool let items: [Item] - init(isExpanded: Bool, height: CGFloat, items: [ItemDescription]) { + init(isExpanded: Bool, containerSideInset: CGFloat, height: CGFloat, items: [ItemDescription]) { + self.sideInset = containerSideInset + 7.0 + self.isExpanded = isExpanded self.itemSize = self.isExpanded ? CGSize(width: 54.0, height: 68.0) : CGSize(width: 28.0, height: 28.0) self.staticItemSize = self.itemSize @@ -1665,7 +1673,7 @@ final class EntityKeyboardTopPanelComponent: Component { } let previousItemLayout = self.itemLayout - let itemLayout = ItemLayout(isExpanded: isExpanded, height: availableSize.height, items: self.items.map { item -> ItemLayout.ItemDescription in + let itemLayout = ItemLayout(isExpanded: isExpanded, containerSideInset: component.containerSideInset, height: availableSize.height, items: self.items.map { item -> ItemLayout.ItemDescription in let isStatic = item.id == AnyHashable("static") return ItemLayout.ItemDescription( isStatic: isStatic, @@ -1821,7 +1829,7 @@ final class EntityKeyboardTopPanelComponent: Component { } let isRound: Bool - if let string = activeContentItemId.base as? String, (string == "recent" || string == "static") { + if let string = activeContentItemId.base as? String, (string == "recent" || string == "static" || string == "trending") { isRound = true } else { isRound = false @@ -1890,7 +1898,13 @@ final class EntityKeyboardTopPanelComponent: Component { for i in 0 ..< component.items.count { if component.items[i].id == itemId { let itemFrame = itemLayout.containerFrame(at: i) - self.scrollView.scrollRectToVisible(itemFrame.insetBy(dx: -2.0, dy: 0.0), animated: true) + let expandedInset: CGFloat + if itemLayout.isExpanded { + expandedInset = -2.0 + } else { + expandedInset = -itemLayout.sideInset - (itemFrame.width + itemLayout.itemSpacing) * 2.0 + } + self.scrollView.scrollRectToVisible(itemFrame.insetBy(dx: expandedInset, dy: 0.0), animated: true) break } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift index 8171d8976e..2194d5b83f 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift @@ -134,12 +134,12 @@ public final class GifPagerContentComponent: Component { public final class InputInteraction { public let performItemAction: (Item, UIView, CGRect) -> Void - public let openGifContextMenu: (TelegramMediaFile, UIView, CGRect, ContextGesture, Bool) -> Void + public let openGifContextMenu: (Item, UIView, CGRect, ContextGesture, Bool) -> Void public let loadMore: (String) -> Void public init( performItemAction: @escaping (Item, UIView, CGRect) -> Void, - openGifContextMenu: @escaping (TelegramMediaFile, UIView, CGRect, ContextGesture, Bool) -> Void, + openGifContextMenu: @escaping (Item, UIView, CGRect, ContextGesture, Bool) -> Void, loadMore: @escaping (String) -> Void ) { self.performItemAction = performItemAction @@ -149,17 +149,22 @@ public final class GifPagerContentComponent: Component { } public final class Item: Equatable { - public let file: TelegramMediaFile + public let file: FileMediaReference + public let contextResult: (ChatContextResultCollection, ChatContextResult)? - public init(file: TelegramMediaFile) { + public init(file: FileMediaReference, contextResult: (ChatContextResultCollection, ChatContextResult)?) { self.file = file + self.contextResult = contextResult } public static func ==(lhs: Item, rhs: Item) -> Bool { if lhs === rhs { return true } - if lhs.file.fileId != rhs.file.fileId { + if lhs.file.media.fileId != rhs.file.media.fileId { + return false + } + if (lhs.contextResult == nil) != (rhs.contextResult != nil) { return false } @@ -242,10 +247,16 @@ public final class GifPagerContentComponent: Component { self.horizontalSpacing = 1.0 self.verticalSpacing = 1.0 - let itemHorizontalSpace = width - self.containerInsets.left - self.containerInsets.right - self.itemSize = floor((width - self.horizontalSpacing * 2.0) / 3.0) + let defaultItemSize: CGFloat = 120.0 + + let itemHorizontalSpace = width - self.containerInsets.left - self.containerInsets.right + var itemsPerRow = Int(floor((itemHorizontalSpace) / (defaultItemSize))) + itemsPerRow = max(3, itemsPerRow) + + self.itemsPerRow = itemsPerRow + + self.itemSize = floor((itemHorizontalSpace - self.horizontalSpacing * CGFloat(itemsPerRow - 1)) / CGFloat(itemsPerRow)) - self.itemsPerRow = Int((itemHorizontalSpace + self.horizontalSpacing) / (self.itemSize + self.horizontalSpacing)) let numRowsInGroup = (itemCount + (self.itemsPerRow - 1)) / self.itemsPerRow self.contentSize = CGSize(width: width, height: self.containerInsets.top + self.containerInsets.bottom + CGFloat(numRowsInGroup) * self.itemSize + CGFloat(max(0, numRowsInGroup - 1)) * self.verticalSpacing) } @@ -265,7 +276,7 @@ public final class GifPagerContentComponent: Component { ) ) - if column == self.itemsPerRow - 1 { + if column == self.itemsPerRow - 1 && index < self.itemCount - 1 { rect.size.width = self.width - self.containerInsets.right - rect.minX } @@ -321,7 +332,7 @@ public final class GifPagerContentComponent: Component { self.item = item self.onUpdateDisplayPlaceholder = onUpdateDisplayPlaceholder - super.init(context: context, file: item?.file, synchronousLoad: attemptSynchronousLoad) + super.init(context: context, file: item?.file.media, synchronousLoad: attemptSynchronousLoad) if item == nil { self.updateDisplayPlaceholder(displayPlaceholder: true, duration: 0.0) @@ -453,6 +464,8 @@ public final class GifPagerContentComponent: Component { self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + self.isMultipleTouchEnabled = false + self.useSublayerTransformForActivation = false self.shouldBegin = { [weak self] point in guard let strongSelf = self else { @@ -475,7 +488,7 @@ public final class GifPagerContentComponent: Component { return } let rect = strongSelf.scrollView.convert(itemLayer.frame, to: strongSelf) - component.inputInteraction.openGifContextMenu(item.file, strongSelf, rect, gesture, true) + component.inputInteraction.openGifContextMenu(item, strongSelf, rect, gesture, component.subject == .recent) } } @@ -483,16 +496,16 @@ public final class GifPagerContentComponent: Component { fatalError("init(coder:) has not been implemented") } - private func openGifContextMenu(file: TelegramMediaFile, sourceView: UIView, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) { + private func openGifContextMenu(item: Item, sourceView: UIView, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) { guard let component = self.component else { return } - component.inputInteraction.openGifContextMenu(file, sourceView, sourceRect, gesture, isSaved) + component.inputInteraction.openGifContextMenu(item, sourceView, sourceRect, gesture, isSaved) } @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { - if let component = self.component, let item = self.item(atPoint: recognizer.location(in: self)), let itemView = self.visibleItemLayers[.media(item.file.fileId)] { + if let component = self.component, let item = self.item(atPoint: recognizer.location(in: self)), let itemView = self.visibleItemLayers[.media(item.file.media.fileId)] { component.inputInteraction.performItemAction(item, self, self.scrollView.convert(itemView.frame, to: self)) } } @@ -629,7 +642,7 @@ public final class GifPagerContentComponent: Component { let itemId: ItemKey if index < component.items.count { item = component.items[index] - itemId = .media(component.items[index].file.fileId) + itemId = .media(component.items[index].file.media.fileId) } else if component.isLoading || component.loadMoreToken != nil { itemId = .placeholder(index) } else { diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift index 126894163b..85d5dc1f55 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift @@ -535,26 +535,37 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { private var groupContext: GroupContext? private var frameSkip: Int - private var displayLink: ConstantDisplayLinkAnimator? + private var displayTimer: Foundation.Timer? private(set) var isPlaying: Bool = false { didSet { if self.isPlaying != oldValue { if self.isPlaying { - if self.displayLink == nil { - self.displayLink = ConstantDisplayLinkAnimator { [weak self] in + if self.displayTimer == nil { + final class TimerTarget: NSObject { + private let f: () -> Void + + init(_ f: @escaping () -> Void) { + self.f = f + } + + @objc func timerEvent() { + self.f() + } + } + let displayTimer = Foundation.Timer(timeInterval: CGFloat(self.frameSkip) / 60.0, target: TimerTarget { [weak self] in guard let strongSelf = self else { return } strongSelf.animationTick() - } - self.displayLink?.frameInterval = self.frameSkip - self.displayLink?.isPaused = false + }, selector: #selector(TimerTarget.timerEvent), userInfo: nil, repeats: true) + self.displayTimer = displayTimer + RunLoop.main.add(displayTimer, forMode: .common) } } else { - if let displayLink = self.displayLink { - self.displayLink = nil - displayLink.invalidate() + if let displayTimer = self.displayTimer { + self.displayTimer = nil + displayTimer.invalidate() } } } diff --git a/submodules/TelegramUI/Resources/Animations/anim_botToKey.tgs b/submodules/TelegramUI/Resources/Animations/anim_botToKey.tgs deleted file mode 100644 index 20939237d6..0000000000 Binary files a/submodules/TelegramUI/Resources/Animations/anim_botToKey.tgs and /dev/null differ diff --git a/submodules/TelegramUI/Resources/Animations/anim_keyToBot.tgs b/submodules/TelegramUI/Resources/Animations/anim_keyToBot.tgs deleted file mode 100644 index 2cc3eed104..0000000000 Binary files a/submodules/TelegramUI/Resources/Animations/anim_keyToBot.tgs and /dev/null differ diff --git a/submodules/TelegramUI/Resources/Animations/anim_keyToSmile.tgs b/submodules/TelegramUI/Resources/Animations/anim_keyToSmile.tgs deleted file mode 100644 index 07c99e1a72..0000000000 Binary files a/submodules/TelegramUI/Resources/Animations/anim_keyToSmile.tgs and /dev/null differ diff --git a/submodules/TelegramUI/Resources/Animations/anim_keyToSticker.tgs b/submodules/TelegramUI/Resources/Animations/anim_keyToSticker.tgs deleted file mode 100644 index 8863943a7d..0000000000 Binary files a/submodules/TelegramUI/Resources/Animations/anim_keyToSticker.tgs and /dev/null differ diff --git a/submodules/TelegramUI/Resources/Animations/anim_botToKey.json b/submodules/TelegramUI/Resources/Animations/input_anim_botToKey.json similarity index 100% rename from submodules/TelegramUI/Resources/Animations/anim_botToKey.json rename to submodules/TelegramUI/Resources/Animations/input_anim_botToKey.json diff --git a/submodules/TelegramUI/Resources/Animations/anim_channelMute.json b/submodules/TelegramUI/Resources/Animations/input_anim_channelMute.json similarity index 100% rename from submodules/TelegramUI/Resources/Animations/anim_channelMute.json rename to submodules/TelegramUI/Resources/Animations/input_anim_channelMute.json diff --git a/submodules/TelegramUI/Resources/Animations/anim_channelUnmute.json b/submodules/TelegramUI/Resources/Animations/input_anim_channelUnmute.json similarity index 100% rename from submodules/TelegramUI/Resources/Animations/anim_channelUnmute.json rename to submodules/TelegramUI/Resources/Animations/input_anim_channelUnmute.json diff --git a/submodules/TelegramUI/Resources/Animations/anim_keyToBot.json b/submodules/TelegramUI/Resources/Animations/input_anim_keyToBot.json similarity index 100% rename from submodules/TelegramUI/Resources/Animations/anim_keyToBot.json rename to submodules/TelegramUI/Resources/Animations/input_anim_keyToBot.json diff --git a/submodules/TelegramUI/Resources/Animations/anim_keyToSmile.json b/submodules/TelegramUI/Resources/Animations/input_anim_keyToSmile.json similarity index 100% rename from submodules/TelegramUI/Resources/Animations/anim_keyToSmile.json rename to submodules/TelegramUI/Resources/Animations/input_anim_keyToSmile.json diff --git a/submodules/TelegramUI/Resources/Animations/anim_keyToSticker.json b/submodules/TelegramUI/Resources/Animations/input_anim_keyToSticker.json similarity index 100% rename from submodules/TelegramUI/Resources/Animations/anim_keyToSticker.json rename to submodules/TelegramUI/Resources/Animations/input_anim_keyToSticker.json diff --git a/submodules/TelegramUI/Resources/Animations/anim_smileToKey.json b/submodules/TelegramUI/Resources/Animations/input_anim_smileToKey.json similarity index 100% rename from submodules/TelegramUI/Resources/Animations/anim_smileToKey.json rename to submodules/TelegramUI/Resources/Animations/input_anim_smileToKey.json diff --git a/submodules/TelegramUI/Resources/Animations/anim_smiletosticker.json b/submodules/TelegramUI/Resources/Animations/input_anim_smileToSticker.json similarity index 100% rename from submodules/TelegramUI/Resources/Animations/anim_smiletosticker.json rename to submodules/TelegramUI/Resources/Animations/input_anim_smileToSticker.json diff --git a/submodules/TelegramUI/Resources/Animations/anim_stickerToKey.json b/submodules/TelegramUI/Resources/Animations/input_anim_stickerToKey.json similarity index 100% rename from submodules/TelegramUI/Resources/Animations/anim_stickerToKey.json rename to submodules/TelegramUI/Resources/Animations/input_anim_stickerToKey.json diff --git a/submodules/TelegramUI/Resources/Animations/anim_stickertosmile.json b/submodules/TelegramUI/Resources/Animations/input_anim_stickerToSmile.json similarity index 100% rename from submodules/TelegramUI/Resources/Animations/anim_stickertosmile.json rename to submodules/TelegramUI/Resources/Animations/input_anim_stickerToSmile.json diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 07e99c1e64..8c5e7ef83e 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1741,7 +1741,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } return true - }, sendBotContextResultAsGif: { [weak self] collection, result, sourceNode, sourceRect, silentPosting in + }, sendBotContextResultAsGif: { [weak self] collection, result, sourceView, sourceRect, silentPosting in guard let strongSelf = self else { return false } @@ -1749,7 +1749,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return false } if let _ = strongSelf.presentationInterfaceState.slowmodeState, strongSelf.presentationInterfaceState.subject != .scheduledMessages { - strongSelf.interfaceInteraction?.displaySlowmodeTooltip(sourceNode.view, sourceRect) + strongSelf.interfaceInteraction?.displaySlowmodeTooltip(sourceView, sourceRect) return false } diff --git a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift index 95c6a9da12..2267e19977 100644 --- a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift @@ -75,7 +75,7 @@ public final class ChatControllerInteraction { let sendMessage: (String) -> Void let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?) -> Bool let sendGif: (FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool - let sendBotContextResultAsGif: (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect, Bool) -> Bool + let sendBotContextResultAsGif: (ChatContextResultCollection, ChatContextResult, UIView, CGRect, Bool) -> Bool let requestMessageActionCallback: (MessageId, MemoryBuffer?, Bool, Bool) -> Void let requestMessageActionUrlAuth: (String, MessageActionUrlSubject) -> Void let activateSwitchInline: (PeerId?, String) -> Void @@ -179,7 +179,7 @@ public final class ChatControllerInteraction { sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?) -> Bool, sendGif: @escaping (FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool, - sendBotContextResultAsGif: @escaping (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect, Bool) -> Bool, + sendBotContextResultAsGif: @escaping (ChatContextResultCollection, ChatContextResult, UIView, CGRect, Bool) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageActionUrlSubject) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index cb2dbacdf5..7e1b2e1ad0 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -1624,9 +1624,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let previousInputPanelBackgroundFrame = self.inputPanelBackgroundNode.frame transition.updateFrame(node: self.inputPanelContainerNode, frame: CGRect(origin: CGPoint(), size: layout.size)) self.inputPanelContainerNode.update(size: layout.size, scrollableDistance: max(0.0, maximumInputNodeHeight - layout.standardInputHeight), isExpansionEnabled: isInputExpansionEnabled, transition: transition) - transition.updatePosition(node: self.inputPanelClippingNode, position: CGRect(origin: apparentInputBackgroundFrame.origin, size: layout.size).center) - transition.updateBounds(node: self.inputPanelClippingNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: apparentInputBackgroundFrame.origin.y), size: layout.size)) - transition.updateFrame(node: self.inputPanelBackgroundNode, frame: apparentInputBackgroundFrame) + transition.updatePosition(node: self.inputPanelClippingNode, position: CGRect(origin: apparentInputBackgroundFrame.origin, size: layout.size).center, beginWithCurrentState: true) + transition.updateBounds(node: self.inputPanelClippingNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: apparentInputBackgroundFrame.origin.y), size: layout.size), beginWithCurrentState: true) + transition.updateFrame(node: self.inputPanelBackgroundNode, frame: apparentInputBackgroundFrame, beginWithCurrentState: true) transition.updateFrame(node: self.contentDimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: apparentInputBackgroundFrame.origin.y))) @@ -1644,7 +1644,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { inputPanelUpdateTransition = .immediate } - self.inputPanelBackgroundNode.update(size: CGSize(width: intrinsicInputPanelBackgroundNodeSize.width, height: intrinsicInputPanelBackgroundNodeSize.height + inputPanelBackgroundExtension), transition: inputPanelUpdateTransition) + self.inputPanelBackgroundNode.update(size: CGSize(width: intrinsicInputPanelBackgroundNodeSize.width, height: intrinsicInputPanelBackgroundNodeSize.height + inputPanelBackgroundExtension), transition: inputPanelUpdateTransition, beginWithCurrentState: true) self.inputPanelBottomBackgroundSeparatorBaseOffset = intrinsicInputPanelBackgroundNodeSize.height inputPanelUpdateTransition.updateFrame(node: self.inputPanelBottomBackgroundSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: intrinsicInputPanelBackgroundNodeSize.height + inputPanelBackgroundExtension), size: CGSize(width: intrinsicInputPanelBackgroundNodeSize.width, height: UIScreenPixel)), beginWithCurrentState: true) diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift index 9a16ad296c..cbaabef11d 100644 --- a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift @@ -616,7 +616,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { guard let controllerInteraction = controllerInteraction else { return } - let _ = controllerInteraction.sendGif(.savedGif(media: item.file), view, rect, false, false) + let _ = controllerInteraction.sendGif(item.file, view, rect, false, false) }, openGifContextMenu: { _, _, _, _, _ in }, @@ -776,7 +776,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { var items: [GifPagerContentComponent.Item] = [] for gifItem in savedGifs { items.append(GifPagerContentComponent.Item( - file: gifItem.contents.get(RecentMediaItem.self)!.media + file: .savedGif(media: gifItem.contents.get(RecentMediaItem.self)!.media), + contextResult: nil )) } return EntityKeyboardGifContent( @@ -800,7 +801,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { if let trendingGifs = trendingGifs { for file in trendingGifs.files { items.append(GifPagerContentComponent.Item( - file: file.file.media + file: file.file, + contextResult: file.contextResult )) } } else { @@ -829,7 +831,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { if let result = result { for file in result.files { items.append(GifPagerContentComponent.Item( - file: file.file.media + file: file.file, + contextResult: file.contextResult )) } loadMoreToken = result.nextOffset @@ -893,7 +896,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { var existingIds = Set() for item in componentValue.component.items { items.append(item) - existingIds.insert(item.file.fileId) + existingIds.insert(item.file.media.fileId) } var loadMoreToken: String? @@ -904,7 +907,10 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { continue } existingIds.insert(file.file.media.fileId) - items.append(GifPagerContentComponent.Item(file: file.file.media)) + items.append(GifPagerContentComponent.Item( + file: file.file, + contextResult: file.contextResult + )) } if !result.isComplete { loadMoreToken = result.nextOffset @@ -1399,13 +1405,18 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { guard let controllerInteraction = controllerInteraction else { return } - let _ = controllerInteraction.sendGif(.savedGif(media: item.file), view, rect, false, false) + + if let (collection, result) = item.contextResult { + let _ = controllerInteraction.sendBotContextResultAsGif(collection, result, view, rect, false) + } else { + let _ = controllerInteraction.sendGif(item.file, view, rect, false, false) + } }, - openGifContextMenu: { [weak self] file, sourceView, sourceRect, gesture, isSaved in + openGifContextMenu: { [weak self] item, sourceView, sourceRect, gesture, isSaved in guard let strongSelf = self else { return } - strongSelf.openGifContextMenu(file: file, sourceView: sourceView, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved) + strongSelf.openGifContextMenu(item: item, sourceView: sourceView, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved) }, loadMore: { [weak self] token in guard let strongSelf = self, let gifContext = strongSelf.gifContext else { @@ -1609,9 +1620,142 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { return (expandedHeight, 0.0) } - private func openGifContextMenu(file: TelegramMediaFile, sourceView: UIView, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) { + private func openGifContextMenu(item: GifPagerContentComponent.Item, sourceView: UIView, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) { + let file = item + let canSaveGif: Bool - if file.fileId.namespace == Namespaces.Media.CloudFile { + if file.file.media.fileId.namespace == Namespaces.Media.CloudFile { + canSaveGif = true + } else { + canSaveGif = false + } + + let _ = (self.context.engine.stickers.isGifSaved(id: file.file.media.fileId) + |> deliverOnMainQueue).start(next: { [weak self] isGifSaved in + guard let strongSelf = self else { + return + } + var isGifSaved = isGifSaved + if !canSaveGif { + 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: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file.file.media], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:]) + + let gallery = GalleryController(context: strongSelf.context, source: .standaloneMessage(message), streamSingleVideo: true, replaceRootController: { _, _ in + }, baseNavigationController: nil) + gallery.setHintWillBePresentedInPreviewingContext(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) + }, action: { _, f in + f(.default) + if isSaved { + let _ = self?.controllerInteraction?.sendGif(file.file, sourceView, sourceRect, false, false) + } else if let (collection, result) = file.contextResult { + let _ = self?.controllerInteraction?.sendBotContextResultAsGif(collection, result, sourceView, sourceRect, false) + } + }))) + + if let currentState = strongSelf.currentState { + let interfaceState = currentState.interfaceState + + var isScheduledMessages = false + if case .scheduledMessages = interfaceState.subject { + isScheduledMessages = true + } + if !isScheduledMessages { + if case let .peer(peerId) = interfaceState.chatLocation { + if peerId != self?.context.account.peerId && peerId.namespace != Namespaces.Peer.SecretChat { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendSilently, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.default) + if isSaved { + let _ = self?.controllerInteraction?.sendGif(file.file, sourceView, sourceRect, true, false) + } else if let (collection, result) = file.contextResult { + let _ = self?.controllerInteraction?.sendBotContextResultAsGif(collection, result, sourceView, sourceRect, true) + } + }))) + } + + if isSaved { + 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) + }, action: { _, f in + f(.default) + + let _ = self?.controllerInteraction?.sendGif(file.file, sourceView, sourceRect, false, true) + }))) + } + } + } + } + + 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: { _, f in + f(.dismissWithoutContent) + + guard let strongSelf = self else { + return + } + let _ = removeSavedGif(postbox: strongSelf.context.account.postbox, mediaId: file.file.media.fileId).start() + }))) + } else if canSaveGif && !isGifSaved { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Preview_SaveGif, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.actionSheet.primaryTextColor) + }, action: { _, 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.file, saved: true) + |> deliverOnMainQueue).start(next: { result in + guard let strongSelf = self else { + return + } + switch result { + case .generic: + strongSelf.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + case let .limitExceeded(limit, premiumLimit): + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + let text: String + if limit == premiumLimit || premiumConfiguration.isPremiumDisabled { + text = presentationData.strings.Premium_MaxSavedGifsFinalText + } else { + text = presentationData.strings.Premium_MaxSavedGifsText("\(premiumLimit)").string + } + strongSelf.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text), elevatedLayout: false, animateInAsReplacement: false, action: { action in + guard let strongSelf = self else { + return false + } + + if case .info = action { + let controller = PremiumIntroScreen(context: context, source: .savedGifs) + strongSelf.controllerInteraction?.navigationController()?.pushViewController(controller) + return true + } + return false + }), nil) + } + }) + }))) + } + + let contextController = ContextController(account: strongSelf.context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceView: sourceView, sourceRect: sourceRect)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + strongSelf.controllerInteraction?.presentGlobalOverlayController(contextController, nil) + }) + + /*let canSaveGif: Bool + if item.file.fileId.namespace == Namespaces.Media.CloudFile { canSaveGif = true } else { canSaveGif = false @@ -1646,15 +1790,19 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { }*/ }))) - /*if let (_, _, _, _, _, _, _, _, interfaceState, _, _, _) = strongSelf.validLayout { + if let currentState = strongSelf.currentState { + let interfaceState = currentState.interfaceState + var isScheduledMessages = false if case .scheduledMessages = interfaceState.subject { isScheduledMessages = true } if !isScheduledMessages { if case let .peer(peerId) = interfaceState.chatLocation { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + if peerId != self?.context.account.peerId && peerId.namespace != Namespaces.Peer.SecretChat { - items.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_SendSilently, icon: { theme in + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendSilently, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in f(.default) @@ -1667,7 +1815,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } if isSaved { - items.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in + 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) }, action: { _, f in f(.default) @@ -1677,7 +1825,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } } } - }*/ + } if isSaved || isGifSaved { items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in @@ -1731,7 +1879,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { let contextController = ContextController(account: strongSelf.context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceView: sourceView, sourceRect: sourceRect)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) strongSelf.controllerInteraction?.presentGlobalOverlayController(contextController, nil) - }) + })*/ } } diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index 6a74a938f4..1d0e5c9275 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -135,7 +135,7 @@ func chatHistoryEntriesForView( } if presentationData.largeEmoji, message.media.isEmpty { - if stickersEnabled && message.text.count == 1, let entities = message.textEntitiesAttribute?.entities, entities.count == 1, case let .CustomEmoji(_, fileId) = entities[0].type, let _ = message.associatedMedia[MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)] { + if stickersEnabled && message.text.count == 1, let entities = message.textEntitiesAttribute?.entities, entities.count == 1, case let .CustomEmoji(_, fileId) = entities[0].type, let file = message.associatedMedia[MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile, !file.isVideoEmoji { contentTypeHint = .animatedEmoji } else if stickersEnabled && message.text.count == 1, let _ = associatedData.animatedEmojiStickers[message.text.basicEmoji.0], (message.textEntitiesAttribute?.entities.isEmpty ?? true) { contentTypeHint = .animatedEmoji diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift index a0a8432548..7e5e257aa2 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift @@ -60,7 +60,7 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS forwardPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, forwardOptionsState: chatPresentationInterfaceState.interfaceState.forwardOptionsState) return forwardPanelNode } else { - let panelNode = ForwardAccessoryPanelNode(context: context, messageIds: forwardMessageIds, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, fontSize: chatPresentationInterfaceState.fontSize, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, forwardOptionsState: chatPresentationInterfaceState.interfaceState.forwardOptionsState) + let panelNode = ForwardAccessoryPanelNode(context: context, messageIds: forwardMessageIds, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, fontSize: chatPresentationInterfaceState.fontSize, nameDisplayOrder: chatPresentationInterfaceState.nameDisplayOrder, forwardOptionsState: chatPresentationInterfaceState.interfaceState.forwardOptionsState, animationCache: chatControllerInteraction?.presentationContext.animationCache, animationRenderer: chatControllerInteraction?.presentationContext.animationRenderer) panelNode.interfaceInteraction = interfaceInteraction return panelNode } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift index b4dc54ec9c..4971c3243e 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift @@ -376,7 +376,9 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee switch attribute { case let .CustomEmoji(_, alt, _): if !alt.isEmpty, let keyword = allEmoticons[alt] { - result.append((alt, item.file, keyword)) + if !item.file.isPremiumEmoji || hasPremium { + result.append((alt, item.file, keyword)) + } } default: break diff --git a/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift b/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift index 288c9d53ba..f8d1b0a914 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift @@ -236,7 +236,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { multiplexedNode.fileSelected = { [weak self] file, sourceNode, sourceRect in if let (collection, result) = file.contextResult { - let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect, false) + let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode.view, sourceRect, false) } else { let _ = self?.controllerInteraction.sendGif(file.file, sourceNode.view, sourceRect, false, false) } diff --git a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift index e827bb3be6..afc1423c61 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift @@ -1470,7 +1470,7 @@ final class ChatMediaInputNode: ChatInputNode { if isSaved { let _ = self?.controllerInteraction.sendGif(file.file, sourceNode.view, sourceRect, false, false) } else if let (collection, result) = file.contextResult { - let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect, false) + let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode.view, sourceRect, false) } }))) @@ -1489,7 +1489,7 @@ final class ChatMediaInputNode: ChatInputNode { if isSaved { let _ = self?.controllerInteraction.sendGif(file.file, sourceNode.view, sourceRect, true, false) } else if let (collection, result) = file.contextResult { - let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect, true) + let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode.view, sourceRect, true) } }))) } diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index f176b3ba38..7b23cb2613 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -60,11 +60,11 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { self.addSubnode(self.iconImageNode) switch item { - case .input, .botInput, .silentPost: - self.iconImageNode.isHidden = true - self.animationView = ComponentView() - default: - break + case .input, .botInput, .silentPost: + self.iconImageNode.isHidden = true + self.animationView = ComponentView() + default: + break } if let text = text { @@ -84,9 +84,11 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { if highlighted { strongSelf.layer.removeAnimation(forKey: "opacity") strongSelf.alpha = 0.4 + strongSelf.layer.allowsGroupOpacity = true } else { strongSelf.alpha = 1.0 strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.layer.allowsGroupOpacity = false } } } @@ -117,7 +119,7 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { fatalError("init(coder:) has not been implemented") } - static func imageAndInsets(item: ChatTextInputAccessoryItem, theme: PresentationTheme, strings: PresentationStrings) -> (UIImage?, String?, String, CGFloat, UIEdgeInsets) { + private static func imageAndInsets(item: ChatTextInputAccessoryItem, theme: PresentationTheme, strings: PresentationStrings) -> (UIImage?, String?, String, CGFloat, UIEdgeInsets) { switch item { case let .input(isEnabled, inputMode), let .botInput(isEnabled, inputMode): switch inputMode { @@ -147,7 +149,7 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { } } - static func calculateWidth(item: ChatTextInputAccessoryItem, image: UIImage?, text: String?, strings: PresentationStrings) -> CGFloat { + private static func calculateWidth(item: ChatTextInputAccessoryItem, image: UIImage?, text: String?, strings: PresentationStrings) -> CGFloat { switch item { case .input, .botInput, .silentPost, .commands, .scheduledMessages: return 32.0 @@ -169,12 +171,12 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { self.iconImageNode.image = AccessoryItemIconButtonNode.imageAndInsets(item: item, theme: self.theme, strings: self.strings).0 let bottomInset: CGFloat = 0.0 - // let iconSize = CGSize(width: 32.0, height: 32.0) let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0) - bottomInset), size: image.size) self.iconImageNode.frame = imageFrame if let animationView = self.animationView { let width = AccessoryItemIconButtonNode.calculateWidth(item: item, image: image, text: "", strings: self.strings) + let iconSize = CGSize(width: width, height: width) let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - width) / 2.0), y: floor((size.height - width) / 2.0) - bottomInset), size: CGSize(width: width, height: width)) @@ -185,15 +187,15 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { if case let .silentPost(muted) = item { if case let .silentPost(previousMuted) = previousItem { if muted { - animationName = "anim_channelMute" + animationName = "input_anim_channelMute" } else { - animationName = "anim_channelUnmute" + animationName = "input_anim_channelUnmute" } if muted != previousMuted { animationMode = .animating(loop: false) } } else { - animationName = "anim_channelMute" + animationName = "input_anim_channelMute" } } else { var previousInputMode: ChatTextInputAccessoryItem.InputMode? @@ -225,60 +227,60 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { case .keyboard: if let previousInputMode = previousInputMode { if case .stickers = previousInputMode { - animationName = "anim_stickerToKey" + animationName = "input_anim_stickerToKey" animationMode = .animating(loop: false) } else if case .emoji = previousInputMode { - animationName = "anim_smileToKey" + animationName = "input_anim_smileToKey" animationMode = .animating(loop: false) } else if case .bot = previousInputMode { - animationName = "anim_botToKey" + animationName = "input_anim_botToKey" animationMode = .animating(loop: false) } else { - animationName = "anim_stickerToKey" + animationName = "input_anim_stickerToKey" } } else { - animationName = "anim_stickerToKey" + animationName = "input_anim_stickerToKey" } case .stickers: if let previousInputMode = previousInputMode { if case .keyboard = previousInputMode { - animationName = "anim_keyToSticker" + animationName = "input_anim_keyToSticker" animationMode = .animating(loop: false) } else if case .emoji = previousInputMode { - animationName = "anim_smileToSticker" + animationName = "input_anim_smileToSticker" animationMode = .animating(loop: false) colorKeys = emojiColorKeys } else { - animationName = "anim_keyToSticker" + animationName = "input_anim_keyToSticker" } } else { - animationName = "anim_keyToSticker" + animationName = "input_anim_keyToSticker" } case .emoji: if let previousInputMode = previousInputMode { if case .keyboard = previousInputMode { - animationName = "anim_keyToSmile" + animationName = "input_anim_keyToSmile" animationMode = .animating(loop: false) } else if case .stickers = previousInputMode { - animationName = "anim_stickerToSmile" + animationName = "input_anim_stickerToSmile" animationMode = .animating(loop: false) colorKeys = emojiColorKeys } else { - animationName = "anim_keyToSmile" + animationName = "input_anim_keyToSmile" } } else { - animationName = "anim_keyToSmile" + animationName = "input_anim_keyToSmile" } case .bot: if let previousInputMode = previousInputMode { if case .keyboard = previousInputMode { - animationName = "anim_keyToBot" + animationName = "input_anim_keyToBot" animationMode = .animating(loop: false) } else { - animationName = "anim_keyToBot" + animationName = "input_anim_keyToBot" } } else { - animationName = "anim_keyToBot" + animationName = "input_anim_keyToBot" } } } else { @@ -291,7 +293,7 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { colors[colorKey] = self.theme.chat.inputPanel.inputControlColor } - let _ = animationView.update( + let animationSize = animationView.update( transition: .immediate, component: AnyComponent(LottieAnimationComponent( animation: LottieAnimationComponent.AnimationItem( @@ -299,7 +301,7 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { mode: animationMode ), colors: colors, - size: animationFrame.size + size: iconSize )), environment: {}, containerSize: animationFrame.size @@ -309,7 +311,7 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { if view.superview == nil { self.view.addSubview(view) } - view.frame = animationFrame + view.frame = CGRect(origin: CGPoint(x: animationFrame.minX + floor((animationFrame.width - animationSize.width) / 2.0), y: animationFrame.minY + floor((animationFrame.height - animationSize.height) / 2.0)), size: animationSize) } } } @@ -430,7 +432,7 @@ final class CustomEmojiContainerView: UIView { preconditionFailure() } - func update(emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)]) { + func update(fontSize: CGFloat, emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)]) { var nextIndexById: [Int64: Int] = [:] var validKeys = Set() @@ -456,7 +458,8 @@ final class CustomEmojiContainerView: UIView { continue } - let size = CGSize(width: 24.0, height: 24.0) + let itemSize: CGFloat = floor(24.0 * fontSize / 17.0) + let size = CGSize(width: itemSize, height: itemSize) view.frame = CGRect(origin: CGPoint(x: floor(rect.midX - size.width / 2.0), y: floor(rect.midY - size.height / 2.0)), size: size) @@ -599,13 +602,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } func updateInputTextState(_ state: ChatTextInputState, keepSendButtonEnabled: Bool, extendedSearchLayout: Bool, accessoryItems: [ChatTextInputAccessoryItem], animated: Bool) { - if state.inputText.length != 0 && self.textInputNode == nil { - self.loadTextInputNode() - } - - if let textInputNode = self.textInputNode, let currentState = self.presentationInterfaceState { - self.updatingInputState = true - + if let currentState = self.presentationInterfaceState { var updateAccessoryButtons = false if accessoryItems.count == self.accessoryItemButtons.count { for i in 0 ..< accessoryItems.count { @@ -642,6 +639,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } self.accessoryItemButtons = updatedButtons } + } + + if state.inputText.length != 0 && self.textInputNode == nil { + self.loadTextInputNode() + } + + if let textInputNode = self.textInputNode, let _ = self.presentationInterfaceState { + self.updatingInputState = true var textColor: UIColor = .black var accentTextColor: UIColor = .blue @@ -915,6 +920,17 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { strongSelf.ensureFocused() } } + recognizer.waitForTouchUp = { [weak self] in + guard let strongSelf = self, let textInputNode = strongSelf.textInputNode else { + return true + } + + if textInputNode.textView.isFirstResponder { + return true + } else { + return false + } + } self.textInputBackgroundNode.isUserInteractionEnabled = true self.textInputBackgroundNode.view.addGestureRecognizer(recognizer) @@ -1011,6 +1027,26 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { strongSelf.ensureFocusedOnTap() } } + recognizer.waitForTouchUp = { [weak self] in + guard let strongSelf = self, let textInputNode = strongSelf.textInputNode else { + return true + } + + if textInputNode.textView.isFirstResponder { + return true + } else if let (_, _, _, bottomInset, _, _, metrics, _, _) = strongSelf.validLayout { + let textFieldWaitsForTouchUp: Bool + if case .regular = metrics.widthClass, bottomInset.isZero { + textFieldWaitsForTouchUp = true + } else { + textFieldWaitsForTouchUp = false + } + + return textFieldWaitsForTouchUp + } else { + return false + } + } textInputNode.view.addGestureRecognizer(recognizer) self.touchDownGestureRecognizer = recognizer @@ -1100,14 +1136,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { let previousAdditionalSideInsets = self.validLayout?.4 self.validLayout = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) - - let textFieldWaitsForTouchUp: Bool - if case .regular = metrics.widthClass, bottomInset.isZero { - textFieldWaitsForTouchUp = true - } else { - textFieldWaitsForTouchUp = false - } - self.touchDownGestureRecognizer?.waitForTouchUp = textFieldWaitsForTouchUp var transition = transition var additionalOffset: CGFloat = 0.0 @@ -2165,6 +2193,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { var rects: [CGRect] = [] var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = [] + let fontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) + if let attributedText = textInputNode.attributedText { let beginning = textInputNode.textView.beginningOfDocument attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, range, _ in @@ -2248,7 +2278,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.customEmojiContainerView = customEmojiContainerView } - customEmojiContainerView.update(emojiRects: customEmojiRects) + customEmojiContainerView.update(fontSize: fontSize, emojiRects: customEmojiRects) } else if let customEmojiContainerView = self.customEmojiContainerView { customEmojiContainerView.removeFromSuperview() self.customEmojiContainerView = nil diff --git a/submodules/TelegramUI/Sources/ForwardAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/ForwardAccessoryPanelNode.swift index 4c6f222d5b..064cb6b28d 100644 --- a/submodules/TelegramUI/Sources/ForwardAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/ForwardAccessoryPanelNode.swift @@ -15,67 +15,70 @@ import TextFormat import Markdown import TelegramNotices import ChatPresentationInterfaceState +import TextNodeWithEntities +import AnimationCache +import MultiAnimationRenderer -func textStringForForwardedMessage(_ message: Message, strings: PresentationStrings) -> (String, Bool) { +func textStringForForwardedMessage(_ message: Message, strings: PresentationStrings) -> (text: String, entities: [MessageTextEntity], isMedia: Bool) { for media in message.media { switch media { - case _ as TelegramMediaImage: - return (strings.Message_Photo, true) - case let file as TelegramMediaFile: - if file.isVideoSticker || file.isAnimatedSticker { - return (strings.Message_Sticker, true) - } - var fileName: String = strings.Message_File - for attribute in file.attributes { - switch attribute { - case .Sticker: - return (strings.Message_Sticker, true) - case let .FileName(name): - fileName = name - case let .Audio(isVoice, _, title, performer, _): - if isVoice { - return (strings.Message_Audio, true) - } else { - if let title = title, let performer = performer, !title.isEmpty, !performer.isEmpty { - return (title + " — " + performer, true) - } else if let title = title, !title.isEmpty { - return (title, true) - } else if let performer = performer, !performer.isEmpty { - return (performer, true) - } else { - return (strings.Message_Audio, true) - } - } - case .Video: - if file.isAnimated { - return (strings.Message_Animation, true) - } else { - return (strings.Message_Video, true) - } - default: - break + case _ as TelegramMediaImage: + return (strings.Message_Photo, [], true) + case let file as TelegramMediaFile: + if file.isVideoSticker || file.isAnimatedSticker { + return (strings.Message_Sticker, [], true) + } + var fileName: String = strings.Message_File + for attribute in file.attributes { + switch attribute { + case .Sticker: + return (strings.Message_Sticker, [], true) + case let .FileName(name): + fileName = name + case let .Audio(isVoice, _, title, performer, _): + if isVoice { + return (strings.Message_Audio, [], true) + } else { + if let title = title, let performer = performer, !title.isEmpty, !performer.isEmpty { + return (title + " — " + performer, [], true) + } else if let title = title, !title.isEmpty { + return (title, [], true) + } else if let performer = performer, !performer.isEmpty { + return (performer, [], true) + } else { + return (strings.Message_Audio, [], true) + } } + case .Video: + if file.isAnimated { + return (strings.Message_Animation, [], true) + } else { + return (strings.Message_Video, [], true) + } + default: + break } - return (fileName, true) - case _ as TelegramMediaContact: - return (strings.Message_Contact, true) - case let game as TelegramMediaGame: - return (game.title, true) - case _ as TelegramMediaMap: - return (strings.Message_Location, true) - case _ as TelegramMediaAction: - return ("", true) - case _ as TelegramMediaPoll: - return (strings.ForwardedPolls(1), true) - case let dice as TelegramMediaDice: - return (dice.emoji, true) - case let invoice as TelegramMediaInvoice: - return (invoice.title, true) - default: - break + } + return (fileName, [], true) + case _ as TelegramMediaContact: + return (strings.Message_Contact, [], true) + case let game as TelegramMediaGame: + return (game.title, [], true) + case _ as TelegramMediaMap: + return (strings.Message_Location, [], true) + case _ as TelegramMediaAction: + return ("", [], true) + case _ as TelegramMediaPoll: + return (strings.ForwardedPolls(1), [], true) + case let dice as TelegramMediaDice: + return (dice.emoji, [], true) + case let invoice as TelegramMediaInvoice: + return (invoice.title, [], true) + default: + break } } - return (message.text, false) + return (message.text, message.textEntitiesAttribute?.entities ?? [], false) } final class ForwardAccessoryPanelNode: AccessoryPanelNode { @@ -89,7 +92,8 @@ final class ForwardAccessoryPanelNode: AccessoryPanelNode { let lineNode: ASImageNode let iconNode: ASImageNode let titleNode: ImmediateTextNode - let textNode: ImmediateTextNode + let textNode: ImmediateTextNodeWithEntities + private var originalText: NSAttributedString? private let actionArea: AccessibilityAreaNode @@ -102,7 +106,7 @@ final class ForwardAccessoryPanelNode: AccessoryPanelNode { private var validLayout: (size: CGSize, inset: CGFloat, interfaceState: ChatPresentationInterfaceState)? - init(context: AccountContext, messageIds: [MessageId], theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, nameDisplayOrder: PresentationPersonNameOrder, forwardOptionsState: ChatInterfaceForwardOptionsState?) { + init(context: AccountContext, messageIds: [MessageId], theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, nameDisplayOrder: PresentationPersonNameOrder, forwardOptionsState: ChatInterfaceForwardOptionsState?, animationCache: AnimationCache?, animationRenderer: MultiAnimationRenderer?) { self.context = context self.messageIds = messageIds self.theme = theme @@ -131,7 +135,7 @@ final class ForwardAccessoryPanelNode: AccessoryPanelNode { self.titleNode.maximumNumberOfLines = 1 self.titleNode.displaysAsynchronously = false - self.textNode = ImmediateTextNode() + self.textNode = ImmediateTextNodeWithEntities() self.textNode.maximumNumberOfLines = 1 self.textNode.displaysAsynchronously = false @@ -148,6 +152,16 @@ final class ForwardAccessoryPanelNode: AccessoryPanelNode { self.addSubnode(self.textNode) self.addSubnode(self.actionArea) + if let animationCache = animationCache, let animationRenderer = animationRenderer { + self.textNode.arguments = TextNodeWithEntities.Arguments( + context: context, + cache: animationCache, + renderer: animationRenderer, + placeholderColor: theme.list.mediaPlaceholderColor, + attemptSynchronous: false + ) + } + self.messageDisposable.set((context.account.postbox.messagesAtIds(messageIds) |> deliverOnMainQueue).start(next: { [weak self] messages in if let strongSelf = self { @@ -157,7 +171,7 @@ final class ForwardAccessoryPanelNode: AccessoryPanelNode { var authors = "" var uniquePeerIds = Set() var title = "" - var text = "" + var text = NSMutableAttributedString(string: "") var sourcePeer: (Bool, String)? for message in messages { if let author = message.forwardInfo?.author ?? message.effectiveAuthor, !uniquePeerIds.contains(author.id) { @@ -178,11 +192,27 @@ final class ForwardAccessoryPanelNode: AccessoryPanelNode { if messages.count == 1 { title = strongSelf.strings.Conversation_ForwardOptions_ForwardTitleSingle - let (string, _) = textStringForForwardedMessage(messages[0], strings: strings) - text = "\(authors): \(string)" + let (string, entities, _) = textStringForForwardedMessage(messages[0], strings: strings) + + text = NSMutableAttributedString(attributedString: NSAttributedString(string: "\(authors): ", font: Font.regular(15.0), textColor: strongSelf.theme.chat.inputPanel.secondaryTextColor)) + + let additionalText = NSMutableAttributedString(attributedString: NSAttributedString(string: string, font: Font.regular(15.0), textColor: strongSelf.theme.chat.inputPanel.secondaryTextColor)) + for entity in entities { + switch entity.type { + case let .CustomEmoji(_, fileId): + let range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound) + if range.lowerBound >= 0 && range.upperBound <= additionalText.length { + additionalText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(stickerPack: nil, fileId: fileId, file: messages[0].associatedMedia[MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile), range: range) + } + default: + break + } + } + + text.append(additionalText) } else { title = strongSelf.strings.Conversation_ForwardOptions_ForwardTitle(Int32(messages.count)) - text = strongSelf.strings.Conversation_ForwardFrom(authors).string + text = NSMutableAttributedString(attributedString: NSAttributedString(string: strongSelf.strings.Conversation_ForwardFrom(authors).string, font: Font.regular(15.0), textColor: strongSelf.theme.chat.inputPanel.secondaryTextColor)) } strongSelf.messages = messages @@ -190,7 +220,9 @@ final class ForwardAccessoryPanelNode: AccessoryPanelNode { strongSelf.authors = authors strongSelf.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor) - strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: strongSelf.theme.chat.inputPanel.secondaryTextColor) + strongSelf.textNode.attributedText = text + strongSelf.originalText = text + strongSelf.textNode.visibility = true let headerString: String if messages.count == 1 { @@ -273,25 +305,21 @@ final class ForwardAccessoryPanelNode: AccessoryPanelNode { let filteredMessages = self.messages - var authors = self.authors ?? "" - if forwardOptionsState?.hideNames == true { - authors = self.strings.DialogList_You - } - var title = "" - var text = "" - if filteredMessages.count == 1, let message = filteredMessages.first { + if filteredMessages.count == 1 { title = self.strings.Conversation_ForwardOptions_ForwardTitleSingle - let (string, _) = textStringForForwardedMessage(message, strings: strings) - text = "\(authors): \(string)" } else { title = self.strings.Conversation_ForwardOptions_ForwardTitle(Int32(filteredMessages.count)) - text = self.strings.Conversation_ForwardFrom(authors).string } self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor) - self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: self.theme.chat.inputPanel.secondaryTextColor) + if let attributedText = self.textNode.attributedText { + let updatedText = NSMutableAttributedString(attributedString: attributedText) + updatedText.addAttribute(.foregroundColor, value: self.theme.chat.inputPanel.secondaryTextColor, range: NSRange(location: 0, length: updatedText.length)) + self.textNode.attributedText = updatedText + self.originalText = updatedText + } if let (size, inset, interfaceState) = self.validLayout { self.updateState(size: size, inset: inset, interfaceState: interfaceState) diff --git a/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift b/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift index 6e55f0cbb4..845e95f258 100644 --- a/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift +++ b/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift @@ -331,7 +331,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { multiplexedNode.fileSelected = { [weak self] file, sourceNode, sourceRect in if let (collection, result) = file.contextResult { - let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect, false) + let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode.view, sourceRect, false) } else { let _ = self?.controllerInteraction.sendGif(file.file, sourceNode.view, sourceRect, false, false) } diff --git a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift index ac30fa69f4..798e6d568a 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift @@ -386,7 +386,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { func beginSelection() { if let _ = self.textInputPanelNode { } else { - let forwardAccessoryPanelNode = ForwardAccessoryPanelNode(context: self.context, messageIds: self.forwardedMessageIds, theme: self.presentationData.theme, strings: self.presentationData.strings, fontSize: self.presentationData.chatFontSize, nameDisplayOrder: self.presentationData.nameDisplayOrder, forwardOptionsState: self.presentationInterfaceState.interfaceState.forwardOptionsState) + let forwardAccessoryPanelNode = ForwardAccessoryPanelNode(context: self.context, messageIds: self.forwardedMessageIds, theme: self.presentationData.theme, strings: self.presentationData.strings, fontSize: self.presentationData.chatFontSize, nameDisplayOrder: self.presentationData.nameDisplayOrder, forwardOptionsState: self.presentationInterfaceState.interfaceState.forwardOptionsState, animationCache: nil, animationRenderer: nil) forwardAccessoryPanelNode.interfaceInteraction = self.interfaceInteraction self.addSubnode(forwardAccessoryPanelNode) self.forwardAccessoryPanelNode = forwardAccessoryPanelNode diff --git a/submodules/TouchDownGesture/Sources/TouchDownGestureRecognizer.swift b/submodules/TouchDownGesture/Sources/TouchDownGestureRecognizer.swift index 2b5a981b6c..22ed7f1421 100644 --- a/submodules/TouchDownGesture/Sources/TouchDownGestureRecognizer.swift +++ b/submodules/TouchDownGesture/Sources/TouchDownGestureRecognizer.swift @@ -6,7 +6,8 @@ public class TouchDownGestureRecognizer: UIGestureRecognizer, UIGestureRecognize public var touchDown: (() -> Void)? private var touchLocation: CGPoint? - public var waitForTouchUp = false + public var waitForTouchUp: (() -> Bool)? + private var isWaitingForTouchUp: Bool = false override public init(target: Any?, action: Selector?) { super.init(target: target, action: action) @@ -14,6 +15,13 @@ public class TouchDownGestureRecognizer: UIGestureRecognizer, UIGestureRecognize self.delegate = self } + override public func reset() { + self.touchLocation = nil + self.isWaitingForTouchUp = false + + super.reset() + } + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } @@ -21,7 +29,8 @@ public class TouchDownGestureRecognizer: UIGestureRecognizer, UIGestureRecognize override public func touchesBegan(_ touches: Set, with event: UIEvent) { super.touchesBegan(touches, with: event) - if self.waitForTouchUp { + if let waitForTouchUp = self.waitForTouchUp, waitForTouchUp() { + self.isWaitingForTouchUp = true if let touch = touches.first { self.touchLocation = touch.location(in: self.view) } @@ -49,14 +58,9 @@ public class TouchDownGestureRecognizer: UIGestureRecognizer, UIGestureRecognize override public func touchesEnded(_ touches: Set, with event: UIEvent) { super.touchesEnded(touches, with: event) - if let touchDown = self.touchDown, self.waitForTouchUp { + if let touchDown = self.touchDown, self.isWaitingForTouchUp { + self.isWaitingForTouchUp = false touchDown() } } - - override public func reset() { - self.touchLocation = nil - - super.reset() - } } diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m index a58c63b7c8..9241d2297b 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m @@ -169,46 +169,31 @@ void applySmoothRoundedCornersImpl(CALayer * _Nonnull layer) { } } -/*@interface _UIPortalView : UIView - -@property(nonatomic, getter=_isGeometryFrozen, setter=_setGeometryFrozen:) _Bool _geometryFrozen; // @synthesize _geometryFrozen=__geometryFrozen; -@property(nonatomic) _Bool forwardsClientHitTestingToSourceView; // @synthesize forwardsClientHitTestingToSourceView=_forwardsClientHitTestingToSourceView; -@property(copy, nonatomic) NSString * _Nullable name; // @synthesize name=_name; -@property(nonatomic) __weak UIView * _Nullable sourceView; // @synthesize sourceView=_sourceView; -- (void)setCenter:(struct CGPoint)arg1; -- (void)setBounds:(struct CGRect)arg1; -- (void)setFrame:(struct CGRect)arg1; -- (void)setHidden:(_Bool)arg1; -@property(nonatomic) _Bool allowsHitTesting; // @dynamic allowsHitTesting; -@property(nonatomic) _Bool allowsBackdropGroups; // @dynamic allowsBackdropGroups; -@property(nonatomic) _Bool matchesPosition; // @dynamic matchesPosition; -@property(nonatomic) _Bool matchesTransform; // @dynamic matchesTransform; -@property(nonatomic) _Bool matchesAlpha; // @dynamic matchesAlpha; -@property(nonatomic) _Bool hidesSourceView; // @dynamic hidesSourceView; -- (instancetype _Nonnull)initWithFrame:(struct CGRect)arg1; -- (instancetype _Nonnull)initWithSourceView:(UIView * _Nullable)arg1; - -@end*/ - UIView * _Nullable makePortalView() { - static Class portalViewClass = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - portalViewClass = NSClassFromString([@[@"_", @"UI", @"Portal", @"View"] componentsJoinedByString:@""]); - }); - if (!portalViewClass) { + if (@available(iOS 12.0, *)) { + static Class portalViewClass = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + portalViewClass = NSClassFromString([@[@"_", @"UI", @"Portal", @"View"] componentsJoinedByString:@""]); + }); + if (!portalViewClass) { + return nil; + } + UIView *view = [[portalViewClass alloc] init]; + if (!view) { + return nil; + } + + if (@available(iOS 13.0, *)) { + view.forwardsClientHitTestingToSourceView = false; + } + view.matchesPosition = true; + view.matchesTransform = true; + view.matchesAlpha = false; + view.allowsHitTesting = false; + + return view; + } else { return nil; } - UIView *view = [[portalViewClass alloc] init]; - if (!view) { - return nil; - } - - view.forwardsClientHitTestingToSourceView = false; - view.matchesPosition = true; - view.matchesTransform = true; - view.matchesAlpha = false; - view.allowsHitTesting = false; - - return view; }