diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 0b75fe6d0f..df5604a3dd 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -10964,3 +10964,5 @@ Sorry for the inconvenience."; "PrivacyInfo.ShowReadTime.ButtonTitle" = "Show My Read Time"; "PrivacyInfo.ShowReadTime.PremiumInfo" = "Subscription will let you see **%@'s** read time without showing yours."; "PrivacyInfo.ShowReadTime.AlwaysToast.Text" = "Set **Last Seen** privacy to 'Nobody' or 'My Contacts.'"; + +"Chat.ToastMessageTagged.Text" = "Message tagged with %@"; diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index 48d595d2f8..63fbb73e3a 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -469,6 +469,13 @@ public final class ShareController: ViewController { } } } + public var enqueued: (([PeerId], [Int64]) -> Void)? { + didSet { + if self.isNodeLoaded { + self.controllerNode.enqueued = enqueued + } + } + } public var openShareAsImage: (([Message]) -> Void)? @@ -713,6 +720,7 @@ public final class ShareController: ViewController { strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) }, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare, immediatePeerId: self.immediatePeerId, fromForeignApp: self.fromForeignApp, forceTheme: self.forceTheme, fromPublicChannel: fromPublicChannel, segmentedValues: self.segmentedValues, shareStory: self.shareStory) self.controllerNode.completed = self.completed + self.controllerNode.enqueued = self.enqueued self.controllerNode.present = { [weak self] c in self?.presentInGlobalOverlay(c) } @@ -1841,7 +1849,7 @@ public final class ShareController: ViewController { case let .progress(value): return .progress(value) case .done: - return .done + return .done([]) } } } @@ -1876,21 +1884,21 @@ public final class ShareController: ViewController { } |> mapToSignal { progressSets -> Signal in if progressSets.isEmpty { - return .single(.done) + return .single(.done([])) } for item in progressSets { if case .progress = item { return .complete() } } - return .single(.done) + return .single(.done([])) } } } private func shareLegacy(text: String, peerIds: [EnginePeer.Id], topicIds: [EnginePeer.Id: Int64], showNames: Bool, silently: Bool) -> Signal { guard let currentContext = self.currentContext as? ShareControllerAppAccountContext else { - return .single(.done) + return .single(.done([])) } return currentContext.context.engine.data.get(EngineDataMap( peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)) @@ -1924,6 +1932,8 @@ public final class ShareController: ViewController { } } + var correlationIds: [Int64] = [] + switch subject { case let .url(url): for peerId in peerIds { @@ -2205,7 +2215,9 @@ public final class ShareController: ViewController { return .fail(.generic) } - messagesToEnqueue.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) + let correlationId = Int64.random(in: Int64.min ... Int64.max) + correlationIds.append(correlationId) + messagesToEnqueue.append(.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: threadId, replyToMessageId: replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])) } for message in messages { for media in message.media { @@ -2277,7 +2289,7 @@ public final class ShareController: ViewController { case let .progress(value): return .progress(value) case .done: - return .done + return .done([]) } } } @@ -2326,7 +2338,7 @@ public final class ShareController: ViewController { } } if !hasStatuses { - return .single(.done) + return .single(.done(correlationIds)) } return .complete() } diff --git a/submodules/ShareController/Sources/ShareControllerNode.swift b/submodules/ShareController/Sources/ShareControllerNode.swift index cec5c9ebd2..7c33a21936 100644 --- a/submodules/ShareController/Sources/ShareControllerNode.swift +++ b/submodules/ShareController/Sources/ShareControllerNode.swift @@ -13,7 +13,7 @@ import ContextUI enum ShareState { case preparing(Bool) case progress(Float) - case done + case done([Int64]) } enum ShareExternalState { @@ -67,6 +67,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate var debugAction: (() -> Void)? var openStats: (() -> Void)? var completed: (([PeerId]) -> Void)? + var enqueued: (([PeerId], [Int64]) -> Void)? var present: ((ViewController) -> Void)? var disabledPeerSelected: ((EnginePeer) -> Void)? @@ -954,7 +955,8 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } } - if case .done = status, !fromForeignApp { + if case let .done(correlationIds) = status, !fromForeignApp { + strongSelf.enqueued?(peerIds, correlationIds) strongSelf.dismiss?(true) return } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index 842e7d34c6..eacb8de5a2 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -1037,7 +1037,8 @@ public extension TelegramEngine.EngineData.Item { func keys(data: TelegramEngine.EngineData) -> [PostboxViewKey] { return [ .cachedPeerData(peerId: self.id), - .basicPeer(data.accountPeerId) + .basicPeer(data.accountPeerId), + .basicPeer(self.id) ] } @@ -1046,6 +1047,10 @@ public extension TelegramEngine.EngineData.Item { assertionFailure() return false } + guard let basicTargetPeerView = views[.basicPeer(self.id)] as? BasicPeerView else { + assertionFailure() + return false + } guard let view = views[.cachedPeerData(peerId: self.id)] as? CachedPeerDataView else { assertionFailure() return false @@ -1055,6 +1060,13 @@ public extension TelegramEngine.EngineData.Item { return false } + guard let targetPeer = basicTargetPeerView.peer as? TelegramUser else { + return false + } + if !targetPeer.flags.contains(.requirePremium) { + return false + } + if self.id.namespace == Namespaces.Peer.CloudUser { if let cachedData = view.cachedPeerData as? CachedUserData { return cachedData.flags.contains(.premiumRequired) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Message.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Message.swift index b6493a342b..6b8e794024 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Message.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Message.swift @@ -2,6 +2,7 @@ import Postbox public final class EngineMessage: Equatable { public typealias Id = MessageId + public typealias StableId = UInt32 public typealias Index = MessageIndex public typealias Tags = MessageTags public typealias Attribute = MessageAttribute diff --git a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift index ec50d045a4..22173d47cb 100644 --- a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift @@ -489,13 +489,13 @@ public extension Message { public extension Message { func areReactionsTags(accountPeerId: PeerId) -> Bool { - /*if self.id.peerId == accountPeerId { + if self.id.peerId == accountPeerId { if let reactionsAttribute = self.reactionsAttribute, !reactionsAttribute.reactions.isEmpty { return reactionsAttribute.isTags } else { return true } - }*/ + } return false } } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index e33f56e967..588fc81f74 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -20,6 +20,7 @@ public enum PresentationResourceKey: Int32 { case navigationShareIcon case navigationSearchIcon case navigationCompactSearchIcon + case navigationCompactTagsSearchIcon case navigationCalendarIcon case navigationMoreIcon case navigationMoreCircledIcon diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift index 0018fc54f4..ab1f46fa75 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift @@ -73,6 +73,12 @@ public struct PresentationResourcesRootController { return generateTintedImage(image: UIImage(bundleImageName: "Chat List/SearchIcon"), color: theme.rootController.navigationBar.accentTextColor) }) } + + public static func navigationCompactTagsSearchIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.navigationCompactTagsSearchIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/NavigationSearchTagsIcon"), color: theme.rootController.navigationBar.accentTextColor) + }) + } public static func navigationCalendarIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.navigationCalendarIcon.rawValue, { theme in diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 40a76fbdc8..e9810f4c1a 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -425,6 +425,7 @@ swift_library( "//submodules/Components/BalancedTextComponent", "//submodules/TelegramUI/Components/VideoMessageCameraScreen", "//submodules/TelegramUI/Components/MediaScrubberComponent", + "//submodules/TelegramUI/Components/Chat/ChatShareMessageTagView", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/Chat/ChatNavigationButton/Sources/ChatNavigationButton.swift b/submodules/TelegramUI/Components/Chat/ChatNavigationButton/Sources/ChatNavigationButton.swift index d273af5b4e..3eb1ce98c6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatNavigationButton/Sources/ChatNavigationButton.swift +++ b/submodules/TelegramUI/Components/Chat/ChatNavigationButton/Sources/ChatNavigationButton.swift @@ -6,7 +6,7 @@ public enum ChatNavigationButtonAction: Equatable { case clearHistory case clearCache case cancelMessageSelection - case search + case search(hasTags: Bool) case dismiss case toggleInfoPanel case spacer diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift index 878eaa3604..322df5a485 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift @@ -175,7 +175,7 @@ public final class ChatRecentActionsController: TelegramBaseController { self.navigationItem.titleView = self.titleView - let rightButton = ChatNavigationButton(action: .search, buttonItem: UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.activateSearch))) + let rightButton = ChatNavigationButton(action: .search(hasTags: false), buttonItem: UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.activateSearch))) self.navigationItem.setRightBarButton(rightButton.buttonItem, animated: false) self.titleView.title = self.presentationData.strings.Channel_AdminLog_TitleAllEvents @@ -235,7 +235,7 @@ public final class ChatRecentActionsController: TelegramBaseController { self.titleView.color = self.presentationData.theme.rootController.navigationBar.primaryTextColor self.updateTitle() - let rightButton = ChatNavigationButton(action: .search, buttonItem: UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.activateSearch))) + let rightButton = ChatNavigationButton(action: .search(hasTags: false), buttonItem: UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.activateSearch))) self.navigationItem.setRightBarButton(rightButton.buttonItem, animated: false) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style diff --git a/submodules/TelegramUI/Components/Chat/ChatShareMessageTagView/BUILD b/submodules/TelegramUI/Components/Chat/ChatShareMessageTagView/BUILD new file mode 100644 index 0000000000..c70d395426 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatShareMessageTagView/BUILD @@ -0,0 +1,28 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatShareMessageTagView", + module_name = "ChatShareMessageTagView", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/UndoUI", + "//submodules/ContextUI", + "//submodules/ReactionSelectionNode", + "//submodules/TelegramUI/Components/EntityKeyboard", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatShareMessageTagView/Sources/ChatShareMessageTagView.swift b/submodules/TelegramUI/Components/Chat/ChatShareMessageTagView/Sources/ChatShareMessageTagView.swift new file mode 100644 index 0000000000..0dd5418766 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatShareMessageTagView/Sources/ChatShareMessageTagView.swift @@ -0,0 +1,155 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import SwiftSignalKit +import Display +import TelegramCore +import TelegramPresentationData +import AccountContext +import UndoUI +import ReactionSelectionNode +import EntityKeyboard + +public final class ChatShareMessageTagView: UIView, UndoOverlayControllerAdditionalView { + private struct Params: Equatable { + var size: CGSize + + init(size: CGSize) { + self.size = size + } + } + + public var interaction: UndoOverlayControllerAdditionalViewInteraction? + + private var reactionContextNode: ReactionContextNode? + private var params: Params? + + public init(context: AccountContext, presentationData: PresentationData, reactionItems: [ReactionItem], completion: @escaping (TelegramMediaFile, UpdateMessageReaction) -> Void) { + super.init(frame: CGRect()) + + let reactionContextNode = ReactionContextNode( + context: context, + animationCache: context.animationCache, + presentationData: presentationData, + items: reactionItems.map(ReactionContextItem.reaction), + selectedItems: Set(), + title: presentationData.strings.Chat_ContextMenuTagsTitle, + alwaysAllowPremiumReactions: false, + allPresetReactionsAreAvailable: true, + getEmojiContent: { animationCache, animationRenderer in + let mappedReactionItems: [EmojiComponentReactionItem] = reactionItems.map { reaction -> EmojiComponentReactionItem in + return EmojiComponentReactionItem(reaction: reaction.reaction.rawValue, file: reaction.stillAnimation) + } + + return EmojiPagerContentComponent.emojiInputData( + context: context, + animationCache: animationCache, + animationRenderer: animationRenderer, + isStandalone: false, + subject: .messageTag, + hasTrending: false, + topReactionItems: mappedReactionItems, + areUnicodeEmojiEnabled: false, + areCustomEmojiEnabled: true, + chatPeerId: context.account.peerId, + selectedItems: Set(), + premiumIfSavedMessages: false + ) + }, + isExpandedUpdated: { [weak self] transition in + guard let self else { + return + } + self.interaction?.disableTimeout() + self.update(transition: transition) + }, + requestLayout: { [weak self] transition in + guard let self else { + return + } + self.update(transition: transition) + }, + requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in + guard let self else { + return + } + self.update(transition: transition) + } + ) + reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in + guard let self else { + return + } + + let _ = (context.engine.stickers.availableReactions() + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak self] availableReactions in + guard let self, let availableReactions else { + return + } + + var file: TelegramMediaFile? + switch updateReaction { + case .builtin: + for reaction in availableReactions.reactions { + if reaction.value == updateReaction.reaction { + file = reaction.centerAnimation + break + } + } + case let .custom(_, fileValue): + file = fileValue + } + + guard let file else { + return + } + + completion(file, updateReaction) + + self.interaction?.dismiss() + }) + } + reactionContextNode.displayTail = false + reactionContextNode.forceTailToRight = true + reactionContextNode.forceDark = false + self.reactionContextNode = reactionContextNode + + self.addSubnode(reactionContextNode) + } + + required public init(coder: NSCoder) { + preconditionFailure() + } + + public func update(size: CGSize, transition: ContainedViewLayoutTransition) { + let params = Params(size: size) + if self.params == params { + return + } + self.params = params + self.update(params: params, transition: transition) + } + + private func update(transition: ContainedViewLayoutTransition) { + if let params = self.params { + self.update(params: params, transition: transition) + } + } + + private func update(params: Params, transition: ContainedViewLayoutTransition) { + guard let reactionContextNode = self.reactionContextNode else { + return + } + + let isFirstTime = reactionContextNode.bounds.isEmpty + + let reactionsAnchorRect = CGRect(origin: CGPoint(x: params.size.width - 1.0, y: 0.0), size: CGSize(width: 1.0, height: 1.0)) + + transition.updateFrame(node: reactionContextNode, frame: CGRect(origin: CGPoint(), size: params.size)) + reactionContextNode.updateLayout(size: params.size, insets: UIEdgeInsets(), anchorRect: reactionsAnchorRect, centerAligned: true, isCoveredByInput: false, isAnimatingOut: false, transition: transition) + if isFirstTime { + reactionContextNode.animateIn(from: reactionsAnchorRect) + } + } +} diff --git a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift index f384ad05c0..f83e5aaeba 100644 --- a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift +++ b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift @@ -357,19 +357,69 @@ public class ImmediateTextNodeWithEntities: TextNode { public var tapAttributeAction: (([NSAttributedString.Key: Any], Int) -> Void)? public var longTapAttributeAction: (([NSAttributedString.Key: Any], Int) -> Void)? + public var customItemLayout: ((CGSize, TelegramMediaFile) -> CGSize)? + private func processedAttributedText() -> NSAttributedString? { var updatedString: NSAttributedString? if let sourceString = self.attributedText { let string = NSMutableAttributedString(attributedString: sourceString) - let fullRange = NSRange(location: 0, length: string.length) - string.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: fullRange, options: [], using: { value, range, _ in - if let value = value as? ChatTextInputTextCustomEmojiAttribute { - if let font = string.attribute(.font, at: range.location, effectiveRange: nil) as? UIFont { - string.addAttribute(NSAttributedString.Key("Attribute__EmbeddedItem"), value: InlineStickerItem(emoji: value, file: value.file, fontSize: font.pointSize), range: range) + var fullRange = NSRange(location: 0, length: string.length) + var originalTextId = 0 + while true { + var found = false + string.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: fullRange, options: [], using: { value, range, stop in + if let value = value as? ChatTextInputTextCustomEmojiAttribute, let font = string.attribute(.font, at: range.location, effectiveRange: nil) as? UIFont { + let updatedSubstring = NSMutableAttributedString(string: "&") + + let replacementRange = NSRange(location: 0, length: updatedSubstring.length) + updatedSubstring.addAttributes(string.attributes(at: range.location, effectiveRange: nil), range: replacementRange) + updatedSubstring.addAttribute(NSAttributedString.Key("Attribute__EmbeddedItem"), value: InlineStickerItem(emoji: value, file: value.file, fontSize: font.pointSize), range: replacementRange) + updatedSubstring.addAttribute(originalTextAttributeKey, value: OriginalTextAttribute(id: originalTextId, string: string.attributedSubstring(from: range).string), range: replacementRange) + originalTextId += 1 + + let itemSize = (font.pointSize * 24.0 / 17.0) + + let runDelegateData = RunDelegateData( + ascent: font.ascender, + descent: font.descender, + width: itemSize + ) + var callbacks = CTRunDelegateCallbacks( + version: kCTRunDelegateCurrentVersion, + dealloc: { dataRef in + Unmanaged.fromOpaque(dataRef).release() + }, + getAscent: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().ascent + }, + getDescent: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().descent + }, + getWidth: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().width + } + ) + + if let runDelegate = CTRunDelegateCreate(&callbacks, Unmanaged.passRetained(runDelegateData).toOpaque()) { + updatedSubstring.addAttribute(NSAttributedString.Key(kCTRunDelegateAttributeName as String), value: runDelegate, range: replacementRange) + } + + string.replaceCharacters(in: range, with: updatedSubstring) + let updatedRange = NSRange(location: range.location, length: updatedSubstring.length) + + found = true + stop.pointee = ObjCBool(true) + fullRange = NSRange(location: updatedRange.upperBound, length: fullRange.upperBound - range.upperBound) } + }) + if !found { + break } - }) + } updatedString = string } @@ -418,16 +468,20 @@ public class ImmediateTextNodeWithEntities: TextNode { let id = InlineStickerItemLayer.Key(id: stickerItem.emoji.fileId, index: index) validIds.append(id) - let itemSize = floor(stickerItem.fontSize * 24.0 / 17.0) + let itemSide = floor(stickerItem.fontSize * 24.0 / 17.0) + var itemSize = CGSize(width: itemSide, height: itemSide) + if let file = stickerItem.file, let customItemLayout = self.customItemLayout { + itemSize = customItemLayout(itemSize, file) + } - let itemFrame = CGRect(origin: item.rect.offsetBy(dx: textLayout.insets.left, dy: textLayout.insets.top + 0.0).center, size: CGSize()).insetBy(dx: -itemSize / 2.0, dy: -itemSize / 2.0) + let itemFrame = CGRect(origin: item.rect.offsetBy(dx: textLayout.insets.left, dy: textLayout.insets.top + 0.0).center, size: CGSize()).insetBy(dx: -itemSize.width / 2.0, dy: -itemSize.height / 2.0) let itemLayer: InlineStickerItemLayer if let current = self.inlineStickerItemLayers[id] { itemLayer = current itemLayer.dynamicColor = item.textColor } else { - let pointSize = floor(itemSize * 1.3) + let pointSize = floor(itemSize.width * 1.3) itemLayer = InlineStickerItemLayer(context: context, userLocation: .other, attemptSynchronousLoad: false, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: pointSize, height: pointSize), dynamicColor: item.textColor) self.inlineStickerItemLayers[id] = itemLayer self.layer.addSublayer(itemLayer) diff --git a/submodules/TelegramUI/Images.xcassets/Chat/NavigationSearchTagsIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/NavigationSearchTagsIcon.imageset/Contents.json new file mode 100644 index 0000000000..c384c06593 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/NavigationSearchTagsIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "savedsearch_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/NavigationSearchTagsIcon.imageset/savedsearch_30.pdf b/submodules/TelegramUI/Images.xcassets/Chat/NavigationSearchTagsIcon.imageset/savedsearch_30.pdf new file mode 100644 index 0000000000..99599cad12 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/NavigationSearchTagsIcon.imageset/savedsearch_30.pdf @@ -0,0 +1,118 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 9.334961 9.588989 cm +0.000000 0.000000 0.000000 scn +1.330000 10.411050 m +1.330000 13.357489 3.718561 15.746050 6.665000 15.746050 c +9.611439 15.746050 12.000000 13.357489 12.000000 10.411050 c +12.000000 7.464611 9.611439 5.076050 6.665000 5.076050 c +3.718561 5.076050 1.330000 7.464611 1.330000 10.411050 c +h +6.665000 17.076050 m +2.984022 17.076050 0.000000 14.092028 0.000000 10.411050 c +0.000000 6.730072 2.984022 3.746050 6.665000 3.746050 c +8.206339 3.746050 9.625477 4.269255 10.754500 5.147752 c +15.578101 0.324150 l +15.902237 0.000015 16.427763 0.000015 16.751900 0.324150 c +17.076035 0.648287 17.076035 1.173813 16.751900 1.497949 c +11.928298 6.321549 l +12.806795 7.450573 13.330000 8.869711 13.330000 10.411050 c +13.330000 14.092028 10.345978 17.076050 6.665000 17.076050 c +h +f* +n +Q +q +1.000000 0.000000 -0.000000 1.000000 2.334961 2.835022 cm +0.000000 0.000000 0.000000 scn +2.665000 9.330017 m +1.193161 9.330017 0.000000 8.136856 0.000000 6.665017 c +0.000000 2.665017 l +0.000000 1.193178 1.193161 0.000017 2.665000 0.000017 c +8.665000 0.000017 l +9.503828 0.000017 10.293703 0.394955 10.797000 1.066017 c +12.522000 3.366017 l +13.099334 4.135795 13.099333 5.194240 12.522000 5.964017 c +10.797000 8.264017 l +10.293703 8.935080 9.503828 9.330017 8.665000 9.330017 c +2.665000 9.330017 l +h +1.330000 6.665017 m +1.330000 7.402317 1.927700 8.000017 2.665000 8.000017 c +8.665000 8.000017 l +9.085201 8.000017 9.480880 7.802178 9.733000 7.466017 c +11.458000 5.166018 l +11.680667 4.869128 11.680667 4.460906 11.458000 4.164017 c +9.733000 1.864017 l +9.480880 1.527856 9.085201 1.330017 8.665000 1.330017 c +2.665000 1.330017 l +1.927700 1.330017 1.330000 1.927717 1.330000 2.665017 c +1.330000 6.665017 l +h +8.665000 3.665017 m +9.217285 3.665017 9.665000 4.112732 9.665000 4.665017 c +9.665000 5.217302 9.217285 5.665017 8.665000 5.665017 c +8.112715 5.665017 7.665000 5.217302 7.665000 4.665017 c +7.665000 4.112732 8.112715 3.665017 8.665000 3.665017 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 2019 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002109 00000 n +0000002132 00000 n +0000002305 00000 n +0000002379 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2438 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index a67226fc53..d0e9a0ecee 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -2258,129 +2258,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return $0.updatedInputMode(f) }) }, openMessageShareMenu: { [weak self] id in - if let strongSelf = self, let messages = strongSelf.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(id), let message = messages.first { - let chatPresentationInterfaceState = strongSelf.presentationInterfaceState - var warnAboutPrivate = false - var canShareToStory = false - if case .peer = chatPresentationInterfaceState.chatLocation, let channel = message.peers[message.id.peerId] as? TelegramChannel { - if case .broadcast = channel.info { - canShareToStory = true - } - if channel.addressName == nil { - warnAboutPrivate = true - } - } - let shareController = ShareController(context: strongSelf.context, subject: .messages(messages), updatedPresentationData: strongSelf.updatedPresentationData, shareAsLink: true) - - shareController.parentNavigationController = strongSelf.navigationController as? NavigationController - - if let message = messages.first, message.media.contains(where: { media in - if media is TelegramMediaContact || media is TelegramMediaPoll { - return true - } else if let file = media as? TelegramMediaFile, file.isSticker || file.isAnimatedSticker || file.isVideoSticker { - return true - } else { - return false - } - }) { - canShareToStory = false - } - if message.text.containsOnlyEmoji { - canShareToStory = false - } - - if canShareToStory { - shareController.shareStory = { [weak self] in - guard let self else { - return - } - Queue.mainQueue().after(0.15) { - self.openStorySharing(messages: messages) - } - } - } - shareController.openShareAsImage = { [weak self] messages in - if let strongSelf = self { - strongSelf.present(ChatQrCodeScreen(context: strongSelf.context, subject: .messages(messages)), in: .window(.root)) - } - } - shareController.dismissed = { [weak self] shared in - if shared { - self?.commitPurposefulAction() - } - } - shareController.actionCompleted = { [weak self] in - if let strongSelf = self { - let content: UndoOverlayContent - if warnAboutPrivate { - content = .linkCopied(text: strongSelf.presentationData.strings.Conversation_PrivateMessageLinkCopiedLong) - } else { - content = .linkCopied(text: strongSelf.presentationData.strings.Conversation_LinkCopied) - } - strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - } - } - shareController.completed = { [weak self] peerIds in - guard let strongSelf = self else { - return - } - - let _ = (strongSelf.context.engine.data.get( - EngineDataList( - peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) - ) - ) - |> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in - guard let strongSelf = self else { - return - } - let peers = peerList.compactMap { $0 } - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - let text: String - var savedMessages = false - if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId { - text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many - savedMessages = true - } else { - if peers.count == 1, let peer = peers.first { - var peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - peerName = peerName.replacingOccurrences(of: "**", with: "") - text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string : presentationData.strings.Conversation_ForwardTooltip_Chat_Many(peerName).string - } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { - var firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") - var secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") - text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string : presentationData.strings.Conversation_ForwardTooltip_TwoChats_Many(firstPeerName, secondPeerName).string - } else if let peer = peers.first { - var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - peerName = peerName.replacingOccurrences(of: "**", with: "") - text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string : presentationData.strings.Conversation_ForwardTooltip_ManyChats_Many(peerName, "\(peers.count - 1)").string - } else { - text = "" - } - } - - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in - if savedMessages, let self, action == .info { - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) - |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let self, let peer else { - return - } - guard let navigationController = self.navigationController as? NavigationController else { - return - } - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) - }) - } - return false - }), in: .current) - }) - } - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(shareController, in: .window(.root), blockInteraction: true) + guard let self else { + return } + self.openMessageShareMenu(id: id) }, presentController: { [weak self] controller, arguments in self?.present(controller, in: .window(.root), with: arguments) }, presentControllerInCurrent: { [weak self] controller, arguments in diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift new file mode 100644 index 0000000000..e7af78dfc7 --- /dev/null +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift @@ -0,0 +1,215 @@ +import Foundation +import TelegramPresentationData +import AccountContext +import Postbox +import TelegramCore +import SwiftSignalKit +import ContextUI +import ChatControllerInteraction +import Display +import UIKit +import UndoUI +import ShareController +import ChatQrCodeScreen +import ChatShareMessageTagView +import ReactionSelectionNode + +extension ChatControllerImpl { + func openMessageShareMenu(id: EngineMessage.Id) { + guard let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(id), let message = messages.first else { + return + } + + let chatPresentationInterfaceState = self.presentationInterfaceState + var warnAboutPrivate = false + var canShareToStory = false + if case .peer = chatPresentationInterfaceState.chatLocation, let channel = message.peers[message.id.peerId] as? TelegramChannel { + if case .broadcast = channel.info { + canShareToStory = true + } + if channel.addressName == nil { + warnAboutPrivate = true + } + } + let shareController = ShareController(context: self.context, subject: .messages(messages), updatedPresentationData: self.updatedPresentationData, shareAsLink: true) + shareController.parentNavigationController = self.navigationController as? NavigationController + + if let message = messages.first, message.media.contains(where: { media in + if media is TelegramMediaContact || media is TelegramMediaPoll { + return true + } else if let file = media as? TelegramMediaFile, file.isSticker || file.isAnimatedSticker || file.isVideoSticker { + return true + } else { + return false + } + }) { + canShareToStory = false + } + if message.text.containsOnlyEmoji { + canShareToStory = false + } + + if canShareToStory { + shareController.shareStory = { [weak self] in + guard let self else { + return + } + Queue.mainQueue().after(0.15) { + self.openStorySharing(messages: messages) + } + } + } + shareController.openShareAsImage = { [weak self] messages in + guard let self else { + return + } + self.present(ChatQrCodeScreen(context: self.context, subject: .messages(messages)), in: .window(.root)) + } + shareController.dismissed = { [weak self] shared in + if shared { + self?.commitPurposefulAction() + } + } + shareController.actionCompleted = { [weak self] in + guard let self else { + return + } + let content: UndoOverlayContent + if warnAboutPrivate { + content = .linkCopied(text: self.presentationData.strings.Conversation_PrivateMessageLinkCopiedLong) + } else { + content = .linkCopied(text: self.presentationData.strings.Conversation_LinkCopied) + } + self.present(UndoOverlayController(presentationData: self.presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + } + shareController.enqueued = { [weak self] peerIds, correlationIds in + guard let self else { + return + } + + let _ = (self.context.engine.data.get( + EngineDataList( + peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) + ) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in + guard let self else { + return + } + let peers = peerList.compactMap { $0 } + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let text: String + var savedMessages = false + if peerIds.count == 1, let peerId = peerIds.first, peerId == self.context.account.peerId { + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many + savedMessages = true + } else { + if peers.count == 1, let peer = peers.first { + var peerName = peer.id == self.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string : presentationData.strings.Conversation_ForwardTooltip_Chat_Many(peerName).string + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + var firstPeerName = firstPeer.id == self.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") + var secondPeerName = secondPeer.id == self.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string : presentationData.strings.Conversation_ForwardTooltip_TwoChats_Many(firstPeerName, secondPeerName).string + } else if let peer = peers.first { + var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string : presentationData.strings.Conversation_ForwardTooltip_ManyChats_Many(peerName, "\(peers.count - 1)").string + } else { + text = "" + } + } + + let reactionItems: Signal<[ReactionItem], NoError> + if savedMessages { + reactionItems = tagMessageReactions(context: self.context) + } else { + reactionItems = .single([]) + } + + let _ = (reactionItems + |> deliverOnMainQueue).startStandalone(next: { [weak self] reactionItems in + guard let self else { + return + } + + self.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, position: savedMessages ? .top : .bottom, animateInAsReplacement: !savedMessages, action: { [weak self] action in + if savedMessages, let self, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) + }) + } + return false + }, additionalView: savedMessages ? { [weak self] () -> UndoOverlayControllerAdditionalView? in + guard let self else { + return nil + } + return ChatShareMessageTagView(context: self.context, presentationData: self.presentationData, reactionItems: reactionItems, completion: { [weak self] file, updateReaction in + guard let self else { + return + } + + let _ = (self.context.account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: context.account.peerId, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 45, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allScheduled), orderStatistics: []) + |> map { view, _, _ -> [EngineMessage.Id] in + //TODO:filter? + for entry in view.entries.reversed() { + if entry.message.id.namespace == Namespaces.Message.Cloud { + return [entry.message.id] + } + } + + return view.entries.compactMap { entry -> EngineMessage.Id? in + for attribute in entry.message.attributes { + if let attribute = attribute as? OutgoingMessageInfoAttribute { + if let correlationId = attribute.correlationId { + if correlationIds.contains(correlationId) { + if entry.message.id.namespace == Namespaces.Message.Cloud { + return entry.message.id + } else { + return nil + } + } + } + } + } + return nil + } + } + |> filter { !$0.isEmpty } + |> take(1) + |> timeout(5.0, queue: .mainQueue(), alternate: .single([])) + |> deliverOnMainQueue).start(next: { [weak self] messageIds in + guard let self else { + return + } + if let messageId = messageIds.first { + let _ = context.engine.messages.setMessageReactions(id: messageId, reactions: [updateReaction]) + + var isBuiltinReaction = false + if case .builtin = updateReaction { + isBuiltinReaction = true + } + self.present(UndoOverlayController(presentationData: presentationData, content: .messageTagged(context: self.context, customEmoji: file, isBuiltinReaction: isBuiltinReaction), elevatedLayout: false, position: .top, animateInAsReplacement: true, action: { _ in + return false + }), in: .current) + } + }) + }) + } : nil), in: .current) + }) + }) + } + self.chatDisplayNode.dismissInput() + self.present(shareController, in: .window(.root), blockInteraction: true) + } +} diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift index 8cf582a26d..5af4d87cc1 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift @@ -108,7 +108,7 @@ func rightNavigationButtonForChatInterfaceState(context: AccountContext, present } else { let buttonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(presentationInterfaceState.theme), style: .plain, target: target, action: selector) buttonItem.accessibilityLabel = strings.Conversation_Search - return ChatNavigationButton(action: .search, buttonItem: buttonItem) + return ChatNavigationButton(action: .search(hasTags: false), buttonItem: buttonItem) } } else { if case .spacer = currentButton?.action { @@ -126,7 +126,7 @@ func rightNavigationButtonForChatInterfaceState(context: AccountContext, present } else { let buttonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(presentationInterfaceState.theme), style: .plain, target: target, action: selector) buttonItem.accessibilityLabel = strings.Conversation_Search - return ChatNavigationButton(action: .search, buttonItem: buttonItem) + return ChatNavigationButton(action: .search(hasTags: false), buttonItem: buttonItem) } } else { if case .spacer = currentButton?.action { @@ -149,13 +149,15 @@ func rightNavigationButtonForChatInterfaceState(context: AccountContext, present if case .scheduledMessages = presentationInterfaceState.subject { return chatInfoNavigationButton } else { - if presentationInterfaceState.hasPlentyOfMessages { - if case .search = currentButton?.action { + let isTags = presentationInterfaceState.hasSearchTags + + if presentationInterfaceState.hasPlentyOfMessages || isTags { + if case .search(isTags) = currentButton?.action { return currentButton } else { - let buttonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(presentationInterfaceState.theme), style: .plain, target: target, action: selector) + let buttonItem = UIBarButtonItem(image: isTags ? PresentationResourcesRootController.navigationCompactTagsSearchIcon(presentationInterfaceState.theme) : PresentationResourcesRootController.navigationCompactSearchIcon(presentationInterfaceState.theme), style: .plain, target: target, action: selector) buttonItem.accessibilityLabel = strings.Conversation_Search - return ChatNavigationButton(action: .search, buttonItem: buttonItem) + return ChatNavigationButton(action: .search(hasTags: isTags), buttonItem: buttonItem) } } else { if case .spacer = currentButton?.action { diff --git a/submodules/TelegramUI/Sources/TopMessageReactions.swift b/submodules/TelegramUI/Sources/TopMessageReactions.swift index 2dceebf154..8166aa46a6 100644 --- a/submodules/TelegramUI/Sources/TopMessageReactions.swift +++ b/submodules/TelegramUI/Sources/TopMessageReactions.swift @@ -5,6 +5,80 @@ import Postbox import AccountContext import ReactionSelectionNode +func tagMessageReactions(context: AccountContext) -> Signal<[ReactionItem], NoError> { + return combineLatest( + context.engine.stickers.availableReactions(), + context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudDefaultTagReactions], namespaces: [ItemCollectionId.Namespace.max - 1], aroundIndex: nil, count: 10000000) + ) + |> take(1) + |> map { availableReactions, view -> [ReactionItem] in + var defaultTagReactions: OrderedItemListView? + for orderedView in view.orderedItemListsViews { + if orderedView.collectionId == Namespaces.OrderedItemList.CloudDefaultTagReactions { + defaultTagReactions = orderedView + } + } + + var result: [ReactionItem] = [] + var existingIds = Set() + + if let defaultTagReactions { + for item in defaultTagReactions.items { + guard let topReaction = item.contents.get(RecentReactionItem.self) else { + continue + } + switch topReaction.content { + case let .builtin(value): + if let reaction = availableReactions?.reactions.first(where: { $0.value == .builtin(value) }) { + guard let centerAnimation = reaction.centerAnimation else { + continue + } + guard let aroundAnimation = reaction.aroundAnimation else { + continue + } + + if existingIds.contains(reaction.value) { + continue + } + existingIds.insert(reaction.value) + + result.append(ReactionItem( + reaction: ReactionItem.Reaction(rawValue: reaction.value), + appearAnimation: reaction.appearAnimation, + stillAnimation: reaction.selectAnimation, + listAnimation: centerAnimation, + largeListAnimation: reaction.activateAnimation, + applicationAnimation: aroundAnimation, + largeApplicationAnimation: reaction.effectAnimation, + isCustom: false + )) + } else { + continue + } + case let .custom(file): + if existingIds.contains(.custom(file.fileId.id)) { + continue + } + existingIds.insert(.custom(file.fileId.id)) + + result.append(ReactionItem( + reaction: ReactionItem.Reaction(rawValue: .custom(file.fileId.id)), + appearAnimation: file, + stillAnimation: file, + listAnimation: file, + largeListAnimation: file, + applicationAnimation: nil, + largeApplicationAnimation: nil, + isCustom: true + )) + } + } + } + + return result + } +} + func topMessageReactions(context: AccountContext, message: Message) -> Signal<[ReactionItem], NoError> { if message.id.peerId == context.account.peerId { var loadTags = false @@ -24,77 +98,7 @@ func topMessageReactions(context: AccountContext, message: Message) -> Signal<[R } if loadTags { - return combineLatest( - context.engine.stickers.availableReactions(), - context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudDefaultTagReactions], namespaces: [ItemCollectionId.Namespace.max - 1], aroundIndex: nil, count: 10000000) - ) - |> take(1) - |> map { availableReactions, view -> [ReactionItem] in - var defaultTagReactions: OrderedItemListView? - for orderedView in view.orderedItemListsViews { - if orderedView.collectionId == Namespaces.OrderedItemList.CloudDefaultTagReactions { - defaultTagReactions = orderedView - } - } - - var result: [ReactionItem] = [] - var existingIds = Set() - - if let defaultTagReactions { - for item in defaultTagReactions.items { - guard let topReaction = item.contents.get(RecentReactionItem.self) else { - continue - } - switch topReaction.content { - case let .builtin(value): - if let reaction = availableReactions?.reactions.first(where: { $0.value == .builtin(value) }) { - guard let centerAnimation = reaction.centerAnimation else { - continue - } - guard let aroundAnimation = reaction.aroundAnimation else { - continue - } - - if existingIds.contains(reaction.value) { - continue - } - existingIds.insert(reaction.value) - - result.append(ReactionItem( - reaction: ReactionItem.Reaction(rawValue: reaction.value), - appearAnimation: reaction.appearAnimation, - stillAnimation: reaction.selectAnimation, - listAnimation: centerAnimation, - largeListAnimation: reaction.activateAnimation, - applicationAnimation: aroundAnimation, - largeApplicationAnimation: reaction.effectAnimation, - isCustom: false - )) - } else { - continue - } - case let .custom(file): - if existingIds.contains(.custom(file.fileId.id)) { - continue - } - existingIds.insert(.custom(file.fileId.id)) - - result.append(ReactionItem( - reaction: ReactionItem.Reaction(rawValue: .custom(file.fileId.id)), - appearAnimation: file, - stillAnimation: file, - listAnimation: file, - largeListAnimation: file, - applicationAnimation: nil, - largeApplicationAnimation: nil, - isCustom: true - )) - } - } - } - - return result - } + return tagMessageReactions(context: context) } } diff --git a/submodules/UndoUI/BUILD b/submodules/UndoUI/BUILD index 369f0e7bcf..ce70e46883 100644 --- a/submodules/UndoUI/BUILD +++ b/submodules/UndoUI/BUILD @@ -29,6 +29,7 @@ swift_library( "//submodules/ComponentFlow:ComponentFlow", "//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode", "//submodules/TelegramUI/Components/EmojiStatusComponent", + "//submodules/TelegramUI/Components/TextNodeWithEntities", ], visibility = [ "//visibility:public", diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index d63b3b5c3e..6ecfea6a6c 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -45,6 +45,7 @@ public enum UndoOverlayContent { case universal(animation: String, scale: CGFloat, colors: [String: UIColor], title: String?, text: String, customUndoText: String?, timeout: Double?) case premiumPaywall(title: String?, text: String, customUndoText: String?, timeout: Double?, linkAction: ((String) -> Void)?) case peers(context: AccountContext, peers: [EnginePeer], title: String?, text: String, customUndoText: String?) + case messageTagged(context: AccountContext, customEmoji: TelegramMediaFile, isBuiltinReaction: Bool) } public enum UndoOverlayAction { @@ -53,6 +54,22 @@ public enum UndoOverlayAction { case commit } +public final class UndoOverlayControllerAdditionalViewInteraction { + public let disableTimeout: () -> Void + public let dismiss: () -> Void + + public init(disableTimeout: @escaping () -> Void, dismiss: @escaping () -> Void) { + self.disableTimeout = disableTimeout + self.dismiss = dismiss + } +} + +public protocol UndoOverlayControllerAdditionalView: UIView { + var interaction: UndoOverlayControllerAdditionalViewInteraction? { get set } + + func update(size: CGSize, transition: ContainedViewLayoutTransition) +} + public final class UndoOverlayController: ViewController { public enum Position { case top @@ -69,6 +86,7 @@ public final class UndoOverlayController: ViewController { private let position: Position private let animateInAsReplacement: Bool private var action: (UndoOverlayAction) -> Bool + private let additionalView: (() -> UndoOverlayControllerAdditionalView?)? private let blurred: Bool private var didPlayPresentationAnimation = false @@ -78,7 +96,7 @@ public final class UndoOverlayController: ViewController { public var tag: Any? - public init(presentationData: PresentationData, content: UndoOverlayContent, elevatedLayout: Bool, position: Position = .bottom, animateInAsReplacement: Bool = false, blurred: Bool = false, action: @escaping (UndoOverlayAction) -> Bool) { + public init(presentationData: PresentationData, content: UndoOverlayContent, elevatedLayout: Bool, position: Position = .bottom, animateInAsReplacement: Bool = false, blurred: Bool = false, action: @escaping (UndoOverlayAction) -> Bool, additionalView: (() -> UndoOverlayControllerAdditionalView?)? = nil) { self.presentationData = presentationData self.content = content self.elevatedLayout = elevatedLayout @@ -86,6 +104,7 @@ public final class UndoOverlayController: ViewController { self.animateInAsReplacement = animateInAsReplacement self.blurred = blurred self.action = action + self.additionalView = additionalView super.init(navigationBarPresentationData: nil) @@ -97,7 +116,7 @@ public final class UndoOverlayController: ViewController { } override public func loadDisplayNode() { - self.displayNode = UndoOverlayControllerNode(presentationData: self.presentationData, content: self.content, elevatedLayout: self.elevatedLayout, placementPosition: self.position, blurred: self.blurred, action: { [weak self] value in + self.displayNode = UndoOverlayControllerNode(presentationData: self.presentationData, content: self.content, elevatedLayout: self.elevatedLayout, placementPosition: self.position, blurred: self.blurred, additionalView: self.additionalView, action: { [weak self] value in return self?.action(value) ?? false }, dismiss: { [weak self] in self?.dismiss() diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 509cf77fe8..7e62d0b886 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -19,6 +19,7 @@ import AccountContext import AnimatedAvatarSetNode import ComponentFlow import EmojiStatusComponent +import TextNodeWithEntities final class UndoOverlayControllerNode: ViewControllerTracingNode { private let presentationData: PresentationData @@ -40,7 +41,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { private var stickerOffset: CGPoint? private var emojiStatus: ComponentView? private let titleNode: ImmediateTextNode - private let textNode: ImmediateTextNode + private let textNode: ImmediateTextNodeWithEntities private let buttonNode: HighlightTrackingButtonNode private let undoButtonTextNode: ImmediateTextNode private let undoButtonNode: HighlightTrackingButtonNode @@ -52,10 +53,13 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { private var content: UndoOverlayContent private let blurred: Bool + private let additionalView: UndoOverlayControllerAdditionalView? + private let effectView: UIView private let animationBackgroundColor: UIColor + private var isTimeoutDisabled: Bool = false private var originalRemainingSeconds: Double private var remainingSeconds: Double private var timer: SwiftSignalKit.Timer? @@ -64,13 +68,15 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { private var fetchResourceDisposable: Disposable? - init(presentationData: PresentationData, content: UndoOverlayContent, elevatedLayout: Bool, placementPosition: UndoOverlayController.Position, blurred: Bool, action: @escaping (UndoOverlayAction) -> Bool, dismiss: @escaping () -> Void) { + init(presentationData: PresentationData, content: UndoOverlayContent, elevatedLayout: Bool, placementPosition: UndoOverlayController.Position, blurred: Bool, additionalView: (() -> UndoOverlayControllerAdditionalView?)?, action: @escaping (UndoOverlayAction) -> Bool, dismiss: @escaping () -> Void) { self.presentationData = presentationData self.elevatedLayout = elevatedLayout self.placementPosition = placementPosition self.blurred = blurred self.content = content + self.additionalView = additionalView?() + self.action = action self.dismiss = dismiss @@ -81,7 +87,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.titleNode.displaysAsynchronously = false self.titleNode.maximumNumberOfLines = 0 - self.textNode = ImmediateTextNode() + self.textNode = ImmediateTextNodeWithEntities() self.textNode.displaysAsynchronously = false self.textNode.maximumNumberOfLines = 0 @@ -1153,6 +1159,39 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } else { displayUndo = false } + case let .messageTagged(context, customEmoji, isBuiltinReaction): + self.avatarNode = nil + self.iconNode = nil + self.iconCheckNode = nil + self.animationNode = AnimationNode(animation: "anim_savedmessages", colors: [:], scale: 0.066) + self.animatedStickerNode = nil + + let rawText = presentationData.strings.Chat_ToastMessageTagged_Text(".") + let attributedText = NSMutableAttributedString(string: rawText.string, font: Font.regular(14.0), textColor: .white) + for range in rawText.ranges { + attributedText.addAttributes([ChatTextInputAttributes.customEmoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: customEmoji.fileId.id, file: customEmoji, custom: nil)], range: range.range) + } + self.textNode.customItemLayout = { size, _ in + if isBuiltinReaction { + return CGSize(width: size.width * 2.0, height: size.height * 2.0) + } + return size + } + + self.textNode.arguments = TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: UIColor(white: 1.0, alpha: 0.1), + attemptSynchronous: false + ) + self.textNode.visibility = true + + self.textNode.attributedText = attributedText + self.textNode.maximumNumberOfLines = 2 + + displayUndo = false + self.originalRemainingSeconds = 3 } self.remainingSeconds = self.originalRemainingSeconds @@ -1181,7 +1220,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { switch content { case .removedChat: self.panelWrapperNode.addSubnode(self.timerTextNode) - case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .copy, .mediaSaved, .paymentSent, .image, .inviteRequestSent, .notificationSoundAdded, .universal, .premiumPaywall, .peers: + case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .copy, .mediaSaved, .paymentSent, .image, .inviteRequestSent, .notificationSoundAdded, .universal, .premiumPaywall, .peers, .messageTagged: if self.textNode.tapAttributeAction != nil || displayUndo { self.isUserInteractionEnabled = true } else { @@ -1251,6 +1290,24 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.animatedStickerNode?.started = { [weak self] in self?.stillStickerNode?.isHidden = true } + + if let additionalView = self.additionalView { + additionalView.interaction = UndoOverlayControllerAdditionalViewInteraction(disableTimeout: { [weak self] in + guard let self else { + return + } + self.isTimeoutDisabled = true + self.timer?.invalidate() + self.remainingSeconds = self.originalRemainingSeconds + self.checkTimer() + }, dismiss: { [weak self] in + guard let self else { + return + } + self.dismiss() + }) + self.view.addSubview(additionalView) + } } deinit { @@ -1265,6 +1322,16 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } self.panelNode.view.addSubview(self.effectView) + + if self.additionalView != nil { + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.dismiss() + } } @objc private func buttonPressed() { @@ -1318,11 +1385,13 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.containerLayoutUpdated(layout: validLayout, transition: .immediate) } } - let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: false, completion: { [weak self] in - self?.checkTimer() - }, queue: .mainQueue()) - self.timer = timer - timer.start() + if !self.isTimeoutDisabled { + let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: false, completion: { [weak self] in + self?.checkTimer() + }, queue: .mainQueue()) + self.timer = timer + timer.start() + } } } @@ -1566,6 +1635,12 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { let avatarsFrame = CGRect(origin: CGPoint(x: 13.0, y: floor((contentHeight - multiAvatarsSize.height) / 2.0) + verticalOffset), size: multiAvatarsSize) transition.updateFrame(node: multiAvatarsNode, frame: avatarsFrame) } + + if let additionalView = self.additionalView { + let additionalViewFrame = CGRect(origin: CGPoint(x: 0.0, y: panelWrapperFrame.maxY), size: CGSize(width: layout.size.width, height: layout.size.height - insets.bottom - panelWrapperFrame.maxY)) + transition.updateFrame(view: additionalView, frame: additionalViewFrame) + additionalView.update(size: additionalViewFrame.size, transition: transition) + } } func animateIn(asReplacement: Bool) { @@ -1602,6 +1677,10 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.animatedStickerNode?.visibility = true + if let additionalView = self.additionalView { + additionalView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + self.checkTimer() } @@ -1617,6 +1696,10 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } self.panelNode.layer.animateScale(from: 1.0, to: 0.96, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) self.panelWrapperNode.layer.animateScale(from: 1.0, to: 0.96, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + + if let additionalView = self.additionalView { + additionalView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + } } func animateOutWithReplacement(completion: @escaping () -> Void) { @@ -1631,9 +1714,24 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if !self.panelNode.frame.insetBy(dx: -60.0, dy: 0.0).contains(point) { - return nil + if let additionalView = self.additionalView { + if let result = additionalView.hitTest(self.view.convert(point, to: additionalView), with: event) { + return result + } } - return super.hitTest(point, with: event) + + if !self.panelNode.frame.insetBy(dx: -60.0, dy: 0.0).contains(point) { + if self.additionalView != nil && self.isTimeoutDisabled { + } else { + return nil + } + } + let result = super.hitTest(point, with: event) + if result == self { + if self.additionalView != nil && self.isTimeoutDisabled { + return self.view + } + } + return result } }