diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 9a3190a2e2..38a08c5474 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -26,8 +26,10 @@ public final class ChatMessageItemAssociatedData: Equatable { public let forcedResourceStatus: FileMediaResourceStatus? public let currentlyPlayingMessageId: EngineMessage.Index? public let isCopyProtectionEnabled: Bool + public let availableReactions: AvailableReactions? + public let defaultReaction: String? - public init(automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, isRecentActions: Bool = false, subject: ChatControllerSubject? = nil, contactsPeerIds: Set = Set(), channelDiscussionGroup: ChannelDiscussionGroupStatus = .unknown, animatedEmojiStickers: [String: [StickerPackItem]] = [:], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]] = [:], forcedResourceStatus: FileMediaResourceStatus? = nil, currentlyPlayingMessageId: EngineMessage.Index? = nil, isCopyProtectionEnabled: Bool = false) { + public init(automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, isRecentActions: Bool = false, subject: ChatControllerSubject? = nil, contactsPeerIds: Set = Set(), channelDiscussionGroup: ChannelDiscussionGroupStatus = .unknown, animatedEmojiStickers: [String: [StickerPackItem]] = [:], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]] = [:], forcedResourceStatus: FileMediaResourceStatus? = nil, currentlyPlayingMessageId: EngineMessage.Index? = nil, isCopyProtectionEnabled: Bool = false, availableReactions: AvailableReactions?) { self.automaticDownloadPeerType = automaticDownloadPeerType self.automaticDownloadNetworkType = automaticDownloadNetworkType self.isRecentActions = isRecentActions @@ -39,6 +41,17 @@ public final class ChatMessageItemAssociatedData: Equatable { self.forcedResourceStatus = forcedResourceStatus self.currentlyPlayingMessageId = currentlyPlayingMessageId self.isCopyProtectionEnabled = isCopyProtectionEnabled + self.availableReactions = availableReactions + + var defaultReaction: String? + if let availableReactions = availableReactions { + for reaction in availableReactions.reactions { + if reaction.title.lowercased().contains("heart") { + defaultReaction = reaction.value + } + } + } + self.defaultReaction = defaultReaction } public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool { @@ -75,6 +88,9 @@ public final class ChatMessageItemAssociatedData: Equatable { if lhs.isCopyProtectionEnabled != rhs.isCopyProtectionEnabled { return false } + if lhs.availableReactions != rhs.availableReactions { + return false + } return true } } diff --git a/submodules/AnimatedStickerNode/BUILD b/submodules/AnimatedStickerNode/BUILD index f983796f85..6727d8fb62 100644 --- a/submodules/AnimatedStickerNode/BUILD +++ b/submodules/AnimatedStickerNode/BUILD @@ -1,5 +1,19 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +config_setting( + name = "debug_build", + values = { + "compilation_mode": "dbg", + }, +) + +optimization_flags = select({ + ":debug_build": [ + "-O", + ], + "//conditions:default": [], +}) + swift_library( name = "AnimatedStickerNode", module_name = "AnimatedStickerNode", @@ -8,7 +22,7 @@ swift_library( ]), copts = [ "-warnings-as-errors", - ], + ] + optimization_flags, deps = [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", diff --git a/submodules/ComponentFlow/Source/Base/Component.swift b/submodules/ComponentFlow/Source/Base/Component.swift index 3584a0d7be..72a660ced3 100644 --- a/submodules/ComponentFlow/Source/Base/Component.swift +++ b/submodules/ComponentFlow/Source/Base/Component.swift @@ -104,6 +104,10 @@ public protocol _TypeErasedComponent { func _isEqual(to other: _TypeErasedComponent) -> Bool } +public protocol ComponentTaggedView: UIView { + func matches(tag: Any) -> Bool +} + public protocol Component: _TypeErasedComponent, Equatable { associatedtype EnvironmentType = Empty associatedtype View: UIView = UIView diff --git a/submodules/ComponentFlow/Source/Host/ComponentHostView.swift b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift index df78de3be2..68d8f0b7dd 100644 --- a/submodules/ComponentFlow/Source/Host/ComponentHostView.swift +++ b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift @@ -1,6 +1,22 @@ import Foundation import UIKit +private func findTaggedViewImpl(view: UIView, tag: Any) -> UIView? { + if let view = view as? ComponentTaggedView { + if view.matches(tag: tag) { + return view + } + } + + for subview in view.subviews { + if let result = findTaggedViewImpl(view: subview, tag: tag) { + return result + } + } + + return nil +} + public final class ComponentHostView: UIView { private var currentComponent: AnyComponent? private var currentContainerSize: CGSize? @@ -78,4 +94,12 @@ public final class ComponentHostView: UIView { let result = super.hitTest(point, with: event) return result } + + public func findTaggedView(tag: Any) -> UIView? { + guard let componentView = self.componentView else { + return nil + } + + return findTaggedViewImpl(view: componentView, tag: tag) + } } diff --git a/submodules/Components/ReactionButtonListComponent/BUILD b/submodules/Components/ReactionButtonListComponent/BUILD new file mode 100644 index 0000000000..990820163e --- /dev/null +++ b/submodules/Components/ReactionButtonListComponent/BUILD @@ -0,0 +1,25 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ReactionButtonListComponent", + module_name = "ReactionButtonListComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display:Display", + "//submodules/ComponentFlow:ComponentFlow", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/AccountContext:AccountContext", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/WebPBinding:WebPBinding", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift new file mode 100644 index 0000000000..3998261793 --- /dev/null +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -0,0 +1,322 @@ +import Foundation +import Display +import ComponentFlow +import SwiftSignalKit +import Postbox +import TelegramCore +import AccountContext +import TelegramPresentationData +import UIKit +import WebPBinding + +public final class ReactionButtonComponent: Component { + public struct ViewTag: Equatable { + public var value: String + + public init(value: String) { + self.value = value + } + } + + public struct Reaction: Equatable { + public var value: String + public var iconFile: TelegramMediaFile? + + public init(value: String, iconFile: TelegramMediaFile?) { + self.value = value + self.iconFile = iconFile + } + + public static func ==(lhs: Reaction, rhs: Reaction) -> Bool { + if lhs.value != rhs.value { + return false + } + if lhs.iconFile?.fileId != rhs.iconFile?.fileId { + return false + } + return true + } + } + + public struct Colors: Equatable { + public var background: UInt32 + public var foreground: UInt32 + public var stroke: UInt32 + + public init( + background: UInt32, + foreground: UInt32, + stroke: UInt32 + ) { + self.background = background + self.foreground = foreground + self.stroke = stroke + } + } + + public let context: AccountContext + public let colors: Colors + public let reaction: Reaction + public let count: Int + public let isSelected: Bool + public let action: (String) -> Void + + public init( + context: AccountContext, + colors: Colors, + reaction: Reaction, + count: Int, + isSelected: Bool, + action: @escaping (String) -> Void + ) { + self.context = context + self.colors = colors + self.reaction = reaction + self.count = count + self.isSelected = isSelected + self.action = action + } + + public static func ==(lhs: ReactionButtonComponent, rhs: ReactionButtonComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.colors != rhs.colors { + return false + } + if lhs.reaction != rhs.reaction { + return false + } + if lhs.count != rhs.count { + return false + } + if lhs.isSelected != rhs.isSelected { + return false + } + return true + } + + public final class View: UIButton, ComponentTaggedView { + public let iconView: UIImageView + private let textView: ComponentHostView + + private var currentComponent: ReactionButtonComponent? + + private let iconImageDisposable = MetaDisposable() + + init() { + self.iconView = UIImageView() + self.iconView.isUserInteractionEnabled = false + + self.textView = ComponentHostView() + self.textView.isUserInteractionEnabled = false + + super.init(frame: CGRect()) + + self.addSubview(self.iconView) + self.addSubview(self.textView) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + deinit { + self.iconImageDisposable.dispose() + } + + @objc private func pressed() { + guard let currentComponent = self.currentComponent else { + return + } + currentComponent.action(currentComponent.reaction.value) + } + + public func matches(tag: Any) -> Bool { + guard let tag = tag as? ViewTag else { + return false + } + guard let currentComponent = self.currentComponent else { + return false + } + if currentComponent.reaction.value == tag.value { + return true + } + return false + } + + func update(component: ReactionButtonComponent, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + let sideInsets: CGFloat = 10.0 + let height: CGFloat = 30.0 + let spacing: CGFloat = 2.0 + + let defaultImageSize = CGSize(width: 20.0, height: 20.0) + + let imageSize: CGSize + if self.currentComponent?.reaction != component.reaction { + if let file = component.reaction.iconFile { + self.iconImageDisposable.set((component.context.account.postbox.mediaBox.resourceData(file.resource) + |> deliverOnMainQueue).start(next: { [weak self] data in + guard let strongSelf = self else { + return + } + + if data.complete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { + if let image = WebP.convert(fromWebP: dataValue) { + strongSelf.iconView.image = image + } + } + })) + imageSize = file.dimensions?.cgSize.aspectFitted(defaultImageSize) ?? defaultImageSize + } else { + imageSize = defaultImageSize + } + } else { + imageSize = self.iconView.bounds.size + } + + self.iconView.frame = CGRect(origin: CGPoint(x: sideInsets, y: floorToScreenPixels((height - imageSize.height) / 2.0)), size: imageSize) + + let textSize: CGSize + if self.currentComponent?.count != component.count || self.currentComponent?.colors != component.colors { + textSize = self.textView.update( + transition: .immediate, + component: AnyComponent(Text( + text: "\(component.count)", + font: Font.regular(13.0), + color: UIColor(argb: component.colors.foreground) + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + } else { + textSize = self.textView.bounds.size + } + + if self.currentComponent?.colors != component.colors { + self.backgroundColor = UIColor(argb: component.colors.background) + } + + if self.currentComponent?.colors != component.colors || self.currentComponent?.isSelected != component.isSelected { + if component.isSelected { + self.layer.borderColor = UIColor(argb: component.colors.stroke).cgColor + self.layer.borderWidth = 1.5 + } else { + self.layer.borderColor = nil + self.layer.borderWidth = 0.0 + } + } + + self.layer.cornerRadius = height / 2.0 + + self.textView.frame = CGRect(origin: CGPoint(x: sideInsets + imageSize.width + spacing, y: floorToScreenPixels((height - textSize.height) / 2.0)), size: textSize) + + self.currentComponent = component + + return CGSize(width: imageSize.width + spacing + textSize.width + sideInsets * 2.0, height: height) + } + } + + public func makeView() -> View { + return View() + } + + public func update(view: View, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) + } +} + +public final class ReactionButtonsLayoutContainer { + public struct Reaction { + public var reaction: ReactionButtonComponent.Reaction + public var count: Int + public var isSelected: Bool + + public init( + reaction: ReactionButtonComponent.Reaction, + count: Int, + isSelected: Bool + ) { + self.reaction = reaction + self.count = count + self.isSelected = isSelected + } + } + + public struct Result { + public struct Item { + public var view: ComponentHostView + public var size: CGSize + } + + public var items: [Item] + public var removedViews: [ComponentHostView] + } + + public private(set) var buttons: [String: ComponentHostView] = [:] + + public init() { + } + + public func update( + context: AccountContext, + action: @escaping (String) -> Void, + reactions: [Reaction], + colors: ReactionButtonComponent.Colors, + constrainedWidth: CGFloat, + transition: Transition + ) -> Result { + var items: [Result.Item] = [] + var removedViews: [ComponentHostView] = [] + + var validIds = Set() + for reaction in reactions { + validIds.insert(reaction.reaction.value) + + let view: ComponentHostView + var itemTransition = transition + if let current = self.buttons[reaction.reaction.value] { + itemTransition = .immediate + view = current + } else { + view = ComponentHostView() + self.buttons[reaction.reaction.value] = view + } + let itemSize = view.update( + transition: itemTransition, + component: AnyComponent(ReactionButtonComponent( + context: context, + colors: colors, + reaction: reaction.reaction, + count: reaction.count, + isSelected: reaction.isSelected, + action: action + )), + environment: {}, + containerSize: CGSize(width: constrainedWidth, height: 1000.0) + ) + items.append(Result.Item( + view: view, + size: itemSize + )) + } + + var removeIds: [String] = [] + for (id, view) in self.buttons { + if !validIds.contains(id) { + removeIds.append(id) + removedViews.append(view) + } + } + for id in removeIds { + self.buttons.removeValue(forKey: id) + } + + return Result( + items: items, + removedViews: removedViews + ) + } +} diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index e1d07222db..c87f5a6b59 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -1126,7 +1126,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } } - func animateOutToReaction(value: String, targetEmptyNode: ASDisplayNode, targetFilledNode: ASDisplayNode, hideNode: Bool, completion: @escaping () -> Void) { + func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) { guard let reactionContextNode = self.reactionContextNode else { self.animateOut(result: .default, completion: completion) return @@ -1141,7 +1141,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi self.reactionContextNodeIsAnimatingOut = true reactionContextNode.willAnimateOutToReaction(value: value) - reactionContextNode.animateOutToReaction(value: value, targetEmptyNode: targetEmptyNode, targetFilledNode: targetFilledNode, hideNode: hideNode, completion: { [weak self] in + reactionContextNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, completion: { [weak self] in guard let strongSelf = self else { return } @@ -2154,10 +2154,10 @@ public final class ContextController: ViewController, StandalonePresentableContr self.dismiss(result: .default, completion: completion) } - public func dismissWithReaction(value: String, targetEmptyNode: ASDisplayNode, targetFilledNode: ASDisplayNode, hideNode: Bool, completion: (() -> Void)?) { + public func dismissWithReaction(value: String, targetView: UIView, hideNode: Bool, completion: (() -> Void)?) { if !self.wasDismissed { self.wasDismissed = true - self.controllerNode.animateOutToReaction(value: value, targetEmptyNode: targetEmptyNode, targetFilledNode: targetFilledNode, hideNode: hideNode, completion: { [weak self] in + self.controllerNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, completion: { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) completion?() }) diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index 7ed049ce8b..c4ee47b208 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -1575,7 +1575,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } private func async(_ f: @escaping () -> Void) { - DispatchQueue.global().async(execute: f) + DispatchQueue.main.async(execute: f) } private func nodeForItem(synchronous: Bool, synchronousLoads: Bool, item: ListViewItem, previousNode: QueueLocalObject?, index: Int, previousItem: ListViewItem?, nextItem: ListViewItem?, params: ListViewItemLayoutParams, updateAnimation: ListViewItemUpdateAnimation, completion: @escaping (QueueLocalObject, ListViewItemNodeLayout, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { diff --git a/submodules/Postbox/Sources/Peer.swift b/submodules/Postbox/Sources/Peer.swift index c9509306f5..d760dece99 100644 --- a/submodules/Postbox/Sources/Peer.swift +++ b/submodules/Postbox/Sources/Peer.swift @@ -166,7 +166,7 @@ public struct PeerId: Hashable, CustomStringConvertible, Comparable, Codable { let offsetIdHighBits = (data >> (32 + 3)) & 0xffffffff let idHighBits = offsetIdHighBits << 32 - if idHighBits == 0 && namespaceBits != 0 { + if idHighBits == 0 && namespaceBits == 3 { if let uint32Value = UInt32(exactly: idLowBits) { self.id = Id(rawValue: Int64(Int32(bitPattern: uint32Value))) } else { diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index c0622a10b7..5536c3e478 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -22,15 +22,18 @@ public final class ReactionContextItem { } public let reaction: ReactionContextItem.Reaction + public let stillAnimation: TelegramMediaFile public let listAnimation: TelegramMediaFile public let applicationAnimation: TelegramMediaFile public init( reaction: ReactionContextItem.Reaction, + stillAnimation: TelegramMediaFile, listAnimation: TelegramMediaFile, applicationAnimation: TelegramMediaFile ) { self.reaction = reaction + self.stillAnimation = stillAnimation self.listAnimation = listAnimation self.applicationAnimation = applicationAnimation } @@ -477,11 +480,11 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { return keyframes } - private func animateFromItemNodeToReaction(itemNode: ReactionNode, targetFilledNode: ASDisplayNode, targetSnapshotView: UIView, hideNode: Bool, completion: @escaping () -> Void) { + private func animateFromItemNodeToReaction(itemNode: ReactionNode, targetView: UIView, targetSnapshotView: UIView, hideNode: Bool, completion: @escaping () -> Void) { let itemFrame: CGRect = itemNode.frame let _ = itemFrame - let targetFrame = self.view.convert(targetFilledNode.view.convert(targetFilledNode.bounds, to: nil), from: nil) + let targetFrame = self.view.convert(targetView.convert(targetView.bounds, to: nil), from: nil) targetSnapshotView.frame = targetFrame self.view.insertSubview(targetSnapshotView, belowSubview: itemNode.view) @@ -507,26 +510,21 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { completedTarget = true intermediateCompletion() + targetSnapshotView?.isHidden = true + if hideNode { - targetFilledNode.isHidden = false - targetFilledNode.layer.animateSpring(from: 0.5 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, initialVelocity: 0.0, damping: 90.0, completion: { _ in + targetView.isHidden = false + targetView.layer.animateSpring(from: 0.5 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, initialVelocity: 0.0, damping: 90.0, completion: { _ in targetSnapshotView?.isHidden = true targetScaleCompleted = true intermediateCompletion() }) } else { - targetSnapshotView?.isHidden = true targetScaleCompleted = true intermediateCompletion() } }) - //let keyframes = self.generateParabollicMotionKeyframes(from: targetPosition, to: targetPosition, elevation: 30.0) - - /*itemNode.layer.animateKeyframes(values: keyframes, duration: duration, keyPath: "position", removeOnCompletion: false, completion: { _ in - }) - targetSnapshotView.layer.animateKeyframes(values: keyframes, duration: duration, keyPath: "position", removeOnCompletion: false)*/ - itemNode.layer.animateScale(from: 1.0, to: (targetSnapshotView.bounds.width * 0.5) / itemNode.bounds.width, duration: duration, removeOnCompletion: false) } @@ -539,19 +537,19 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } } - public func animateOutToReaction(value: String, targetEmptyNode: ASDisplayNode, targetFilledNode: ASDisplayNode, hideNode: Bool, completion: @escaping () -> Void) { + public func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) { for itemNode in self.itemNodes { if itemNode.item.reaction.rawValue != value { continue } - if let targetSnapshotView = targetFilledNode.view.snapshotContentTree() { + if let targetSnapshotView = targetView.snapshotContentTree() { if hideNode { - targetFilledNode.isHidden = true + targetView.isHidden = true } itemNode.isExtracted = true let selfSourceRect = itemNode.view.convert(itemNode.view.bounds, to: self.view) - let selfTargetRect = self.view.convert(targetFilledNode.bounds, from: targetFilledNode.view) + let selfTargetRect = self.view.convert(targetView.bounds, from: targetView) let expandedScale: CGFloat = 3.0 let expandedSize = CGSize(width: floor(selfSourceRect.width * expandedScale), height: floor(selfSourceRect.height * expandedScale)) @@ -568,11 +566,11 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { transition.animatePositionWithKeyframes(node: itemNode, keyframes: self.generateParabollicMotionKeyframes(from: selfSourceRect.center, to: expandedFrame.center, elevation: 30.0)) let additionalAnimationNode = AnimatedStickerNode() - let incomingMessage: Bool = self.isLeftAligned + let incomingMessage: Bool = expandedFrame.midX < self.bounds.width / 2.0 let animationFrame = expandedFrame.insetBy(dx: -expandedFrame.width * 0.5, dy: -expandedFrame.height * 0.5) .offsetBy(dx: incomingMessage ? (expandedFrame.width - 50.0) : (-expandedFrame.width + 50.0), dy: 0.0) - //animationFrame = animationFrame.offsetBy(dx: CGFloat.random(in: -30.0 ... 30.0), dy: CGFloat.random(in: -30.0 ... 30.0)) - additionalAnimationNode.setup(source: AnimatedStickerResourceSource(account: itemNode.context.account, resource: itemNode.item.applicationAnimation.resource), width: Int(animationFrame.width * 2.0), height: Int(animationFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: nil)) + + additionalAnimationNode.setup(source: AnimatedStickerResourceSource(account: itemNode.context.account, resource: itemNode.item.applicationAnimation.resource), width: Int(animationFrame.width * 2.0), height: Int(animationFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: itemNode.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(itemNode.item.applicationAnimation.resource.id))) additionalAnimationNode.frame = animationFrame if incomingMessage { additionalAnimationNode.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) @@ -598,7 +596,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { }) DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0 * UIView.animationDurationFactor(), execute: { - self.animateFromItemNodeToReaction(itemNode: itemNode, targetFilledNode: targetFilledNode, targetSnapshotView: targetSnapshotView, hideNode: hideNode, completion: { + self.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, targetSnapshotView: targetSnapshotView, hideNode: hideNode, completion: { mainAnimationCompleted = true intermediateCompletion() }) @@ -614,11 +612,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if self.contentContainer.bounds.contains(contentPoint) { return self.contentContainer.hitTest(contentPoint, with: event) } - /*for itemNode in self.itemNodes { - if !itemNode.alpha.isZero && itemNode.frame.contains(contentPoint) { - return self.view - } - }*/ + return nil } @@ -659,3 +653,135 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } } +public final class StandaloneReactionAnimation: ASDisplayNode { + private let itemNode: ReactionNode + private let hapticFeedback = HapticFeedback() + + public init(context: AccountContext, theme: PresentationTheme, reaction: ReactionContextItem) { + self.itemNode = ReactionNode(context: context, theme: theme, item: reaction) + + super.init() + + self.isUserInteractionEnabled = false + + self.addSubnode(self.itemNode) + } + + public func animateReactionSelection(targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) { + guard let targetSnapshotView = targetView.snapshotContentTree() else { + completion() + return + } + if hideNode { + targetView.isHidden = true + } + + self.itemNode.isExtracted = true + let sourceItemSize: CGFloat = 40.0 + let selfTargetRect = self.view.convert(targetView.bounds, from: targetView) + + let expandedScale: CGFloat = 3.0 + let expandedSize = CGSize(width: floor(sourceItemSize * expandedScale), height: floor(sourceItemSize * expandedScale)) + + let expandedFrame = CGRect(origin: CGPoint(x: floor(selfTargetRect.midX - expandedSize.width / 2.0), y: floor(selfTargetRect.midY - expandedSize.height / 2.0)), size: expandedSize) + + self.addSubnode(itemNode) + itemNode.frame = expandedFrame + itemNode.updateLayout(size: expandedFrame.size, isExpanded: true, transition: .immediate) + + itemNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.18) + itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) + + let additionalAnimationNode = AnimatedStickerNode() + let incomingMessage: Bool = expandedFrame.midX < self.bounds.width / 2.0 + let animationFrame = expandedFrame.insetBy(dx: -expandedFrame.width * 0.5, dy: -expandedFrame.height * 0.5) + .offsetBy(dx: incomingMessage ? (expandedFrame.width - 50.0) : (-expandedFrame.width + 50.0), dy: 0.0) + + additionalAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.itemNode.context.account, resource: self.itemNode.item.applicationAnimation.resource), width: Int(animationFrame.width * 2.0), height: Int(animationFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: self.itemNode.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.itemNode.item.applicationAnimation.resource.id))) + additionalAnimationNode.frame = animationFrame + if incomingMessage { + additionalAnimationNode.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + } + additionalAnimationNode.updateLayout(size: animationFrame.size) + self.addSubnode(additionalAnimationNode) + + additionalAnimationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) + + var mainAnimationCompleted = false + var additionalAnimationCompleted = false + let intermediateCompletion: () -> Void = { + if mainAnimationCompleted && additionalAnimationCompleted { + completion() + } + } + + additionalAnimationNode.completed = { _ in + additionalAnimationCompleted = true + intermediateCompletion() + } + + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1 * UIView.animationDurationFactor(), execute: { + additionalAnimationNode.visibility = true + }) + + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0 * UIView.animationDurationFactor(), execute: { + self.animateFromItemNodeToReaction(itemNode: self.itemNode, targetView: targetView, targetSnapshotView: targetSnapshotView, hideNode: hideNode, completion: { + mainAnimationCompleted = true + intermediateCompletion() + }) + }) + } + + private func animateFromItemNodeToReaction(itemNode: ReactionNode, targetView: UIView, targetSnapshotView: UIView, hideNode: Bool, completion: @escaping () -> Void) { + let itemFrame: CGRect = itemNode.frame + let _ = itemFrame + + let targetFrame = self.view.convert(targetView.convert(targetView.bounds, to: nil), from: nil) + + targetSnapshotView.frame = targetFrame + self.view.insertSubview(targetSnapshotView, belowSubview: itemNode.view) + + var completedTarget = false + var targetScaleCompleted = false + let intermediateCompletion: () -> Void = { + if completedTarget && targetScaleCompleted { + completion() + } + } + + let targetPosition = targetFrame.center + let _ = targetPosition + let duration: Double = 0.16 + + itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.9, removeOnCompletion: false) + targetSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.8) + targetSnapshotView.layer.animateScale(from: itemNode.bounds.width / targetSnapshotView.bounds.width, to: 0.5, duration: duration, removeOnCompletion: false, completion: { [weak self, weak targetSnapshotView] _ in + if let strongSelf = self { + strongSelf.hapticFeedback.tap() + } + completedTarget = true + intermediateCompletion() + + targetSnapshotView?.isHidden = true + + if hideNode { + targetView.isHidden = false + targetView.layer.animateSpring(from: 0.5 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, initialVelocity: 0.0, damping: 90.0, completion: { _ in + targetSnapshotView?.isHidden = true + targetScaleCompleted = true + intermediateCompletion() + }) + } else { + targetScaleCompleted = true + intermediateCompletion() + } + }) + + itemNode.layer.animateScale(from: 1.0, to: (targetSnapshotView.bounds.width * 0.5) / itemNode.bounds.width, duration: duration, removeOnCompletion: false) + } + + public func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { + self.bounds = self.bounds.offsetBy(dx: 0.0, dy: offset.y) + transition.animateOffsetAdditive(node: self, offset: -offset.y) + } +} diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index 22378a3f05..fdb54662f2 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -39,6 +39,7 @@ final class ReactionNode: ASDisplayNode { let context: AccountContext let item: ReactionContextItem private let staticImageNode: TransformImageNode + private let stillAnimationNode: AnimatedStickerNode private var animationNode: AnimatedStickerNode? private var fetchStickerDisposable: Disposable? @@ -48,11 +49,14 @@ final class ReactionNode: ASDisplayNode { var isExtracted: Bool = false + var didSetupStillAnimation: Bool = false + init(context: AccountContext, theme: PresentationTheme, item: ReactionContextItem) { self.context = context self.item = item self.staticImageNode = TransformImageNode() + self.stillAnimationNode = AnimatedStickerNode() super.init() @@ -60,14 +64,18 @@ final class ReactionNode: ASDisplayNode { self.addSubnode(self.staticImageNode) - /*self.addSubnode(self.animationNode) + self.addSubnode(self.stillAnimationNode) - self.animationNode.updateLayout(size: self.intrinsicSize) - self.animationNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicSize)*/ + self.stillAnimationNode.started = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.staticImageNode.isHidden = true + } + self.fetchStickerDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: .standalone(resource: item.stillAnimation.resource)).start() self.fetchStickerDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: .standalone(resource: item.listAnimation.resource)).start() self.fetchFullAnimationDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: .standalone(resource: item.applicationAnimation.resource)).start() - //let _ = context.meshAnimationCache.get(resource: item.applicationAnimation.resource) } deinit { @@ -81,35 +89,8 @@ final class ReactionNode: ASDisplayNode { let animationSize = self.item.listAnimation.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0) var animationDisplaySize = animationSize.aspectFitted(intrinsicSize) - var scalingFactor: CGFloat = 1.0 - var offsetFactor: CGFloat = 0.0 - switch self.item.reaction.rawValue { - case "💸": - scalingFactor = 1.25 - offsetFactor = -0.04 - case "👍": - scalingFactor = 1.4 - offsetFactor = 0.02 - case "👎": - scalingFactor = 1.4 - offsetFactor = -0.01 - case "😂": - scalingFactor = 1.2 - case "🍆": - scalingFactor = 1.1 - offsetFactor = -0.01 - case "👻": - scalingFactor = 1.2 - case "🎃": - scalingFactor = 1.15 - offsetFactor = -0.08 - case "🎈": - offsetFactor = 0.03 - case "🎉": - offsetFactor = -0.01 - default: - break - } + let scalingFactor: CGFloat = 1.0 + let offsetFactor: CGFloat = 0.0 animationDisplaySize.width = floor(animationDisplaySize.width * scalingFactor) animationDisplaySize.height = floor(animationDisplaySize.height * scalingFactor) @@ -129,7 +110,7 @@ final class ReactionNode: ASDisplayNode { strongSelf.staticImageNode.isHidden = true } - animationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.listAnimation.resource), width: Int(size.width * 2.0), height: Int(size.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: nil)) + animationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.listAnimation.resource), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.listAnimation.resource.id))) animationNode.frame = animationFrame animationNode.updateLayout(size: animationFrame.size) if transition.isAnimated, !self.staticImageNode.frame.isEmpty { @@ -137,355 +118,38 @@ final class ReactionNode: ASDisplayNode { transition.animatePositionAdditive(node: animationNode, offset: CGPoint(x: self.staticImageNode.frame.midX - animationFrame.midX, y: self.staticImageNode.frame.midY - animationFrame.midY)) } animationNode.visibility = true + + self.stillAnimationNode.alpha = 0.0 + self.stillAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in + self?.stillAnimationNode.visibility = false + }) + + animationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) } if self.validSize != size { self.validSize = size - self.staticImageNode.setSignal(chatMessageAnimatedSticker(postbox: self.context.account.postbox, file: item.listAnimation, small: false, size: CGSize(width: animationDisplaySize.width * UIScreenScale, height: animationDisplaySize.height * UIScreenScale), fitzModifier: nil, fetched: false, onlyFullSize: false, thumbnail: false, synchronousLoad: false)) + self.staticImageNode.setSignal(chatMessageAnimatedSticker(postbox: self.context.account.postbox, file: item.stillAnimation, small: false, size: CGSize(width: animationDisplaySize.width * UIScreenScale, height: animationDisplaySize.height * UIScreenScale), fitzModifier: nil, fetched: false, onlyFullSize: false, thumbnail: false, synchronousLoad: false)) let imageApply = self.staticImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: animationDisplaySize, boundingSize: animationDisplaySize, intrinsicInsets: UIEdgeInsets())) imageApply() transition.updateFrame(node: self.staticImageNode, frame: animationFrame) } + + if !self.didSetupStillAnimation { + self.didSetupStillAnimation = true + + self.stillAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation.resource), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .loop, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.stillAnimation.resource.id))) + self.stillAnimationNode.position = animationFrame.center + self.stillAnimationNode.bounds = CGRect(origin: CGPoint(), size: animationFrame.size) + self.stillAnimationNode.updateLayout(size: animationFrame.size) + self.stillAnimationNode.visibility = true + } else { + transition.updatePosition(node: self.stillAnimationNode, position: animationFrame.center) + transition.updateTransformScale(node: self.stillAnimationNode, scale: animationFrame.size.width / self.stillAnimationNode.bounds.width) + } } func didAppear() { } } - -/*final class ReactionSelectionNode: ASDisplayNode { - private let account: Account - private let theme: PresentationTheme - private let reactions: [ReactionGestureItem] - - private let backgroundNode: ASImageNode - private let backgroundShadowNode: ASImageNode - private let bubbleNodes: [(ASImageNode, ASImageNode)] - private var reactionNodes: [ReactionNode] = [] - private var hasSelectedNode = false - - private let hapticFeedback = HapticFeedback() - - private var shadowBlur: CGFloat = 8.0 - private var minimizedReactionSize: CGFloat = 28.0 - private var smallCircleSize: CGFloat = 14.0 - - private var isRightAligned: Bool = false - - public init(account: Account, theme: PresentationTheme, reactions: [ReactionGestureItem]) { - self.account = account - self.theme = theme - self.reactions = reactions - - self.backgroundNode = ASImageNode() - self.backgroundNode.displaysAsynchronously = false - self.backgroundNode.displayWithoutProcessing = true - - self.backgroundShadowNode = ASImageNode() - self.backgroundShadowNode.displaysAsynchronously = false - self.backgroundShadowNode.displayWithoutProcessing = true - - self.bubbleNodes = (0 ..< 2).map { i -> (ASImageNode, ASImageNode) in - let imageNode = ASImageNode() - imageNode.displaysAsynchronously = false - imageNode.displayWithoutProcessing = true - - let shadowNode = ASImageNode() - shadowNode.displaysAsynchronously = false - shadowNode.displayWithoutProcessing = true - - return (imageNode, shadowNode) - } - - super.init() - - self.bubbleNodes.forEach { _, shadow in - //self.addSubnode(shadow) - } - self.addSubnode(self.backgroundShadowNode) - self.bubbleNodes.forEach { foreground, _ in - //self.addSubnode(foreground) - } - self.addSubnode(self.backgroundNode) - } - - func updateLayout(constrainedSize: CGSize, startingPoint: CGPoint, offsetFromStart: CGFloat, isInitial: Bool, touchPoint: CGPoint) { - let initialAnchorX = startingPoint.x - - var isRightAligned = false - if initialAnchorX > constrainedSize.width / 2.0 { - isRightAligned = true - } - - let reactionSideInset: CGFloat = 10.0 - let reactionSpacing: CGFloat = 6.0 - let minReactionSpacing: CGFloat = 2.0 - let minimizedReactionSize = self.minimizedReactionSize - let contentWidth: CGFloat = CGFloat(self.reactions.count) * (minimizedReactionSize) + CGFloat(self.reactions.count - 1) * reactionSpacing + reactionSideInset * 2.0 - let spaceForMaximizedReaction = CGFloat(self.reactions.count - 1) * reactionSpacing - CGFloat(self.reactions.count - 1) * minReactionSpacing - let maximizedReactionSize: CGFloat = minimizedReactionSize + spaceForMaximizedReaction - let backgroundHeight: CGFloat = floor(self.minimizedReactionSize * 1.8) - - var backgroundFrame = CGRect(origin: CGPoint(x: -shadowBlur, y: -shadowBlur), size: CGSize(width: contentWidth + shadowBlur * 2.0, height: backgroundHeight + shadowBlur * 2.0)) - if constrainedSize.width > 500.0 { - backgroundFrame = backgroundFrame.offsetBy(dx: constrainedSize.width - contentWidth - 44.0, dy: startingPoint.y - backgroundHeight - 12.0) - } else { - backgroundFrame = backgroundFrame.offsetBy(dx: floor((constrainedSize.width - contentWidth) / 2.0), dy: startingPoint.y - backgroundHeight - 12.0) - } - backgroundFrame.origin.x = max(0.0, backgroundFrame.minX) - backgroundFrame.origin.x = min(constrainedSize.width - backgroundFrame.width, backgroundFrame.minX) - - let anchorMinX = backgroundFrame.minX + shadowBlur + backgroundHeight / 2.0 - let anchorMaxX = backgroundFrame.maxX - shadowBlur - backgroundHeight / 2.0 - let anchorX = max(anchorMinX, min(anchorMaxX, offsetFromStart)) - - var maximizedIndex = -1 - /*if let reaction = self.reactions.last, case .reply = reaction { - maximizedIndex = self.reactions.count - 1 - }*/ - if backgroundFrame.insetBy(dx: -10.0, dy: -10.0).offsetBy(dx: 0.0, dy: 10.0).contains(touchPoint) { - maximizedIndex = Int(((touchPoint.x - anchorMinX) / (anchorMaxX - anchorMinX)) * CGFloat(self.reactionNodes.count)) - maximizedIndex = max(0, min(self.reactionNodes.count - 1, maximizedIndex)) - } - - let interReactionSpacing: CGFloat - if maximizedIndex != -1 { - interReactionSpacing = minReactionSpacing - } else { - interReactionSpacing = reactionSpacing - } - - if isInitial && self.reactionNodes.isEmpty { - self.shadowBlur = floor(minimizedReactionSize * 0.26) - self.smallCircleSize = 14.0 - - self.backgroundNode.image = generateBubbleImage(foreground: .white, diameter: backgroundHeight, shadowBlur: self.shadowBlur) - self.backgroundShadowNode.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: backgroundHeight, shadowBlur: self.shadowBlur) - for i in 0 ..< self.bubbleNodes.count { - self.bubbleNodes[i].0.image = generateBubbleImage(foreground: .white, diameter: CGFloat(i + 1) * self.smallCircleSize, shadowBlur: self.shadowBlur) - self.bubbleNodes[i].1.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: CGFloat(i + 1) * self.smallCircleSize, shadowBlur: self.shadowBlur) - } - - self.reactionNodes = self.reactions.map { reaction -> ReactionNode in - return ReactionNode(account: self.account, theme: self.theme, reaction: reaction, maximizedReactionSize: maximizedReactionSize - 12.0, loadFirstFrame: true) - } - self.reactionNodes.forEach(self.addSubnode(_:)) - } - - let minimizedReactionVerticalInset: CGFloat = floor((backgroundHeight - minimizedReactionSize) / 2.0) - - - /*if maximizedIndex == -1 { - backgroundFrame.size.width -= maximizedReactionSize - minimizedReactionSize - backgroundFrame.origin.x += maximizedReactionSize - minimizedReactionSize - }*/ - - self.isRightAligned = isRightAligned - - let backgroundTransition: ContainedViewLayoutTransition - if isInitial { - backgroundTransition = .immediate - } else { - backgroundTransition = .animated(duration: 0.18, curve: .easeInOut) - } - backgroundTransition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - backgroundTransition.updateFrame(node: self.backgroundShadowNode, frame: backgroundFrame) - - var reactionX: CGFloat = backgroundFrame.minX + shadowBlur + reactionSideInset - if maximizedIndex != -1 { - self.hasSelectedNode = false - } else { - self.hasSelectedNode = true - } - - for iterationIndex in 0 ..< self.reactionNodes.count { - var i = iterationIndex - let isMaximized = i == maximizedIndex - if !isRightAligned { - i = self.reactionNodes.count - 1 - i - } - - let reactionSize: CGFloat - if isMaximized { - reactionSize = maximizedReactionSize - } else { - reactionSize = minimizedReactionSize - } - - let transition: ContainedViewLayoutTransition - if isInitial { - transition = .immediate - } else { - transition = .animated(duration: 0.18, curve: .easeInOut) - } - - if self.reactionNodes[i].isMaximized != isMaximized { - self.reactionNodes[i].isMaximized = isMaximized - self.reactionNodes[i].updateIsAnimating(isMaximized, animated: !isInitial) - if isMaximized && !isInitial { - self.hapticFeedback.tap() - } - } - - var reactionFrame = CGRect(origin: CGPoint(x: reactionX, y: backgroundFrame.maxY - shadowBlur - minimizedReactionVerticalInset - reactionSize), size: CGSize(width: reactionSize, height: reactionSize)) - if isMaximized { - reactionFrame.origin.x -= 7.0 - reactionFrame.size.width += 14.0 - } - self.reactionNodes[i].updateLayout(size: reactionFrame.size, scale: reactionFrame.size.width / (maximizedReactionSize + 14.0), transition: transition, displayText: isMaximized) - - transition.updateFrame(node: self.reactionNodes[i], frame: reactionFrame, beginWithCurrentState: true) - - reactionX += reactionSize + interReactionSpacing - } - - let mainBubbleFrame = CGRect(origin: CGPoint(x: anchorX - self.smallCircleSize - shadowBlur, y: backgroundFrame.maxY - shadowBlur - self.smallCircleSize - shadowBlur), size: CGSize(width: self.smallCircleSize * 2.0 + shadowBlur * 2.0, height: self.smallCircleSize * 2.0 + shadowBlur * 2.0)) - self.bubbleNodes[1].0.frame = mainBubbleFrame - self.bubbleNodes[1].1.frame = mainBubbleFrame - - let secondaryBubbleFrame = CGRect(origin: CGPoint(x: mainBubbleFrame.midX - 10.0 - (self.smallCircleSize + shadowBlur * 2.0) / 2.0, y: mainBubbleFrame.midY + 10.0 - (self.smallCircleSize + shadowBlur * 2.0) / 2.0), size: CGSize(width: self.smallCircleSize + shadowBlur * 2.0, height: self.smallCircleSize + shadowBlur * 2.0)) - self.bubbleNodes[0].0.frame = secondaryBubbleFrame - self.bubbleNodes[0].1.frame = secondaryBubbleFrame - } - - func animateIn() { - self.bubbleNodes[1].0.layer.animateScale(from: 0.01, to: 1.0, duration: 0.11, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) - self.bubbleNodes[1].1.layer.animateScale(from: 0.01, to: 1.0, duration: 0.11, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) - - self.bubbleNodes[0].0.layer.animateScale(from: 0.01, to: 1.0, duration: 0.11, delay: 0.05, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) - self.bubbleNodes[0].1.layer.animateScale(from: 0.01, to: 1.0, duration: 0.11, delay: 0.05, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) - - let backgroundOffset: CGPoint - if self.isRightAligned { - backgroundOffset = CGPoint(x: (self.backgroundNode.frame.width - shadowBlur) / 2.0 - 42.0, y: 10.0) - } else { - backgroundOffset = CGPoint(x: -(self.backgroundNode.frame.width - shadowBlur) / 2.0 + 42.0, y: 10.0) - } - let damping: CGFloat = 100.0 - - for i in 0 ..< self.reactionNodes.count { - let animationOffset: Double = 1.0 - Double(i) / Double(self.reactionNodes.count - 1) - var nodeOffset: CGPoint - if self.isRightAligned { - nodeOffset = CGPoint(x: self.reactionNodes[i].frame.minX - (self.backgroundNode.frame.maxX - shadowBlur) / 2.0 - 42.0, y: 10.0) - } else { - nodeOffset = CGPoint(x: self.reactionNodes[i].frame.minX - (self.backgroundNode.frame.minX + shadowBlur) / 2.0 - 42.0, y: 10.0) - } - nodeOffset.x = 0.0 - nodeOffset.y = 30.0 - self.reactionNodes[i].layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.04, delay: animationOffset * 0.1) - self.reactionNodes[i].layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, delay: animationOffset * 0.1, initialVelocity: 0.0, damping: damping) - //self.reactionNodes[i].layer.animateSpring(from: NSValue(cgPoint: nodeOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, delay: animationOffset * 0.1, initialVelocity: 0.0, damping: damping, additive: true) - } - - self.backgroundNode.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, damping: damping) - self.backgroundNode.layer.animateSpring(from: NSValue(cgPoint: backgroundOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, initialVelocity: 0.0, damping: damping, additive: true) - self.backgroundShadowNode.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, damping: damping) - self.backgroundShadowNode.layer.animateSpring(from: NSValue(cgPoint: backgroundOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, initialVelocity: 0.0, damping: damping, additive: true) - } - - func animateOut(into targetNode: ASDisplayNode?, hideTarget: Bool, completion: @escaping () -> Void) { - self.hapticFeedback.prepareTap() - - var completedContainer = false - var completedTarget = true - - let intermediateCompletion: () -> Void = { - if completedContainer && completedTarget { - completion() - } - } - - if let targetNode = targetNode { - for i in 0 ..< self.reactionNodes.count { - if let isMaximized = self.reactionNodes[i].isMaximized, isMaximized { - targetNode.recursivelyEnsureDisplaySynchronously(true) - if let snapshotView = self.reactionNodes[i].view.snapshotContentTree(), let targetSnapshotView = targetNode.view.snapshotContentTree() { - targetSnapshotView.frame = self.view.convert(targetNode.bounds, from: targetNode.view) - self.reactionNodes[i].isHidden = true - self.view.addSubview(targetSnapshotView) - self.view.addSubview(snapshotView) - completedTarget = false - let targetPosition = self.view.convert(targetNode.bounds.center, from: targetNode.view) - let duration: Double = 0.3 - if hideTarget { - targetNode.isHidden = true - } - - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) - targetSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - targetSnapshotView.layer.animateScale(from: snapshotView.bounds.width / targetSnapshotView.bounds.width, to: 0.5, duration: 0.3, removeOnCompletion: false) - - - let sourcePoint = snapshotView.center - let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: sourcePoint.y - 30.0) - - let x1 = sourcePoint.x - let y1 = sourcePoint.y - let x2 = midPoint.x - let y2 = midPoint.y - let x3 = targetPosition.x - let y3 = targetPosition.y - - let a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) - let b = (x1 * x1 * (y2 - y3) + x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1)) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) - let c = (x2 * x2 * (x3 * y1 - x1 * y3) + x2 * (x1 * x1 * y3 - x3 * x3 * y1) + x1 * x3 * (x3 - x1) * y2) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) - - var keyframes: [AnyObject] = [] - for i in 0 ..< 10 { - let k = CGFloat(i) / CGFloat(10 - 1) - let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k - let y = a * x * x + b * x + c - keyframes.append(NSValue(cgPoint: CGPoint(x: x, y: y))) - } - - snapshotView.layer.animateKeyframes(values: keyframes, duration: 0.3, keyPath: "position", removeOnCompletion: false, completion: { [weak self] _ in - if let strongSelf = self { - strongSelf.hapticFeedback.tap() - } - completedTarget = true - if hideTarget { - targetNode.isHidden = false - targetNode.layer.animateSpring(from: 0.5 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, initialVelocity: 0.0, damping: 90.0) - } - intermediateCompletion() - }) - targetSnapshotView.layer.animateKeyframes(values: keyframes, duration: 0.3, keyPath: "position", removeOnCompletion: false) - - snapshotView.layer.animateScale(from: 1.0, to: (targetSnapshotView.bounds.width * 0.5) / snapshotView.bounds.width, duration: 0.3, removeOnCompletion: false) - } - break - } - } - } - - //self.backgroundNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) - self.backgroundShadowNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) - self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.backgroundShadowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in - completedContainer = true - intermediateCompletion() - }) - for (node, shadow) in self.bubbleNodes { - node.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) - node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - shadow.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) - shadow.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - } - for i in 0 ..< self.reactionNodes.count { - self.reactionNodes[i].layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) - self.reactionNodes[i].layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - } - } - - func selectedReaction() -> ReactionGestureItem? { - for i in 0 ..< self.reactionNodes.count { - if let isMaximized = self.reactionNodes[i].isMaximized, isMaximized { - return self.reactionNodes[i].reaction - } - } - return nil - } -} - -*/ diff --git a/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift b/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift index 8d3d9233cb..54590b1f19 100644 --- a/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift +++ b/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift @@ -172,6 +172,12 @@ public func combineLatest(queue: Queue? = nil, _ s1: Signal, _ s2: Signal, _ s3: Signal, _ s4: Signal, _ s5: Signal, _ s6: Signal, _ s7: Signal, _ s8: Signal, _ s9: Signal, _ s10: Signal, _ s11: Signal, _ s12: Signal, _ s13: Signal, _ s14: Signal, _ s15: Signal) -> Signal<(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15), E> { + return combineLatestAny([signalOfAny(s1), signalOfAny(s2), signalOfAny(s3), signalOfAny(s4), signalOfAny(s5), signalOfAny(s6), signalOfAny(s7), signalOfAny(s8), signalOfAny(s9), signalOfAny(s10), signalOfAny(s11), signalOfAny(s12), signalOfAny(s13), signalOfAny(s14), signalOfAny(s15)], combine: { values in + return (values[0] as! T1, values[1] as! T2, values[2] as! T3, values[3] as! T4, values[4] as! T5, values[5] as! T6, values[6] as! T7, values[7] as! T8, values[8] as! T9, values[9] as! T10, values[10] as! T11, values[11] as! T12, values[12] as! T13, values[13] as! T14, values[14] as! T15) + }, initialValues: [:], queue: queue) +} + public func combineLatest(queue: Queue? = nil, _ signals: [Signal]) -> Signal<[T], E> { if signals.count == 0 { return single([T](), E.self) diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index ce8a056354..587e4be879 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -12,8 +12,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-457104426] = { return Api.InputGeoPoint.parse_inputGeoPointEmpty($0) } dict[1210199983] = { return Api.InputGeoPoint.parse_inputGeoPoint($0) } dict[-784000893] = { return Api.payments.ValidatedRequestedInfo.parse_validatedRequestedInfo($0) } - dict[1185349556] = { return Api.ChatFull.parse_chatFull($0) } - dict[1449537070] = { return Api.ChatFull.parse_channelFull($0) } + dict[-779165146] = { return Api.ChatFull.parse_chatFull($0) } + dict[-516145888] = { return Api.ChatFull.parse_channelFull($0) } dict[-591909213] = { return Api.PollResults.parse_pollResults($0) } dict[-1070776313] = { return Api.ChatParticipant.parse_chatParticipant($0) } dict[-462696732] = { return Api.ChatParticipant.parse_chatParticipantCreator($0) } @@ -289,6 +289,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1299263278] = { return Api.Update.parse_updateBotCommands($0) } dict[1885586395] = { return Api.Update.parse_updatePendingJoinRequests($0) } dict[299870598] = { return Api.Update.parse_updateBotChatInviteRequester($0) } + dict[357013699] = { return Api.Update.parse_updateMessageReactions($0) } dict[136574537] = { return Api.messages.VotesList.parse_votesList($0) } dict[1558266229] = { return Api.PopularContact.parse_popularContact($0) } dict[-592373577] = { return Api.GroupCallParticipantVideoSourceGroup.parse_groupCallParticipantVideoSourceGroup($0) } @@ -341,6 +342,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-842824308] = { return Api.account.WallPapers.parse_wallPapers($0) } dict[1012306921] = { return Api.InputTheme.parse_inputTheme($0) } dict[-175567375] = { return Api.InputTheme.parse_inputThemeSlug($0) } + dict[142306870] = { return Api.MessageReactions.parse_messageReactions($0) } dict[-2032041631] = { return Api.Poll.parse_poll($0) } dict[-1195615476] = { return Api.InputNotifyPeer.parse_inputNotifyPeer($0) } dict[423314455] = { return Api.InputNotifyPeer.parse_inputNotifyUsers($0) } @@ -489,6 +491,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1036572727] = { return Api.account.PasswordInputSettings.parse_passwordInputSettings($0) } dict[878078826] = { return Api.PageTableCell.parse_pageTableCell($0) } dict[-1626209256] = { return Api.ChatBannedRights.parse_chatBannedRights($0) } + dict[-1626924713] = { return Api.messages.AvailableReactions.parse_availableReactionsNotModified($0) } + dict[1989032621] = { return Api.messages.AvailableReactions.parse_availableReactions($0) } dict[1968737087] = { return Api.InputClientProxy.parse_inputClientProxy($0) } dict[649453030] = { return Api.messages.MessageEditData.parse_messageEditData($0) } dict[1705297877] = { return Api.messages.SponsoredMessages.parse_sponsoredMessages($0) } @@ -513,6 +517,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-524237339] = { return Api.PageTableRow.parse_pageTableRow($0) } dict[453805082] = { return Api.DraftMessage.parse_draftMessageEmpty($0) } dict[-40996577] = { return Api.DraftMessage.parse_draftMessage($0) } + dict[-1553558980] = { return Api.messages.MessageReactionsList.parse_messageReactionsList($0) } dict[-1014526429] = { return Api.help.Country.parse_country($0) } dict[-1660637285] = { return Api.StatsGroupTopPoster.parse_statsGroupTopPoster($0) } dict[-2128640689] = { return Api.account.SentEmailCode.parse_sentEmailCode($0) } @@ -690,7 +695,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-742634630] = { return Api.User.parse_userEmpty($0) } dict[1073147056] = { return Api.User.parse_user($0) } dict[-1868117372] = { return Api.Message.parse_messageEmpty($0) } - dict[-2049520670] = { return Api.Message.parse_message($0) } + dict[940666592] = { return Api.Message.parse_message($0) } dict[721967202] = { return Api.Message.parse_messageService($0) } dict[1398765469] = { return Api.StatsGroupTopInviter.parse_statsGroupTopInviter($0) } dict[186120336] = { return Api.messages.RecentStickers.parse_recentStickersNotModified($0) } @@ -760,6 +765,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1834973166] = { return Api.BaseTheme.parse_baseThemeTinted($0) } dict[1527845466] = { return Api.BaseTheme.parse_baseThemeArctic($0) } dict[398898678] = { return Api.help.Support.parse_support($0) } + dict[1873957073] = { return Api.ReactionCount.parse_reactionCount($0) } dict[1474492012] = { return Api.MessagesFilter.parse_inputMessagesFilterEmpty($0) } dict[-1777752804] = { return Api.MessagesFilter.parse_inputMessagesFilterPhotos($0) } dict[-1614803355] = { return Api.MessagesFilter.parse_inputMessagesFilterVideo($0) } @@ -785,7 +791,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[594408994] = { return Api.EmojiKeyword.parse_emojiKeywordDeleted($0) } dict[-290921362] = { return Api.upload.CdnFile.parse_cdnFileReuploadNeeded($0) } dict[-1449145777] = { return Api.upload.CdnFile.parse_cdnFile($0) } + dict[1679961905] = { return Api.AvailableReaction.parse_availableReaction($0) } dict[415997816] = { return Api.help.InviteText.parse_inviteText($0) } + dict[-1826077446] = { return Api.MessageUserReaction.parse_messageUserReaction($0) } dict[1984755728] = { return Api.BotInlineMessage.parse_botInlineMessageMediaAuto($0) } dict[-1937807902] = { return Api.BotInlineMessage.parse_botInlineMessageText($0) } dict[85477117] = { return Api.BotInlineMessage.parse_botInlineMessageMediaGeo($0) } @@ -1164,6 +1172,8 @@ public struct Api { _1.serialize(buffer, boxed) case let _1 as Api.InputTheme: _1.serialize(buffer, boxed) + case let _1 as Api.MessageReactions: + _1.serialize(buffer, boxed) case let _1 as Api.Poll: _1.serialize(buffer, boxed) case let _1 as Api.InputNotifyPeer: @@ -1262,6 +1272,8 @@ public struct Api { _1.serialize(buffer, boxed) case let _1 as Api.ChatBannedRights: _1.serialize(buffer, boxed) + case let _1 as Api.messages.AvailableReactions: + _1.serialize(buffer, boxed) case let _1 as Api.InputClientProxy: _1.serialize(buffer, boxed) case let _1 as Api.messages.MessageEditData: @@ -1292,6 +1304,8 @@ public struct Api { _1.serialize(buffer, boxed) case let _1 as Api.DraftMessage: _1.serialize(buffer, boxed) + case let _1 as Api.messages.MessageReactionsList: + _1.serialize(buffer, boxed) case let _1 as Api.help.Country: _1.serialize(buffer, boxed) case let _1 as Api.StatsGroupTopPoster: @@ -1544,6 +1558,8 @@ public struct Api { _1.serialize(buffer, boxed) case let _1 as Api.help.Support: _1.serialize(buffer, boxed) + case let _1 as Api.ReactionCount: + _1.serialize(buffer, boxed) case let _1 as Api.MessagesFilter: _1.serialize(buffer, boxed) case let _1 as Api.messages.Dialogs: @@ -1554,8 +1570,12 @@ public struct Api { _1.serialize(buffer, boxed) case let _1 as Api.upload.CdnFile: _1.serialize(buffer, boxed) + case let _1 as Api.AvailableReaction: + _1.serialize(buffer, boxed) case let _1 as Api.help.InviteText: _1.serialize(buffer, boxed) + case let _1 as Api.MessageUserReaction: + _1.serialize(buffer, boxed) case let _1 as Api.BotInlineMessage: _1.serialize(buffer, boxed) case let _1 as Api.InputPeerNotifySettings: diff --git a/submodules/TelegramApi/Sources/Api1.swift b/submodules/TelegramApi/Sources/Api1.swift index 2a084da138..732b6d3f7a 100644 --- a/submodules/TelegramApi/Sources/Api1.swift +++ b/submodules/TelegramApi/Sources/Api1.swift @@ -893,6 +893,62 @@ public struct messages { } } + } + public enum AvailableReactions: TypeConstructorDescription { + case availableReactionsNotModified + case availableReactions(hash: Int32, reactions: [Api.AvailableReaction]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .availableReactionsNotModified: + if boxed { + buffer.appendInt32(-1626924713) + } + + break + case .availableReactions(let hash, let reactions): + if boxed { + buffer.appendInt32(1989032621) + } + serializeInt32(hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(reactions.count)) + for item in reactions { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .availableReactionsNotModified: + return ("availableReactionsNotModified", []) + case .availableReactions(let hash, let reactions): + return ("availableReactions", [("hash", hash), ("reactions", reactions)]) + } + } + + public static func parse_availableReactionsNotModified(_ reader: BufferReader) -> AvailableReactions? { + return Api.messages.AvailableReactions.availableReactionsNotModified + } + public static func parse_availableReactions(_ reader: BufferReader) -> AvailableReactions? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.AvailableReaction]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.AvailableReaction.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.AvailableReactions.availableReactions(hash: _1!, reactions: _2!) + } + else { + return nil + } + } + } public enum MessageEditData: TypeConstructorDescription { case messageEditData(flags: Int32) @@ -1161,6 +1217,68 @@ public struct messages { } } + } + public enum MessageReactionsList: TypeConstructorDescription { + case messageReactionsList(flags: Int32, count: Int32, reactions: [Api.MessageUserReaction], users: [Api.User], nextOffset: String?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .messageReactionsList(let flags, let count, let reactions, let users, let nextOffset): + if boxed { + buffer.appendInt32(-1553558980) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(reactions.count)) + for item in reactions { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .messageReactionsList(let flags, let count, let reactions, let users, let nextOffset): + return ("messageReactionsList", [("flags", flags), ("count", count), ("reactions", reactions), ("users", users), ("nextOffset", nextOffset)]) + } + } + + public static func parse_messageReactionsList(_ reader: BufferReader) -> MessageReactionsList? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: [Api.MessageUserReaction]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageUserReaction.self) + } + var _4: [Api.User]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + var _5: String? + if Int(_1!) & Int(1 << 0) != 0 {_5 = parseString(reader) } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.messages.MessageReactionsList.messageReactionsList(flags: _1!, count: _2!, reactions: _3!, users: _4!, nextOffset: _5) + } + else { + return nil + } + } + } public enum PeerSettings: TypeConstructorDescription { case peerSettings(settings: Api.PeerSettings, chats: [Api.Chat], users: [Api.User]) diff --git a/submodules/TelegramApi/Sources/Api2.swift b/submodules/TelegramApi/Sources/Api2.swift index 1e8b33342b..7e5f5d77c2 100644 --- a/submodules/TelegramApi/Sources/Api2.swift +++ b/submodules/TelegramApi/Sources/Api2.swift @@ -160,14 +160,14 @@ public extension Api { } public enum ChatFull: TypeConstructorDescription { - case chatFull(flags: Int32, id: Int64, about: String, participants: Api.ChatParticipants, chatPhoto: Api.Photo?, notifySettings: Api.PeerNotifySettings, exportedInvite: Api.ExportedChatInvite?, botInfo: [Api.BotInfo]?, pinnedMsgId: Int32?, folderId: Int32?, call: Api.InputGroupCall?, ttlPeriod: Int32?, groupcallDefaultJoinAs: Api.Peer?, themeEmoticon: String?, requestsPending: Int32?, recentRequesters: [Int64]?) - case channelFull(flags: Int32, id: Int64, about: String, participantsCount: Int32?, adminsCount: Int32?, kickedCount: Int32?, bannedCount: Int32?, onlineCount: Int32?, readInboxMaxId: Int32, readOutboxMaxId: Int32, unreadCount: Int32, chatPhoto: Api.Photo, notifySettings: Api.PeerNotifySettings, exportedInvite: Api.ExportedChatInvite?, botInfo: [Api.BotInfo], migratedFromChatId: Int64?, migratedFromMaxId: Int32?, pinnedMsgId: Int32?, stickerset: Api.StickerSet?, availableMinId: Int32?, folderId: Int32?, linkedChatId: Int64?, location: Api.ChannelLocation?, slowmodeSeconds: Int32?, slowmodeNextSendDate: Int32?, statsDc: Int32?, pts: Int32, call: Api.InputGroupCall?, ttlPeriod: Int32?, pendingSuggestions: [String]?, groupcallDefaultJoinAs: Api.Peer?, themeEmoticon: String?, requestsPending: Int32?, recentRequesters: [Int64]?, defaultSendAs: Api.Peer?) + case chatFull(flags: Int32, id: Int64, about: String, participants: Api.ChatParticipants, chatPhoto: Api.Photo?, notifySettings: Api.PeerNotifySettings, exportedInvite: Api.ExportedChatInvite?, botInfo: [Api.BotInfo]?, pinnedMsgId: Int32?, folderId: Int32?, call: Api.InputGroupCall?, ttlPeriod: Int32?, groupcallDefaultJoinAs: Api.Peer?, themeEmoticon: String?, requestsPending: Int32?, recentRequesters: [Int64]?, availableReactions: [String]?) + case channelFull(flags: Int32, id: Int64, about: String, participantsCount: Int32?, adminsCount: Int32?, kickedCount: Int32?, bannedCount: Int32?, onlineCount: Int32?, readInboxMaxId: Int32, readOutboxMaxId: Int32, unreadCount: Int32, chatPhoto: Api.Photo, notifySettings: Api.PeerNotifySettings, exportedInvite: Api.ExportedChatInvite?, botInfo: [Api.BotInfo], migratedFromChatId: Int64?, migratedFromMaxId: Int32?, pinnedMsgId: Int32?, stickerset: Api.StickerSet?, availableMinId: Int32?, folderId: Int32?, linkedChatId: Int64?, location: Api.ChannelLocation?, slowmodeSeconds: Int32?, slowmodeNextSendDate: Int32?, statsDc: Int32?, pts: Int32, call: Api.InputGroupCall?, ttlPeriod: Int32?, pendingSuggestions: [String]?, groupcallDefaultJoinAs: Api.Peer?, themeEmoticon: String?, requestsPending: Int32?, recentRequesters: [Int64]?, defaultSendAs: Api.Peer?, availableReactions: [String]?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .chatFull(let flags, let id, let about, let participants, let chatPhoto, let notifySettings, let exportedInvite, let botInfo, let pinnedMsgId, let folderId, let call, let ttlPeriod, let groupcallDefaultJoinAs, let themeEmoticon, let requestsPending, let recentRequesters): + case .chatFull(let flags, let id, let about, let participants, let chatPhoto, let notifySettings, let exportedInvite, let botInfo, let pinnedMsgId, let folderId, let call, let ttlPeriod, let groupcallDefaultJoinAs, let themeEmoticon, let requestsPending, let recentRequesters, let availableReactions): if boxed { - buffer.appendInt32(1185349556) + buffer.appendInt32(-779165146) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(id, buffer: buffer, boxed: false) @@ -193,10 +193,15 @@ public extension Api { for item in recentRequesters! { serializeInt64(item, buffer: buffer, boxed: false) }} + if Int(flags) & Int(1 << 18) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(availableReactions!.count)) + for item in availableReactions! { + serializeString(item, buffer: buffer, boxed: false) + }} break - case .channelFull(let flags, let id, let about, let participantsCount, let adminsCount, let kickedCount, let bannedCount, let onlineCount, let readInboxMaxId, let readOutboxMaxId, let unreadCount, let chatPhoto, let notifySettings, let exportedInvite, let botInfo, let migratedFromChatId, let migratedFromMaxId, let pinnedMsgId, let stickerset, let availableMinId, let folderId, let linkedChatId, let location, let slowmodeSeconds, let slowmodeNextSendDate, let statsDc, let pts, let call, let ttlPeriod, let pendingSuggestions, let groupcallDefaultJoinAs, let themeEmoticon, let requestsPending, let recentRequesters, let defaultSendAs): + case .channelFull(let flags, let id, let about, let participantsCount, let adminsCount, let kickedCount, let bannedCount, let onlineCount, let readInboxMaxId, let readOutboxMaxId, let unreadCount, let chatPhoto, let notifySettings, let exportedInvite, let botInfo, let migratedFromChatId, let migratedFromMaxId, let pinnedMsgId, let stickerset, let availableMinId, let folderId, let linkedChatId, let location, let slowmodeSeconds, let slowmodeNextSendDate, let statsDc, let pts, let call, let ttlPeriod, let pendingSuggestions, let groupcallDefaultJoinAs, let themeEmoticon, let requestsPending, let recentRequesters, let defaultSendAs, let availableReactions): if boxed { - buffer.appendInt32(1449537070) + buffer.appendInt32(-516145888) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(id, buffer: buffer, boxed: false) @@ -245,16 +250,21 @@ public extension Api { serializeInt64(item, buffer: buffer, boxed: false) }} if Int(flags) & Int(1 << 29) != 0 {defaultSendAs!.serialize(buffer, true)} + if Int(flags) & Int(1 << 30) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(availableReactions!.count)) + for item in availableReactions! { + serializeString(item, buffer: buffer, boxed: false) + }} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .chatFull(let flags, let id, let about, let participants, let chatPhoto, let notifySettings, let exportedInvite, let botInfo, let pinnedMsgId, let folderId, let call, let ttlPeriod, let groupcallDefaultJoinAs, let themeEmoticon, let requestsPending, let recentRequesters): - return ("chatFull", [("flags", flags), ("id", id), ("about", about), ("participants", participants), ("chatPhoto", chatPhoto), ("notifySettings", notifySettings), ("exportedInvite", exportedInvite), ("botInfo", botInfo), ("pinnedMsgId", pinnedMsgId), ("folderId", folderId), ("call", call), ("ttlPeriod", ttlPeriod), ("groupcallDefaultJoinAs", groupcallDefaultJoinAs), ("themeEmoticon", themeEmoticon), ("requestsPending", requestsPending), ("recentRequesters", recentRequesters)]) - case .channelFull(let flags, let id, let about, let participantsCount, let adminsCount, let kickedCount, let bannedCount, let onlineCount, let readInboxMaxId, let readOutboxMaxId, let unreadCount, let chatPhoto, let notifySettings, let exportedInvite, let botInfo, let migratedFromChatId, let migratedFromMaxId, let pinnedMsgId, let stickerset, let availableMinId, let folderId, let linkedChatId, let location, let slowmodeSeconds, let slowmodeNextSendDate, let statsDc, let pts, let call, let ttlPeriod, let pendingSuggestions, let groupcallDefaultJoinAs, let themeEmoticon, let requestsPending, let recentRequesters, let defaultSendAs): - return ("channelFull", [("flags", flags), ("id", id), ("about", about), ("participantsCount", participantsCount), ("adminsCount", adminsCount), ("kickedCount", kickedCount), ("bannedCount", bannedCount), ("onlineCount", onlineCount), ("readInboxMaxId", readInboxMaxId), ("readOutboxMaxId", readOutboxMaxId), ("unreadCount", unreadCount), ("chatPhoto", chatPhoto), ("notifySettings", notifySettings), ("exportedInvite", exportedInvite), ("botInfo", botInfo), ("migratedFromChatId", migratedFromChatId), ("migratedFromMaxId", migratedFromMaxId), ("pinnedMsgId", pinnedMsgId), ("stickerset", stickerset), ("availableMinId", availableMinId), ("folderId", folderId), ("linkedChatId", linkedChatId), ("location", location), ("slowmodeSeconds", slowmodeSeconds), ("slowmodeNextSendDate", slowmodeNextSendDate), ("statsDc", statsDc), ("pts", pts), ("call", call), ("ttlPeriod", ttlPeriod), ("pendingSuggestions", pendingSuggestions), ("groupcallDefaultJoinAs", groupcallDefaultJoinAs), ("themeEmoticon", themeEmoticon), ("requestsPending", requestsPending), ("recentRequesters", recentRequesters), ("defaultSendAs", defaultSendAs)]) + case .chatFull(let flags, let id, let about, let participants, let chatPhoto, let notifySettings, let exportedInvite, let botInfo, let pinnedMsgId, let folderId, let call, let ttlPeriod, let groupcallDefaultJoinAs, let themeEmoticon, let requestsPending, let recentRequesters, let availableReactions): + return ("chatFull", [("flags", flags), ("id", id), ("about", about), ("participants", participants), ("chatPhoto", chatPhoto), ("notifySettings", notifySettings), ("exportedInvite", exportedInvite), ("botInfo", botInfo), ("pinnedMsgId", pinnedMsgId), ("folderId", folderId), ("call", call), ("ttlPeriod", ttlPeriod), ("groupcallDefaultJoinAs", groupcallDefaultJoinAs), ("themeEmoticon", themeEmoticon), ("requestsPending", requestsPending), ("recentRequesters", recentRequesters), ("availableReactions", availableReactions)]) + case .channelFull(let flags, let id, let about, let participantsCount, let adminsCount, let kickedCount, let bannedCount, let onlineCount, let readInboxMaxId, let readOutboxMaxId, let unreadCount, let chatPhoto, let notifySettings, let exportedInvite, let botInfo, let migratedFromChatId, let migratedFromMaxId, let pinnedMsgId, let stickerset, let availableMinId, let folderId, let linkedChatId, let location, let slowmodeSeconds, let slowmodeNextSendDate, let statsDc, let pts, let call, let ttlPeriod, let pendingSuggestions, let groupcallDefaultJoinAs, let themeEmoticon, let requestsPending, let recentRequesters, let defaultSendAs, let availableReactions): + return ("channelFull", [("flags", flags), ("id", id), ("about", about), ("participantsCount", participantsCount), ("adminsCount", adminsCount), ("kickedCount", kickedCount), ("bannedCount", bannedCount), ("onlineCount", onlineCount), ("readInboxMaxId", readInboxMaxId), ("readOutboxMaxId", readOutboxMaxId), ("unreadCount", unreadCount), ("chatPhoto", chatPhoto), ("notifySettings", notifySettings), ("exportedInvite", exportedInvite), ("botInfo", botInfo), ("migratedFromChatId", migratedFromChatId), ("migratedFromMaxId", migratedFromMaxId), ("pinnedMsgId", pinnedMsgId), ("stickerset", stickerset), ("availableMinId", availableMinId), ("folderId", folderId), ("linkedChatId", linkedChatId), ("location", location), ("slowmodeSeconds", slowmodeSeconds), ("slowmodeNextSendDate", slowmodeNextSendDate), ("statsDc", statsDc), ("pts", pts), ("call", call), ("ttlPeriod", ttlPeriod), ("pendingSuggestions", pendingSuggestions), ("groupcallDefaultJoinAs", groupcallDefaultJoinAs), ("themeEmoticon", themeEmoticon), ("requestsPending", requestsPending), ("recentRequesters", recentRequesters), ("defaultSendAs", defaultSendAs), ("availableReactions", availableReactions)]) } } @@ -307,6 +317,10 @@ public extension Api { if Int(_1!) & Int(1 << 17) != 0 {if let _ = reader.readInt32() { _16 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) } } + var _17: [String]? + if Int(_1!) & Int(1 << 18) != 0 {if let _ = reader.readInt32() { + _17 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -323,8 +337,9 @@ public extension Api { let _c14 = (Int(_1!) & Int(1 << 16) == 0) || _14 != nil let _c15 = (Int(_1!) & Int(1 << 17) == 0) || _15 != nil let _c16 = (Int(_1!) & Int(1 << 17) == 0) || _16 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 { - return Api.ChatFull.chatFull(flags: _1!, id: _2!, about: _3!, participants: _4!, chatPhoto: _5, notifySettings: _6!, exportedInvite: _7, botInfo: _8, pinnedMsgId: _9, folderId: _10, call: _11, ttlPeriod: _12, groupcallDefaultJoinAs: _13, themeEmoticon: _14, requestsPending: _15, recentRequesters: _16) + let _c17 = (Int(_1!) & Int(1 << 18) == 0) || _17 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 { + return Api.ChatFull.chatFull(flags: _1!, id: _2!, about: _3!, participants: _4!, chatPhoto: _5, notifySettings: _6!, exportedInvite: _7, botInfo: _8, pinnedMsgId: _9, folderId: _10, call: _11, ttlPeriod: _12, groupcallDefaultJoinAs: _13, themeEmoticon: _14, requestsPending: _15, recentRequesters: _16, availableReactions: _17) } else { return nil @@ -423,6 +438,10 @@ public extension Api { if Int(_1!) & Int(1 << 29) != 0 {if let signature = reader.readInt32() { _35 = Api.parse(reader, signature: signature) as? Api.Peer } } + var _36: [String]? + if Int(_1!) & Int(1 << 30) != 0 {if let _ = reader.readInt32() { + _36 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -458,8 +477,9 @@ public extension Api { let _c33 = (Int(_1!) & Int(1 << 28) == 0) || _33 != nil let _c34 = (Int(_1!) & Int(1 << 28) == 0) || _34 != nil let _c35 = (Int(_1!) & Int(1 << 29) == 0) || _35 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 && _c23 && _c24 && _c25 && _c26 && _c27 && _c28 && _c29 && _c30 && _c31 && _c32 && _c33 && _c34 && _c35 { - return Api.ChatFull.channelFull(flags: _1!, id: _2!, about: _3!, participantsCount: _4, adminsCount: _5, kickedCount: _6, bannedCount: _7, onlineCount: _8, readInboxMaxId: _9!, readOutboxMaxId: _10!, unreadCount: _11!, chatPhoto: _12!, notifySettings: _13!, exportedInvite: _14, botInfo: _15!, migratedFromChatId: _16, migratedFromMaxId: _17, pinnedMsgId: _18, stickerset: _19, availableMinId: _20, folderId: _21, linkedChatId: _22, location: _23, slowmodeSeconds: _24, slowmodeNextSendDate: _25, statsDc: _26, pts: _27!, call: _28, ttlPeriod: _29, pendingSuggestions: _30, groupcallDefaultJoinAs: _31, themeEmoticon: _32, requestsPending: _33, recentRequesters: _34, defaultSendAs: _35) + let _c36 = (Int(_1!) & Int(1 << 30) == 0) || _36 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 && _c23 && _c24 && _c25 && _c26 && _c27 && _c28 && _c29 && _c30 && _c31 && _c32 && _c33 && _c34 && _c35 && _c36 { + return Api.ChatFull.channelFull(flags: _1!, id: _2!, about: _3!, participantsCount: _4, adminsCount: _5, kickedCount: _6, bannedCount: _7, onlineCount: _8, readInboxMaxId: _9!, readOutboxMaxId: _10!, unreadCount: _11!, chatPhoto: _12!, notifySettings: _13!, exportedInvite: _14, botInfo: _15!, migratedFromChatId: _16, migratedFromMaxId: _17, pinnedMsgId: _18, stickerset: _19, availableMinId: _20, folderId: _21, linkedChatId: _22, location: _23, slowmodeSeconds: _24, slowmodeNextSendDate: _25, statsDc: _26, pts: _27!, call: _28, ttlPeriod: _29, pendingSuggestions: _30, groupcallDefaultJoinAs: _31, themeEmoticon: _32, requestsPending: _33, recentRequesters: _34, defaultSendAs: _35, availableReactions: _36) } else { return nil @@ -4857,6 +4877,7 @@ public extension Api { case updateBotCommands(peer: Api.Peer, botId: Int64, commands: [Api.BotCommand]) case updatePendingJoinRequests(peer: Api.Peer, requestsPending: Int32, recentRequesters: [Int64]) case updateBotChatInviteRequester(peer: Api.Peer, date: Int32, userId: Int64, about: String, invite: Api.ExportedChatInvite, qts: Int32) + case updateMessageReactions(peer: Api.Peer, msgId: Int32, reactions: Api.MessageReactions) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -5694,6 +5715,14 @@ public extension Api { invite.serialize(buffer, true) serializeInt32(qts, buffer: buffer, boxed: false) break + case .updateMessageReactions(let peer, let msgId, let reactions): + if boxed { + buffer.appendInt32(357013699) + } + peer.serialize(buffer, true) + serializeInt32(msgId, buffer: buffer, boxed: false) + reactions.serialize(buffer, true) + break } } @@ -5889,6 +5918,8 @@ public extension Api { return ("updatePendingJoinRequests", [("peer", peer), ("requestsPending", requestsPending), ("recentRequesters", recentRequesters)]) case .updateBotChatInviteRequester(let peer, let date, let userId, let about, let invite, let qts): return ("updateBotChatInviteRequester", [("peer", peer), ("date", date), ("userId", userId), ("about", about), ("invite", invite), ("qts", qts)]) + case .updateMessageReactions(let peer, let msgId, let reactions): + return ("updateMessageReactions", [("peer", peer), ("msgId", msgId), ("reactions", reactions)]) } } @@ -7599,6 +7630,27 @@ public extension Api { return nil } } + public static func parse_updateMessageReactions(_ reader: BufferReader) -> Update? { + var _1: Api.Peer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _2: Int32? + _2 = reader.readInt32() + var _3: Api.MessageReactions? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.MessageReactions + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.Update.updateMessageReactions(peer: _1!, msgId: _2!, reactions: _3!) + } + else { + return nil + } + } } public enum PopularContact: TypeConstructorDescription { @@ -8868,6 +8920,60 @@ public extension Api { } } + } + public enum MessageReactions: TypeConstructorDescription { + case messageReactions(flags: Int32, results: [Api.ReactionCount], recentReactons: [Api.MessageUserReaction]?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .messageReactions(let flags, let results, let recentReactons): + if boxed { + buffer.appendInt32(142306870) + } + serializeInt32(flags, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(results.count)) + for item in results { + item.serialize(buffer, true) + } + if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(recentReactons!.count)) + for item in recentReactons! { + item.serialize(buffer, true) + }} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .messageReactions(let flags, let results, let recentReactons): + return ("messageReactions", [("flags", flags), ("results", results), ("recentReactons", recentReactons)]) + } + } + + public static func parse_messageReactions(_ reader: BufferReader) -> MessageReactions? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.ReactionCount]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ReactionCount.self) + } + var _3: [Api.MessageUserReaction]? + if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageUserReaction.self) + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + if _c1 && _c2 && _c3 { + return Api.MessageReactions.messageReactions(flags: _1!, results: _2!, recentReactons: _3) + } + else { + return nil + } + } + } public enum Poll: TypeConstructorDescription { case poll(id: Int64, flags: Int32, question: String, answers: [Api.PollAnswer], closePeriod: Int32?, closeDate: Int32?) @@ -17551,7 +17657,7 @@ public extension Api { } public enum Message: TypeConstructorDescription { case messageEmpty(flags: Int32, id: Int32, peerId: Api.Peer?) - case message(flags: Int32, id: Int32, fromId: Api.Peer?, peerId: Api.Peer, fwdFrom: Api.MessageFwdHeader?, viaBotId: Int64?, replyTo: Api.MessageReplyHeader?, date: Int32, message: String, media: Api.MessageMedia?, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, views: Int32?, forwards: Int32?, replies: Api.MessageReplies?, editDate: Int32?, postAuthor: String?, groupedId: Int64?, restrictionReason: [Api.RestrictionReason]?, ttlPeriod: Int32?) + case message(flags: Int32, id: Int32, fromId: Api.Peer?, peerId: Api.Peer, fwdFrom: Api.MessageFwdHeader?, viaBotId: Int64?, replyTo: Api.MessageReplyHeader?, date: Int32, message: String, media: Api.MessageMedia?, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, views: Int32?, forwards: Int32?, replies: Api.MessageReplies?, editDate: Int32?, postAuthor: String?, groupedId: Int64?, reactions: Api.MessageReactions?, restrictionReason: [Api.RestrictionReason]?, ttlPeriod: Int32?) case messageService(flags: Int32, id: Int32, fromId: Api.Peer?, peerId: Api.Peer, replyTo: Api.MessageReplyHeader?, date: Int32, action: Api.MessageAction, ttlPeriod: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { @@ -17564,9 +17670,9 @@ public extension Api { serializeInt32(id, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {peerId!.serialize(buffer, true)} break - case .message(let flags, let id, let fromId, let peerId, let fwdFrom, let viaBotId, let replyTo, let date, let message, let media, let replyMarkup, let entities, let views, let forwards, let replies, let editDate, let postAuthor, let groupedId, let restrictionReason, let ttlPeriod): + case .message(let flags, let id, let fromId, let peerId, let fwdFrom, let viaBotId, let replyTo, let date, let message, let media, let replyMarkup, let entities, let views, let forwards, let replies, let editDate, let postAuthor, let groupedId, let reactions, let restrictionReason, let ttlPeriod): if boxed { - buffer.appendInt32(-2049520670) + buffer.appendInt32(940666592) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(id, buffer: buffer, boxed: false) @@ -17590,6 +17696,7 @@ public extension Api { if Int(flags) & Int(1 << 15) != 0 {serializeInt32(editDate!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 16) != 0 {serializeString(postAuthor!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 17) != 0 {serializeInt64(groupedId!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 20) != 0 {reactions!.serialize(buffer, true)} if Int(flags) & Int(1 << 22) != 0 {buffer.appendInt32(481674261) buffer.appendInt32(Int32(restrictionReason!.count)) for item in restrictionReason! { @@ -17617,8 +17724,8 @@ public extension Api { switch self { case .messageEmpty(let flags, let id, let peerId): return ("messageEmpty", [("flags", flags), ("id", id), ("peerId", peerId)]) - case .message(let flags, let id, let fromId, let peerId, let fwdFrom, let viaBotId, let replyTo, let date, let message, let media, let replyMarkup, let entities, let views, let forwards, let replies, let editDate, let postAuthor, let groupedId, let restrictionReason, let ttlPeriod): - return ("message", [("flags", flags), ("id", id), ("fromId", fromId), ("peerId", peerId), ("fwdFrom", fwdFrom), ("viaBotId", viaBotId), ("replyTo", replyTo), ("date", date), ("message", message), ("media", media), ("replyMarkup", replyMarkup), ("entities", entities), ("views", views), ("forwards", forwards), ("replies", replies), ("editDate", editDate), ("postAuthor", postAuthor), ("groupedId", groupedId), ("restrictionReason", restrictionReason), ("ttlPeriod", ttlPeriod)]) + case .message(let flags, let id, let fromId, let peerId, let fwdFrom, let viaBotId, let replyTo, let date, let message, let media, let replyMarkup, let entities, let views, let forwards, let replies, let editDate, let postAuthor, let groupedId, let reactions, let restrictionReason, let ttlPeriod): + return ("message", [("flags", flags), ("id", id), ("fromId", fromId), ("peerId", peerId), ("fwdFrom", fwdFrom), ("viaBotId", viaBotId), ("replyTo", replyTo), ("date", date), ("message", message), ("media", media), ("replyMarkup", replyMarkup), ("entities", entities), ("views", views), ("forwards", forwards), ("replies", replies), ("editDate", editDate), ("postAuthor", postAuthor), ("groupedId", groupedId), ("reactions", reactions), ("restrictionReason", restrictionReason), ("ttlPeriod", ttlPeriod)]) case .messageService(let flags, let id, let fromId, let peerId, let replyTo, let date, let action, let ttlPeriod): return ("messageService", [("flags", flags), ("id", id), ("fromId", fromId), ("peerId", peerId), ("replyTo", replyTo), ("date", date), ("action", action), ("ttlPeriod", ttlPeriod)]) } @@ -17696,12 +17803,16 @@ public extension Api { if Int(_1!) & Int(1 << 16) != 0 {_17 = parseString(reader) } var _18: Int64? if Int(_1!) & Int(1 << 17) != 0 {_18 = reader.readInt64() } - var _19: [Api.RestrictionReason]? - if Int(_1!) & Int(1 << 22) != 0 {if let _ = reader.readInt32() { - _19 = Api.parseVector(reader, elementSignature: 0, elementType: Api.RestrictionReason.self) + var _19: Api.MessageReactions? + if Int(_1!) & Int(1 << 20) != 0 {if let signature = reader.readInt32() { + _19 = Api.parse(reader, signature: signature) as? Api.MessageReactions } } - var _20: Int32? - if Int(_1!) & Int(1 << 25) != 0 {_20 = reader.readInt32() } + var _20: [Api.RestrictionReason]? + if Int(_1!) & Int(1 << 22) != 0 {if let _ = reader.readInt32() { + _20 = Api.parseVector(reader, elementSignature: 0, elementType: Api.RestrictionReason.self) + } } + var _21: Int32? + if Int(_1!) & Int(1 << 25) != 0 {_21 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = (Int(_1!) & Int(1 << 8) == 0) || _3 != nil @@ -17720,10 +17831,11 @@ public extension Api { let _c16 = (Int(_1!) & Int(1 << 15) == 0) || _16 != nil let _c17 = (Int(_1!) & Int(1 << 16) == 0) || _17 != nil let _c18 = (Int(_1!) & Int(1 << 17) == 0) || _18 != nil - let _c19 = (Int(_1!) & Int(1 << 22) == 0) || _19 != nil - let _c20 = (Int(_1!) & Int(1 << 25) == 0) || _20 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 { - return Api.Message.message(flags: _1!, id: _2!, fromId: _3, peerId: _4!, fwdFrom: _5, viaBotId: _6, replyTo: _7, date: _8!, message: _9!, media: _10, replyMarkup: _11, entities: _12, views: _13, forwards: _14, replies: _15, editDate: _16, postAuthor: _17, groupedId: _18, restrictionReason: _19, ttlPeriod: _20) + let _c19 = (Int(_1!) & Int(1 << 20) == 0) || _19 != nil + let _c20 = (Int(_1!) & Int(1 << 22) == 0) || _20 != nil + let _c21 = (Int(_1!) & Int(1 << 25) == 0) || _21 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 { + return Api.Message.message(flags: _1!, id: _2!, fromId: _3, peerId: _4!, fwdFrom: _5, viaBotId: _6, replyTo: _7, date: _8!, message: _9!, media: _10, replyMarkup: _11, entities: _12, views: _13, forwards: _14, replies: _15, editDate: _16, postAuthor: _17, groupedId: _18, reactions: _19, restrictionReason: _20, ttlPeriod: _21) } else { return nil @@ -19734,6 +19846,48 @@ public extension Api { return Api.BaseTheme.baseThemeArctic } + } + public enum ReactionCount: TypeConstructorDescription { + case reactionCount(flags: Int32, reaction: String, count: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .reactionCount(let flags, let reaction, let count): + if boxed { + buffer.appendInt32(1873957073) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(reaction, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .reactionCount(let flags, let reaction, let count): + return ("reactionCount", [("flags", flags), ("reaction", reaction), ("count", count)]) + } + } + + public static func parse_reactionCount(_ reader: BufferReader) -> ReactionCount? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.ReactionCount.reactionCount(flags: _1!, reaction: _2!, count: _3!) + } + else { + return nil + } + } + } public enum MessagesFilter: TypeConstructorDescription { case inputMessagesFilterEmpty @@ -20034,6 +20188,106 @@ public extension Api { } } + } + public enum AvailableReaction: TypeConstructorDescription { + case availableReaction(reaction: String, title: String, staticIcon: Api.Document, selectAnimation: Api.Document, activateAnimation: Api.Document, effectAnimation: Api.Document) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .availableReaction(let reaction, let title, let staticIcon, let selectAnimation, let activateAnimation, let effectAnimation): + if boxed { + buffer.appendInt32(1679961905) + } + serializeString(reaction, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + staticIcon.serialize(buffer, true) + selectAnimation.serialize(buffer, true) + activateAnimation.serialize(buffer, true) + effectAnimation.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .availableReaction(let reaction, let title, let staticIcon, let selectAnimation, let activateAnimation, let effectAnimation): + return ("availableReaction", [("reaction", reaction), ("title", title), ("staticIcon", staticIcon), ("selectAnimation", selectAnimation), ("activateAnimation", activateAnimation), ("effectAnimation", effectAnimation)]) + } + } + + public static func parse_availableReaction(_ reader: BufferReader) -> AvailableReaction? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + var _3: Api.Document? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.Document + } + var _4: Api.Document? + if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.Document + } + var _5: Api.Document? + if let signature = reader.readInt32() { + _5 = Api.parse(reader, signature: signature) as? Api.Document + } + var _6: Api.Document? + if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.Document + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.AvailableReaction.availableReaction(reaction: _1!, title: _2!, staticIcon: _3!, selectAnimation: _4!, activateAnimation: _5!, effectAnimation: _6!) + } + else { + return nil + } + } + + } + public enum MessageUserReaction: TypeConstructorDescription { + case messageUserReaction(userId: Int64, reaction: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .messageUserReaction(let userId, let reaction): + if boxed { + buffer.appendInt32(-1826077446) + } + serializeInt64(userId, buffer: buffer, boxed: false) + serializeString(reaction, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .messageUserReaction(let userId, let reaction): + return ("messageUserReaction", [("userId", userId), ("reaction", reaction)]) + } + } + + public static func parse_messageUserReaction(_ reader: BufferReader) -> MessageUserReaction? { + var _1: Int64? + _1 = reader.readInt64() + var _2: String? + _2 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.MessageUserReaction.messageUserReaction(userId: _1!, reaction: _2!) + } + else { + return nil + } + } + } public enum BotInlineMessage: TypeConstructorDescription { case botInlineMessageMediaAuto(flags: Int32, message: String, entities: [Api.MessageEntity]?, replyMarkup: Api.ReplyMarkup?) diff --git a/submodules/TelegramApi/Sources/Api4.swift b/submodules/TelegramApi/Sources/Api4.swift index b6495fb5bd..15a5792e21 100644 --- a/submodules/TelegramApi/Sources/Api4.swift +++ b/submodules/TelegramApi/Sources/Api4.swift @@ -4503,6 +4503,94 @@ public extension Api { return result }) } + + public static func sendReaction(flags: Int32, peer: Api.InputPeer, msgId: Int32, reaction: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(627641572) + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + serializeInt32(msgId, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(reaction!, buffer: buffer, boxed: false)} + return (FunctionDescription(name: "messages.sendReaction", parameters: [("flags", flags), ("peer", peer), ("msgId", msgId), ("reaction", reaction)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } + + public static func getMessagesReactions(peer: Api.InputPeer, id: [Int32]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1950707482) + peer.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(id.count)) + for item in id { + serializeInt32(item, buffer: buffer, boxed: false) + } + return (FunctionDescription(name: "messages.getMessagesReactions", parameters: [("peer", peer), ("id", id)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } + + public static func getMessageReactionsList(flags: Int32, peer: Api.InputPeer, id: Int32, reaction: String?, offset: String?, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-521245833) + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + serializeInt32(id, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(reaction!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {serializeString(offset!, buffer: buffer, boxed: false)} + serializeInt32(limit, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.getMessageReactionsList", parameters: [("flags", flags), ("peer", peer), ("id", id), ("reaction", reaction), ("offset", offset), ("limit", limit)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.MessageReactionsList? in + let reader = BufferReader(buffer) + var result: Api.messages.MessageReactionsList? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.MessageReactionsList + } + return result + }) + } + + public static func setChatAvailableReactions(peer: Api.InputPeer, availableReactions: [String]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(335875750) + peer.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(availableReactions.count)) + for item in availableReactions { + serializeString(item, buffer: buffer, boxed: false) + } + return (FunctionDescription(name: "messages.setChatAvailableReactions", parameters: [("peer", peer), ("availableReactions", availableReactions)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } + + public static func getAvailableReactions(hash: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(417243308) + serializeInt32(hash, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.getAvailableReactions", parameters: [("hash", hash)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.AvailableReactions? in + let reader = BufferReader(buffer) + var result: Api.messages.AvailableReactions? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.AvailableReactions + } + return result + }) + } } public struct channels { public static func readHistory(channel: Api.InputChannel, maxId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index b9b4ec40c2..8012a5d5ae 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -1048,6 +1048,7 @@ public class Account { self.managedOperationsDisposable.add(managedApplyPendingMessageReactionsActions(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) self.managedOperationsDisposable.add(managedSynchronizeEmojiKeywordsOperations(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedApplyPendingScheduledMessagesActions(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) + self.managedOperationsDisposable.add(managedSynchronizeAvailableReactions(postbox: self.postbox, network: self.network).start()) if !supplementary { self.managedOperationsDisposable.add(managedChatListFilters(postbox: self.postbox, network: self.network, accountPeerId: self.peerId).start()) diff --git a/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift index 221b859e23..2a4b008e46 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift @@ -2,7 +2,7 @@ import Foundation import Postbox import TelegramApi -/*extension ReactionsMessageAttribute { +extension ReactionsMessageAttribute { func withUpdatedResults(_ reactions: Api.MessageReactions) -> ReactionsMessageAttribute { switch reactions { case let .messageReactions(flags, results, _): @@ -32,7 +32,7 @@ import TelegramApi return ReactionsMessageAttribute(reactions: reactions) } } -}*/ +} public func mergedMessageReactions(attributes: [MessageAttribute]) -> ReactionsMessageAttribute? { var current: ReactionsMessageAttribute? @@ -84,7 +84,7 @@ public func mergedMessageReactions(attributes: [MessageAttribute]) -> ReactionsM } } -/*extension ReactionsMessageAttribute { +extension ReactionsMessageAttribute { convenience init(apiReactions: Api.MessageReactions) { switch apiReactions { case let .messageReactions(_, results, _): @@ -96,4 +96,4 @@ public func mergedMessageReactions(attributes: [MessageAttribute]) -> ReactionsM }) } } -}*/ +} diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 8e0bc7539a..e26b60dd06 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -116,7 +116,7 @@ public func tagsForStoreMessage(incoming: Bool, attributes: [MessageAttribute], func apiMessagePeerId(_ messsage: Api.Message) -> PeerId? { switch messsage { - case let .message(_, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): let chatPeerId = messagePeerId return chatPeerId.peerId case let .messageEmpty(_, _, peerId): @@ -132,7 +132,7 @@ func apiMessagePeerId(_ messsage: Api.Message) -> PeerId? { func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { switch message { - case let .message(_, _, fromId, chatPeerId, fwdHeader, viaBotId, _, _, _, media, _, entities, _, _, _, _, _, _, _, _): + case let .message(_, _, fromId, chatPeerId, fwdHeader, viaBotId, _, _, _, media, _, entities, _, _, _, _, _, _, _, _, _): let peerId: PeerId = chatPeerId.peerId var result = [peerId] @@ -228,7 +228,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { func apiMessageAssociatedMessageIds(_ message: Api.Message) -> [MessageId]? { switch message { - case let .message(_, _, _, chatPeerId, _, _, replyTo, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, _, _, chatPeerId, _, _, replyTo, _, _, _, _, _, _, _, _, _, _, _, _, _, _): if let replyTo = replyTo { let peerId: PeerId = chatPeerId.peerId @@ -374,7 +374,7 @@ func messageTextEntitiesFromApiEntities(_ entities: [Api.MessageEntity]) -> [Mes extension StoreMessage { convenience init?(apiMessage: Api.Message, namespace: MessageId.Namespace = Namespaces.Message.Cloud) { switch apiMessage { - case let .message(flags, id, fromId, chatPeerId, fwdFrom, viaBotId, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId/*, reactions*/, restrictionReason, ttlPeriod): + case let .message(flags, id, fromId, chatPeerId, fwdFrom, viaBotId, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId, reactions, restrictionReason, ttlPeriod): let resolvedFromId = fromId?.peerId ?? chatPeerId.peerId let peerId: PeerId @@ -546,9 +546,9 @@ extension StoreMessage { attributes.append(ContentRequiresValidationMessageAttribute()) } - /*if let reactions = reactions { + if let reactions = reactions { attributes.append(ReactionsMessageAttribute(apiReactions: reactions)) - }*/ + } if let replies = replies { let recentRepliersPeerIds: [PeerId]? diff --git a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift index 4fb823fb76..f903311bfa 100644 --- a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift @@ -813,7 +813,7 @@ public final class AccountViewTracker { } public func updateReactionsForMessageIds(messageIds: Set) { - /*self.queue.async { + self.queue.async { var addedMessageIds: [MessageId] = [] let timestamp = Int32(CFAbsoluteTimeGetCurrent()) for messageId in messageIds { @@ -870,7 +870,7 @@ public final class AccountViewTracker { break loop } } - return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) + return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) }) default: break @@ -892,7 +892,7 @@ public final class AccountViewTracker { } } } - }*/ + } } public func updateSeenLiveLocationForMessageIds(messageIds: Set) { diff --git a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift index 4de1450257..1d7e8ecf57 100644 --- a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift +++ b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift @@ -64,7 +64,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes var updatedTimestamp: Int32? if let apiMessage = apiMessage { switch apiMessage { - case let .message(_, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _): updatedTimestamp = date case .messageEmpty: break diff --git a/submodules/TelegramCore/Sources/State/AvailableReactions.swift b/submodules/TelegramCore/Sources/State/AvailableReactions.swift new file mode 100644 index 0000000000..c041c41fd2 --- /dev/null +++ b/submodules/TelegramCore/Sources/State/AvailableReactions.swift @@ -0,0 +1,258 @@ +import Foundation +import TelegramApi +import Postbox +import SwiftSignalKit + +public final class AvailableReactions: Equatable, Codable { + public final class Reaction: Equatable, Codable { + private enum CodingKeys: String, CodingKey { + case value + case title + case staticIcon + case selectAnimation + case activateAnimation + case effectAnimation + } + + public let value: String + public let title: String + public let staticIcon: TelegramMediaFile + public let selectAnimation: TelegramMediaFile + public let activateAnimation: TelegramMediaFile + public let effectAnimation: TelegramMediaFile + + public init( + value: String, + title: String, + staticIcon: TelegramMediaFile, + selectAnimation: TelegramMediaFile, + activateAnimation: TelegramMediaFile, + effectAnimation: TelegramMediaFile + ) { + self.value = value + self.title = title + self.staticIcon = staticIcon + self.selectAnimation = selectAnimation + self.activateAnimation = activateAnimation + self.effectAnimation = effectAnimation + } + + public static func ==(lhs: Reaction, rhs: Reaction) -> Bool { + if lhs.value != rhs.value { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.staticIcon != rhs.staticIcon { + return false + } + if lhs.selectAnimation != rhs.selectAnimation { + return false + } + if lhs.activateAnimation != rhs.activateAnimation { + return false + } + if lhs.effectAnimation != rhs.effectAnimation { + return false + } + return true + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.value = try container.decode(String.self, forKey: .value) + self.title = try container.decode(String.self, forKey: .title) + + let staticIconData = try container.decode(AdaptedPostboxDecoder.RawObjectData.self, forKey: .staticIcon) + self.staticIcon = TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: staticIconData.data))) + + let selectAnimationData = try container.decode(AdaptedPostboxDecoder.RawObjectData.self, forKey: .selectAnimation) + self.selectAnimation = TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: selectAnimationData.data))) + + let activateAnimationData = try container.decode(AdaptedPostboxDecoder.RawObjectData.self, forKey: .activateAnimation) + self.activateAnimation = TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: activateAnimationData.data))) + + let effectAnimationData = try container.decode(AdaptedPostboxDecoder.RawObjectData.self, forKey: .effectAnimation) + self.effectAnimation = TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: effectAnimationData.data))) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.value, forKey: .value) + try container.encode(self.title, forKey: .title) + + try container.encode(PostboxEncoder().encodeObjectToRawData(self.staticIcon), forKey: .staticIcon) + try container.encode(PostboxEncoder().encodeObjectToRawData(self.selectAnimation), forKey: .selectAnimation) + try container.encode(PostboxEncoder().encodeObjectToRawData(self.activateAnimation), forKey: .activateAnimation) + try container.encode(PostboxEncoder().encodeObjectToRawData(self.effectAnimation), forKey: .effectAnimation) + } + } + + private enum CodingKeys: String, CodingKey { + case hash + case reactions + } + + public let hash: Int32 + public let reactions: [Reaction] + + public init( + hash: Int32, + reactions: [Reaction] + ) { + self.hash = hash + self.reactions = reactions + } + + public static func ==(lhs: AvailableReactions, rhs: AvailableReactions) -> Bool { + if lhs.hash != rhs.hash { + return false + } + if lhs.reactions != rhs.reactions { + return false + } + return true + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.hash = try container.decode(Int32.self, forKey: .hash) + self.reactions = try container.decode([Reaction].self, forKey: .reactions) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.hash, forKey: .hash) + try container.encode(self.reactions, forKey: .reactions) + } +} + +private extension AvailableReactions.Reaction { + convenience init?(apiReaction: Api.AvailableReaction) { + switch apiReaction { + case let .availableReaction(reaction, title, staticIcon, selectAnimation, activateAnimation, effectAnimation): + guard let staticIconFile = telegramMediaFileFromApiDocument(staticIcon) else { + return nil + } + guard let selectAnimationFile = telegramMediaFileFromApiDocument(selectAnimation) else { + return nil + } + guard let activateAnimationFile = telegramMediaFileFromApiDocument(activateAnimation) else { + return nil + } + guard let effectAnimationFile = telegramMediaFileFromApiDocument(effectAnimation) else { + return nil + } + self.init( + value: reaction, + title: title, + staticIcon: staticIconFile, + selectAnimation: selectAnimationFile, + activateAnimation: activateAnimationFile, + effectAnimation: effectAnimationFile + ) + } + } +} + +func _internal_cachedAvailableReactions(postbox: Postbox) -> Signal { + return postbox.transaction { transaction -> AvailableReactions? in + return _internal_cachedAvailableReactions(transaction: transaction) + } +} + +func _internal_cachedAvailableReactions(transaction: Transaction) -> AvailableReactions? { + let key = ValueBoxKey(length: 8) + key.setInt64(0, value: 0) + + let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.availableReactions, key: key))?.get(AvailableReactions.self) + if let cached = cached { + return cached + } else { + return nil + } +} + +func _internal_setCachedAvailableReactions(transaction: Transaction, availableReactions: AvailableReactions) { + let key = ValueBoxKey(length: 8) + key.setInt64(0, value: 0) + + if let entry = CodableEntry(availableReactions) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.availableReactions, key: key), entry: entry, collectionSpec: ItemCacheCollectionSpec(lowWaterItemCount: 10, highWaterItemCount: 10)) + } +} + +func managedSynchronizeAvailableReactions(postbox: Postbox, network: Network) -> Signal { + let poll = Signal { subscriber in + let signal: Signal = _internal_cachedAvailableReactions(postbox: postbox) + |> mapToSignal { current in + return (network.request(Api.functions.messages.getAvailableReactions(hash: current?.hash ?? 0)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + return postbox.transaction { transaction -> Signal in + guard let result = result else { + return .complete() + } + switch result { + case let .availableReactions(hash, reactions): + let availableReactions = AvailableReactions( + hash: hash, + reactions: reactions.compactMap(AvailableReactions.Reaction.init(apiReaction:)) + ) + _internal_setCachedAvailableReactions(transaction: transaction, availableReactions: availableReactions) + case .availableReactionsNotModified: + break + } + + var signals: [Signal] = [] + + if let availableReactions = _internal_cachedAvailableReactions(transaction: transaction) { + var resources: [MediaResource] = [] + + for reaction in availableReactions.reactions { + resources.append(reaction.staticIcon.resource) + resources.append(reaction.selectAnimation.resource) + resources.append(reaction.activateAnimation.resource) + resources.append(reaction.effectAnimation.resource) + } + + for resource in resources { + signals.append( + fetchedMediaResource(mediaBox: postbox.mediaBox, reference: .standalone(resource: resource)) + |> ignoreValues + |> `catch` { _ -> Signal in + return .complete() + } + ) + } + } + + return combineLatest(signals) + |> ignoreValues + } + |> switchToLatest + }) + } + + return signal.start(completed: { + subscriber.putCompletion() + }) + } + + return ( + poll + |> then( + .complete() + |> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()) + ) + ) + |> restart +} diff --git a/submodules/TelegramCore/Sources/State/MessageReactions.swift b/submodules/TelegramCore/Sources/State/MessageReactions.swift index 828f9ff0fd..454c3b44b2 100644 --- a/submodules/TelegramCore/Sources/State/MessageReactions.swift +++ b/submodules/TelegramCore/Sources/State/MessageReactions.swift @@ -32,8 +32,7 @@ private enum RequestUpdateMessageReactionError { } private func requestUpdateMessageReaction(postbox: Postbox, network: Network, stateManager: AccountStateManager, messageId: MessageId) -> Signal { - return .never() - /*return postbox.transaction { transaction -> (Peer, String?)? in + return postbox.transaction { transaction -> (Peer, String?)? in guard let peer = transaction.getPeer(messageId.peerId) else { return nil } @@ -89,7 +88,7 @@ private func requestUpdateMessageReaction(postbox: Postbox, network: Network, st |> castError(RequestUpdateMessageReactionError.self) |> ignoreValues } - }*/ + } } private final class ManagedApplyPendingMessageReactionsActionsHelper { diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index 4444f3e01c..1394b7a585 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 135 + return 136 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/State/UpdateMessageService.swift b/submodules/TelegramCore/Sources/State/UpdateMessageService.swift index cc46893f9c..7778a947fc 100644 --- a/submodules/TelegramCore/Sources/State/UpdateMessageService.swift +++ b/submodules/TelegramCore/Sources/State/UpdateMessageService.swift @@ -58,7 +58,7 @@ class UpdateMessageService: NSObject, MTMessageService { self.putNext(groups) } case let .updateShortChatMessage(flags, id, fromId, chatId, message, pts, ptsCount, date, fwdFrom, viaBotId, replyHeader, entities, ttlPeriod): - let generatedMessage = Api.Message.message(flags: flags, id: id, fromId: .peerUser(userId: fromId), peerId: Api.Peer.peerChat(chatId: chatId), fwdFrom: fwdFrom, viaBotId: viaBotId, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil/*, reactions: nil*/, restrictionReason: nil, ttlPeriod: ttlPeriod) + let generatedMessage = Api.Message.message(flags: flags, id: id, fromId: .peerUser(userId: fromId), peerId: Api.Peer.peerChat(chatId: chatId), fwdFrom: fwdFrom, viaBotId: viaBotId, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod) let update = Api.Update.updateNewMessage(message: generatedMessage, pts: pts, ptsCount: ptsCount) let groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil) if groups.count != 0 { @@ -74,7 +74,7 @@ class UpdateMessageService: NSObject, MTMessageService { let generatedPeerId = Api.Peer.peerUser(userId: userId) - let generatedMessage = Api.Message.message(flags: flags, id: id, fromId: generatedFromId, peerId: generatedPeerId, fwdFrom: fwdFrom, viaBotId: viaBotId, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil/*, reactions: nil*/, restrictionReason: nil, ttlPeriod: ttlPeriod) + let generatedMessage = Api.Message.message(flags: flags, id: id, fromId: generatedFromId, peerId: generatedPeerId, fwdFrom: fwdFrom, viaBotId: viaBotId, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod) let update = Api.Update.updateNewMessage(message: generatedMessage, pts: pts, ptsCount: ptsCount) let groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil) if groups.count != 0 { diff --git a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift index 7cd9d2e01a..ad371823e9 100644 --- a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift +++ b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift @@ -90,7 +90,7 @@ extension Api.MessageMedia { extension Api.Message { var rawId: Int32 { switch self { - case let .message(_, id, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, id, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): return id case let .messageEmpty(_, id, _): return id @@ -101,7 +101,7 @@ extension Api.Message { func id(namespace: MessageId.Namespace = Namespaces.Message.Cloud) -> MessageId? { switch self { - case let .message(_, id, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, id, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): let peerId: PeerId = messagePeerId.peerId return MessageId(peerId: peerId, namespace: namespace, id: id) case let .messageEmpty(_, id, peerId): @@ -118,7 +118,7 @@ extension Api.Message { var timestamp: Int32? { switch self { - case let .message(_, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _): return date case let .messageService(_, _, _, _, _, date, _, _): return date @@ -129,7 +129,7 @@ extension Api.Message { var preCachedResources: [(MediaResource, Data)]? { switch self { - case let .message(_, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _): + case let .message(_, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _, _): return media?.preCachedResources default: return nil diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index ee5e3c8f28..0dca08190e 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -80,6 +80,7 @@ public struct Namespaces { public static let cachedPeerInvitationImporters: Int8 = 16 public static let cachedPeerExportedInvitations: Int8 = 17 public static let cachedSendAsPeers: Int8 = 18 + public static let availableReactions: Int8 = 19 } public struct UnorderedItemList { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index cbd73601ba..2d63b65899 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -267,14 +267,14 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee switch result { case let .chatFull(fullChat, chats, users): switch fullChat { - case let .chatFull(_, _, _, _, _, notifySettings, _, _, _, _, _, _, _, _, _, _): + case let .chatFull(_, _, _, _, _, notifySettings, _, _, _, _, _, _, _, _, _, _, _): transaction.updateCurrentPeerNotificationSettings([peerId: TelegramPeerNotificationSettings(apiSettings: notifySettings)]) case .channelFull: break } switch fullChat { - case let .chatFull(chatFullFlags, _, chatFullAbout, chatFullParticipants, chatFullChatPhoto, _, chatFullExportedInvite, chatFullBotInfo, chatFullPinnedMsgId, _, chatFullCall, _, chatFullGroupcallDefaultJoinAs, chatFullThemeEmoticon, chatFullRequestsPending, _): + case let .chatFull(chatFullFlags, _, chatFullAbout, chatFullParticipants, chatFullChatPhoto, _, chatFullExportedInvite, chatFullBotInfo, chatFullPinnedMsgId, _, chatFullCall, _, chatFullGroupcallDefaultJoinAs, chatFullThemeEmoticon, chatFullRequestsPending, _, _): var botInfos: [CachedPeerBotInfo] = [] for botInfo in chatFullBotInfo ?? [] { switch botInfo { @@ -396,14 +396,14 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee switch result { case let .chatFull(fullChat, chats, users): switch fullChat { - case let .channelFull(_, _, _, _, _, _, _, _, _, _, _, _, notifySettings, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .channelFull(_, _, _, _, _, _, _, _, _, _, _, _, notifySettings, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): transaction.updateCurrentPeerNotificationSettings([peerId: TelegramPeerNotificationSettings(apiSettings: notifySettings)]) case .chatFull: break } switch fullChat { - case let .channelFull(flags, _, about, participantsCount, adminsCount, kickedCount, bannedCount, _, _, _, _, chatPhoto, _, apiExportedInvite, apiBotInfos, migratedFromChatId, migratedFromMaxId, pinnedMsgId, stickerSet, minAvailableMsgId, _, linkedChatId, location, slowmodeSeconds, slowmodeNextSendDate, statsDc, _, inputCall, ttl, pendingSuggestions, groupcallDefaultJoinAs, themeEmoticon, requestsPending, _, defaultSendAs): + case let .channelFull(flags, _, about, participantsCount, adminsCount, kickedCount, bannedCount, _, _, _, _, chatPhoto, _, apiExportedInvite, apiBotInfos, migratedFromChatId, migratedFromMaxId, pinnedMsgId, stickerSet, minAvailableMsgId, _, linkedChatId, location, slowmodeSeconds, slowmodeNextSendDate, statsDc, _, inputCall, ttl, pendingSuggestions, groupcallDefaultJoinAs, themeEmoticon, requestsPending, _, defaultSendAs, _): var channelFlags = CachedChannelFlags() if (flags & (1 << 3)) != 0 { channelFlags.insert(.canDisplayParticipants) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift index c123d7319b..ea1f4ea76a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift @@ -95,5 +95,9 @@ public extension TelegramEngine { ) } } + + public func availableReactions() -> Signal { + return _internal_cachedAvailableReactions(postbox: self.account.postbox) + } } } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 9514e8bea8..e56a7b583f 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -247,6 +247,7 @@ swift_library( "//submodules/MeshAnimationCache:MeshAnimationCache", "//submodules/DirectMediaImageCache:DirectMediaImageCache", "//submodules/CodeInputView:CodeInputView", + "//submodules/Components/ReactionButtonListComponent:ReactionButtonListComponent", ] + select({ "@build_bazel_rules_apple//apple:ios_armv7": [], "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 3b413b7fdc..50e9fa9128 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -430,6 +430,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private weak var currentReactionContextController: ContextController? private weak var currentReactionContextItemNode: ListViewItemNode? + private weak var currentStandaloneReactionAnimation: StandaloneReactionAnimation? + private weak var currentStandaloneReactionItemNode: ListViewItemNode? + private var screenCaptureManager: ScreenCaptureDetectionManager? private let chatAdditionalDataDisposable = MetaDisposable() @@ -942,46 +945,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - let additionalAnimatedEmojiStickers = strongSelf.context.engine.stickers.loadedStickerPack(reference: .animatedEmojiAnimations, forceActualized: false) - |> map { animatedEmoji -> [String: [Int: StickerPackItem]] in - let sequence = "0️⃣1️⃣2️⃣3️⃣4️⃣5️⃣6️⃣7️⃣8️⃣9️⃣".strippedEmoji - var animatedEmojiStickers: [String: [Int: StickerPackItem]] = [:] - switch animatedEmoji { - case let .result(_, items, _): - for item in items { - let indexKeys = item.getStringRepresentationsOfIndexKeys() - if indexKeys.count > 1, let first = indexKeys.first, let last = indexKeys.last { - let emoji: String? - let indexEmoji: String? - if sequence.contains(first.strippedEmoji) { - emoji = last - indexEmoji = first - } else if sequence.contains(last.strippedEmoji) { - emoji = first - indexEmoji = last - } else { - emoji = nil - indexEmoji = nil - } - - if let emoji = emoji?.strippedEmoji, let indexEmoji = indexEmoji?.strippedEmoji.first, let strIndex = sequence.firstIndex(of: indexEmoji) { - let index = sequence.distance(from: sequence.startIndex, to: strIndex) - if animatedEmojiStickers[emoji] != nil { - animatedEmojiStickers[emoji]![index] = item - } else { - animatedEmojiStickers[emoji] = [index: item] - } - } - } - } - default: - break - } - return animatedEmojiStickers - } - - let _ = combineLatest(queue: .mainQueue(), contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction), strongSelf.context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false), additionalAnimatedEmojiStickers, ApplicationSpecificNotice.getChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager) - ).start(next: { actions, animatedEmojiStickers, additionalAnimatedEmojiStickers, chatTextSelectionTips in + let _ = combineLatest(queue: .mainQueue(), + contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction), + strongSelf.context.engine.stickers.availableReactions(), + ApplicationSpecificNotice.getChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager) + ).start(next: { actions, availableReactions, chatTextSelectionTips in var actions = actions guard let strongSelf = self, !actions.items.isEmpty else { @@ -1023,32 +991,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G actions.context = strongSelf.context - if canAddMessageReactions(message: message) { - if case let .result(_, stickers, _) = animatedEmojiStickers { - for fullSticker in additionalAnimatedEmojiStickers { - guard let fullStickerItem = fullSticker.value.first?.value else { - continue - } - inner: for sticker in stickers { - for attribute in sticker.file.attributes { - if case let .Sticker(displayText, _, _) = attribute { - if displayText == fullSticker.key { - actions.reactionItems.append(ReactionContextItem( - reaction: ReactionContextItem.Reaction(rawValue: displayText), - listAnimation: sticker.file, - applicationAnimation: fullStickerItem.file - )) - break inner - } - } - } - } - } + if canAddMessageReactions(message: message), let availableReactions = availableReactions { + for reaction in availableReactions.reactions { + actions.reactionItems.append(ReactionContextItem( + reaction: ReactionContextItem.Reaction(rawValue: reaction.value), + stillAnimation: reaction.selectAnimation, + listAnimation: reaction.activateAnimation, + applicationAnimation: reaction.effectAnimation + )) } - - actions.reactionItems.sort(by: { lhs, rhs in - return lhs.reaction.rawValue < rhs.reaction.rawValue - }) } let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: message, selectAll: selectAll)), items: .single(actions), recognizer: recognizer, gesture: gesture) @@ -1084,18 +1035,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let controller = controller else { return } - if let itemNode = itemNode, let (targetEmptyNode, targetFilledNode) = itemNode.targetReactionNode(value: updatedReaction) { + if let itemNode = itemNode, let targetView = itemNode.targetReactionView(value: updatedReaction) { strongSelf.currentReactionContextController = controller strongSelf.currentReactionContextItemNode = itemNode - controller.dismissWithReaction(value: updatedReaction, targetEmptyNode: targetEmptyNode, targetFilledNode: targetFilledNode, hideNode: true, completion: { [weak itemNode, weak targetFilledNode] in - guard let strongSelf = self, let itemNode = itemNode, let targetFilledNode = targetFilledNode else { + controller.dismissWithReaction(value: updatedReaction, targetView: targetView, hideNode: true, completion: { [weak itemNode, weak targetView] in + guard let strongSelf = self, let itemNode = itemNode, let targetView = targetView else { return } let _ = strongSelf let _ = itemNode - let _ = targetFilledNode + let _ = targetView /*let targetFrame = targetFilledNode.view.convert(targetFilledNode.bounds, to: itemNode.view).offsetBy(dx: 0.0, dy: itemNode.insets.top) @@ -1145,7 +1096,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.window?.presentInGlobalOverlay(controller) }) } - }, updateMessageReaction: { [weak self] message in + }, updateMessageReaction: { [weak self] message, value in guard let strongSelf = self else { return } @@ -1154,7 +1105,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - var updatedReaction: String? = "❤" + var updatedReaction: String? = value for attribute in message.attributes { if let attribute = attribute as? ReactionsMessageAttribute { for reaction in attribute.reactions { @@ -1171,21 +1122,48 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item { - if item.message.id == message.id { - itemNode.awaitingAppliedReaction = (updatedReaction, { [weak itemNode] in - if let updatedReaction = updatedReaction, let itemNode = itemNode, let (_, targetFilledNode) = itemNode.targetReactionNode(value: updatedReaction) { - targetFilledNode.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, damping: 90.0) - - if let strongSelf = self { - if strongSelf.selectPollOptionFeedback == nil { - strongSelf.selectPollOptionFeedback = HapticFeedback() - } - strongSelf.selectPollOptionFeedback?.tap() + if updatedReaction != nil { + strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item { + if item.message.id == message.id { + itemNode.awaitingAppliedReaction = (updatedReaction, { [weak itemNode] in + guard let strongSelf = self else { + return } - } - }) + if let updatedReaction = updatedReaction, let itemNode = itemNode, let item = itemNode.item, let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: updatedReaction) { + for reaction in availableReactions.reactions { + if reaction.value == updatedReaction { + let standaloneReactionAnimation = StandaloneReactionAnimation(context: strongSelf.context, theme: strongSelf.presentationData.theme, reaction: ReactionContextItem( + reaction: ReactionContextItem.Reaction(rawValue: reaction.value), + stillAnimation: reaction.selectAnimation, + listAnimation: reaction.activateAnimation, + applicationAnimation: reaction.effectAnimation + )) + + strongSelf.currentStandaloneReactionAnimation = standaloneReactionAnimation + strongSelf.currentStandaloneReactionItemNode = itemNode + + strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) + standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds + standaloneReactionAnimation.animateReactionSelection(targetView: targetView, hideNode: true, completion: { [weak standaloneReactionAnimation] in + standaloneReactionAnimation?.removeFromSupernode() + }) + + break + } + } + + /*targetView.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, damping: 90.0) + + if let strongSelf = self { + if strongSelf.selectPollOptionFeedback == nil { + strongSelf.selectPollOptionFeedback = HapticFeedback() + } + strongSelf.selectPollOptionFeedback?.tap() + }*/ + } + }) + } } } } @@ -4781,6 +4759,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + if let standaloneReactionItemNode = strongSelf.currentStandaloneReactionItemNode, let currentStandaloneReactionAnimation = strongSelf.currentStandaloneReactionAnimation { + if let itemNode = itemNode { + if itemNode === standaloneReactionItemNode { + currentStandaloneReactionAnimation.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition) + } + } else { + currentStandaloneReactionAnimation.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition) + } + } + strongSelf.chatDisplayNode.messageTransitionNode.addExternalOffset(offset: offset, transition: transition, itemNode: itemNode) } @@ -14319,15 +14307,8 @@ extension Peer { } func canAddMessageReactions(message: Message) -> Bool { - return false - /*if let peer = message.peers[message.id.peerId] { - if let channel = peer as? TelegramChannel { - if case .group = channel.info { - } else { - return false - } - } else if let _ = peer as? TelegramGroup { - } else { + if let peer = message.peers[message.id.peerId] { + if let _ = peer as? TelegramSecretChat { return false } } else { @@ -14338,5 +14319,5 @@ func canAddMessageReactions(message: Message) -> Bool { return false } } - return true*/ + return true } diff --git a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift index f7fe5294b8..54ebd5b6ad 100644 --- a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift @@ -51,7 +51,7 @@ public final class ChatControllerInteraction { let openPeer: (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void let openPeerMention: (String) -> Void let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void - let updateMessageReaction: (Message) -> Void + let updateMessageReaction: (Message, String) -> Void let activateMessagePinch: (PinchSourceContainerNode) -> Void let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void let navigateToMessage: (MessageId, MessageId) -> Void @@ -147,7 +147,7 @@ public final class ChatControllerInteraction { openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void, - updateMessageReaction: @escaping (Message) -> Void, + updateMessageReaction: @escaping (Message, String) -> Void, activateMessagePinch: @escaping (PinchSourceContainerNode) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, @@ -313,7 +313,7 @@ public final class ChatControllerInteraction { static var `default`: ChatControllerInteraction { return ChatControllerInteraction(openMessage: { _, _ in - return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, updateMessageReaction: { _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _ in return false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in + return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _ in return false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, presentControllerInCurrent: { _, _ in }, navigationController: { return nil }, chatControllerNode: { diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 8d48a75b67..890beafc65 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -312,7 +312,7 @@ private final class ChatHistoryTransactionOpaqueState { } } -private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHistoryView, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, animatedEmojiStickers: [String: [StickerPackItem]], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]], subject: ChatControllerSubject?, currentlyPlayingMessageId: MessageIndex?, isCopyProtectionEnabled: Bool) -> ChatMessageItemAssociatedData { +private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHistoryView, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, animatedEmojiStickers: [String: [StickerPackItem]], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]], subject: ChatControllerSubject?, currentlyPlayingMessageId: MessageIndex?, isCopyProtectionEnabled: Bool, availableReactions: AvailableReactions?) -> ChatMessageItemAssociatedData { var automaticMediaDownloadPeerType: MediaAutoDownloadPeerType = .channel var contactsPeerIds: Set = Set() var channelDiscussionGroup: ChatMessageItemAssociatedData.ChannelDiscussionGroupStatus = .unknown @@ -361,7 +361,7 @@ private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHist } } - return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadNetworkType: automaticDownloadNetworkType, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled) + return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadNetworkType: automaticDownloadNetworkType, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions) } private extension ChatHistoryLocationInput { @@ -479,6 +479,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { private let galleryHiddenMesageAndMediaDisposable = MetaDisposable() private let messageProcessingManager = ChatMessageThrottledProcessingManager() + private let messageWithReactionsProcessingManager = ChatMessageThrottledProcessingManager() let adSeenProcessingManager = ChatMessageThrottledProcessingManager() private let seenLiveLocationProcessingManager = ChatMessageThrottledProcessingManager() private let unsupportedMessageProcessingManager = ChatMessageThrottledProcessingManager() @@ -689,6 +690,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.messageProcessingManager.process = { [weak context] messageIds in context?.account.viewTracker.updateViewCountForMessageIds(messageIds: messageIds, clientId: clientId.with { $0 }) } + self.messageWithReactionsProcessingManager.process = { [weak context] messageIds in + context?.account.viewTracker.updateReactionsForMessageIds(messageIds: messageIds) + } self.adSeenProcessingManager.process = { [weak self] messageIds in guard let strongSelf = self, let adMessagesContext = strongSelf.adMessagesContext else { return @@ -977,6 +981,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { customThreadOutgoingReadState = .single(nil) } + let availableReactions = context.engine.stickers.availableReactions() + let historyViewTransitionDisposable = combineLatest(queue: messageViewQueue, historyViewUpdate, self.chatPresentationDataPromise.get(), @@ -991,8 +997,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { customChannelDiscussionReadState, customThreadOutgoingReadState, self.currentlyPlayingMessageIdPromise.get(), - adMessages - ).start(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, currentlyPlayingMessageId, adMessages in + adMessages, + availableReactions + ).start(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, currentlyPlayingMessageId, adMessages, availableReactions in func applyHole() { Queue.mainQueue().async { if let strongSelf = self { @@ -1113,7 +1120,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { isCopyProtectionEnabled = peer.isCopyProtectionEnabled } } - let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled) + let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions) let filteredEntries = chatHistoryEntriesForView( location: chatLocation, @@ -1767,6 +1774,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let toLaterRange = (historyView.filteredEntries.count - 1 - (visible.firstIndex - 1), historyView.filteredEntries.count - 1) var messageIdsWithViewCount: [MessageId] = [] + var messageIdsWithPossibleReactions: [MessageId] = [] var messageIdsWithLiveLocation: [MessageId] = [] var messageIdsWithUnsupportedMedia: [MessageId] = [] var messageIdsWithRefreshMedia: [MessageId] = [] @@ -1802,8 +1810,11 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { contentRequiredValidation = true } } + var hasAction = false for media in message.media { - if let _ = media as? TelegramMediaUnsupported { + if let _ = media as? TelegramMediaAction { + hasAction = true + } else if let _ = media as? TelegramMediaUnsupported { contentRequiredValidation = true } else if message.flags.contains(.Incoming), let media = media as? TelegramMediaMap, let liveBroadcastingTimeout = media.liveBroadcastingTimeout { let timestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) @@ -1830,6 +1841,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } } + if !hasAction { + messageIdsWithPossibleReactions.append(message.id) + } if contentRequiredValidation { messageIdsWithUnsupportedMedia.append(message.id) } @@ -1967,6 +1981,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if !messageIdsWithUnseenPersonalMention.isEmpty { self.messageMentionProcessingManager.add(messageIdsWithUnseenPersonalMention) } + if !messageIdsWithPossibleReactions.isEmpty { + self.messageWithReactionsProcessingManager.add(messageIdsWithPossibleReactions) + } self.currentEarlierPrefetchMessages = toEarlierMediaMessages self.currentLaterPrefetchMessages = toLaterMediaMessages diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index 620bc7c79a..b635febb2d 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -889,7 +889,23 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { isReplyThread = true } - let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, item.message.isSelfExpiring) + let statusSuggestedWidthAndContinue = makeDateAndStatusLayout(ChatMessageDateAndStatusNode.Arguments( + context: item.context, + presentationData: item.presentationData, + edited: edited, + impressionCount: viewCount, + dateText: dateText, + type: statusType, + layoutInput: .standalone, + constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), + availableReactions: item.associatedData.availableReactions, + reactions: dateReactions, + replyCount: dateReplies, + isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, + hasAutoremove: item.message.isSelfExpiring + )) + + let (dateAndStatusSize, dateAndStatusApply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) var viaBotApply: (TextNodeLayout, () -> TextNode)? var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)? @@ -1301,41 +1317,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } if let (_, f) = strongSelf.awaitingAppliedReaction { - /*var bounds = strongSelf.bounds - let offset = bounds.origin.x - bounds.origin.x = 0.0 - strongSelf.bounds = bounds - var shadowBounds = strongSelf.shadowNode.bounds - let shadowOffset = shadowBounds.origin.x - shadowBounds.origin.x = 0.0 - strongSelf.shadowNode.bounds = shadowBounds - if !offset.isZero { - strongSelf.layer.animateBoundsOriginXAdditive(from: offset, to: 0.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring) - } - if !shadowOffset.isZero { - strongSelf.shadowNode.layer.animateBoundsOriginXAdditive(from: shadowOffset, to: 0.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring) - } - if let swipeToReplyNode = strongSelf.swipeToReplyNode { - strongSelf.swipeToReplyNode = nil - swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in - swipeToReplyNode?.removeFromSupernode() - }) - swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - } - */ strongSelf.awaitingAppliedReaction = nil - /*var targetNode: ASDisplayNode? - var hideTarget = false - if let awaitingAppliedReaction = awaitingAppliedReaction { - for contentNode in strongSelf.contentNodes { - if let (reactionNode, count) = contentNode.reactionTargetNode(value: awaitingAppliedReaction) { - targetNode = reactionNode - hideTarget = count == 1 - break - } - } - } - strongSelf.reactionRecognizer?.complete(into: targetNode, hideTarget: hideTarget)*/ + f() } } @@ -2241,9 +2224,9 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - override func targetReactionNode(value: String) -> (ASDisplayNode, ASDisplayNode)? { + override func targetReactionView(value: String) -> UIView? { if !self.dateAndStatusNode.isHidden { - return self.dateAndStatusNode.reactionNode(value: value) + return self.dateAndStatusNode.reactionView(value: value) } return nil } diff --git a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift index bf3ac2689e..b275905129 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift @@ -508,7 +508,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { if let (media, flags) = mediaAndFlags { if let file = media as? TelegramMediaFile { if file.mimeType == "application/x-tgtheme-ios", let size = file.size, size < 16 * 1024 { - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, attributes, file, imageDateAndStatus, .full, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, file, imageDateAndStatus, .full, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else if file.isInstantVideo { @@ -540,12 +540,12 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } } - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, attributes, file, imageDateAndStatus, automaticDownload, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, file, imageDateAndStatus, automaticDownload, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else if file.isSticker || file.isAnimatedSticker { let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, attributes, file, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, file, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else { @@ -570,7 +570,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } else if let image = media as? TelegramMediaImage { if !flags.contains(.preferMediaInline) { let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: image) - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, attributes, image, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, image, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else if let dimensions = largestImageRepresentation(image.representations)?.dimensions { @@ -582,11 +582,11 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } } else if let image = media as? TelegramMediaWebFile { let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: image) - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, attributes, image, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, image, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else if let wallpaper = media as? WallpaperPreviewMedia { - let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, attributes, wallpaper, imageDateAndStatus, .full, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) + let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, wallpaper, imageDateAndStatus, .full, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout if case let .file(_, _, _, _, isTheme, _) = wallpaper.content, isTheme { @@ -613,52 +613,39 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { default: break } - - var statusSizeAndApply: (CGSize, (Bool) -> Void)? let textConstrainedSize = CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height - insets.top - insets.bottom) - - if let textStatusType = textStatusType { - statusSizeAndApply = statusLayout(context, presentationData, edited, viewCount, dateText, textStatusType, textConstrainedSize, dateReactions, dateReplies, message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, message.isSelfExpiring) - } var updatedAdditionalImageBadge: ChatMessageInteractiveMediaBadge? if let _ = additionalImageBadgeContent { updatedAdditionalImageBadge = currentAdditionalImageBadgeNode ?? ChatMessageInteractiveMediaBadge() } - var upatedTextCutout = textCutout - if statusInText, let (statusSize, _) = statusSizeAndApply { - upatedTextCutout.bottomRight = statusSize - } + let upatedTextCutout = textCutout let (textLayout, textApply) = textAsyncLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: 12, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: upatedTextCutout, insets: UIEdgeInsets())) - var textFrame = CGRect(origin: CGPoint(), size: textLayout.size) - - var statusFrame: CGRect? - - if statusInText, let (statusSize, _) = statusSizeAndApply { - var frame = CGRect(origin: CGPoint(x: textFrame.maxX - statusSize.width, y: textFrame.maxY - statusSize.height), size: statusSize) - - /*let trailingLineWidth = textLayout.trailingLineWidth - if textLayout.size.width - trailingLineWidth >= statusSize.width { - frame.origin = CGPoint(x: textFrame.maxX - statusSize.width, y: textFrame.maxY - statusSize.height) - } else if trailingLineWidth + statusSize.width < textConstrainedSize.width { - frame.origin = CGPoint(x: textFrame.minX + trailingLineWidth, y: textFrame.maxY - statusSize.height) - } else { - frame.origin = CGPoint(x: textFrame.maxX - statusSize.width, y: textFrame.maxY) - }*/ - - if let inlineImageSize = inlineImageSize { - if frame.origin.y < inlineImageSize.height + 4.0 { - frame.origin.y = inlineImageSize.height + 4.0 - } - } - - frame = frame.offsetBy(dx: insets.left, dy: insets.top) - statusFrame = frame + var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))? + if statusInText, let textStatusType = textStatusType { + statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments( + context: context, + presentationData: presentationData, + edited: edited, + impressionCount: viewCount, + dateText: dateText, + type: textStatusType, + layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, preferAdditionalInset: false), + constrainedSize: textConstrainedSize, + availableReactions: associatedData.availableReactions, + reactions: dateReactions, + replyCount: dateReplies, + isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, + hasAutoremove: message.isSelfExpiring + )) } + let _ = statusSuggestedWidthAndContinue + + var textFrame = CGRect(origin: CGPoint(), size: textLayout.size) textFrame = textFrame.offsetBy(dx: insets.left, dy: insets.top) @@ -666,12 +653,6 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { var boundingSize = textFrame.size var lineHeight = textLayout.rawTextSize.height - if let statusFrame = statusFrame { - boundingSize = textFrame.union(statusFrame).size - if let _ = actionTitle { - lineHeight = boundingSize.height - } - } if let inlineImageSize = inlineImageSize { if boundingSize.height < inlineImageSize.height { boundingSize.height = inlineImageSize.height @@ -681,6 +662,10 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } } + if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { + boundingSize.width = max(boundingSize.width, statusSuggestedWidthAndContinue.0) + } + var finalizeContentImageLayout: ((CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> ChatMessageInteractiveMediaNode))? if let refineContentImageLayout = refineContentImageLayout { let (refinedWidth, finalizeImageLayout) = refineContentImageLayout(textConstrainedSize, automaticPlayback, true, ImageCorners(radius: 4.0)) @@ -767,10 +752,6 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { adjustedBoundingSize.height += imageHeightAddition + 5.0 adjustedLineHeight += imageHeightAddition + 4.0 - - if !statusInText, let (statusSize, _) = statusSizeAndApply { - statusFrame = CGRect(origin: CGPoint(), size: statusSize) - } } var contentFileSizeAndApply: (CGSize, (Bool) -> ChatMessageInteractiveFileNode)? @@ -807,10 +788,18 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { adjustedBoundingSize.height += 7.0 + size.height } - var adjustedStatusFrame: CGRect? + var statusSizeAndApply: ((CGSize), (Bool) -> Void)? + if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { + statusSizeAndApply = statusSuggestedWidthAndContinue.1(boundingWidth - insets.left - insets.right) + } + if let statusSizeAndApply = statusSizeAndApply { + adjustedBoundingSize.height += statusSizeAndApply.0.height + } + + /*var adjustedStatusFrame: CGRect? if statusInText, let statusFrame = statusFrame { adjustedStatusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusFrame.size.width - insets.right, y: statusFrame.origin.y), size: statusFrame.size) - } + }*/ adjustedBoundingSize.width = max(boundingWidth, adjustedBoundingSize.width) @@ -830,6 +819,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { hasAnimation = true transition = .animated(duration: duration, curve: .easeInOut) } + let _ = hasAnimation strongSelf.lineNode.image = lineImage strongSelf.lineNode.frame = CGRect(origin: CGPoint(x: 13.0, y: insets.top), size: CGSize(width: 2.0, height: adjustedLineHeight - insets.top - insets.bottom - 2.0)) @@ -884,10 +874,6 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } contentImageNode.frame = contentImageFrame - - if !statusInText, let statusFrame = statusFrame { - adjustedStatusFrame = CGRect(origin: CGPoint(x: contentImageFrame.width - statusFrame.size.width - 2.0, y: contentImageFrame.height - statusFrame.size.height - 2.0), size: statusFrame.size) - } } else if let contentImageNode = strongSelf.contentImageNode { contentImageNode.visibility = false contentImageNode.removeFromSupernode() @@ -958,6 +944,15 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } strongSelf.textNode.frame = textFrame.offsetBy(dx: 0.0, dy: textVerticalOffset) + if let statusSizeAndApply = statusSizeAndApply { + if strongSelf.statusNode.supernode == nil { + strongSelf.addSubnode(strongSelf.statusNode) + } + strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: strongSelf.textNode.frame.minX, y: strongSelf.textNode.frame.maxY), size: statusSizeAndApply.0) + statusSizeAndApply.1(animation.isAnimated) + } else if strongSelf.statusNode.supernode != nil { + strongSelf.statusNode.removeFromSupernode() + } if let (size, apply) = actionButtonSizeAndApply { let buttonNode = apply() @@ -976,22 +971,6 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { buttonNode.removeFromSupernode() strongSelf.buttonNode = nil } - - if let (_, statusApply) = statusSizeAndApply, let adjustedStatusFrame = adjustedStatusFrame { - strongSelf.statusNode.frame = adjustedStatusFrame.offsetBy(dx: 0.0, dy: textVerticalOffset) - if statusInText { - if strongSelf.statusNode.supernode != strongSelf { - strongSelf.addSubnode(strongSelf.statusNode) - } - } else if let contentImageNode = strongSelf.contentImageNode { - if strongSelf.statusNode.supernode != contentImageNode { - contentImageNode.addSubnode(strongSelf.statusNode) - } - } - statusApply(hasAnimation) - } else if strongSelf.statusNode.supernode != nil { - strongSelf.statusNode.removeFromSupernode() - } } }) }) diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift index 01a062a262..d5d0a0a17f 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleContentNode.swift @@ -210,7 +210,7 @@ class ChatMessageBubbleContentNode: ASDisplayNode { func applyAbsoluteOffsetSpring(value: CGFloat, duration: Double, damping: CGFloat) { } - func reactionTargetNode(value: String) -> (ASDisplayNode, ASDisplayNode)? { + func reactionTargetView(value: String) -> UIView? { return nil } diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 3c91d78eb3..ca05e7394b 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -460,7 +460,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode self.shadowNode = ChatMessageShadowNode() self.clippingNode = ChatMessageBubbleClippingNode() - self.clippingNode.clipsToBounds = true + self.clippingNode.clipsToBounds = false self.messageAccessibilityArea = AccessibilityAreaNode() @@ -689,7 +689,18 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode let widthDifference = self.backgroundNode.frame.width - textInput.backgroundView.frame.width let heightDifference = self.backgroundNode.frame.height - textInput.backgroundView.frame.height - transition.animateFrame(layer: self.clippingNode.layer, from: CGRect(origin: CGPoint(x: self.clippingNode.frame.minX, y: textInput.backgroundView.frame.minY), size: textInput.backgroundView.frame.size)) + if let type = self.backgroundNode.type { + if case .none = type { + } else { + self.clippingNode.clipsToBounds = true + } + } + transition.animateFrame(layer: self.clippingNode.layer, from: CGRect(origin: CGPoint(x: self.clippingNode.frame.minX, y: textInput.backgroundView.frame.minY), size: textInput.backgroundView.frame.size), completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.clippingNode.clipsToBounds = false + }) transition.vertical.animateOffsetAdditive(layer: self.clippingNode.layer, offset: textInput.backgroundView.frame.minY - self.clippingNode.frame.minY) @@ -909,7 +920,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode forwardInfoLayout: (ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, String?, CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode), replyInfoLayout: (ChatPresentationData, PresentationStrings, AccountContext, ChatMessageReplyInfoType, Message, CGSize) -> (CGSize, () -> ChatMessageReplyInfoNode), actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationChatBubbleCorners, PresentationStrings, ReplyMarkupMessageAttribute, Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (Bool) -> ChatMessageActionButtonsNode)), - mosaicStatusLayout: (AccountContext, ChatPresentationData, Bool, Int?, String, ChatMessageDateAndStatusType, CGSize, [MessageReaction], Int, Bool, Bool) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode), + mosaicStatusLayout: (ChatMessageDateAndStatusNode.Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode)), layoutConstants: ChatMessageItemLayoutConstants, currentItem: ChatMessageItem?, currentForwardInfo: (Peer?, String?)?, @@ -1513,7 +1524,23 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode isReplyThread = true } - mosaicStatusSizeAndApply = mosaicStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: 200.0, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, message.isSelfExpiring) + let statusSuggestedWidthAndContinue = mosaicStatusLayout(ChatMessageDateAndStatusNode.Arguments( + context: item.context, + presentationData: item.presentationData, + edited: edited, + impressionCount: viewCount, + dateText: dateText, + type: statusType, + layoutInput: .standalone, + constrainedSize: CGSize(width: 200.0, height: CGFloat.greatestFiniteMagnitude), + availableReactions: item.associatedData.availableReactions, + reactions: dateReactions, + replyCount: dateReplies, + isPinned: message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, + hasAutoremove: message.isSelfExpiring + )) + + mosaicStatusSizeAndApply = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) } } @@ -2128,11 +2155,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode strongSelf.backgroundNode.setType(type: backgroundType, highlighted: strongSelf.highlightedState, graphics: graphics, maskMode: strongSelf.backgroundMaskMode, hasWallpaper: hasWallpaper, transition: transition, backgroundNode: presentationContext.backgroundNode) strongSelf.backgroundWallpaperNode.setType(type: backgroundType, theme: item.presentationData.theme, essentialGraphics: graphics, maskMode: strongSelf.backgroundMaskMode, backgroundNode: presentationContext.backgroundNode) strongSelf.shadowNode.setType(type: backgroundType, hasWallpaper: hasWallpaper, graphics: graphics) - if case .none = backgroundType { - strongSelf.clippingNode.clipsToBounds = false - } else { - strongSelf.clippingNode.clipsToBounds = true - } strongSelf.backgroundType = backgroundType @@ -2606,6 +2628,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode if case .System = animation, !strongSelf.mainContextSourceNode.isExtractedToContextPreview { if !strongSelf.backgroundNode.frame.equalTo(backgroundFrame) { strongSelf.backgroundFrameTransition = (strongSelf.backgroundNode.frame, backgroundFrame) + if let type = strongSelf.backgroundNode.type { + if case .none = type { + } else { + strongSelf.clippingNode.clipsToBounds = true + } + } } if let shareButtonNode = strongSelf.shareButtonNode { let currentBackgroundFrame = strongSelf.backgroundNode.frame @@ -2694,41 +2722,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode strongSelf.updateSearchTextHighlightState() if let (_, f) = strongSelf.awaitingAppliedReaction { - /*var bounds = strongSelf.bounds - let offset = bounds.origin.x - bounds.origin.x = 0.0 - strongSelf.bounds = bounds - var shadowBounds = strongSelf.shadowNode.bounds - let shadowOffset = shadowBounds.origin.x - shadowBounds.origin.x = 0.0 - strongSelf.shadowNode.bounds = shadowBounds - if !offset.isZero { - strongSelf.layer.animateBoundsOriginXAdditive(from: offset, to: 0.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring) - } - if !shadowOffset.isZero { - strongSelf.shadowNode.layer.animateBoundsOriginXAdditive(from: shadowOffset, to: 0.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring) - } - if let swipeToReplyNode = strongSelf.swipeToReplyNode { - strongSelf.swipeToReplyNode = nil - swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in - swipeToReplyNode?.removeFromSupernode() - }) - swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - } - */ strongSelf.awaitingAppliedReaction = nil - /*var targetNode: ASDisplayNode? - var hideTarget = false - if let awaitingAppliedReaction = awaitingAppliedReaction { - for contentNode in strongSelf.contentNodes { - if let (reactionNode, count) = contentNode.reactionTargetNode(value: awaitingAppliedReaction) { - targetNode = reactionNode - hideTarget = count == 1 - break - } - } - } - strongSelf.reactionRecognizer?.complete(into: targetNode, hideTarget: hideTarget)*/ + f() } @@ -2821,6 +2816,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode if CGFloat(1.0).isLessThanOrEqualTo(progress) { self.backgroundFrameTransition = nil + + self.clippingNode.clipsToBounds = false } } } @@ -2828,7 +2825,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { switch recognizer.state { case .ended: - if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation, let item = self.item { if let action = self.gestureRecognized(gesture: gesture, location: location, recognizer: nil) { if case .doubleTap = gesture { self.mainContainerNode.cancelGesture() @@ -2839,14 +2836,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode case let .optionalAction(f): f() case let .openContextMenu(tapMessage, selectAll, subFrame): - if canAddMessageReactions(message: tapMessage) { - self.item?.controllerInteraction.updateMessageReaction(tapMessage) + if canAddMessageReactions(message: tapMessage), let defaultReaction = item.associatedData.defaultReaction { + item.controllerInteraction.updateMessageReaction(tapMessage, defaultReaction) } else { - self.item?.controllerInteraction.openMessageContextMenu(tapMessage, selectAll, self, subFrame, nil) + item.controllerInteraction.openMessageContextMenu(tapMessage, selectAll, self, subFrame, nil) } } } else if case .tap = gesture { - self.item?.controllerInteraction.clickThroughMessage() + item.controllerInteraction.clickThroughMessage() } } default: @@ -3667,10 +3664,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode return self.mainContextSourceNode.isExtractedToContextPreview || hasWallpaper || isPreview } - override func targetReactionNode(value: String) -> (ASDisplayNode, ASDisplayNode)? { + override func targetReactionView(value: String) -> UIView? { for contentNode in self.contentNodes { - if let (emptyNode, filledNode) = contentNode.reactionTargetNode(value: value) { - return (emptyNode, filledNode) + if let result = contentNode.reactionTargetView(value: value) { + return result } } return nil diff --git a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift index 788fdf811f..620888a171 100644 --- a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift @@ -41,6 +41,13 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { self.addSubnode(self.buttonNode) self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + + self.dateAndStatusNode.reactionSelected = { [weak self] value in + guard let strongSelf = self, let item = strongSelf.item else { + return + } + item.controllerInteraction.updateMessageReaction(item.message, value) + } } override func accessibilityActivate() -> Bool { @@ -144,7 +151,9 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in let avatarSize = CGSize(width: 40.0, height: 40.0) - let maxTextWidth = max(1.0, constrainedSize.width - avatarSize.width - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right) + let sideInsets = layoutConstants.text.bubbleInsets.right * 2.0 + + let maxTextWidth = max(1.0, constrainedSize.width - avatarSize.width - 7.0 - sideInsets) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: 5, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) @@ -187,18 +196,28 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { statusType = nil } - var statusSize = CGSize() - var statusApply: ((Bool) -> Void)? - + var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))? if let statusType = statusType { var isReplyThread = false if case .replyThread = item.chatLocation { isReplyThread = true } - let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, item.message.isSelfExpiring) - statusSize = size - statusApply = apply + statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments( + context: item.context, + presentationData: item.presentationData, + edited: edited, + impressionCount: viewCount, + dateText: dateText, + type: statusType, + layoutInput: .trailingContent(contentWidth: 1000.0, preferAdditionalInset: true), + constrainedSize: CGSize(width: constrainedSize.width - sideInsets, height: .greatestFiniteMagnitude), + availableReactions: item.associatedData.availableReactions, + reactions: dateReactions, + replyCount: dateReplies, + isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, + hasAutoremove: item.message.isSelfExpiring + )) } let buttonImage: UIImage @@ -226,27 +245,30 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, nil, nil, item.presentationData.strings.Conversation_ViewContactDetails, titleColor, titleHighlightedColor) - var maxContentWidth: CGFloat = 0.0 - maxContentWidth = max(maxContentWidth, statusSize.width) - maxContentWidth = max(maxContentWidth, titleLayout.size.width) - maxContentWidth = max(maxContentWidth, textLayout.size.width) + var maxContentWidth: CGFloat = avatarSize.width + 7.0 + if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { + maxContentWidth = max(maxContentWidth, statusSuggestedWidthAndContinue.0) + } + maxContentWidth = max(maxContentWidth, avatarSize.width + 7.0 + titleLayout.size.width) + maxContentWidth = max(maxContentWidth, avatarSize.width + 7.0 + textLayout.size.width) maxContentWidth = max(maxContentWidth, buttonWidth) - let contentWidth = avatarSize.width + maxContentWidth + layoutConstants.text.bubbleInsets.right + 8.0 + let contentWidth = maxContentWidth + layoutConstants.text.bubbleInsets.right * 2.0 return (contentWidth, { boundingWidth in - let layoutSize: CGSize - let statusFrame: CGRect - - let baseAvatarFrame = CGRect(origin: CGPoint(), size: avatarSize) + let baseAvatarFrame = CGRect(origin: CGPoint(x: layoutConstants.text.bubbleInsets.right, y: layoutConstants.text.bubbleInsets.top), size: avatarSize) let (buttonSize, buttonApply) = continueLayout(boundingWidth - layoutConstants.text.bubbleInsets.right * 2.0) let buttonSpacing: CGFloat = 4.0 - layoutSize = CGSize(width: contentWidth, height: 49.0 + textLayout.size.height + buttonSize.height + buttonSpacing) - statusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusSize.width - layoutConstants.text.bubbleInsets.right, y: layoutSize.height - statusSize.height - 9.0 - buttonSpacing - buttonSize.height), size: statusSize) + let statusSizeAndApply = statusSuggestedWidthAndContinue?.1(boundingWidth - sideInsets) + + var layoutSize = CGSize(width: contentWidth, height: 49.0 + textLayout.size.height + buttonSize.height + buttonSpacing) + if let statusSizeAndApply = statusSizeAndApply { + layoutSize.height += statusSizeAndApply.0.height - 4.0 + } let buttonFrame = CGRect(origin: CGPoint(x: layoutConstants.text.bubbleInsets.right, y: layoutSize.height - 9.0 - buttonSize.height), size: buttonSize) - let avatarFrame = baseAvatarFrame.offsetBy(dx: 5.0, dy: 5.0) + let avatarFrame = baseAvatarFrame.offsetBy(dx: 0.0, dy: 5.0) var customLetters: [String] = [] if let selectedContact = selectedContact, selectedContact.peerId == nil { @@ -279,16 +301,14 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.textNode.frame = CGRect(origin: CGPoint(x: avatarFrame.maxX + 7.0, y: avatarFrame.minY + 20.0), size: textLayout.size) strongSelf.buttonNode.frame = buttonFrame - if let statusApply = statusApply { + if let statusSizeAndApply = statusSizeAndApply { + strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: layoutConstants.text.bubbleInsets.left, y: strongSelf.textNode.frame.maxY + 2.0), size: statusSizeAndApply.0) if strongSelf.dateAndStatusNode.supernode == nil { strongSelf.addSubnode(strongSelf.dateAndStatusNode) + statusSizeAndApply.1(false) + } else { + statusSizeAndApply.1(animation.isAnimated) } - var hasAnimation = true - if case .None = animation { - hasAnimation = false - } - statusApply(hasAnimation) - strongSelf.dateAndStatusNode.frame = statusFrame } else if strongSelf.dateAndStatusNode.supernode != nil { strongSelf.dateAndStatusNode.removeFromSupernode() } @@ -348,6 +368,9 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { if self.buttonNode.frame.contains(point) { return .openMessage } + if self.dateAndStatusNode.supernode != nil, let _ = self.dateAndStatusNode.hitTest(self.view.convert(point, to: self.dateAndStatusNode.view), with: nil) { + return .ignore + } return .none } @@ -365,10 +388,17 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { } } - override func reactionTargetNode(value: String) -> (ASDisplayNode, ASDisplayNode)? { + override func reactionTargetView(value: String) -> UIView? { if !self.dateAndStatusNode.isHidden { - return self.dateAndStatusNode.reactionNode(value: value) + return self.dateAndStatusNode.reactionView(value: value) } return nil } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.dateAndStatusNode.supernode != nil, let result = self.dateAndStatusNode.hitTest(self.view.convert(point, to: self.dateAndStatusNode.view), with: event) { + return result + } + return super.hitTest(point, with: event) + } } diff --git a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift index ae92920b6a..e6cdb1e5ab 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift @@ -8,6 +8,7 @@ import SwiftSignalKit import TelegramPresentationData import AccountContext import AppBundle +import ReactionButtonListComponent private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) { if let _ = layer.animation(forKey: "clockFrameAnimation") { @@ -81,64 +82,62 @@ private final class StatusReactionNode: ASDisplayNode { self.selectedImageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: selectedImage.size) } } - - /*if self.isSelected != isSelected { - let wasSelected = self.isSelected - self.isSelected = isSelected - - self.emptyImageNode.isHidden = isSelected && count <= 1 - self.selectedImageNode.isHidden = !isSelected - - if let wasSelected = wasSelected, wasSelected, !isSelected { - if let image = self.selectedImageNode.image { - let leftImage = generateImage(image.size, rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - UIGraphicsPushContext(context) - image.draw(in: CGRect(origin: CGPoint(), size: size)) - UIGraphicsPopContext() - context.clear(CGRect(origin: CGPoint(x: size.width / 2.0, y: 0.0), size: CGSize(width: size.width / 2.0, height: size.height))) - }) - let rightImage = generateImage(image.size, rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - UIGraphicsPushContext(context) - image.draw(in: CGRect(origin: CGPoint(), size: size)) - UIGraphicsPopContext() - context.clear(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width / 2.0, height: size.height))) - }) - if let leftImage = leftImage, let rightImage = rightImage { - let leftView = UIImageView() - leftView.image = leftImage - leftView.frame = self.selectedImageNode.frame - let rightView = UIImageView() - rightView.image = rightImage - rightView.frame = self.selectedImageNode.frame - self.view.addSubview(leftView) - self.view.addSubview(rightView) - - let duration: Double = 0.3 - - leftView.layer.animateRotation(from: 0.0, to: -CGFloat.pi * 0.7, duration: duration, removeOnCompletion: false) - rightView.layer.animateRotation(from: 0.0, to: CGFloat.pi * 0.7, duration: duration, removeOnCompletion: false) - - leftView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -6.0, y: 8.0), duration: duration, removeOnCompletion: false, additive: true) - rightView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 6.0, y: 8.0), duration: duration, removeOnCompletion: false, additive: true) - - leftView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { [weak leftView] _ in - leftView?.removeFromSuperview() - }) - - rightView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { [weak rightView] _ in - rightView?.removeFromSuperview() - }) - } - } - } - }*/ } } class ChatMessageDateAndStatusNode: ASDisplayNode { + enum LayoutInput { + case trailingContent(contentWidth: CGFloat, preferAdditionalInset: Bool) + case standalone + } + + struct Arguments { + var context: AccountContext + var presentationData: ChatPresentationData + var edited: Bool + var impressionCount: Int? + var dateText: String + var type: ChatMessageDateAndStatusType + var layoutInput: LayoutInput + var constrainedSize: CGSize + var availableReactions: AvailableReactions? + var reactions: [MessageReaction] + var replyCount: Int + var isPinned: Bool + var hasAutoremove: Bool + + init( + context: AccountContext, + presentationData: ChatPresentationData, + edited: Bool, + impressionCount: Int?, + dateText: String, + type: ChatMessageDateAndStatusType, + layoutInput: LayoutInput, + constrainedSize: CGSize, + availableReactions: AvailableReactions?, + reactions: [MessageReaction], + replyCount: Int, + isPinned: Bool, + hasAutoremove: Bool + ) { + self.context = context + self.presentationData = presentationData + self.edited = edited + self.impressionCount = impressionCount + self.dateText = dateText + self.type = type + self.layoutInput = layoutInput + self.availableReactions = availableReactions + self.constrainedSize = constrainedSize + self.reactions = reactions + self.replyCount = replyCount + self.isPinned = isPinned + self.hasAutoremove = hasAutoremove + } + } + private var backgroundNode: ASImageNode? private var blurredBackgroundNode: NavigationBackgroundNode? private var checkSentNode: ASImageNode? @@ -148,6 +147,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { private let dateNode: TextNode private var impressionIcon: ASImageNode? private var reactionNodes: [StatusReactionNode] = [] + private let reactionButtonsContainer = ReactionButtonsLayoutContainer() private var reactionCountNode: TextNode? private var reactionButtonNode: HighlightTrackingButtonNode? private var repliesIcon: ASImageNode? @@ -175,6 +175,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } } } + var reactionSelected: ((String) -> Void)? override init() { self.dateNode = TextNode() @@ -192,7 +193,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } } - func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize, _ reactions: [MessageReaction], _ replies: Int, _ isPinned: Bool, _ hasAutoremove: Bool) -> (CGSize, (Bool) -> Void) { + func asyncLayout() -> (_ arguments: Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void)) { let dateLayout = TextNode.asyncLayout(self.dateNode) var checkReadNode = self.checkReadNode @@ -212,7 +213,9 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { let previousLayoutSize = self.layoutSize - return { context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replyCount, isPinned, hasAutoremove in + let reactionButtonsContainer = self.reactionButtonsContainer + + return { [weak self] arguments in let dateColor: UIColor var backgroundImage: UIImage? var blurredBackgroundColor: (UIColor, Bool)? @@ -226,148 +229,145 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { var impressionImage: UIImage? var repliesImage: UIImage? - let themeUpdated = presentationData.theme != currentTheme || type != currentType + let themeUpdated = arguments.presentationData.theme != currentTheme || arguments.type != currentType - let graphics = PresentationResourcesChat.principalGraphics(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners) - let isDefaultWallpaper = serviceMessageColorHasDefaultWallpaper(presentationData.theme.wallpaper) + let graphics = PresentationResourcesChat.principalGraphics(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper, bubbleCorners: arguments.presentationData.chatBubbleCorners) + let isDefaultWallpaper = serviceMessageColorHasDefaultWallpaper(arguments.presentationData.theme.wallpaper) let offset: CGFloat = -UIScreenPixel - let checkSize: CGFloat = floor(floor(presentationData.fontSize.baseDisplaySize * 11.0 / 17.0)) + let checkSize: CGFloat = floor(floor(arguments.presentationData.fontSize.baseDisplaySize * 11.0 / 17.0)) - switch type { - case .BubbleIncoming: - dateColor = presentationData.theme.theme.chat.message.incoming.secondaryTextColor - leftInset = 10.0 - loadedCheckFullImage = PresentationResourcesChat.chatOutgoingFullCheck(presentationData.theme.theme, size: checkSize) - loadedCheckPartialImage = PresentationResourcesChat.chatOutgoingPartialCheck(presentationData.theme.theme, size: checkSize) - clockFrameImage = graphics.clockBubbleIncomingFrameImage - clockMinImage = graphics.clockBubbleIncomingMinImage - if impressionCount != nil { - impressionImage = graphics.incomingDateAndStatusImpressionIcon - } - if replyCount != 0 { - repliesImage = graphics.incomingDateAndStatusRepliesIcon - } else if isPinned { - repliesImage = graphics.incomingDateAndStatusPinnedIcon - } - if hasAutoremove { - //selfExpiringImage = graphics.incomingDateAndStatusSelfExpiringIcon - } - case let .BubbleOutgoing(status): - dateColor = presentationData.theme.theme.chat.message.outgoing.secondaryTextColor - outgoingStatus = status - leftInset = 10.0 - loadedCheckFullImage = PresentationResourcesChat.chatOutgoingFullCheck(presentationData.theme.theme, size: checkSize) - loadedCheckPartialImage = PresentationResourcesChat.chatOutgoingPartialCheck(presentationData.theme.theme, size: checkSize) - clockFrameImage = graphics.clockBubbleOutgoingFrameImage - clockMinImage = graphics.clockBubbleOutgoingMinImage - if impressionCount != nil { - impressionImage = graphics.outgoingDateAndStatusImpressionIcon - } - if replyCount != 0 { - repliesImage = graphics.outgoingDateAndStatusRepliesIcon - } else if isPinned { - repliesImage = graphics.outgoingDateAndStatusPinnedIcon - } - if hasAutoremove { - //selfExpiringImage = graphics.outgoingDateAndStatusSelfExpiringIcon - } - case .ImageIncoming: - dateColor = presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor - backgroundImage = graphics.dateAndStatusMediaBackground - leftInset = 0.0 - loadedCheckFullImage = PresentationResourcesChat.chatMediaFullCheck(presentationData.theme.theme, size: checkSize) - loadedCheckPartialImage = PresentationResourcesChat.chatMediaPartialCheck(presentationData.theme.theme, size: checkSize) - clockFrameImage = graphics.clockMediaFrameImage - clockMinImage = graphics.clockMediaMinImage - if impressionCount != nil { - impressionImage = graphics.mediaImpressionIcon - } - if replyCount != 0 { - repliesImage = graphics.mediaRepliesIcon - } else if isPinned { - repliesImage = graphics.mediaPinnedIcon - } - if hasAutoremove { - //selfExpiringImage = graphics.mediaSelfExpiringIcon - } - case let .ImageOutgoing(status): - dateColor = presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor - outgoingStatus = status - backgroundImage = graphics.dateAndStatusMediaBackground - leftInset = 0.0 - loadedCheckFullImage = PresentationResourcesChat.chatMediaFullCheck(presentationData.theme.theme, size: checkSize) - loadedCheckPartialImage = PresentationResourcesChat.chatMediaPartialCheck(presentationData.theme.theme, size: checkSize) - clockFrameImage = graphics.clockMediaFrameImage - clockMinImage = graphics.clockMediaMinImage - if impressionCount != nil { - impressionImage = graphics.mediaImpressionIcon - } - if replyCount != 0 { - repliesImage = graphics.mediaRepliesIcon - } else if isPinned { - repliesImage = graphics.mediaPinnedIcon - } - if hasAutoremove { - //selfExpiringImage = graphics.mediaSelfExpiringIcon - } - case .FreeIncoming: - let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) - dateColor = serviceColor.primaryText - - blurredBackgroundColor = (selectDateFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper), dateFillNeedsBlur(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)) - leftInset = 0.0 - loadedCheckFullImage = PresentationResourcesChat.chatFreeFullCheck(presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper) - loadedCheckPartialImage = PresentationResourcesChat.chatFreePartialCheck(presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper) - clockFrameImage = graphics.clockFreeFrameImage - clockMinImage = graphics.clockFreeMinImage - if impressionCount != nil { - impressionImage = graphics.freeImpressionIcon - } - if replyCount != 0 { - repliesImage = graphics.freeRepliesIcon - } else if isPinned { - repliesImage = graphics.freePinnedIcon - } - if hasAutoremove { - //selfExpiringImage = graphics.freeSelfExpiringIcon - } - case let .FreeOutgoing(status): - let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) - dateColor = serviceColor.primaryText - outgoingStatus = status - //backgroundImage = graphics.dateAndStatusFreeBackground - blurredBackgroundColor = (selectDateFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper), dateFillNeedsBlur(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)) - leftInset = 0.0 - loadedCheckFullImage = PresentationResourcesChat.chatFreeFullCheck(presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper) - loadedCheckPartialImage = PresentationResourcesChat.chatFreePartialCheck(presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper) - clockFrameImage = graphics.clockFreeFrameImage - clockMinImage = graphics.clockFreeMinImage - if impressionCount != nil { - impressionImage = graphics.freeImpressionIcon - } - if replyCount != 0 { - repliesImage = graphics.freeRepliesIcon - } else if isPinned { - repliesImage = graphics.freePinnedIcon - } - if hasAutoremove { - //selfExpiringImage = graphics.freeSelfExpiringIcon - } + let reactionColors: ReactionButtonComponent.Colors + switch arguments.type { + case .BubbleIncoming, .ImageIncoming, .FreeIncoming: + reactionColors = ReactionButtonComponent.Colors( + background: arguments.presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(0.1).argb, + foreground: arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor.argb, + stroke: arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor.argb + ) + case .BubbleOutgoing, .ImageOutgoing, .FreeOutgoing: + reactionColors = ReactionButtonComponent.Colors( + background: arguments.presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(0.1).argb, + foreground: arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor.argb, + stroke: arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor.argb + ) } - var updatedDateText = dateText - if edited { - updatedDateText = "\(presentationData.strings.Conversation_MessageEditedLabel) \(updatedDateText)" - } - if let impressionCount = impressionCount { - updatedDateText = compactNumericCountString(impressionCount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) + " " + updatedDateText + switch arguments.type { + case .BubbleIncoming: + dateColor = arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor + leftInset = 10.0 + loadedCheckFullImage = PresentationResourcesChat.chatOutgoingFullCheck(arguments.presentationData.theme.theme, size: checkSize) + loadedCheckPartialImage = PresentationResourcesChat.chatOutgoingPartialCheck(arguments.presentationData.theme.theme, size: checkSize) + clockFrameImage = graphics.clockBubbleIncomingFrameImage + clockMinImage = graphics.clockBubbleIncomingMinImage + if arguments.impressionCount != nil { + impressionImage = graphics.incomingDateAndStatusImpressionIcon + } + if arguments.replyCount != 0 { + repliesImage = graphics.incomingDateAndStatusRepliesIcon + } else if arguments.isPinned { + repliesImage = graphics.incomingDateAndStatusPinnedIcon + } + case let .BubbleOutgoing(status): + dateColor = arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor + outgoingStatus = status + leftInset = 10.0 + loadedCheckFullImage = PresentationResourcesChat.chatOutgoingFullCheck(arguments.presentationData.theme.theme, size: checkSize) + loadedCheckPartialImage = PresentationResourcesChat.chatOutgoingPartialCheck(arguments.presentationData.theme.theme, size: checkSize) + clockFrameImage = graphics.clockBubbleOutgoingFrameImage + clockMinImage = graphics.clockBubbleOutgoingMinImage + if arguments.impressionCount != nil { + impressionImage = graphics.outgoingDateAndStatusImpressionIcon + } + if arguments.replyCount != 0 { + repliesImage = graphics.outgoingDateAndStatusRepliesIcon + } else if arguments.isPinned { + repliesImage = graphics.outgoingDateAndStatusPinnedIcon + } + case .ImageIncoming: + dateColor = arguments.presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor + backgroundImage = graphics.dateAndStatusMediaBackground + leftInset = 0.0 + loadedCheckFullImage = PresentationResourcesChat.chatMediaFullCheck(arguments.presentationData.theme.theme, size: checkSize) + loadedCheckPartialImage = PresentationResourcesChat.chatMediaPartialCheck(arguments.presentationData.theme.theme, size: checkSize) + clockFrameImage = graphics.clockMediaFrameImage + clockMinImage = graphics.clockMediaMinImage + if arguments.impressionCount != nil { + impressionImage = graphics.mediaImpressionIcon + } + if arguments.replyCount != 0 { + repliesImage = graphics.mediaRepliesIcon + } else if arguments.isPinned { + repliesImage = graphics.mediaPinnedIcon + } + case let .ImageOutgoing(status): + dateColor = arguments.presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor + outgoingStatus = status + backgroundImage = graphics.dateAndStatusMediaBackground + leftInset = 0.0 + loadedCheckFullImage = PresentationResourcesChat.chatMediaFullCheck(arguments.presentationData.theme.theme, size: checkSize) + loadedCheckPartialImage = PresentationResourcesChat.chatMediaPartialCheck(arguments.presentationData.theme.theme, size: checkSize) + clockFrameImage = graphics.clockMediaFrameImage + clockMinImage = graphics.clockMediaMinImage + if arguments.impressionCount != nil { + impressionImage = graphics.mediaImpressionIcon + } + if arguments.replyCount != 0 { + repliesImage = graphics.mediaRepliesIcon + } else if arguments.isPinned { + repliesImage = graphics.mediaPinnedIcon + } + case .FreeIncoming: + let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper) + dateColor = serviceColor.primaryText + + blurredBackgroundColor = (selectDateFillStaticColor(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper), dateFillNeedsBlur(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper)) + leftInset = 0.0 + loadedCheckFullImage = PresentationResourcesChat.chatFreeFullCheck(arguments.presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper) + loadedCheckPartialImage = PresentationResourcesChat.chatFreePartialCheck(arguments.presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper) + clockFrameImage = graphics.clockFreeFrameImage + clockMinImage = graphics.clockFreeMinImage + if arguments.impressionCount != nil { + impressionImage = graphics.freeImpressionIcon + } + if arguments.replyCount != 0 { + repliesImage = graphics.freeRepliesIcon + } else if arguments.isPinned { + repliesImage = graphics.freePinnedIcon + } + case let .FreeOutgoing(status): + let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper) + dateColor = serviceColor.primaryText + outgoingStatus = status + blurredBackgroundColor = (selectDateFillStaticColor(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper), dateFillNeedsBlur(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper)) + leftInset = 0.0 + loadedCheckFullImage = PresentationResourcesChat.chatFreeFullCheck(arguments.presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper) + loadedCheckPartialImage = PresentationResourcesChat.chatFreePartialCheck(arguments.presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper) + clockFrameImage = graphics.clockFreeFrameImage + clockMinImage = graphics.clockFreeMinImage + if arguments.impressionCount != nil { + impressionImage = graphics.freeImpressionIcon + } + if arguments.replyCount != 0 { + repliesImage = graphics.freeRepliesIcon + } else if arguments.isPinned { + repliesImage = graphics.freePinnedIcon + } } - let dateFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 11.0 / 17.0)) - let (date, dateApply) = dateLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: updatedDateText, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: constrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + var updatedDateText = arguments.dateText + if arguments.edited { + updatedDateText = "\(arguments.presentationData.strings.Conversation_MessageEditedLabel) \(updatedDateText)" + } + if let impressionCount = arguments.impressionCount { + updatedDateText = compactNumericCountString(impressionCount, decimalSeparator: arguments.presentationData.dateTimeFormat.decimalSeparator) + " " + updatedDateText + } - let checkOffset = floor(presentationData.fontSize.baseDisplaySize * 6.0 / 17.0) + let dateFont = Font.regular(floor(arguments.presentationData.fontSize.baseDisplaySize * 11.0 / 17.0)) + let (date, dateApply) = dateLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: updatedDateText, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: arguments.constrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let checkOffset = floor(arguments.presentationData.fontSize.baseDisplaySize * 6.0 / 17.0) let statusWidth: CGFloat @@ -408,8 +408,57 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { if let outgoingStatus = outgoingStatus { switch outgoingStatus { - case .Sending: - statusWidth = floor(floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0)) + case .Sending: + statusWidth = floor(floor(arguments.presentationData.fontSize.baseDisplaySize * 13.0 / 17.0)) + + if checkReadNode == nil { + checkReadNode = ASImageNode() + checkReadNode?.isLayerBacked = true + checkReadNode?.displaysAsynchronously = false + checkReadNode?.displayWithoutProcessing = true + } + + if checkSentNode == nil { + checkSentNode = ASImageNode() + checkSentNode?.isLayerBacked = true + checkSentNode?.displaysAsynchronously = false + checkSentNode?.displayWithoutProcessing = true + } + + if clockFrameNode == nil { + clockFrameNode = ASImageNode() + clockFrameNode?.isLayerBacked = true + clockFrameNode?.displaysAsynchronously = false + clockFrameNode?.displayWithoutProcessing = true + clockFrameNode?.frame = CGRect(origin: CGPoint(), size: clockFrameImage?.size ?? CGSize()) + } + + if clockMinNode == nil { + clockMinNode = ASImageNode() + clockMinNode?.isLayerBacked = true + clockMinNode?.displaysAsynchronously = false + clockMinNode?.displayWithoutProcessing = true + clockMinNode?.frame = CGRect(origin: CGPoint(), size: clockMinImage?.size ?? CGSize()) + } + clockPosition = CGPoint(x: leftInset + date.size.width + 8.5, y: 7.5 + offset) + case let .Sent(read): + let hideStatus: Bool + switch arguments.type { + case .BubbleOutgoing, .FreeOutgoing, .ImageOutgoing: + hideStatus = false + default: + hideStatus = arguments.impressionCount != nil + } + + if hideStatus { + statusWidth = 0.0 + + checkReadNode = nil + checkSentNode = nil + clockFrameNode = nil + clockMinNode = nil + } else { + statusWidth = floor(floor(arguments.presentationData.fontSize.baseDisplaySize * 13.0 / 17.0)) if checkReadNode == nil { checkReadNode = ASImageNode() @@ -425,72 +474,23 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { checkSentNode?.displayWithoutProcessing = true } - if clockFrameNode == nil { - clockFrameNode = ASImageNode() - clockFrameNode?.isLayerBacked = true - clockFrameNode?.displaysAsynchronously = false - clockFrameNode?.displayWithoutProcessing = true - clockFrameNode?.frame = CGRect(origin: CGPoint(), size: clockFrameImage?.size ?? CGSize()) - } - - if clockMinNode == nil { - clockMinNode = ASImageNode() - clockMinNode?.isLayerBacked = true - clockMinNode?.displaysAsynchronously = false - clockMinNode?.displayWithoutProcessing = true - clockMinNode?.frame = CGRect(origin: CGPoint(), size: clockMinImage?.size ?? CGSize()) - } - clockPosition = CGPoint(x: leftInset + date.size.width + 8.5, y: 7.5 + offset) - case let .Sent(read): - let hideStatus: Bool - switch type { - case .BubbleOutgoing, .FreeOutgoing, .ImageOutgoing: - hideStatus = false - default: - hideStatus = impressionCount != nil - } - - if hideStatus { - statusWidth = 0.0 - - checkReadNode = nil - checkSentNode = nil - clockFrameNode = nil - clockMinNode = nil - } else { - statusWidth = floor(floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0)) - - if checkReadNode == nil { - checkReadNode = ASImageNode() - checkReadNode?.isLayerBacked = true - checkReadNode?.displaysAsynchronously = false - checkReadNode?.displayWithoutProcessing = true - } - - if checkSentNode == nil { - checkSentNode = ASImageNode() - checkSentNode?.isLayerBacked = true - checkSentNode?.displaysAsynchronously = false - checkSentNode?.displayWithoutProcessing = true - } - - clockFrameNode = nil - clockMinNode = nil - - let checkSize = loadedCheckFullImage!.size - - if read { - checkReadFrame = CGRect(origin: CGPoint(x: leftInset + impressionWidth + date.size.width + 5.0 + statusWidth - checkSize.width, y: 3.0 + offset), size: checkSize) - } - checkSentFrame = CGRect(origin: CGPoint(x: leftInset + impressionWidth + date.size.width + 5.0 + statusWidth - checkSize.width - checkOffset, y: 3.0 + offset), size: checkSize) - } - case .Failed: - statusWidth = 0.0 - - checkReadNode = nil - checkSentNode = nil clockFrameNode = nil clockMinNode = nil + + let checkSize = loadedCheckFullImage!.size + + if read { + checkReadFrame = CGRect(origin: CGPoint(x: leftInset + impressionWidth + date.size.width + 5.0 + statusWidth - checkSize.width, y: 3.0 + offset), size: checkSize) + } + checkSentFrame = CGRect(origin: CGPoint(x: leftInset + impressionWidth + date.size.width + 5.0 + statusWidth - checkSize.width - checkOffset, y: 3.0 + offset), size: checkSize) + } + case .Failed: + statusWidth = 0.0 + + checkReadNode = nil + checkSentNode = nil + clockFrameNode = nil + clockMinNode = nil } } else { statusWidth = 0.0 @@ -524,11 +524,11 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { let reactionTrailingSpacing: CGFloat = 4.0 var reactionInset: CGFloat = 0.0 - if !reactions.isEmpty { - reactionInset = -1.0 + CGFloat(reactions.count) * reactionSize + CGFloat(reactions.count - 1) * reactionSpacing + reactionTrailingSpacing + if !"".isEmpty && !arguments.reactions.isEmpty { + reactionInset = -1.0 + CGFloat(arguments.reactions.count) * reactionSize + CGFloat(arguments.reactions.count - 1) * reactionSpacing + reactionTrailingSpacing var count = 0 - for reaction in reactions { + for reaction in arguments.reactions { count += Int(reaction.count) } @@ -546,20 +546,20 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { reactionCountLayoutAndApply = layoutAndApply } - if replyCount > 0 { + if arguments.replyCount > 0 { let countString: String - if replyCount > 1000000 { - countString = "\(replyCount / 1000000)M" - } else if replyCount > 1000 { - countString = "\(replyCount / 1000)K" + if arguments.replyCount > 1000000 { + countString = "\(arguments.replyCount / 1000000)M" + } else if arguments.replyCount > 1000 { + countString = "\(arguments.replyCount / 1000)K" } else { - countString = "\(replyCount)" + countString = "\(arguments.replyCount)" } let layoutAndApply = makeReplyCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0))) reactionInset += 14.0 + layoutAndApply.0.size.width + 4.0 replyCountLayoutAndApply = layoutAndApply - } else if isPinned { + } else if arguments.isPinned { reactionInset += 12.0 } @@ -567,327 +567,479 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { let layoutSize = CGSize(width: leftInset + impressionWidth + date.size.width + statusWidth + backgroundInsets.left + backgroundInsets.right, height: date.size.height + backgroundInsets.top + backgroundInsets.bottom) - return (layoutSize, { [weak self] animated in - if let strongSelf = self { - strongSelf.theme = presentationData.theme - strongSelf.type = type - strongSelf.layoutSize = layoutSize - - if backgroundImage != nil { - if let currentBackgroundNode = currentBackgroundNode { - if currentBackgroundNode.supernode == nil { - strongSelf.backgroundNode = currentBackgroundNode - currentBackgroundNode.image = backgroundImage - strongSelf.insertSubnode(currentBackgroundNode, at: 0) - } else if themeUpdated { - currentBackgroundNode.image = backgroundImage - } + let verticalReactionsInset: CGFloat + let verticalInset: CGFloat + let resultingWidth: CGFloat + let resultingHeight: CGFloat + + let reactionButtons: ReactionButtonsLayoutContainer.Result + switch arguments.layoutInput { + case .standalone: + verticalReactionsInset = 0.0 + verticalInset = 0.0 + resultingWidth = layoutSize.width + resultingHeight = layoutSize.height + reactionButtons = reactionButtonsContainer.update( + context: arguments.context, + action: { value in + guard let strongSelf = self else { + return } - if let backgroundNode = strongSelf.backgroundNode { - let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate - if let previousLayoutSize = previousLayoutSize { - backgroundNode.frame = backgroundNode.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0) - } - transition.updateFrame(node: backgroundNode, frame: CGRect(origin: CGPoint(), size: layoutSize)) - } - } else { - if let backgroundNode = strongSelf.backgroundNode { - backgroundNode.removeFromSupernode() - strongSelf.backgroundNode = nil - } - } - - if let blurredBackgroundColor = blurredBackgroundColor { - if let blurredBackgroundNode = strongSelf.blurredBackgroundNode { - blurredBackgroundNode.updateColor(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1, transition: .immediate) - let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate - if let previousLayoutSize = previousLayoutSize { - blurredBackgroundNode.frame = blurredBackgroundNode.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0) - } - transition.updateFrame(node: blurredBackgroundNode, frame: CGRect(origin: CGPoint(), size: layoutSize)) - blurredBackgroundNode.update(size: blurredBackgroundNode.bounds.size, cornerRadius: blurredBackgroundNode.bounds.height / 2.0, transition: transition) - } else { - let blurredBackgroundNode = NavigationBackgroundNode(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1) - strongSelf.blurredBackgroundNode = blurredBackgroundNode - strongSelf.insertSubnode(blurredBackgroundNode, at: 0) - blurredBackgroundNode.frame = CGRect(origin: CGPoint(), size: layoutSize) - blurredBackgroundNode.update(size: blurredBackgroundNode.bounds.size, cornerRadius: blurredBackgroundNode.bounds.height / 2.0, transition: .immediate) - } - } else if let blurredBackgroundNode = strongSelf.blurredBackgroundNode { - strongSelf.blurredBackgroundNode = nil - blurredBackgroundNode.removeFromSupernode() - } - - strongSelf.dateNode.displaysAsynchronously = !presentationData.isPreview - let _ = dateApply() - - if let currentImpressionIcon = currentImpressionIcon { - currentImpressionIcon.displaysAsynchronously = !presentationData.isPreview - if currentImpressionIcon.image !== impressionImage { - currentImpressionIcon.image = impressionImage - } - if currentImpressionIcon.supernode == nil { - strongSelf.impressionIcon = currentImpressionIcon - strongSelf.addSubnode(currentImpressionIcon) - } - currentImpressionIcon.frame = CGRect(origin: CGPoint(x: leftInset + backgroundInsets.left, y: backgroundInsets.top + 1.0 + offset + floor((date.size.height - impressionSize.height) / 2.0)), size: impressionSize) - } else if let impressionIcon = strongSelf.impressionIcon { - impressionIcon.removeFromSupernode() - strongSelf.impressionIcon = nil - } - - strongSelf.dateNode.frame = CGRect(origin: CGPoint(x: leftInset + backgroundInsets.left + impressionWidth, y: backgroundInsets.top + 1.0 + offset), size: date.size) - - if let clockFrameNode = clockFrameNode { - if strongSelf.clockFrameNode == nil { - strongSelf.clockFrameNode = clockFrameNode - clockFrameNode.image = clockFrameImage - strongSelf.addSubnode(clockFrameNode) - } else if themeUpdated { - clockFrameNode.image = clockFrameImage - } - clockFrameNode.position = CGPoint(x: backgroundInsets.left + clockPosition.x + reactionInset, y: backgroundInsets.top + clockPosition.y) - if let clockFrameNode = strongSelf.clockFrameNode { - maybeAddRotationAnimation(clockFrameNode.layer, duration: 6.0) - } - } else if let clockFrameNode = strongSelf.clockFrameNode { - clockFrameNode.removeFromSupernode() - strongSelf.clockFrameNode = nil - } - - if let clockMinNode = clockMinNode { - if strongSelf.clockMinNode == nil { - strongSelf.clockMinNode = clockMinNode - clockMinNode.image = clockMinImage - strongSelf.addSubnode(clockMinNode) - } else if themeUpdated { - clockMinNode.image = clockMinImage - } - clockMinNode.position = CGPoint(x: backgroundInsets.left + clockPosition.x + reactionInset, y: backgroundInsets.top + clockPosition.y) - if let clockMinNode = strongSelf.clockMinNode { - maybeAddRotationAnimation(clockMinNode.layer, duration: 1.0) - } - } else if let clockMinNode = strongSelf.clockMinNode { - clockMinNode.removeFromSupernode() - strongSelf.clockMinNode = nil - } - - if let checkSentNode = checkSentNode, let checkReadNode = checkReadNode { - var animateSentNode = false - if strongSelf.checkSentNode == nil { - checkSentNode.image = loadedCheckFullImage - strongSelf.checkSentNode = checkSentNode - strongSelf.addSubnode(checkSentNode) - animateSentNode = animated - } else if themeUpdated { - checkSentNode.image = loadedCheckFullImage + strongSelf.reactionSelected?(value) + }, + reactions: [], + colors: reactionColors, + constrainedWidth: arguments.constrainedSize.width, + transition: .immediate + ) + case let .trailingContent(contentWidth, preferAdditionalInset): + reactionButtons = reactionButtonsContainer.update( + context: arguments.context, + action: { value in + guard let strongSelf = self else { + return } + strongSelf.reactionSelected?(value) + }, + reactions: arguments.reactions.map { reaction in + var iconFile: TelegramMediaFile? - if let checkSentFrame = checkSentFrame { - if checkSentNode.isHidden { - animateSentNode = animated - } - checkSentNode.isHidden = false - checkSentNode.frame = checkSentFrame.offsetBy(dx: backgroundInsets.left + reactionInset, dy: backgroundInsets.top) - } else { - checkSentNode.isHidden = true - } - - var animateReadNode = false - if strongSelf.checkReadNode == nil { - animateReadNode = animated - checkReadNode.image = loadedCheckPartialImage - strongSelf.checkReadNode = checkReadNode - strongSelf.addSubnode(checkReadNode) - } else if themeUpdated { - checkReadNode.image = loadedCheckPartialImage - } - - if let checkReadFrame = checkReadFrame { - if checkReadNode.isHidden { - animateReadNode = animated - } - checkReadNode.isHidden = false - checkReadNode.frame = checkReadFrame.offsetBy(dx: backgroundInsets.left + reactionInset, dy: backgroundInsets.top) - } else { - checkReadNode.isHidden = true - } - - if animateSentNode { - strongSelf.checkSentNode?.layer.animateScale(from: 1.3, to: 1.0, duration: 0.1) - } - if animateReadNode { - strongSelf.checkReadNode?.layer.animateScale(from: 1.3, to: 1.0, duration: 0.1) - } - } else if let checkSentNode = strongSelf.checkSentNode, let checkReadNode = strongSelf.checkReadNode { - checkSentNode.removeFromSupernode() - checkReadNode.removeFromSupernode() - strongSelf.checkSentNode = nil - strongSelf.checkReadNode = nil - } - - var reactionOffset: CGFloat = leftInset - reactionInset + backgroundInsets.left - for i in 0 ..< reactions.count { - let node: StatusReactionNode - if strongSelf.reactionNodes.count > i { - node = strongSelf.reactionNodes[i] - } else { - node = StatusReactionNode() - if strongSelf.reactionNodes.count > i { - let previousNode = strongSelf.reactionNodes[i] - if animated { - previousNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousNode] _ in - previousNode?.removeFromSupernode() - }) - } else { - previousNode.removeFromSupernode() + if let availableReactions = arguments.availableReactions { + for availableReaction in availableReactions.reactions { + if availableReaction.value == reaction.value { + iconFile = availableReaction.staticIcon + break } - strongSelf.reactionNodes[i] = node - } else { - strongSelf.reactionNodes.append(node) } } - node.update(type: type, value: reactions[i].value, isSelected: reactions[i].isSelected, count: Int(reactions[i].count), theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, animated: false) - if node.supernode == nil { - strongSelf.addSubnode(node) - if animated { - node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - } - node.frame = CGRect(origin: CGPoint(x: reactionOffset, y: backgroundInsets.top + offset + 1.0), size: CGSize(width: reactionSize, height: reactionSize)) - reactionOffset += reactionSize + reactionSpacing - } - if !reactions.isEmpty { - reactionOffset += reactionTrailingSpacing - } - for _ in reactions.count ..< strongSelf.reactionNodes.count { - let node = strongSelf.reactionNodes.removeLast() - if animated { - if let previousLayoutSize = previousLayoutSize { - node.frame = node.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0) - } - node.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false) - node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in - node?.removeFromSupernode() - }) - } else { - node.removeFromSupernode() + return ReactionButtonsLayoutContainer.Reaction( + reaction: ReactionButtonComponent.Reaction( + value: reaction.value, + iconFile: iconFile + ), + count: Int(reaction.count), + isSelected: reaction.isSelected + ) + }, + colors: reactionColors, + constrainedWidth: arguments.constrainedSize.width, + transition: .immediate + ) + + var reactionButtonsSize = CGSize() + var currentRowWidth: CGFloat = 0.0 + for item in reactionButtons.items { + if currentRowWidth + item.size.width > arguments.constrainedSize.width { + reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth) + if !reactionButtonsSize.height.isZero { + reactionButtonsSize.height += 6.0 } + reactionButtonsSize.height += item.size.height + currentRowWidth = 0.0 } - if let (layout, apply) = reactionCountLayoutAndApply { - let node = apply() - if strongSelf.reactionCountNode !== node { - strongSelf.reactionCountNode?.removeFromSupernode() - strongSelf.addSubnode(node) - strongSelf.reactionCountNode = node - if animated { - node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - } - } - node.frame = CGRect(origin: CGPoint(x: reactionOffset + 1.0, y: backgroundInsets.top + 1.0 + offset), size: layout.size) - reactionOffset += 1.0 + layout.size.width + 4.0 - } else if let reactionCountNode = strongSelf.reactionCountNode { - strongSelf.reactionCountNode = nil - if animated { - if let previousLayoutSize = previousLayoutSize { - reactionCountNode.frame = reactionCountNode.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0) - } - reactionCountNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak reactionCountNode] _ in - reactionCountNode?.removeFromSupernode() - }) - } else { - reactionCountNode.removeFromSupernode() - } + if !currentRowWidth.isZero { + currentRowWidth += 6.0 } - - if let currentRepliesIcon = currentRepliesIcon { - currentRepliesIcon.displaysAsynchronously = !presentationData.isPreview - if currentRepliesIcon.image !== repliesImage { - currentRepliesIcon.image = repliesImage - } - if currentRepliesIcon.supernode == nil { - strongSelf.repliesIcon = currentRepliesIcon - strongSelf.addSubnode(currentRepliesIcon) - if animated { - currentRepliesIcon.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - } - } - currentRepliesIcon.frame = CGRect(origin: CGPoint(x: reactionOffset - 2.0, y: backgroundInsets.top + offset + floor((date.size.height - repliesIconSize.height) / 2.0)), size: repliesIconSize) - reactionOffset += 9.0 - } else if let repliesIcon = strongSelf.repliesIcon { - strongSelf.repliesIcon = nil - if animated { - if let previousLayoutSize = previousLayoutSize { - repliesIcon.frame = repliesIcon.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0) - } - repliesIcon.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak repliesIcon] _ in - repliesIcon?.removeFromSupernode() - }) - } else { - repliesIcon.removeFromSupernode() - } + currentRowWidth += item.size.width + } + if !currentRowWidth.isZero && !reactionButtons.items.isEmpty { + reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth) + if !reactionButtonsSize.height.isZero { + reactionButtonsSize.height += 6.0 } - - if let (layout, apply) = replyCountLayoutAndApply { - let node = apply() - if strongSelf.replyCountNode !== node { - strongSelf.replyCountNode?.removeFromSupernode() - strongSelf.addSubnode(node) - strongSelf.replyCountNode = node - if animated { - node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - } - } - node.frame = CGRect(origin: CGPoint(x: reactionOffset + 4.0, y: backgroundInsets.top + 1.0 + offset), size: layout.size) - reactionOffset += 4.0 + layout.size.width - } else if let replyCountNode = strongSelf.replyCountNode { - strongSelf.replyCountNode = nil - if animated { - if let previousLayoutSize = previousLayoutSize { - replyCountNode.frame = replyCountNode.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0) - } - replyCountNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak replyCountNode] _ in - replyCountNode?.removeFromSupernode() - }) - } else { - replyCountNode.removeFromSupernode() - } + reactionButtonsSize.height += reactionButtons.items[0].size.height + } + + if reactionButtonsSize.width.isZero { + verticalReactionsInset = 0.0 + if contentWidth + layoutSize.width > arguments.constrainedSize.width { + resultingWidth = layoutSize.width + verticalInset = 0.0 + resultingHeight = layoutSize.height + verticalInset + } else { + resultingWidth = contentWidth + layoutSize.width + verticalInset = -layoutSize.height + resultingHeight = 0.0 + } + } else { + if preferAdditionalInset { + verticalReactionsInset = 5.0 + } else { + verticalReactionsInset = 2.0 + } + if currentRowWidth + layoutSize.width > arguments.constrainedSize.width { + resultingWidth = max(layoutSize.width, reactionButtonsSize.width) + resultingHeight = verticalReactionsInset + reactionButtonsSize.height + layoutSize.height + verticalInset = verticalReactionsInset + reactionButtonsSize.height + } else { + resultingWidth = layoutSize.width + currentRowWidth + verticalInset = verticalReactionsInset + reactionButtonsSize.height - layoutSize.height + resultingHeight = verticalReactionsInset + reactionButtonsSize.height } } - }) - } - } - - static func asyncLayout(_ node: ChatMessageDateAndStatusNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize, _ reactions: [MessageReaction], _ replies: Int, _ isPinned: Bool, _ hasAutoremove: Bool) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode) { - let currentLayout = node?.asyncLayout() - return { context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replies, isPinned, hasAutoremove in - let resultNode: ChatMessageDateAndStatusNode - let resultSizeAndApply: (CGSize, (Bool) -> Void) - if let node = node, let currentLayout = currentLayout { - resultNode = node - resultSizeAndApply = currentLayout(context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replies, isPinned, hasAutoremove) - } else { - resultNode = ChatMessageDateAndStatusNode() - resultSizeAndApply = resultNode.asyncLayout()(context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replies, isPinned, hasAutoremove) } - return (resultSizeAndApply.0, { animated in - resultSizeAndApply.1(animated) - return resultNode + return (resultingWidth, { boundingWidth in + return (CGSize(width: boundingWidth, height: resultingHeight), { animated in + if let strongSelf = self { + let leftOffset = boundingWidth - layoutSize.width + + strongSelf.theme = arguments.presentationData.theme + strongSelf.type = arguments.type + strongSelf.layoutSize = layoutSize + + var reactionButtonPosition = CGPoint(x: 0.0, y: verticalReactionsInset) + for item in reactionButtons.items { + if reactionButtonPosition.x + item.size.width > boundingWidth { + reactionButtonPosition.x = 0.0 + reactionButtonPosition.y += item.size.height + 6.0 + } + + if item.view.superview == nil { + strongSelf.view.addSubview(item.view) + } + item.view.frame = CGRect(origin: reactionButtonPosition, size: item.size) + reactionButtonPosition.x += item.size.width + 6.0 + } + + for view in reactionButtons.removedViews { + view.removeFromSuperview() + } + + if backgroundImage != nil { + if let currentBackgroundNode = currentBackgroundNode { + if currentBackgroundNode.supernode == nil { + strongSelf.backgroundNode = currentBackgroundNode + currentBackgroundNode.image = backgroundImage + strongSelf.insertSubnode(currentBackgroundNode, at: 0) + } else if themeUpdated { + currentBackgroundNode.image = backgroundImage + } + } + if let backgroundNode = strongSelf.backgroundNode { + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate + if let previousLayoutSize = previousLayoutSize { + backgroundNode.frame = backgroundNode.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0) + } + transition.updateFrame(node: backgroundNode, frame: CGRect(origin: CGPoint(), size: layoutSize)) + } + } else { + if let backgroundNode = strongSelf.backgroundNode { + backgroundNode.removeFromSupernode() + strongSelf.backgroundNode = nil + } + } + + if let blurredBackgroundColor = blurredBackgroundColor { + if let blurredBackgroundNode = strongSelf.blurredBackgroundNode { + blurredBackgroundNode.updateColor(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1, transition: .immediate) + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate + if let previousLayoutSize = previousLayoutSize { + blurredBackgroundNode.frame = blurredBackgroundNode.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0) + } + transition.updateFrame(node: blurredBackgroundNode, frame: CGRect(origin: CGPoint(), size: layoutSize)) + blurredBackgroundNode.update(size: blurredBackgroundNode.bounds.size, cornerRadius: blurredBackgroundNode.bounds.height / 2.0, transition: transition) + } else { + let blurredBackgroundNode = NavigationBackgroundNode(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1) + strongSelf.blurredBackgroundNode = blurredBackgroundNode + strongSelf.insertSubnode(blurredBackgroundNode, at: 0) + blurredBackgroundNode.frame = CGRect(origin: CGPoint(), size: layoutSize) + blurredBackgroundNode.update(size: blurredBackgroundNode.bounds.size, cornerRadius: blurredBackgroundNode.bounds.height / 2.0, transition: .immediate) + } + } else if let blurredBackgroundNode = strongSelf.blurredBackgroundNode { + strongSelf.blurredBackgroundNode = nil + blurredBackgroundNode.removeFromSupernode() + } + + strongSelf.dateNode.displaysAsynchronously = !arguments.presentationData.isPreview + let _ = dateApply() + + if let currentImpressionIcon = currentImpressionIcon { + currentImpressionIcon.displaysAsynchronously = !arguments.presentationData.isPreview + if currentImpressionIcon.image !== impressionImage { + currentImpressionIcon.image = impressionImage + } + if currentImpressionIcon.supernode == nil { + strongSelf.impressionIcon = currentImpressionIcon + strongSelf.addSubnode(currentImpressionIcon) + } + currentImpressionIcon.frame = CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left, y: backgroundInsets.top + 1.0 + offset + verticalInset + floor((date.size.height - impressionSize.height) / 2.0)), size: impressionSize) + } else if let impressionIcon = strongSelf.impressionIcon { + impressionIcon.removeFromSupernode() + strongSelf.impressionIcon = nil + } + + strongSelf.dateNode.frame = CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left + impressionWidth, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: date.size) + + if let clockFrameNode = clockFrameNode { + if strongSelf.clockFrameNode == nil { + strongSelf.clockFrameNode = clockFrameNode + clockFrameNode.image = clockFrameImage + strongSelf.addSubnode(clockFrameNode) + } else if themeUpdated { + clockFrameNode.image = clockFrameImage + } + clockFrameNode.position = CGPoint(x: leftOffset + backgroundInsets.left + clockPosition.x + reactionInset, y: backgroundInsets.top + clockPosition.y + verticalInset) + if let clockFrameNode = strongSelf.clockFrameNode { + maybeAddRotationAnimation(clockFrameNode.layer, duration: 6.0) + } + } else if let clockFrameNode = strongSelf.clockFrameNode { + clockFrameNode.removeFromSupernode() + strongSelf.clockFrameNode = nil + } + + if let clockMinNode = clockMinNode { + if strongSelf.clockMinNode == nil { + strongSelf.clockMinNode = clockMinNode + clockMinNode.image = clockMinImage + strongSelf.addSubnode(clockMinNode) + } else if themeUpdated { + clockMinNode.image = clockMinImage + } + clockMinNode.position = CGPoint(x: leftOffset + backgroundInsets.left + clockPosition.x + reactionInset, y: backgroundInsets.top + clockPosition.y + verticalInset) + if let clockMinNode = strongSelf.clockMinNode { + maybeAddRotationAnimation(clockMinNode.layer, duration: 1.0) + } + } else if let clockMinNode = strongSelf.clockMinNode { + clockMinNode.removeFromSupernode() + strongSelf.clockMinNode = nil + } + + if let checkSentNode = checkSentNode, let checkReadNode = checkReadNode { + var animateSentNode = false + if strongSelf.checkSentNode == nil { + checkSentNode.image = loadedCheckFullImage + strongSelf.checkSentNode = checkSentNode + strongSelf.addSubnode(checkSentNode) + animateSentNode = animated + } else if themeUpdated { + checkSentNode.image = loadedCheckFullImage + } + + if let checkSentFrame = checkSentFrame { + if checkSentNode.isHidden { + animateSentNode = animated + } + checkSentNode.isHidden = false + checkSentNode.frame = checkSentFrame.offsetBy(dx: leftOffset + backgroundInsets.left + reactionInset, dy: backgroundInsets.top + verticalInset) + } else { + checkSentNode.isHidden = true + } + + var animateReadNode = false + if strongSelf.checkReadNode == nil { + animateReadNode = animated + checkReadNode.image = loadedCheckPartialImage + strongSelf.checkReadNode = checkReadNode + strongSelf.addSubnode(checkReadNode) + } else if themeUpdated { + checkReadNode.image = loadedCheckPartialImage + } + + if let checkReadFrame = checkReadFrame { + if checkReadNode.isHidden { + animateReadNode = animated + } + checkReadNode.isHidden = false + checkReadNode.frame = checkReadFrame.offsetBy(dx: leftOffset + backgroundInsets.left + reactionInset, dy: backgroundInsets.top + verticalInset) + } else { + checkReadNode.isHidden = true + } + + if animateSentNode { + strongSelf.checkSentNode?.layer.animateScale(from: 1.3, to: 1.0, duration: 0.1) + } + if animateReadNode { + strongSelf.checkReadNode?.layer.animateScale(from: 1.3, to: 1.0, duration: 0.1) + } + } else if let checkSentNode = strongSelf.checkSentNode, let checkReadNode = strongSelf.checkReadNode { + checkSentNode.removeFromSupernode() + checkReadNode.removeFromSupernode() + strongSelf.checkSentNode = nil + strongSelf.checkReadNode = nil + } + + var reactionOffset: CGFloat = leftOffset + leftInset - reactionInset + backgroundInsets.left + if !"".isEmpty { + for i in 0 ..< arguments.reactions.count { + let node: StatusReactionNode + if strongSelf.reactionNodes.count > i { + node = strongSelf.reactionNodes[i] + } else { + node = StatusReactionNode() + if strongSelf.reactionNodes.count > i { + let previousNode = strongSelf.reactionNodes[i] + if animated { + previousNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousNode] _ in + previousNode?.removeFromSupernode() + }) + } else { + previousNode.removeFromSupernode() + } + strongSelf.reactionNodes[i] = node + } else { + strongSelf.reactionNodes.append(node) + } + } + + node.update(type: arguments.type, value: arguments.reactions[i].value, isSelected: arguments.reactions[i].isSelected, count: Int(arguments.reactions[i].count), theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper, animated: false) + if node.supernode == nil { + strongSelf.addSubnode(node) + if animated { + node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + node.frame = CGRect(origin: CGPoint(x: reactionOffset, y: backgroundInsets.top + offset + verticalInset + 1.0), size: CGSize(width: reactionSize, height: reactionSize)) + reactionOffset += reactionSize + reactionSpacing + } + if !arguments.reactions.isEmpty { + reactionOffset += reactionTrailingSpacing + } + + for _ in arguments.reactions.count ..< strongSelf.reactionNodes.count { + let node = strongSelf.reactionNodes.removeLast() + if animated { + if let previousLayoutSize = previousLayoutSize { + node.frame = node.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0) + } + node.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false) + node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in + node?.removeFromSupernode() + }) + } else { + node.removeFromSupernode() + } + } + } + + if let (layout, apply) = reactionCountLayoutAndApply { + let node = apply() + if strongSelf.reactionCountNode !== node { + strongSelf.reactionCountNode?.removeFromSupernode() + strongSelf.addSubnode(node) + strongSelf.reactionCountNode = node + if animated { + node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + } + node.frame = CGRect(origin: CGPoint(x: reactionOffset + 1.0, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: layout.size) + reactionOffset += 1.0 + layout.size.width + 4.0 + } else if let reactionCountNode = strongSelf.reactionCountNode { + strongSelf.reactionCountNode = nil + if animated { + if let previousLayoutSize = previousLayoutSize { + reactionCountNode.frame = reactionCountNode.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0) + } + reactionCountNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak reactionCountNode] _ in + reactionCountNode?.removeFromSupernode() + }) + } else { + reactionCountNode.removeFromSupernode() + } + } + + if let currentRepliesIcon = currentRepliesIcon { + currentRepliesIcon.displaysAsynchronously = !arguments.presentationData.isPreview + if currentRepliesIcon.image !== repliesImage { + currentRepliesIcon.image = repliesImage + } + if currentRepliesIcon.supernode == nil { + strongSelf.repliesIcon = currentRepliesIcon + strongSelf.addSubnode(currentRepliesIcon) + if animated { + currentRepliesIcon.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + } + currentRepliesIcon.frame = CGRect(origin: CGPoint(x: reactionOffset - 2.0, y: backgroundInsets.top + offset + verticalInset + floor((date.size.height - repliesIconSize.height) / 2.0)), size: repliesIconSize) + reactionOffset += 9.0 + } else if let repliesIcon = strongSelf.repliesIcon { + strongSelf.repliesIcon = nil + if animated { + if let previousLayoutSize = previousLayoutSize { + repliesIcon.frame = repliesIcon.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0) + } + repliesIcon.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak repliesIcon] _ in + repliesIcon?.removeFromSupernode() + }) + } else { + repliesIcon.removeFromSupernode() + } + } + + if let (layout, apply) = replyCountLayoutAndApply { + let node = apply() + if strongSelf.replyCountNode !== node { + strongSelf.replyCountNode?.removeFromSupernode() + strongSelf.addSubnode(node) + strongSelf.replyCountNode = node + if animated { + node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + } + node.frame = CGRect(origin: CGPoint(x: reactionOffset + 4.0, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: layout.size) + reactionOffset += 4.0 + layout.size.width + } else if let replyCountNode = strongSelf.replyCountNode { + strongSelf.replyCountNode = nil + if animated { + if let previousLayoutSize = previousLayoutSize { + replyCountNode.frame = replyCountNode.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0) + } + replyCountNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak replyCountNode] _ in + replyCountNode?.removeFromSupernode() + }) + } else { + replyCountNode.removeFromSupernode() + } + } + } + }) }) } } - func reactionNode(value: String) -> (ASDisplayNode, ASDisplayNode)? { - for node in self.reactionNodes { - return (node.selectedImageNode, node.selectedImageNode) + static func asyncLayout(_ node: ChatMessageDateAndStatusNode?) -> (_ arguments: Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode)) { + let currentLayout = node?.asyncLayout() + return { arguments in + let resultNode: ChatMessageDateAndStatusNode + let resultSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void)) + if let node = node, let currentLayout = currentLayout { + resultNode = node + resultSuggestedWidthAndContinue = currentLayout(arguments) + } else { + resultNode = ChatMessageDateAndStatusNode() + resultSuggestedWidthAndContinue = resultNode.asyncLayout()(arguments) + } + + return (resultSuggestedWidthAndContinue.0, { boundingWidth in + let (size, apply) = resultSuggestedWidthAndContinue.1(boundingWidth) + return (size, { animated in + apply(animated) + + return resultNode + }) + }) + } + } + + func reactionView(value: String) -> UIView? { + for (_, button) in self.reactionButtonsContainer.buttons { + if let result = button.findTaggedView(tag: ReactionButtonComponent.ViewTag(value: value)) as? ReactionButtonComponent.View { + return result.iconView + } } return nil } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + for (_, button) in self.reactionButtonsContainer.buttons { + if button.frame.contains(point) { + if let result = button.hitTest(self.view.convert(point, to: button), with: event) { + return result + } + } + } if self.pressed != nil { if self.bounds.contains(point) { return self.view diff --git a/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift index 7287acd743..30ba4badc4 100644 --- a/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift @@ -56,6 +56,13 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { let _ = item.controllerInteraction.displayImportedMessageTooltip(sourceNode) } } + + self.interactiveFileNode.dateAndStatusNode.reactionSelected = { [weak self] value in + guard let strongSelf = self, let item = strongSelf.item else { + return + } + item.controllerInteraction.updateMessageReaction(item.message, value) + } } override func accessibilityActivate() -> Bool { @@ -161,9 +168,16 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { self.interactiveFileNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } - override func reactionTargetNode(value: String) -> (ASDisplayNode, ASDisplayNode)? { + override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + if self.interactiveFileNode.dateAndStatusNode.supernode != nil, let _ = self.interactiveFileNode.dateAndStatusNode.hitTest(self.view.convert(point, to: self.interactiveFileNode.dateAndStatusNode.view), with: nil) { + return .ignore + } + return super.tapActionAtPoint(point, gesture: gesture, isEstimating: isEstimating) + } + + override func reactionTargetView(value: String) -> UIView? { if !self.interactiveFileNode.dateAndStatusNode.isHidden { - return self.interactiveFileNode.dateAndStatusNode.reactionNode(value: value) + return self.interactiveFileNode.dateAndStatusNode.reactionView(value: value) } return nil } diff --git a/submodules/TelegramUI/Sources/ChatMessageGameBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageGameBubbleContentNode.swift index ca9c3a037c..7235e824f7 100644 --- a/submodules/TelegramUI/Sources/ChatMessageGameBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageGameBubbleContentNode.swift @@ -140,9 +140,9 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode { return self.contentNode.transitionNode(media: media) } - override func reactionTargetNode(value: String) -> (ASDisplayNode, ASDisplayNode)? { + override func reactionTargetView(value: String) -> UIView? { if !self.contentNode.statusNode.isHidden { - return self.contentNode.statusNode.reactionNode(value: value) + return self.contentNode.statusNode.reactionView(value: value) } return nil } diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift index 2e84df6df2..0f676c9eca 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift @@ -1248,9 +1248,9 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } } - override func targetReactionNode(value: String) -> (ASDisplayNode, ASDisplayNode)? { + override func targetReactionView(value: String) -> UIView? { if !self.interactiveVideoNode.dateAndStatusNode.isHidden { - return self.interactiveVideoNode.dateAndStatusNode.reactionNode(value: value) + return self.interactiveVideoNode.dateAndStatusNode.reactionView(value: value) } return nil } diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift index c13b85d8bd..c6a638689d 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -280,9 +280,6 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: context, file: file, message: message, isRecentActions: isRecentActions, isGlobalSearch: false) } - var statusSize: CGSize? - var statusApply: ((Bool) -> Void)? - var consumableContentIcon: UIImage? for attribute in message.attributes { if let attribute = attribute as? ConsumableContentMessageAttribute { @@ -298,36 +295,6 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } } - if let statusType = dateAndStatusType { - var edited = false - if attributes.updatingMedia != nil { - edited = true - } - var viewCount: Int? - var dateReplies = 0 - let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: message.attributes)?.reactions ?? [] - for attribute in message.attributes { - if let attribute = attribute as? EditedMessageAttribute { - edited = !attribute.isHidden - } else if let attribute = attribute as? ViewCountMessageAttribute { - viewCount = attribute.count - } else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = chatLocation { - if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info { - dateReplies = Int(attribute.count) - } - } - } - if forcedIsEdited { - edited = true - } - - let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings) - - let (size, apply) = statusLayout(context, presentationData, edited, viewCount, dateText, statusType, constrainedSize, dateReactions, dateReplies, isPinned && !associatedData.isInPinnedListMode, message.isSelfExpiring) - statusSize = size - statusApply = apply - } - var candidateTitleString: NSAttributedString? var candidateDescriptionString: NSAttributedString? @@ -430,14 +397,80 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { let maxVoiceLength: CGFloat = 30.0 let minVoiceLength: CGFloat = 2.0 + let progressDiameter: CGFloat = 44.0 + + var iconFrame: CGRect? + let progressFrame: CGRect + let controlAreaWidth: CGFloat + + if hasThumbnail { + let currentIconFrame = CGRect(origin: CGPoint(x: -1.0, y: -7.0), size: CGSize(width: 74.0, height: 74.0)) + iconFrame = currentIconFrame + progressFrame = CGRect( + origin: CGPoint( + x: currentIconFrame.minX + floor((currentIconFrame.size.width - progressDiameter) / 2.0), + y: currentIconFrame.minY + floor((currentIconFrame.size.height - progressDiameter) / 2.0) + ), + size: CGSize(width: progressDiameter, height: progressDiameter) + ) + controlAreaWidth = 86.0 + } else { + progressFrame = CGRect( + origin: CGPoint(x: 3.0, y: isVoice ? -3.0 : 0.0), + size: CGSize(width: progressDiameter, height: progressDiameter) + ) + controlAreaWidth = progressFrame.maxX + 8.0 + } + + var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))? + if let statusType = dateAndStatusType { + var edited = false + if attributes.updatingMedia != nil { + edited = true + } + var viewCount: Int? + var dateReplies = 0 + let dateReactions: [MessageReaction] = mergedMessageReactions(attributes: message.attributes)?.reactions ?? [] + for attribute in message.attributes { + if let attribute = attribute as? EditedMessageAttribute { + edited = !attribute.isHidden + } else if let attribute = attribute as? ViewCountMessageAttribute { + viewCount = attribute.count + } else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = chatLocation { + if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info { + dateReplies = Int(attribute.count) + } + } + } + if forcedIsEdited { + edited = true + } + + let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings) + + statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments( + context: context, + presentationData: presentationData, + edited: edited, + impressionCount: viewCount, + dateText: dateText, + type: statusType, + layoutInput: .trailingContent(contentWidth: iconFrame == nil ? 1000.0 : controlAreaWidth, preferAdditionalInset: true), + constrainedSize: constrainedSize, + availableReactions: associatedData.availableReactions, + reactions: dateReactions, + replyCount: dateReplies, + isPinned: isPinned && !associatedData.isInPinnedListMode, + hasAutoremove: message.isSelfExpiring + )) + } + var minLayoutWidth: CGFloat if hasThumbnail { minLayoutWidth = max(titleLayout.size.width, descriptionMaxWidth) + 86.0 } else if isVoice { - var descriptionAndStatusWidth = descriptionLayout.size.width - if let statusSize = statusSize { - descriptionAndStatusWidth += 6 + statusSize.width - } + let descriptionAndStatusWidth = descriptionLayout.size.width + let calcDuration = max(minVoiceLength, min(maxVoiceLength, CGFloat(audioDuration))) minLayoutWidth = minVoiceWidth + (maxVoiceWidth - minVoiceWidth) * (calcDuration - minVoiceLength) / (maxVoiceLength - minVoiceLength) minLayoutWidth = max(descriptionAndStatusWidth + 56, minLayoutWidth) @@ -445,8 +478,8 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { minLayoutWidth = max(titleLayout.size.width, descriptionMaxWidth) + 44.0 + 8.0 } - if let statusSize = statusSize { - minLayoutWidth = max(minLayoutWidth, statusSize.width) + if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { + minLayoutWidth = max(minLayoutWidth, statusSuggestedWidthAndContinue.0) } let fileIconImage: UIImage? @@ -459,32 +492,6 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } return (minLayoutWidth, { boundingWidth in - let progressDiameter: CGFloat = 44.0 - - var iconFrame: CGRect? - let progressFrame: CGRect - let streamingCacheStatusFrame: CGRect - let controlAreaWidth: CGFloat - - if hasThumbnail { - let currentIconFrame = CGRect(origin: CGPoint(x: -1.0, y: -7.0), size: CGSize(width: 74.0, height: 74.0)) - iconFrame = currentIconFrame - progressFrame = CGRect( - origin: CGPoint( - x: currentIconFrame.minX + floor((currentIconFrame.size.width - progressDiameter) / 2.0), - y: currentIconFrame.minY + floor((currentIconFrame.size.height - progressDiameter) / 2.0) - ), - size: CGSize(width: progressDiameter, height: progressDiameter) - ) - controlAreaWidth = 86.0 - } else { - progressFrame = CGRect( - origin: CGPoint(x: 3.0, y: isVoice ? -3.0 : 0.0), - size: CGSize(width: progressDiameter, height: progressDiameter) - ) - controlAreaWidth = progressFrame.maxX + 8.0 - } - let titleAndDescriptionHeight = titleLayout.size.height - 1.0 + descriptionLayout.size.height let normHeight: CGFloat @@ -513,29 +520,21 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { fittedLayoutSize = CGSize(width: unionSize.width, height: unionSize.height) } - var statusFrame: CGRect? - if let statusSize = statusSize { - fittedLayoutSize.width = max(fittedLayoutSize.width, statusSize.width) - statusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusSize.width, y: fittedLayoutSize.height - statusSize.height + 10.0), size: statusSize) + var statusSizeAndApply: (CGSize, (Bool) -> Void)? + if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { + statusSizeAndApply = statusSuggestedWidthAndContinue.1(boundingWidth) } - - if let statusFrameValue = statusFrame, descriptionFrame.intersects(statusFrameValue) { - let intersection = descriptionFrame.intersection(statusFrameValue) - let addedWidth = intersection.width + 20 - fittedLayoutSize.width += addedWidth - } - if let statusFrameValue = statusFrame, let iconFrame = iconFrame { - if iconFrame.intersects(statusFrameValue) { - fittedLayoutSize.height += 15.0 - statusFrame = statusFrameValue.offsetBy(dx: 0.0, dy: 15.0) - } - } else if let statusFrameValue = statusFrame { - if progressFrame.intersects(statusFrameValue) { - fittedLayoutSize.height += 10.0 - statusFrame = statusFrameValue.offsetBy(dx: 0.0, dy: 10.0) + var statusOffset: CGFloat = 0.0 + if let statusSizeAndApply = statusSizeAndApply { + fittedLayoutSize.width = max(fittedLayoutSize.width, statusSizeAndApply.0.width) + fittedLayoutSize.height += statusSizeAndApply.0.height + if !statusSizeAndApply.0.height.isZero && iconFrame == nil { + statusOffset = -10.0 + fittedLayoutSize.height += statusOffset } } + let streamingCacheStatusFrame: CGRect if (isAudio && !isVoice) || file.previewRepresentations.isEmpty { streamingCacheStatusFrame = CGRect(origin: CGPoint(x: progressFrame.maxX - streamingProgressDiameter + 2.0, y: progressFrame.maxY - streamingProgressDiameter + 2.0), size: CGSize(width: streamingProgressDiameter, height: streamingProgressDiameter)) } else { @@ -569,13 +568,18 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { strongSelf.consumableContentNode.removeFromSupernode() } - if let statusApply = statusApply, let statusFrame = statusFrame { + let statusReferenceFrame: CGRect + if let iconFrame = iconFrame { + statusReferenceFrame = iconFrame + } else { + statusReferenceFrame = progressFrame.offsetBy(dx: 0.0, dy: 8.0) + } + if let statusSizeAndApply = statusSizeAndApply { if strongSelf.dateAndStatusNode.supernode == nil { strongSelf.addSubnode(strongSelf.dateAndStatusNode) } - - strongSelf.dateAndStatusNode.frame = statusFrame - statusApply(false) + strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: statusReferenceFrame.minX, y: statusReferenceFrame.maxY + statusOffset), size: statusSizeAndApply.0) + statusSizeAndApply.1(false) } else if strongSelf.dateAndStatusNode.supernode != nil { strongSelf.dateAndStatusNode.removeFromSupernode() } @@ -1123,6 +1127,13 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { self.playerUpdateTimer?.invalidate() self.playerUpdateTimer = nil } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.dateAndStatusNode.supernode != nil, let result = self.dateAndStatusNode.hitTest(self.view.convert(point, to: self.dateAndStatusNode.view), with: event) { + return result + } + return super.hitTest(point, with: event) + } } diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift index 92db82170c..b0066c044e 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -289,7 +289,23 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { isReplyThread = true } - let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: max(1.0, maxDateAndStatusWidth), height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, item.message.isSelfExpiring) + let statusSuggestedWidthAndContinue = makeDateAndStatusLayout(ChatMessageDateAndStatusNode.Arguments( + context: item.context, + presentationData: item.presentationData, + edited: edited && !sentViaBot, + impressionCount: viewCount, + dateText: dateText, + type: statusType, + layoutInput: .standalone, + constrainedSize: CGSize(width: max(1.0, maxDateAndStatusWidth), height: CGFloat.greatestFiniteMagnitude), + availableReactions: item.associatedData.availableReactions, + reactions: dateReactions, + replyCount: dateReplies, + isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, + hasAutoremove: item.message.isSelfExpiring + )) + + let (dateAndStatusSize, dateAndStatusApply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) var displayVideoFrame = videoFrame displayVideoFrame.size.width *= imageScale diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index 73a7399f60..1c965bfcef 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -345,7 +345,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } - func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> Void))) { + func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> Void))) { let currentMessage = self.message let currentMedia = self.media let imageLayout = self.imageNode.asyncLayout() @@ -359,7 +359,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio let currentAutomaticDownload = self.automaticDownload let currentAutomaticPlayback = self.automaticPlayback - return { [weak self] context, presentationData, dateTimeFormat, message, attributes, media, dateAndStatus, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode in + return { [weak self] context, presentationData, dateTimeFormat, message, associatedData, attributes, media, dateAndStatus, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode in var nativeSize: CGSize let isSecretMedia = message.containsSecretMedia @@ -468,7 +468,24 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio var statusApply: ((Bool) -> Void)? if let dateAndStatus = dateAndStatus { - let (size, apply) = statusLayout(context, presentationData, dateAndStatus.edited, dateAndStatus.viewCount, dateAndStatus.dateText, dateAndStatus.type, CGSize(width: nativeSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude), dateAndStatus.dateReactions, dateAndStatus.dateReplies, dateAndStatus.isPinned, message.isSelfExpiring) + let statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments( + context: context, + presentationData: presentationData, + edited: dateAndStatus.edited, + impressionCount: dateAndStatus.viewCount, + dateText: dateAndStatus.dateText, + type: dateAndStatus.type, + layoutInput: .standalone, + constrainedSize: CGSize(width: nativeSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude), + availableReactions: associatedData.availableReactions, + reactions: dateAndStatus.dateReactions, + replyCount: dateAndStatus.dateReplies, + isPinned: dateAndStatus.isPinned, + hasAutoremove: message.isSelfExpiring + )) + + let (size, apply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) + statusSize = size statusApply = apply } @@ -1484,12 +1501,12 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio } } - static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> ChatMessageInteractiveMediaNode))) { + static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> ChatMessageInteractiveMediaNode))) { let currentAsyncLayout = node?.asyncLayout() - return { context, presentationData, dateTimeFormat, message, attributes, media, dateAndStatus, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode in + return { context, presentationData, dateTimeFormat, message, associatedData, attributes, media, dateAndStatus, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode in var imageNode: ChatMessageInteractiveMediaNode - var imageLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> Void))) + var imageLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> Void))) if let node = node, let currentAsyncLayout = currentAsyncLayout { imageNode = node @@ -1499,7 +1516,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio imageLayout = imageNode.asyncLayout() } - let (unboundSize, initialWidth, continueLayout) = imageLayout(context, presentationData, dateTimeFormat, message, attributes, media, dateAndStatus, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode) + let (unboundSize, initialWidth, continueLayout) = imageLayout(context, presentationData, dateTimeFormat, message, associatedData, attributes, media, dateAndStatus, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode) return (unboundSize, initialWidth, { constrainedSize, automaticPlayback, wideLayout, corners in let (finalWidth, finalLayout) = continueLayout(constrainedSize, automaticPlayback, wideLayout, corners) diff --git a/submodules/TelegramUI/Sources/ChatMessageInvoiceBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageInvoiceBubbleContentNode.swift index 30d1e5d001..cf7b7883dd 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInvoiceBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInvoiceBubbleContentNode.swift @@ -136,9 +136,9 @@ final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode { return self.contentNode.transitionNode(media: media) } - override func reactionTargetNode(value: String) -> (ASDisplayNode, ASDisplayNode)? { + override func reactionTargetView(value: String) -> UIView? { if !self.contentNode.statusNode.isHidden { - return self.contentNode.statusNode.reactionNode(value: value) + return self.contentNode.statusNode.reactionView(value: value) } return nil } diff --git a/submodules/TelegramUI/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Sources/ChatMessageItemView.swift index 727641def8..43d64f554f 100644 --- a/submodules/TelegramUI/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Sources/ChatMessageItemView.swift @@ -869,7 +869,7 @@ public class ChatMessageItemView: ListViewItemNode { } } - func targetReactionNode(value: String) -> (ASDisplayNode, ASDisplayNode)? { + func targetReactionView(value: String) -> UIView? { return nil } diff --git a/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift index fd9c03a372..16c8909b93 100644 --- a/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift @@ -245,9 +245,26 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { isReplyThread = true } - let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, item.message.isSelfExpiring) - statusSize = size - statusApply = apply + let statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments( + context: item.context, + presentationData: item.presentationData, + edited: edited, + impressionCount: viewCount, + dateText: dateText, + type: statusType, + layoutInput: .standalone, + constrainedSize: CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude), + availableReactions: item.associatedData.availableReactions, + reactions: dateReactions, + replyCount: dateReplies, + isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, + hasAutoremove: item.message.isSelfExpiring + )) + + let (dateAndStatusSize, dateAndStatusApply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) + + statusSize = dateAndStatusSize + statusApply = dateAndStatusApply } let contentWidth: CGFloat @@ -487,9 +504,9 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { } } - override func reactionTargetNode(value: String) -> (ASDisplayNode, ASDisplayNode)? { + override func reactionTargetView(value: String) -> UIView? { if !self.dateAndStatusNode.isHidden { - return self.dateAndStatusNode.reactionNode(value: value) + return self.dateAndStatusNode.reactionView(value: value) } return nil } diff --git a/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift index e9e06b8348..94b5662f31 100644 --- a/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift @@ -207,7 +207,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { ) } - let (unboundSize, initialWidth, refineLayout) = interactiveImageLayout(item.context, item.presentationData, item.presentationData.dateTimeFormat, item.message, item.attributes, selectedMedia!, dateAndStatus, automaticDownload, item.associatedData.automaticDownloadPeerType, sizeCalculation, layoutConstants, contentMode) + let (unboundSize, initialWidth, refineLayout) = interactiveImageLayout(item.context, item.presentationData, item.presentationData.dateTimeFormat, item.message, item.associatedData, item.attributes, selectedMedia!, dateAndStatus, automaticDownload, item.associatedData.automaticDownloadPeerType, sizeCalculation, layoutConstants, contentMode) let forceFullCorners = false let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 7.0, hidesBackground: .emptyWallpaper, forceFullCorners: forceFullCorners, forceAlignment: .none) @@ -388,9 +388,9 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { return false } - override func reactionTargetNode(value: String) -> (ASDisplayNode, ASDisplayNode)? { + override func reactionTargetView(value: String) -> UIView? { if !self.interactiveImageNode.dateAndStatusNode.isHidden { - return self.interactiveImageNode.dateAndStatusNode.reactionNode(value: value) + return self.interactiveImageNode.dateAndStatusNode.reactionView(value: value) } return nil } diff --git a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift index e8478110b0..adf5291082 100644 --- a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift @@ -1057,8 +1057,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { statusType = nil } - var statusSize: CGSize? - var statusApply: ((Bool) -> Void)? + var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))? if let statusType = statusType { var isReplyThread = false @@ -1066,11 +1065,25 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { isReplyThread = true } - let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, item.message.isSelfExpiring) - statusSize = size - statusApply = apply + statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments( + context: item.context, + presentationData: item.presentationData, + edited: edited, + impressionCount: viewCount, + dateText: dateText, + type: statusType, + layoutInput: .trailingContent(contentWidth: 100.0, preferAdditionalInset: true), + constrainedSize: textConstrainedSize, + availableReactions: item.associatedData.availableReactions, + reactions: dateReactions, + replyCount: dateReplies, + isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, + hasAutoremove: item.message.isSelfExpiring + )) } + let _ = statusSuggestedWidthAndContinue + var poll: TelegramMediaPoll? for media in item.message.media { if let media = media as? TelegramMediaPoll { @@ -1152,21 +1165,21 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { var textFrame = CGRect(origin: CGPoint(x: -textInsets.left, y: -textInsets.top), size: textLayout.size) var textFrameWithoutInsets = CGRect(origin: CGPoint(x: textFrame.origin.x + textInsets.left, y: textFrame.origin.y + textInsets.top), size: CGSize(width: textFrame.width - textInsets.left - textInsets.right, height: textFrame.height - textInsets.top - textInsets.bottom)) - var statusFrame: CGRect? + /*var statusFrame: CGRect? if let statusSize = statusSize { statusFrame = CGRect(origin: CGPoint(x: textFrameWithoutInsets.maxX - statusSize.width, y: textFrameWithoutInsets.maxY - statusSize.height), size: statusSize) - } + }*/ textFrame = textFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top) textFrameWithoutInsets = textFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top) - statusFrame = statusFrame?.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top) + //statusFrame = statusFrame?.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top) var boundingSize: CGSize = textFrameWithoutInsets.size boundingSize.width += additionalTextRightInset boundingSize.width = max(boundingSize.width, typeLayout.size.width) - boundingSize.width = max(boundingSize.width, votersLayout.size.width + 4.0 + (statusSize?.width ?? 0.0)) - boundingSize.width = max(boundingSize.width, buttonSubmitInactiveTextLayout.size.width + 4.0 + (statusSize?.width ?? 0.0)) - boundingSize.width = max(boundingSize.width, buttonViewResultsTextLayout.size.width + 4.0 + (statusSize?.width ?? 0.0)) + boundingSize.width = max(boundingSize.width, votersLayout.size.width + 4.0/* + (statusSize?.width ?? 0.0)*/) + boundingSize.width = max(boundingSize.width, buttonSubmitInactiveTextLayout.size.width + 4.0/* + (statusSize?.width ?? 0.0)*/) + boundingSize.width = max(boundingSize.width, buttonViewResultsTextLayout.size.width + 4.0/* + (statusSize?.width ?? 0.0)*/) boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom @@ -1288,7 +1301,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { let buttonSubmitActiveTextFrame = CGRect(origin: CGPoint(x: floor((resultSize.width - buttonSubmitActiveTextLayout.size.width) / 2.0), y: optionsButtonSpacing), size: buttonSubmitActiveTextLayout.size) let buttonViewResultsTextFrame = CGRect(origin: CGPoint(x: floor((resultSize.width - buttonViewResultsTextLayout.size.width) / 2.0), y: optionsButtonSpacing), size: buttonViewResultsTextLayout.size) - var adjustedStatusFrame: CGRect? + /*var adjustedStatusFrame: CGRect? if let statusFrame = statusFrame { var localStatusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusFrame.size.width - layoutConstants.text.bubbleInsets.right, y: resultSize.height - statusFrame.size.height - 6.0), size: statusFrame.size) if localStatusFrame.minX <= buttonViewResultsTextFrame.maxX || localStatusFrame.minX <= buttonSubmitActiveTextFrame.maxX { @@ -1296,7 +1309,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { resultSize.height += 10.0 } adjustedStatusFrame = localStatusFrame - } + }*/ return (resultSize, { [weak self] animation, synchronousLoad in if let strongSelf = self { @@ -1367,7 +1380,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } strongSelf.optionNodes = updatedOptionNodes - if let statusApply = statusApply, let adjustedStatusFrame = adjustedStatusFrame { + /*if let statusApply = statusApply, let adjustedStatusFrame = adjustedStatusFrame { let previousStatusFrame = strongSelf.statusNode.frame strongSelf.statusNode.frame = adjustedStatusFrame var hasAnimation = true @@ -1385,7 +1398,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.statusNode.layer.animatePosition(from: previousPosition, to: statusPosition, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) } } - } else if strongSelf.statusNode.supernode != nil { + } else */if strongSelf.statusNode.supernode != nil { strongSelf.statusNode.removeFromSupernode() } @@ -1749,9 +1762,9 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } - override func reactionTargetNode(value: String) -> (ASDisplayNode, ASDisplayNode)? { + override func reactionTargetView(value: String) -> UIView? { if !self.statusNode.isHidden { - return self.statusNode.reactionNode(value: value) + return self.statusNode.reactionView(value: value) } return nil } diff --git a/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift index 1561814f21..804d67eb01 100644 --- a/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift @@ -88,82 +88,66 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode { statusType = nil } - var statusSize: CGSize? - var statusApply: ((Bool) -> Void)? + let entities = [MessageTextEntity(range: 0.. (CGSize, (Bool) -> Void))? if let statusType = statusType { var isReplyThread = false if case .replyThread = item.chatLocation { isReplyThread = true } - let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, item.message.isSelfExpiring) - statusSize = size - statusApply = apply + statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments( + context: item.context, + presentationData: item.presentationData, + edited: edited, + impressionCount: viewCount, + dateText: dateText, + type: statusType, + layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, preferAdditionalInset: false), + constrainedSize: textConstrainedSize, + availableReactions: item.associatedData.availableReactions, + reactions: dateReactions, + replyCount: dateReplies, + isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, + hasAutoremove: item.message.isSelfExpiring + )) } - let entities = [MessageTextEntity(range: 0.. TextNode)? var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)? @@ -931,41 +947,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } if let (_, f) = strongSelf.awaitingAppliedReaction { - /*var bounds = strongSelf.bounds - let offset = bounds.origin.x - bounds.origin.x = 0.0 - strongSelf.bounds = bounds - var shadowBounds = strongSelf.shadowNode.bounds - let shadowOffset = shadowBounds.origin.x - shadowBounds.origin.x = 0.0 - strongSelf.shadowNode.bounds = shadowBounds - if !offset.isZero { - strongSelf.layer.animateBoundsOriginXAdditive(from: offset, to: 0.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring) - } - if !shadowOffset.isZero { - strongSelf.shadowNode.layer.animateBoundsOriginXAdditive(from: shadowOffset, to: 0.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring) - } - if let swipeToReplyNode = strongSelf.swipeToReplyNode { - strongSelf.swipeToReplyNode = nil - swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in - swipeToReplyNode?.removeFromSupernode() - }) - swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - } - */ strongSelf.awaitingAppliedReaction = nil - /*var targetNode: ASDisplayNode? - var hideTarget = false - if let awaitingAppliedReaction = awaitingAppliedReaction { - for contentNode in strongSelf.contentNodes { - if let (reactionNode, count) = contentNode.reactionTargetNode(value: awaitingAppliedReaction) { - targetNode = reactionNode - hideTarget = count == 1 - break - } - } - } - strongSelf.reactionRecognizer?.complete(into: targetNode, hideTarget: hideTarget)*/ + f() } } @@ -1512,9 +1495,9 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } - override func targetReactionNode(value: String) -> (ASDisplayNode, ASDisplayNode)? { + override func targetReactionView(value: String) -> UIView? { if !self.dateAndStatusNode.isHidden { - return self.dateAndStatusNode.reactionNode(value: value) + return self.dateAndStatusNode.reactionView(value: value) } return nil } diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index c87866aab2..230fc3926d 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -65,6 +65,13 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { self.textAccessibilityOverlayNode.openUrl = { [weak self] url in self?.item?.controllerInteraction.openUrl(url, false, false, nil) } + + self.statusNode.reactionSelected = { [weak self] value in + guard let strongSelf = self, let item = strongSelf.item else { + return + } + item.controllerInteraction.updateMessageReaction(item.message, value) + } } required init?(coder aDecoder: NSCoder) { @@ -157,20 +164,6 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { statusType = nil } - var statusSize: CGSize? - var statusApply: ((Bool) -> Void)? - - if let statusType = statusType { - var isReplyThread = false - if case .replyThread = item.chatLocation { - isReplyThread = true - } - - let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, item.message.isSelfExpiring) - statusSize = size - statusApply = apply - } - let rawText: String let attributedText: NSAttributedString var messageEntities: [MessageTextEntity]? @@ -265,7 +258,6 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { let messageTheme = incoming ? item.presentationData.theme.theme.chat.message.incoming : item.presentationData.theme.theme.chat.message.outgoing let textFont = item.presentationData.messageFont - let forceStatusNewline = false if let entities = entities { attributedText = stringWithAppliedEntities(rawText, entities: entities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: item.presentationData.messageBoldFont, italicFont: item.presentationData.messageItalicFont, boldItalicFont: item.presentationData.messageBoldItalicFont, fixedFont: item.presentationData.messageFixedFont, blockQuoteFont: item.presentationData.messageBlockQuoteFont) @@ -275,65 +267,61 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { attributedText = NSAttributedString(string: " ", font: textFont, textColor: messageTheme.primaryTextColor) } - var cutout: TextNodeCutout? - if let statusSize = statusSize, !forceStatusNewline { - cutout = TextNodeCutout(bottomRight: statusSize) - } + let cutout: TextNodeCutout? = nil let textInsets = UIEdgeInsets(top: 2.0, left: 2.0, bottom: 5.0, right: 2.0) let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: cutout, insets: textInsets, lineColor: messageTheme.accentControlColor)) + var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))? + if let statusType = statusType { + var isReplyThread = false + if case .replyThread = item.chatLocation { + isReplyThread = true + } + + statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments( + context: item.context, + presentationData: item.presentationData, + edited: edited, + impressionCount: viewCount, + dateText: dateText, + type: statusType, + layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, preferAdditionalInset: false), + constrainedSize: textConstrainedSize, + availableReactions: item.associatedData.availableReactions, + reactions: dateReactions, + replyCount: dateReplies, + isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, + hasAutoremove: item.message.isSelfExpiring + )) + } + var textFrame = CGRect(origin: CGPoint(x: -textInsets.left, y: -textInsets.top), size: textLayout.size) var textFrameWithoutInsets = CGRect(origin: CGPoint(x: textFrame.origin.x + textInsets.left, y: textFrame.origin.y + textInsets.top), size: CGSize(width: textFrame.width - textInsets.left - textInsets.right, height: textFrame.height - textInsets.top - textInsets.bottom)) - var statusFrame: CGRect? - if let statusSize = statusSize { - statusFrame = CGRect(origin: CGPoint(x: textFrameWithoutInsets.maxX - statusSize.width, y: textFrameWithoutInsets.maxY - statusSize.height), size: statusSize) - } - textFrame = textFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top) textFrameWithoutInsets = textFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top) - statusFrame = statusFrame?.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top) - var suggestedBoundingWidth: CGFloat - if let statusFrame = statusFrame { - suggestedBoundingWidth = textFrameWithoutInsets.union(statusFrame).width - } else { - suggestedBoundingWidth = textFrameWithoutInsets.width + var suggestedBoundingWidth: CGFloat = textFrameWithoutInsets.width + if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { + suggestedBoundingWidth = max(suggestedBoundingWidth, statusSuggestedWidthAndContinue.0) } - suggestedBoundingWidth += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right + let sideInsets = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right + suggestedBoundingWidth += sideInsets return (suggestedBoundingWidth, { boundingWidth in var boundingSize: CGSize - var adjustedStatusFrame: CGRect? - if let statusFrame = statusFrame { - let centeredTextFrame = CGRect(origin: CGPoint(x: floor((boundingWidth - textFrame.size.width) / 2.0), y: 0.0), size: textFrame.size) - let statusOverlapsCenteredText = CGRect(origin: CGPoint(), size: statusFrame.size).intersects(centeredTextFrame) - - if !forceStatusNewline || statusOverlapsCenteredText { - boundingSize = textFrameWithoutInsets.union(statusFrame).size - boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right - boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom - adjustedStatusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusFrame.size.width - layoutConstants.text.bubbleInsets.right, y: statusFrame.origin.y), size: statusFrame.size) - } else { - boundingSize = textFrameWithoutInsets.size - boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right - boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom - adjustedStatusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusFrame.size.width - layoutConstants.text.bubbleInsets.right, y: boundingSize.height - statusFrame.height - layoutConstants.text.bubbleInsets.bottom), size: statusFrame.size) - } - } else { - boundingSize = textFrameWithoutInsets.size - boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right - boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom + let statusSizeAndApply = statusSuggestedWidthAndContinue?.1(boundingWidth - sideInsets) + + boundingSize = textFrameWithoutInsets.size + if let statusSizeAndApply = statusSizeAndApply { + boundingSize.height += statusSizeAndApply.0.height } - if attributedText.string.isEmpty, var adjustedStatusFrameValue = adjustedStatusFrame { - adjustedStatusFrameValue.origin.y = 1.0 - boundingSize.height = adjustedStatusFrameValue.maxY + 5.0 - adjustedStatusFrame = adjustedStatusFrameValue - } + boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right + boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom return (boundingSize, { [weak self] animation, _ in if let strongSelf = self { @@ -366,37 +354,11 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.textNode.displaysAsynchronously = !item.presentationData.isPreview && !item.presentationData.theme.theme.forceSync let _ = textApply() - if let statusApply = statusApply, let adjustedStatusFrame = adjustedStatusFrame { - let previousStatusFrame = strongSelf.statusNode.frame - strongSelf.statusNode.frame = adjustedStatusFrame - var hasAnimation = true - if case .None = animation { - hasAnimation = false - } - statusApply(hasAnimation) - if strongSelf.statusNode.supernode == nil { - strongSelf.addSubnode(strongSelf.statusNode) - } else { - if case let .System(duration) = animation { - let delta = CGPoint(x: previousStatusFrame.maxX - adjustedStatusFrame.maxX, y: previousStatusFrame.minY - adjustedStatusFrame.minY) - let statusPosition = strongSelf.statusNode.layer.position - let previousPosition = CGPoint(x: statusPosition.x + delta.x, y: statusPosition.y + delta.y) - strongSelf.statusNode.layer.animatePosition(from: previousPosition, to: statusPosition, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) - } - } - } else if strongSelf.statusNode.supernode != nil { - strongSelf.statusNode.removeFromSupernode() - } - - var adjustedTextFrame = textFrame - if forceStatusNewline { - adjustedTextFrame.origin.x = floor((boundingWidth - adjustedTextFrame.width) / 2.0) - } - strongSelf.textNode.frame = adjustedTextFrame + strongSelf.textNode.frame = textFrame if let textSelectionNode = strongSelf.textSelectionNode { - let shouldUpdateLayout = textSelectionNode.frame.size != adjustedTextFrame.size - textSelectionNode.frame = adjustedTextFrame - textSelectionNode.highlightAreaNode.frame = adjustedTextFrame + let shouldUpdateLayout = textSelectionNode.frame.size != textFrame.size + textSelectionNode.frame = textFrame + textSelectionNode.highlightAreaNode.frame = textFrame if shouldUpdateLayout { textSelectionNode.updateLayout() } @@ -404,6 +366,18 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.textAccessibilityOverlayNode.frame = textFrame strongSelf.textAccessibilityOverlayNode.cachedLayout = textLayout + if let statusSizeAndApply = statusSizeAndApply { + strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: textFrameWithoutInsets.minX, y: textFrameWithoutInsets.maxY), size: statusSizeAndApply.0) + if strongSelf.statusNode.supernode == nil { + strongSelf.addSubnode(strongSelf.statusNode) + statusSizeAndApply.1(false) + } else { + statusSizeAndApply.1(animation.isAnimated) + } + } else if strongSelf.statusNode.supernode != nil { + strongSelf.statusNode.removeFromSupernode() + } + if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported) { strongSelf.statusNode.pressed = { guard let strongSelf = self else { @@ -470,6 +444,13 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.statusNode.supernode != nil, let result = self.statusNode.hitTest(self.view.convert(point, to: self.statusNode.view), with: event) { + return result + } + return super.hitTest(point, with: event) + } + override func updateTouchesAtPoint(_ point: CGPoint?) { if let item = self.item { var rects: [CGRect]? @@ -618,9 +599,9 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } - override func reactionTargetNode(value: String) -> (ASDisplayNode, ASDisplayNode)? { + override func reactionTargetView(value: String) -> UIView? { if !self.statusNode.isHidden { - return self.statusNode.reactionNode(value: value) + return self.statusNode.reactionView(value: value) } return nil } diff --git a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift index 7825df1a59..8192563e04 100644 --- a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -543,9 +543,9 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { self.contentNode.updateTouchesAtPoint(point.flatMap { $0.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY) }) } - override func reactionTargetNode(value: String) -> (ASDisplayNode, ASDisplayNode)? { + override func reactionTargetView(value: String) -> UIView? { if !self.contentNode.statusNode.isHidden { - return self.contentNode.statusNode.reactionNode(value: value) + return self.contentNode.statusNode.reactionView(value: value) } return nil } diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index 1b841cee2e..7faabe8d07 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -255,7 +255,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { self?.openPeerMention(name) }, openMessageContextMenu: { [weak self] message, selectAll, node, frame, _ in self?.openMessageContextMenu(message: message, selectAll: selectAll, node: node, frame: frame) - }, updateMessageReaction: { _ in + }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift index fbb438847d..0118afeb5e 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift @@ -114,7 +114,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.titleUpdated(title: new) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .changeAbout(prev, new): var peers = SimpleDictionary() var author: Peer? @@ -145,14 +145,14 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: let peers = SimpleDictionary() let attributes: [MessageAttribute] = [] let prevMessage = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: prev, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: new, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: !prev.isEmpty ? .eventLogPreviousDescription(prevMessage) : nil) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: !prev.isEmpty ? .eventLogPreviousDescription(prevMessage) : nil) } case let .changeUsername(prev, new): var peers = SimpleDictionary() @@ -183,7 +183,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } let action: TelegramMediaActionType = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: var previousAttributes: [MessageAttribute] = [] var attributes: [MessageAttribute] = [] @@ -202,7 +202,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let prevMessage = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: prevText, attributes: previousAttributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: text, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: !prev.isEmpty ? .eventLogPreviousLink(prevMessage) : nil) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: !prev.isEmpty ? .eventLogPreviousLink(prevMessage) : nil) } case let .changePhoto(_, new): var peers = SimpleDictionary() @@ -221,7 +221,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.photoUpdated(image: photo) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .toggleInvites(value): var peers = SimpleDictionary() var author: Peer? @@ -248,7 +248,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .toggleSignatures(value): var peers = SimpleDictionary() var author: Peer? @@ -275,7 +275,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .updatePinned(message): switch self.id.contentIndex { case .header: @@ -306,7 +306,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: if let message = message { var peers = SimpleDictionary() @@ -324,7 +324,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: message.effectiveAuthor, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) } else { var peers = SimpleDictionary() var author: Peer? @@ -346,7 +346,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 0), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) } } case let .editMessage(prev, message): @@ -391,7 +391,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -408,7 +408,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: message.effectiveAuthor, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: filterOriginalMessageFlags(message), read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: !prev.text.isEmpty || !message.text.isEmpty ? .eventLogPreviousMessage(filterOriginalMessageFlags(prev)) : nil) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: filterOriginalMessageFlags(message), read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: !prev.text.isEmpty || !message.text.isEmpty ? .eventLogPreviousMessage(filterOriginalMessageFlags(prev)) : nil) } case let .deleteMessage(message): switch self.id.contentIndex { @@ -434,7 +434,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -458,7 +458,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: message.effectiveAuthor, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) } case .participantJoin, .participantLeave: var peers = SimpleDictionary() @@ -476,7 +476,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { action = TelegramMediaActionType.removedMembers(peerIds: [self.entry.event.peerId]) } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .participantInvite(participant): var peers = SimpleDictionary() var author: Peer? @@ -493,7 +493,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action: TelegramMediaActionType action = TelegramMediaActionType.addedMembers(peerIds: [participant.peer.id]) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .participantToggleBan(prev, new): var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -623,7 +623,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: text, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .participantToggleAdmin(prev, new): var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -856,7 +856,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: text, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .changeStickerPack(_, new): var peers = SimpleDictionary() var author: Peer? @@ -885,7 +885,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .togglePreHistoryHidden(value): var peers = SimpleDictionary() var author: Peer? @@ -915,7 +915,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .updateDefaultBannedRights(prev, new): var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -973,7 +973,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: text, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .pollStopped(message): switch self.id.contentIndex { case .header: @@ -1001,7 +1001,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -1018,7 +1018,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: message.author, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: filterOriginalMessageFlags(message), read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: nil) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: filterOriginalMessageFlags(message), read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: nil) } case let .linkedPeerUpdated(previous, updated): var peers = SimpleDictionary() @@ -1074,7 +1074,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .changeGeoLocation(_, updated): var peers = SimpleDictionary() var author: Peer? @@ -1096,12 +1096,12 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let mediaMap = TelegramMediaMap(latitude: updated.latitude, longitude: updated.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: text, attributes: [], media: [mediaMap], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) } else { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) } case let .updateSlowmode(_, newValue): var peers = SimpleDictionary() @@ -1132,7 +1132,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .startGroupCall, .endGroupCall: var peers = SimpleDictionary() var author: Peer? @@ -1169,7 +1169,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .groupCallUpdateParticipantMuteStatus(participantId, isMuted): var peers = SimpleDictionary() var author: Peer? @@ -1203,7 +1203,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .updateGroupCallSettings(joinMuted): var peers = SimpleDictionary() var author: Peer? @@ -1232,7 +1232,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .groupCallUpdateParticipantVolume(participantId, volume): var peers = SimpleDictionary() var author: Peer? @@ -1263,7 +1263,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .deleteExportedInvitation(invite): var peers = SimpleDictionary() var author: Peer? @@ -1289,7 +1289,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .revokeExportedInvitation(invite): var peers = SimpleDictionary() var author: Peer? @@ -1315,7 +1315,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .editExportedInvitation(_, updatedInvite): var peers = SimpleDictionary() var author: Peer? @@ -1341,7 +1341,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .participantJoinedViaInvite(invite): var peers = SimpleDictionary() var author: Peer? @@ -1367,7 +1367,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .changeHistoryTTL(_, updatedValue): var peers = SimpleDictionary() var author: Peer? @@ -1398,7 +1398,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .changeTheme(_, updatedValue): var peers = SimpleDictionary() var author: Peer? @@ -1429,7 +1429,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .participantJoinByRequest(invite, approvedBy): var peers = SimpleDictionary() var author: Peer? @@ -1462,7 +1462,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .toggleCopyProtection(value): var peers = SimpleDictionary() var author: Peer? @@ -1489,7 +1489,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .sendMessage(message): switch self.id.contentIndex { case .header: @@ -1514,7 +1514,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -1531,7 +1531,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: message.effectiveAuthor, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) } } } diff --git a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift index 3a5d0d7850..3252348360 100644 --- a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift +++ b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift @@ -108,7 +108,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { var selectStickerImpl: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? self.controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in - return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, updateMessageReaction: { _ in }, activateMessagePinch: { _ in + return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { fileReference, _, _, _, _, node, rect in return selectStickerImpl?(fileReference, node, rect) ?? false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 9c22d09ff8..3c10a7bc5c 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -69,7 +69,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in - }, updateMessageReaction: { _ in + }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index f9042e2600..ecd2a6d228 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -1835,7 +1835,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node)), items: .single(ContextController.Items(items: items)), recognizer: nil, gesture: gesture) strongSelf.controller?.window?.presentInGlobalOverlay(controller) }) - }, updateMessageReaction: { _ in + }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { [weak self] message, node, rect, gesture in guard let strongSelf = self else { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index d118725be3..8fbf0a89f4 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1239,7 +1239,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { let controllerInteraction: ChatControllerInteraction controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in - return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, updateMessageReaction: { _ in }, activateMessagePinch: { _ in + return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: { message in tapMessage?(message) @@ -1307,7 +1307,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { chatLocation = .peer(messages.first!.id.peerId) } - return ChatMessageItem(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, disableAnimations: false, largeEmoji: false, chatBubbleCorners: chatBubbleCorners, animatedEmojiScale: 1.0, isPreview: true), context: context, chatLocation: chatLocation, associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .contact, automaticDownloadNetworkType: .cellular, isRecentActions: false, subject: nil, contactsPeerIds: Set(), animatedEmojiStickers: [:], forcedResourceStatus: forcedResourceStatus), controllerInteraction: controllerInteraction, content: content, disableDate: true, additionalContent: nil) + return ChatMessageItem(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, disableAnimations: false, largeEmoji: false, chatBubbleCorners: chatBubbleCorners, animatedEmojiScale: 1.0, isPreview: true), context: context, chatLocation: chatLocation, associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .contact, automaticDownloadNetworkType: .cellular, isRecentActions: false, subject: nil, contactsPeerIds: Set(), animatedEmojiStickers: [:], forcedResourceStatus: forcedResourceStatus, availableReactions: nil), controllerInteraction: controllerInteraction, content: content, disableDate: true, additionalContent: nil) } public func makeChatMessageDateHeaderItem(context: AccountContext, timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader {