diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index be3d8a4d89..d3e6796483 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -6718,6 +6718,7 @@ Sorry for the inconvenience."; "Conversation.Theme.Apply" = "Apply Theme"; "Conversation.Theme.NoTheme" = "No\nTheme"; "Conversation.Theme.Reset" = "Reset Theme for This Chat"; +"Conversation.Theme.DontSetTheme" = "Do Not Set Theme"; "Conversation.Theme.SwitchToDark" = "Switch to dark appearance"; "Conversation.Theme.SwitchToLight" = "Switch to light appearance"; diff --git a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift index 6bef3c79bd..e0362933b5 100644 --- a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift +++ b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift @@ -1046,7 +1046,9 @@ public final class AnimatedStickerNode: ASDisplayNode { if frame.isLastFrame { var stopped = false var stopNow = false - if case .once = strongSelf.playbackMode { + if case .still = strongSelf.playbackMode { + stopNow = true + } else if case .once = strongSelf.playbackMode { stopNow = true } else if case let .count(count) = strongSelf.playbackMode { strongSelf.currentLoopCount += 1 @@ -1143,7 +1145,9 @@ public final class AnimatedStickerNode: ASDisplayNode { if frame.isLastFrame { var stopped = false var stopNow = false - if case .once = strongSelf.playbackMode { + if case .still = strongSelf.playbackMode { + stopNow = true + } else if case .once = strongSelf.playbackMode { stopNow = true } else if case let .count(count) = strongSelf.playbackMode { strongSelf.currentLoopCount += 1 diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 3cce18b827..1fb071c560 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -1724,6 +1724,15 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.textNode.frame = textNodeFrame var animateInputActivitiesFrame = false + let inputActivities = inputActivities?.filter({ + switch $0.1 { + case .speakingInGroupCall, .interactingWithEmoji, .seeingEmojiInteraction: + return false + default: + return true + } + }) + if let inputActivities = inputActivities, !inputActivities.isEmpty { if strongSelf.inputActivitiesNode.supernode == nil { strongSelf.contextContainer.addSubnode(strongSelf.inputActivitiesNode) diff --git a/submodules/ChatListUI/Sources/Node/ChatListTypingNode.swift b/submodules/ChatListUI/Sources/Node/ChatListTypingNode.swift index d4887c62ab..e2669b386b 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListTypingNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListTypingNode.swift @@ -61,7 +61,7 @@ final class ChatListInputActivitiesNode: ASDisplayNode { text = strings.DialogList_Typing case .choosingSticker: text = strings.Activity_ChoosingSticker - case .speakingInGroupCall: + case .speakingInGroupCall, .seeingEmojiInteraction, .interactingWithEmoji: text = "" } let string = NSAttributedString(string: text, font: textFont, textColor: color) @@ -81,6 +81,8 @@ final class ChatListInputActivitiesNode: ASDisplayNode { state = .typingText(string, lightColor) case .choosingSticker: state = .choosingSticker(string, lightColor) + case .seeingEmojiInteraction, .interactingWithEmoji: + state = .none } } else { let text: String @@ -105,7 +107,7 @@ final class ChatListInputActivitiesNode: ASDisplayNode { text = strings.DialogList_SingleTypingSuffix(peerTitle).string case .choosingSticker: text = strings.DialogList_SingleChoosingStickerSuffix(peerTitle).string - case .speakingInGroupCall: + case .speakingInGroupCall, .seeingEmojiInteraction, .interactingWithEmoji: text = "" } } else { @@ -128,6 +130,8 @@ final class ChatListInputActivitiesNode: ASDisplayNode { state = .typingText(string, lightColor) case .choosingSticker: state = .choosingSticker(string, lightColor) + case .seeingEmojiInteraction, .interactingWithEmoji: + state = .none } } } else { diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 1dcd653361..200fd4ef90 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -182,6 +182,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-651419003] = { return Api.SendMessageAction.parse_speakingInGroupCallAction($0) } dict[-606432698] = { return Api.SendMessageAction.parse_sendMessageHistoryImportAction($0) } dict[-1336228175] = { return Api.SendMessageAction.parse_sendMessageChooseStickerAction($0) } + dict[1781674934] = { return Api.SendMessageAction.parse_sendMessageEmojiInteraction($0) } + dict[-1234857938] = { return Api.SendMessageAction.parse_sendMessageEmojiInteractionSeen($0) } dict[-1137792208] = { return Api.PrivacyKey.parse_privacyKeyStatusTimestamp($0) } dict[1343122938] = { return Api.PrivacyKey.parse_privacyKeyChatInvite($0) } dict[1030105979] = { return Api.PrivacyKey.parse_privacyKeyPhoneCall($0) } diff --git a/submodules/TelegramApi/Sources/Api2.swift b/submodules/TelegramApi/Sources/Api2.swift index 1a8c28588a..501560e675 100644 --- a/submodules/TelegramApi/Sources/Api2.swift +++ b/submodules/TelegramApi/Sources/Api2.swift @@ -4278,6 +4278,8 @@ public extension Api { case speakingInGroupCallAction case sendMessageHistoryImportAction(progress: Int32) case sendMessageChooseStickerAction + case sendMessageEmojiInteraction(emoticon: String, interaction: Api.DataJSON) + case sendMessageEmojiInteractionSeen(emoticon: String) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -4376,6 +4378,19 @@ public extension Api { buffer.appendInt32(-1336228175) } + break + case .sendMessageEmojiInteraction(let emoticon, let interaction): + if boxed { + buffer.appendInt32(1781674934) + } + serializeString(emoticon, buffer: buffer, boxed: false) + interaction.serialize(buffer, true) + break + case .sendMessageEmojiInteractionSeen(let emoticon): + if boxed { + buffer.appendInt32(-1234857938) + } + serializeString(emoticon, buffer: buffer, boxed: false) break } } @@ -4414,6 +4429,10 @@ public extension Api { return ("sendMessageHistoryImportAction", [("progress", progress)]) case .sendMessageChooseStickerAction: return ("sendMessageChooseStickerAction", []) + case .sendMessageEmojiInteraction(let emoticon, let interaction): + return ("sendMessageEmojiInteraction", [("emoticon", emoticon), ("interaction", interaction)]) + case .sendMessageEmojiInteractionSeen(let emoticon): + return ("sendMessageEmojiInteractionSeen", [("emoticon", emoticon)]) } } @@ -4513,6 +4532,33 @@ public extension Api { public static func parse_sendMessageChooseStickerAction(_ reader: BufferReader) -> SendMessageAction? { return Api.SendMessageAction.sendMessageChooseStickerAction } + public static func parse_sendMessageEmojiInteraction(_ reader: BufferReader) -> SendMessageAction? { + var _1: String? + _1 = parseString(reader) + var _2: Api.DataJSON? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.DataJSON + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.SendMessageAction.sendMessageEmojiInteraction(emoticon: _1!, interaction: _2!) + } + else { + return nil + } + } + public static func parse_sendMessageEmojiInteractionSeen(_ reader: BufferReader) -> SendMessageAction? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.SendMessageAction.sendMessageEmojiInteractionSeen(emoticon: _1!) + } + else { + return nil + } + } } public enum PrivacyKey: TypeConstructorDescription { diff --git a/submodules/TelegramCore/Sources/State/ManagedLocalInputActivities.swift b/submodules/TelegramCore/Sources/State/ManagedLocalInputActivities.swift index f06531a28b..b8fac6d1bf 100644 --- a/submodules/TelegramCore/Sources/State/ManagedLocalInputActivities.swift +++ b/submodules/TelegramCore/Sources/State/ManagedLocalInputActivities.swift @@ -131,6 +131,10 @@ private func actionFromActivity(_ activity: PeerInputActivity?) -> Api.SendMessa return .speakingInGroupCallAction case .choosingSticker: return .sendMessageChooseStickerAction + case let .interactingWithEmoji(emoticon, interaction): + return .sendMessageEmojiInteraction(emoticon: emoticon, interaction: interaction?.apiDataJson ?? .dataJSON(data: "")) + case let .seeingEmojiInteraction(emoticon): + return .sendMessageEmojiInteractionSeen(emoticon: emoticon) } } else { return .sendMessageCancelAction diff --git a/submodules/TelegramCore/Sources/State/PeerInputActivity.swift b/submodules/TelegramCore/Sources/State/PeerInputActivity.swift index eff6f99446..56db93edfe 100644 --- a/submodules/TelegramCore/Sources/State/PeerInputActivity.swift +++ b/submodules/TelegramCore/Sources/State/PeerInputActivity.swift @@ -1,6 +1,42 @@ import Foundation import TelegramApi +public struct EmojiInteraction: Equatable { + public let animation: Int + + public init(animation: Int) { + self.animation = animation + } + + public init?(apiDataJson: Api.DataJSON) { + if case let .dataJSON(string) = apiDataJson, let data = string.data(using: .utf8) { + do { + let decodedData = try JSONSerialization.jsonObject(with: data, options: []) + guard let item = decodedData as? [String: Any] else { + return nil + } + guard let animation = item["animation"] as? Int else { + return nil + } + self.animation = animation + } catch { + return nil + } + } else { + return nil + } + } + + public var apiDataJson: Api.DataJSON { + let dict = ["animation": animation] + if let data = try? JSONSerialization.data(withJSONObject: dict, options: []), let dataString = String(data: data, encoding: .utf8) { + return .dataJSON(data: dataString) + } else { + return .dataJSON(data: "") + } + } +} + public enum PeerInputActivity: Comparable { case typingText case uploadingFile(progress: Int32) @@ -12,6 +48,8 @@ public enum PeerInputActivity: Comparable { case uploadingInstantVideo(progress: Int32) case speakingInGroupCall(timestamp: Int32) case choosingSticker + case interactingWithEmoji(emoticon: String, interaction: EmojiInteraction?) + case seeingEmojiInteraction(emoticon: String) public var key: Int32 { switch self { @@ -35,6 +73,10 @@ public enum PeerInputActivity: Comparable { return 8 case .choosingSticker: return 9 + case .interactingWithEmoji: + return 10 + case .seeingEmojiInteraction: + return 11 } } @@ -70,6 +112,10 @@ extension PeerInputActivity { self = .choosingSticker case .sendMessageHistoryImportAction: return nil + case let .sendMessageEmojiInteraction(emoticon, interaction): + self = .interactingWithEmoji(emoticon: emoticon, interaction: EmojiInteraction(apiDataJson: interaction)) + case let .sendMessageEmojiInteractionSeen(emoticon): + self = .seeingEmojiInteraction(emoticon: emoticon) } } } diff --git a/submodules/TelegramCore/Sources/State/PeerInputActivityManager.swift b/submodules/TelegramCore/Sources/State/PeerInputActivityManager.swift index f7fa72537c..892b6f21c6 100644 --- a/submodules/TelegramCore/Sources/State/PeerInputActivityManager.swift +++ b/submodules/TelegramCore/Sources/State/PeerInputActivityManager.swift @@ -328,7 +328,9 @@ final class PeerInputActivityManager { let timeout: Double switch activity { - case .speakingInGroupCall: + case .interactingWithEmoji: + timeout = 2.0 + case .speakingInGroupCall, .seeingEmojiInteraction: timeout = 3.0 default: timeout = 8.0 diff --git a/submodules/TelegramCore/Sources/Utils/JSON.swift b/submodules/TelegramCore/Sources/Utils/JSON.swift index 49dbf2801d..eea17f436c 100644 --- a/submodules/TelegramCore/Sources/Utils/JSON.swift +++ b/submodules/TelegramCore/Sources/Utils/JSON.swift @@ -2,7 +2,6 @@ import Foundation import Postbox import TelegramApi - extension JSON { private init?(_ object: Any) { if let object = object as? JSONValue { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 1d58e6c258..affaf58e31 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -2863,6 +2863,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return false } return strongSelf.chatDisplayNode.messageTransitionNode.isAnimatingMessage(stableId: stableId) + }, getMessageTransitionNode: { [weak self] in + guard let strongSelf = self else { + return nil + } + return strongSelf.chatDisplayNode.messageTransitionNode }, requestMessageUpdate: { [weak self] id in if let strongSelf = self { strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id) @@ -7353,11 +7358,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let postbox = self.context.account.postbox let previousPeerCache = Atomic<[PeerId: Peer]>(value: [:]) - var activityCategory: PeerActivitySpace.Category = .global - if case let .replyThread(replyThreadMessage) = self.chatLocation { - activityCategory = .thread(makeMessageThreadId(replyThreadMessage.messageId)) + + let activitySpace: PeerActivitySpace + switch self.chatLocation { + case let .peer(peerId): + activitySpace = PeerActivitySpace(peerId: peerId, category: .global) + case let .replyThread(replyThreadMessage): + activitySpace = PeerActivitySpace(peerId: replyThreadMessage.messageId.peerId, category: .thread(makeMessageThreadId(replyThreadMessage.messageId))) } - self.peerInputActivitiesDisposable = (self.context.account.peerInputActivities(peerId: PeerActivitySpace(peerId: peerId, category: activityCategory)) + + self.peerInputActivitiesDisposable = (self.context.account.peerInputActivities(peerId: activitySpace) |> mapToSignal { activities -> Signal<[(Peer, PeerInputActivity)], NoError> in var foundAllPeers = true var cachedResult: [(Peer, PeerInputActivity)] = [] @@ -7390,7 +7400,33 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } |> deliverOnMainQueue).start(next: { [weak self] activities in if let strongSelf = self { - strongSelf.chatTitleView?.inputActivities = (peerId, activities) + let displayActivities = activities.filter({ + switch $0.1 { + case .speakingInGroupCall, .interactingWithEmoji: + return false + default: + return true + } + }) + strongSelf.chatTitleView?.inputActivities = (peerId, displayActivities) + + for activity in activities { + if case let .interactingWithEmoji(emoticon, maybeInteraction) = activity.1, let interaction = maybeInteraction { + var found = false + strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode({ itemNode in + if !found, let itemNode = itemNode as? ChatMessageAnimatedStickerItemNode, let item = itemNode.item { + if item.message.text.strippedEmoji == emoticon { + itemNode.playAdditionalAnimation(index: interaction.animation, incoming: true) + found = true + } + } + }) + + if found { + let _ = strongSelf.context.account.updateLocalInputActivity(peerId: activitySpace, activity: .seeingEmojiInteraction(emoticon: emoticon), isPresent: true) + } + } + } } }) } diff --git a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift index c372ea2366..bbb88a1eaf 100644 --- a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift @@ -123,6 +123,7 @@ public final class ChatControllerInteraction { let copyText: (String) -> Void let displayUndo: (UndoOverlayContent) -> Void let isAnimatingMessage: (UInt32) -> Bool + var getMessageTransitionNode: () -> ChatMessageTransitionNode? let requestMessageUpdate: (MessageId) -> Void let cancelInteractiveKeyboardGestures: () -> Void @@ -217,6 +218,7 @@ public final class ChatControllerInteraction { copyText: @escaping (String) -> Void, displayUndo: @escaping (UndoOverlayContent) -> Void, isAnimatingMessage: @escaping (UInt32) -> Bool, + getMessageTransitionNode: @escaping () -> ChatMessageTransitionNode?, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, @@ -297,6 +299,7 @@ public final class ChatControllerInteraction { self.copyText = copyText self.displayUndo = displayUndo self.isAnimatingMessage = isAnimatingMessage + self.getMessageTransitionNode = getMessageTransitionNode self.requestMessageUpdate = requestMessageUpdate self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures @@ -353,6 +356,8 @@ public final class ChatControllerInteraction { }, displayUndo: { _ in }, isAnimatingMessage: { _ in return false + }, getMessageTransitionNode: { + return nil }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index 48abd5ec17..827a6d276b 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -1290,36 +1290,76 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - private func playAdditionalAnimation(_ name: String) { - let source = AnimatedStickerNodeLocalFileSource(name: name) - guard let item = self.item, let path = source.path, let animationSize = self.animationSize, let animationNode = self.animationNode, self.additionalAnimationNodes.count < 4 else { + func playAdditionalAnimation(index: Int, incoming: Bool) { + guard let item = self.item else { return } - let incoming = item.message.effectivelyIncoming(item.context.account.peerId) + + let textEmoji = item.message.text.strippedEmoji + let animationName: String? + switch textEmoji { + case "❤": + if index == 2 { + animationName = "TestHearts2" + } else { + animationName = "TestHearts" + } + case "🎆": + animationName = "TestFireworks" + default: + animationName = nil + } + + guard let animationName = animationName else { + return + } + + let source = AnimatedStickerNodeLocalFileSource(name: animationName) + guard let path = source.path, let animationSize = self.animationSize, let animationNode = self.animationNode, self.additionalAnimationNodes.count < 4 else { + return + } + + if let animationNode = animationNode as? AnimatedStickerNode { + let _ = animationNode.playIfNeeded() + } + + let incomingMessage = item.message.effectivelyIncoming(item.context.account.peerId) self.supernode?.view.bringSubviewToFront(self.view) - - let resource = BundleResource(name: name, path: path) + + let resource = BundleResource(name: animationName, path: path) let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(resource.id) let additionalAnimationNode = AnimatedStickerNode() - additionalAnimationNode.setup(source: source, width: Int(animationSize.width * 3.0), height: Int(animationSize.height * 3.0), playbackMode: .once, mode: .direct(cachePathPrefix: pathPrefix)) + additionalAnimationNode.setup(source: source, width: Int(animationSize.width * 2.0), height: Int(animationSize.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: pathPrefix)) + var animationFrame = animationNode.frame.insetBy(dx: -animationNode.frame.width, dy: -animationNode.frame.height) + .offsetBy(dx: incomingMessage ? animationNode.frame.width - 10.0 : -animationNode.frame.width + 10.0, dy: 0.0) + animationFrame = animationFrame.offsetBy(dx: CGFloat.random(in: -30.0 ... 30.0), dy: CGFloat.random(in: -30.0 ... 30.0)) + additionalAnimationNode.frame = animationFrame + if incomingMessage { + additionalAnimationNode.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + } + var decorationNode: ChatMessageTransitionNode.DecorationItemNode? + if let transitionNode = item.controllerInteraction.getMessageTransitionNode() { + decorationNode = transitionNode.add(decorationNode: additionalAnimationNode, itemNode: self) + } else { + self.addSubnode(additionalAnimationNode) + } additionalAnimationNode.completed = { [weak self, weak additionalAnimationNode] _ in self?.additionalAnimationNodes.removeAll(where: { $0 === additionalAnimationNode }) additionalAnimationNode?.removeFromSupernode() + if let decorationNode = decorationNode, let transitionNode = item.controllerInteraction.getMessageTransitionNode() { + transitionNode.remove(decorationNode: decorationNode) + } } - var animationFrame = animationNode.frame.insetBy(dx: -animationNode.frame.width, dy: -animationNode.frame.height) - .offsetBy(dx: incoming ? animationNode.frame.width - 10.0 : -animationNode.frame.width + 10.0, dy: 0.0) - animationFrame = animationFrame.offsetBy(dx: CGFloat.random(in: -30.0 ... 30.0), dy: CGFloat.random(in: -30.0 ... 30.0)) - additionalAnimationNode.frame = animationFrame - if incoming { - additionalAnimationNode.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) - } - self.addSubnode(additionalAnimationNode) self.additionalAnimationNodes.append(additionalAnimationNode) additionalAnimationNode.play() + + if !incoming { + item.context.account.updateLocalInputActivity(peerId: PeerActivitySpace(peerId: item.message.id.peerId, category: .global), activity: .interactingWithEmoji(emoticon: textEmoji, interaction: EmojiInteraction(animation: index)), isPresent: true) + } } private func gestureRecognized(gesture: TapLongTapOrDoubleTapGesture, location: CGPoint, recognizer: TapLongTapOrDoubleTapGestureRecognizer?) -> InternalBubbleTapAction? { @@ -1437,12 +1477,12 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { return .optionalAction({ if firstScalar.value == heart { if self.additionalAnimationNodes.count % 2 == 0 { - self.playAdditionalAnimation("TestHearts") + self.playAdditionalAnimation(index: 1, incoming: false) } else { - self.playAdditionalAnimation("TestHearts2") + self.playAdditionalAnimation(index: 2, incoming: false) } } else if firstScalar.value == fireworks { - self.playAdditionalAnimation("TestFireworks") + self.playAdditionalAnimation(index: 1, incoming: false) } if shouldPlay { diff --git a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift index ab733459c0..d0bbec86af 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift @@ -172,6 +172,54 @@ public final class ChatMessageTransitionNode: ASDisplayNode { case videoMessage(VideoMessage) case mediaInput(MediaInput) } + + final class DecorationItemNode: ASDisplayNode { + let itemNode: ChatMessageItemView + private let contentNode: ASDisplayNode + private let getContentAreaInScreenSpace: () -> CGRect + + private let scrollingContainer: ASDisplayNode + private let containerNode: ASDisplayNode + private let clippingNode: ASDisplayNode + + init(itemNode: ChatMessageItemView, contentNode: ASDisplayNode, getContentAreaInScreenSpace: @escaping () -> CGRect) { + self.itemNode = itemNode + self.contentNode = contentNode + self.getContentAreaInScreenSpace = getContentAreaInScreenSpace + + self.clippingNode = ASDisplayNode() + self.clippingNode.clipsToBounds = true + + self.scrollingContainer = ASDisplayNode() + self.containerNode = ASDisplayNode() + + super.init() + + self.addSubnode(self.clippingNode) + self.clippingNode.addSubnode(self.scrollingContainer) + self.scrollingContainer.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.contentNode) + } + + func updateLayout(size: CGSize) { + self.clippingNode.frame = CGRect(origin: CGPoint(), size: size) + + let absoluteRect = self.itemNode.view.convert(self.itemNode.view.bounds, to: self.view) + self.containerNode.frame = absoluteRect + } + + func addExternalOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { + if transition.isAnimated { + assert(true) + } + self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: -offset) + transition.animateOffsetAdditive(node: self.scrollingContainer, offset: offset) + } + + func addContentOffset(offset: CGFloat) { + self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: offset) + } + } private final class AnimatingItemNode: ASDisplayNode { let itemNode: ChatMessageItemView @@ -553,6 +601,7 @@ public final class ChatMessageTransitionNode: ASDisplayNode { private var currentPendingItem: (Int64, Source, () -> Void)? private var animatingItemNodes: [AnimatingItemNode] = [] + private var decorationItemNodes: [DecorationItemNode] = [] var hasScheduledTransitions: Bool { return self.currentPendingItem != nil @@ -585,6 +634,20 @@ public final class ChatMessageTransitionNode: ASDisplayNode { self.currentPendingItem = (correlationId, source, initiated) self.listNode.setCurrentSendAnimationCorrelationId(correlationId) } + + func add(decorationNode: ASDisplayNode, itemNode: ChatMessageItemView) -> DecorationItemNode { + let decorationItemNode = DecorationItemNode(itemNode: itemNode, contentNode: decorationNode, getContentAreaInScreenSpace: self.getContentAreaInScreenSpace) + decorationItemNode.updateLayout(size: self.bounds.size) + self.decorationItemNodes.append(decorationItemNode) + self.addSubnode(decorationItemNode) + + return decorationItemNode + } + + func remove(decorationNode: DecorationItemNode) { + self.decorationItemNodes.removeAll(where: { $0 === decorationNode }) + decorationNode.removeFromSupernode() + } private func beginAnimation(itemNode: ChatMessageItemView, source: Source) { var contextSourceNode: ContextExtractedContentContainingNode? @@ -646,12 +709,22 @@ public final class ChatMessageTransitionNode: ASDisplayNode { for animatingItemNode in self.animatingItemNodes { animatingItemNode.addExternalOffset(offset: offset, transition: transition, itemNode: itemNode) } + if itemNode == nil { + for decorationItemNode in self.decorationItemNodes { + decorationItemNode.addExternalOffset(offset: offset, transition: transition) + } + } } func addContentOffset(offset: CGFloat, itemNode: ListViewItemNode?) { for animatingItemNode in self.animatingItemNodes { animatingItemNode.addContentOffset(offset: offset, itemNode: itemNode) } + if itemNode == nil { + for decorationItemNode in self.decorationItemNodes { + decorationItemNode.addContentOffset(offset: offset) + } + } } func isAnimatingMessage(stableId: UInt32) -> Bool { diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index 944dc4546c..aad12e7d66 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -527,6 +527,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, displayUndo: { _ in }, isAnimatingMessage: { _ in return false + }, getMessageTransitionNode: { + return nil }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, diff --git a/submodules/TelegramUI/Sources/ChatThemeScreen.swift b/submodules/TelegramUI/Sources/ChatThemeScreen.swift index e25e983680..81255beb9a 100644 --- a/submodules/TelegramUI/Sources/ChatThemeScreen.swift +++ b/submodules/TelegramUI/Sources/ChatThemeScreen.swift @@ -338,6 +338,27 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { self.placeholderNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: rect.minX + emojiFrame.minX, y: rect.minY + emojiFrame.minY), size: emojiFrame.size), within: containerSize) } + override func selected() { + super.selected() + + if let animatedStickerNode = self.animatedStickerNode { + Queue.mainQueue().after(0.1) { + let started = animatedStickerNode.playIfNeeded() + if started { + let scale: CGFloat = 2.6 + animatedStickerNode.transform = CATransform3DMakeScale(scale, scale, 1.0) + animatedStickerNode.layer.animateSpring(from: 1.0 as NSNumber, to: scale as NSNumber, keyPath: "transform.scale", duration: 0.45) + + animatedStickerNode.completed = { [weak animatedStickerNode] _ in + animatedStickerNode?.transform = CATransform3DIdentity + animatedStickerNode?.layer.animateSpring(from: scale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45) + } + } + } + } + + } + func asyncLayout() -> (ThemeSettingsThemeIconItem, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) { let makeTextLayout = TextNode.asyncLayout(self.textNode) let makeEmojiLayout = TextNode.asyncLayout(self.emojiNode) @@ -437,8 +458,11 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { strongSelf.animatedStickerNode = animatedStickerNode strongSelf.emojiContainerNode.insertSubnode(animatedStickerNode, belowSubnode: strongSelf.placeholderNode) let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) - animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource), width: 96, height: 96, mode: .direct(cachePathPrefix: pathPrefix)) + animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource), width: 128, height: 128, playbackMode: .still(.start), mode: .direct(cachePathPrefix: pathPrefix)) + + animatedStickerNode.anchorPoint = CGPoint(x: 0.5, y: 1.0) } + animatedStickerNode.autoplay = true animatedStickerNode.visibility = strongSelf.visibilityStatus strongSelf.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).start()) @@ -734,7 +758,7 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega self.animationNode.isUserInteractionEnabled = false self.doneButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), height: 52.0, cornerRadius: 11.0, gloss: false) - self.doneButton.title = self.presentationData.strings.Conversation_Theme_Apply + self.doneButton.title = initiallySelectedEmoticon == nil ? self.presentationData.strings.Conversation_Theme_DontSetTheme : self.presentationData.strings.Conversation_Theme_Apply self.listNode = ListView() self.listNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) @@ -783,9 +807,7 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega let presentationData = strongSelf.presentationData var entries: [ThemeSettingsThemeEntry] = [] - if strongSelf.initiallySelectedEmoticon != nil { - entries.append(ThemeSettingsThemeEntry(index: 0, emoticon: nil, emojiFile: nil, themeReference: nil, selected: selectedEmoticon == nil, theme: presentationData.theme, strings: presentationData.strings, wallpaper: nil)) - } + entries.append(ThemeSettingsThemeEntry(index: 0, emoticon: nil, emojiFile: nil, themeReference: nil, selected: selectedEmoticon == nil, theme: presentationData.theme, strings: presentationData.strings, wallpaper: nil)) for theme in themes { var emoticon = theme.emoji if emoticon == "🦁" { @@ -804,7 +826,13 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega strongSelf.selectedEmoticon = emoticon let _ = ensureThemeVisible(listNode: strongSelf.listNode, emoticon: emoticon, animated: true) - strongSelf.doneButton.title = emoticon == nil ? strongSelf.presentationData.strings.Conversation_Theme_Reset : strongSelf.presentationData.strings.Conversation_Theme_Apply + let doneButtonTitle: String + if emoticon == nil { + doneButtonTitle = strongSelf.initiallySelectedEmoticon == nil ? strongSelf.presentationData.strings.Conversation_Theme_DontSetTheme : strongSelf.presentationData.strings.Conversation_Theme_Reset + } else { + doneButtonTitle = strongSelf.presentationData.strings.Conversation_Theme_Apply + } + strongSelf.doneButton.title = doneButtonTitle } } let previousEntries = strongSelf.entries ?? [] diff --git a/submodules/TelegramUI/Sources/ChatTitleView.swift b/submodules/TelegramUI/Sources/ChatTitleView.swift index 312e858394..f844e2f04a 100644 --- a/submodules/TelegramUI/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Sources/ChatTitleView.swift @@ -343,7 +343,9 @@ final class ChatTitleView: UIView, NavigationBarTitleView { stringValue = strings.Activity_UploadingVideoMessage case .choosingSticker: stringValue = strings.Activity_ChoosingSticker - case .speakingInGroupCall: + case let .seeingEmojiInteraction(emoticon): + stringValue = "enjoying \(emoticon) animations" + case .speakingInGroupCall, .interactingWithEmoji: stringValue = "" } } else { @@ -372,10 +374,12 @@ final class ChatTitleView: UIView, NavigationBarTitleView { state = .uploading(string, color) case .playingGame: state = .playingGame(string, color) - case .speakingInGroupCall: + case .speakingInGroupCall, .interactingWithEmoji: state = .typingText(string, color) case .choosingSticker: state = .choosingSticker(string, color) + case .seeingEmojiInteraction: + state = .choosingSticker(string, color) } } else { if let titleContent = self.titleContent { diff --git a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift index 40c422bc22..41982f4aee 100644 --- a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift +++ b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift @@ -153,6 +153,8 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { }, displayUndo: { _ in }, isAnimatingMessage: { _ in return false + }, getMessageTransitionNode: { + return nil }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 56d805c414..06bf5324e4 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -145,6 +145,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, displayUndo: { _ in }, isAnimatingMessage: { _ in return false + }, getMessageTransitionNode: { + return nil }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(backgroundNode: nil)) diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index b32b8e0228..613f958f1d 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -2180,6 +2180,8 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }, displayUndo: { _ in }, isAnimatingMessage: { _ in return false + }, getMessageTransitionNode: { + return nil }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index a58ea0267c..e5c39382fd 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1292,6 +1292,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, displayUndo: { _ in }, isAnimatingMessage: { _ in return false + }, getMessageTransitionNode: { + return nil }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,