Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AccessoryPanelNode",
module_name = "AccessoryPanelNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/TelegramPresentationData",
"//submodules/ChatPresentationInterfaceState",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,23 @@
import Foundation
import UIKit
import AsyncDisplayKit
import TelegramPresentationData
import ChatPresentationInterfaceState
open class AccessoryPanelNode: ASDisplayNode {
open var originalFrameBeforeDismissed: CGRect?
open var dismiss: (() -> Void)?
open var interfaceInteraction: ChatPanelInterfaceInteraction?
open func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
}
open func updateState(size: CGSize, inset: CGFloat, interfaceState: ChatPresentationInterfaceState) {
}
open func animateIn() {
}
open func animateOut() {
}
}
@@ -0,0 +1,35 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatAvatarNavigationNode",
module_name = "ChatAvatarNavigationNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/AvatarNode",
"//submodules/ContextUI",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUniversalVideoContent",
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//submodules/GalleryUI",
"//submodules/Components/HierarchyTrackingLayer",
"//submodules/AccountContext",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/AvatarVideoNode",
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
"//submodules/Components/ComponentDisplayAdapters",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,359 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import AvatarNode
import ContextUI
import TelegramPresentationData
import TelegramUniversalVideoContent
import UniversalMediaPlayer
import GalleryUI
import HierarchyTrackingLayer
import AccountContext
import ComponentFlow
import EmojiStatusComponent
import AvatarVideoNode
import AvatarStoryIndicatorComponent
import ComponentDisplayAdapters
private let normalFont = avatarPlaceholderFont(size: 16.0)
private let smallFont = avatarPlaceholderFont(size: 12.0)
public final class ChatAvatarNavigationNode: ASDisplayNode {
private var context: AccountContext?
private let containerNode: ContextControllerSourceNode
public var avatarNode: AvatarNode
private var avatarVideoNode: AvatarVideoNode?
public private(set) var avatarStoryView: ComponentView<Empty>?
public var storyData: (hasUnseen: Bool, hasUnseenCloseFriends: Bool, hasLiveItems: Bool)?
public var statusView: ComponentView<Empty>
private var starView: StarView?
private var cachedDataDisposable = MetaDisposable()
private var hierarchyTrackingLayer: HierarchyTrackingLayer?
private var trackingIsInHierarchy: Bool = false {
didSet {
if self.trackingIsInHierarchy != oldValue {
Queue.mainQueue().justDispatch {
self.updateVideoVisibility()
}
}
}
}
public var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
public var contextActionIsEnabled: Bool = false {
didSet {
if self.contextActionIsEnabled != oldValue {
self.containerNode.isGestureEnabled = self.contextActionIsEnabled
}
}
}
override public init() {
self.containerNode = ContextControllerSourceNode()
self.containerNode.isGestureEnabled = false
self.avatarNode = AvatarNode(font: normalFont)
self.statusView = ComponentView()
super.init()
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.avatarNode)
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self else {
return
}
strongSelf.contextAction?(strongSelf.containerNode, gesture)
}
self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 37.0, height: 37.0)).offsetBy(dx: 10.0, dy: 1.0)
self.avatarNode.frame = self.containerNode.bounds
#if DEBUG
//self.hasUnseenStories = true
#endif
}
deinit {
self.cachedDataDisposable.dispose()
}
override public func didLoad() {
super.didLoad()
self.view.isOpaque = false
}
public func setStatus(context: AccountContext, content: EmojiStatusComponent.Content) {
let statusSize = self.statusView.update(
transition: .immediate,
component: AnyComponent(EmojiStatusComponent(
context: context,
animationCache: context.animationCache,
animationRenderer: context.animationRenderer,
content: content,
isVisibleForAnimations: true,
action: nil
)),
environment: {},
containerSize: CGSize(width: 32.0, height: 32.0)
)
if let statusComponentView = self.statusView.view {
if statusComponentView.superview == nil {
self.containerNode.view.addSubview(statusComponentView)
}
statusComponentView.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - statusSize.width) / 2.0), y: floor((self.containerNode.bounds.height - statusSize.height) / 2.0)), size: statusSize)
}
self.avatarNode.isHidden = true
}
public func setPeer(context: AccountContext, theme: PresentationTheme, peer: EnginePeer?, authorOfMessage: MessageReference? = nil, overrideImage: AvatarNodeImageOverride? = nil, emptyColor: UIColor? = nil, clipStyle: AvatarNodeClipStyle = .round, synchronousLoad: Bool = false, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0), storeUnrounded: Bool = false) {
self.context = context
if let statusComponentView = self.statusView.view {
self.statusView = ComponentView()
statusComponentView.removeFromSuperview()
}
self.avatarNode.isHidden = false
self.avatarNode.setPeer(context: context, theme: theme, peer: peer, authorOfMessage: authorOfMessage, overrideImage: overrideImage, emptyColor: emptyColor, clipStyle: clipStyle, synchronousLoad: synchronousLoad, displayDimensions: displayDimensions, storeUnrounded: storeUnrounded)
if let peer, peer.isSubscription {
let starView: StarView
if let current = self.starView {
starView = current
} else {
starView = StarView()
self.starView = starView
self.containerNode.view.addSubview(starView)
}
starView.outlineColor = theme.rootController.navigationBar.opaqueBackgroundColor
let starSize = CGSize(width: 15.0, height: 15.0)
let starFrame = CGRect(origin: CGPoint(x: self.containerNode.bounds.width - starSize.width + 1.0, y: self.containerNode.bounds.height - starSize.height + 1.0), size: starSize)
starView.frame = starFrame
} else if let starView = self.starView {
self.starView = nil
starView.removeFromSuperview()
}
if let peer = peer, peer.isPremium {
self.cachedDataDisposable.set((context.account.postbox.peerView(id: peer.id)
|> deliverOnMainQueue).start(next: { [weak self] peerView in
guard let strongSelf = self else {
return
}
let cachedPeerData = peerView.cachedData as? CachedUserData
var personalPhoto: TelegramMediaImage?
var profilePhoto: TelegramMediaImage?
var isKnown = false
if let cachedPeerData = cachedPeerData {
if case let .known(maybePersonalPhoto) = cachedPeerData.personalPhoto {
personalPhoto = maybePersonalPhoto
isKnown = true
}
if case let .known(maybePhoto) = cachedPeerData.photo {
profilePhoto = maybePhoto
isKnown = true
}
}
if isKnown {
let photo = personalPhoto ?? profilePhoto
if let photo = photo, !photo.videoRepresentations.isEmpty || photo.emojiMarkup != nil {
let videoNode: AvatarVideoNode
if let current = strongSelf.avatarVideoNode {
videoNode = current
} else {
videoNode = AvatarVideoNode(context: context)
strongSelf.avatarNode.contentNode.addSubnode(videoNode)
strongSelf.avatarVideoNode = videoNode
}
videoNode.update(peer: peer, photo: photo, size: CGSize(width: 37.0, height: 37.0))
if strongSelf.hierarchyTrackingLayer == nil {
let hierarchyTrackingLayer = HierarchyTrackingLayer()
hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.trackingIsInHierarchy = true
}
hierarchyTrackingLayer.didExitHierarchy = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.trackingIsInHierarchy = false
}
strongSelf.hierarchyTrackingLayer = hierarchyTrackingLayer
strongSelf.layer.addSublayer(hierarchyTrackingLayer)
}
} else {
if let avatarVideoNode = strongSelf.avatarVideoNode {
avatarVideoNode.removeFromSupernode()
strongSelf.avatarVideoNode = nil
}
strongSelf.hierarchyTrackingLayer?.removeFromSuperlayer()
strongSelf.hierarchyTrackingLayer = nil
}
strongSelf.updateVideoVisibility()
} else {
if let photo = peer.largeProfileImage, photo.hasVideo {
let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peer.id).start()
}
}
}))
} else {
self.cachedDataDisposable.set(nil)
self.avatarVideoNode?.removeFromSupernode()
self.avatarVideoNode = nil
self.hierarchyTrackingLayer?.removeFromSuperlayer()
self.hierarchyTrackingLayer = nil
}
}
public func updateStoryView(transition: ContainedViewLayoutTransition, theme: PresentationTheme) {
if let storyData = self.storyData {
let avatarStoryView: ComponentView<Empty>
if let current = self.avatarStoryView {
avatarStoryView = current
} else {
avatarStoryView = ComponentView()
self.avatarStoryView = avatarStoryView
}
let _ = avatarStoryView.update(
transition: ComponentTransition(transition),
component: AnyComponent(AvatarStoryIndicatorComponent(
hasUnseen: storyData.hasUnseen,
hasUnseenCloseFriendsItems: storyData.hasUnseenCloseFriends,
hasLiveItems: storyData.hasLiveItems,
colors: AvatarStoryIndicatorComponent.Colors(theme: theme),
activeLineWidth: 1.0,
inactiveLineWidth: 1.0,
counters: nil
)),
environment: {},
containerSize: self.avatarNode.bounds.insetBy(dx: 2.0, dy: 2.0).size
)
if let avatarStoryComponentView = avatarStoryView.view {
if avatarStoryComponentView.superview == nil {
self.containerNode.view.insertSubview(avatarStoryComponentView, at: 0)
}
avatarStoryComponentView.frame = self.avatarNode.frame
}
} else {
if let avatarStoryView = self.avatarStoryView {
self.avatarStoryView = nil
avatarStoryView.view?.removeFromSuperview()
}
}
}
override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return CGSize(width: 37.0, height: 37.0)
}
public func onLayout() {
}
public final class SnapshotState {
fileprivate let snapshotView: UIView?
fileprivate let snapshotStatusView: UIView?
fileprivate init(snapshotView: UIView?, snapshotStatusView: UIView?) {
self.snapshotView = snapshotView
self.snapshotStatusView = snapshotStatusView
}
}
public func prepareSnapshotState() -> SnapshotState {
let snapshotView = self.avatarNode.view.snapshotView(afterScreenUpdates: false)
let snapshotStatusView = self.statusView.view?.snapshotView(afterScreenUpdates: false)
return SnapshotState(
snapshotView: snapshotView,
snapshotStatusView: snapshotStatusView
)
}
public func animateFromSnapshot(_ snapshotState: SnapshotState) {
self.avatarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.16)
self.avatarNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true)
self.statusView.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.16)
self.statusView.view?.layer.animateScale(from: 0.1, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true)
if let snapshotView = snapshotState.snapshotView {
snapshotView.frame = self.frame
self.containerNode.view.insertSubview(snapshotView, at: 0)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
snapshotView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
}
if let snapshotStatusView = snapshotState.snapshotStatusView {
snapshotStatusView.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - snapshotStatusView.bounds.width) / 2.0), y: floor((self.containerNode.bounds.height - snapshotStatusView.bounds.height) / 2.0)), size: snapshotStatusView.bounds.size)
self.containerNode.view.insertSubview(snapshotStatusView, at: 0)
snapshotStatusView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotStatusView] _ in
snapshotStatusView?.removeFromSuperview()
})
snapshotStatusView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
}
}
private func updateVideoVisibility() {
let isVisible = self.trackingIsInHierarchy
self.avatarVideoNode?.updateVisibility(isVisible)
if let videoNode = self.avatarVideoNode {
videoNode.updateLayout(size: self.avatarNode.frame.size, cornerRadius: self.avatarNode.frame.size.width / 2.0, transition: .immediate)
videoNode.frame = self.avatarNode.bounds
}
}
}
private class StarView: UIView {
let outline = SimpleLayer()
let foreground = SimpleLayer()
var outlineColor: UIColor = .white {
didSet {
self.outline.layerTintColor = self.outlineColor.cgColor
}
}
override init(frame: CGRect) {
self.outline.contents = UIImage(bundleImageName: "Premium/Stars/StarMediumOutline")?.cgImage
self.foreground.contents = UIImage(bundleImageName: "Premium/Stars/StarMedium")?.cgImage
super.init(frame: frame)
self.layer.addSublayer(self.outline)
self.layer.addSublayer(self.foreground)
}
required init?(coder: NSCoder) {
preconditionFailure()
}
override func layoutSubviews() {
self.outline.frame = self.bounds
self.foreground.frame = self.bounds
}
}
@@ -0,0 +1,32 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatBotInfoItem",
module_name = "ChatBotInfoItem",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/AsyncDisplayKit",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/TextFormat",
"//submodules/UrlEscaping",
"//submodules/PhotoResources",
"//submodules/AccountContext",
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//submodules/TelegramUniversalVideoContent",
"//submodules/WallpaperBackgroundNode",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,619 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TextFormat
import UrlEscaping
import PhotoResources
import AccountContext
import UniversalMediaPlayer
import TelegramUniversalVideoContent
import WallpaperBackgroundNode
import ChatControllerInteraction
import ChatMessageBubbleContentNode
private let messageFont = Font.regular(17.0)
private let messageBoldFont = Font.semibold(17.0)
private let messageItalicFont = Font.italic(17.0)
private let messageBoldItalicFont = Font.semiboldItalic(17.0)
private let messageFixedFont = UIFont(name: "Menlo-Regular", size: 16.0) ?? UIFont.systemFont(ofSize: 17.0)
public final class ChatBotInfoItem: ListViewItem {
fileprivate let title: String
fileprivate let text: String
fileprivate let photo: TelegramMediaImage?
fileprivate let video: TelegramMediaFile?
fileprivate let controllerInteraction: ChatControllerInteraction
fileprivate let presentationData: ChatPresentationData
fileprivate let context: AccountContext
public init(title: String, text: String, photo: TelegramMediaImage?, video: TelegramMediaFile?, controllerInteraction: ChatControllerInteraction, presentationData: ChatPresentationData, context: AccountContext) {
self.title = title
self.text = text
self.photo = photo
self.video = video
self.controllerInteraction = controllerInteraction
self.presentationData = presentationData
self.context = context
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
let configure = {
let node = ChatBotInfoItemNode()
let nodeLayout = node.asyncLayout()
let (layout, apply) = nodeLayout(self, params)
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply(.None) })
})
}
}
if Thread.isMainThread {
async {
configure()
}
} else {
configure()
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ChatBotInfoItemNode {
let nodeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = nodeLayout(self, params)
Queue.mainQueue().async {
completion(layout, { _ in
apply(animation)
})
}
}
}
}
}
}
public final class ChatBotInfoItemNode: ListViewItemNode {
public var controllerInteraction: ChatControllerInteraction?
public let offsetContainer: ASDisplayNode
public let backgroundNode: ASImageNode
public let imageNode: TransformImageNode
public var videoNode: UniversalVideoNode?
public let titleNode: TextNode
public let textNode: TextNode
private var linkHighlightingNode: LinkHighlightingNode?
private let fetchDisposable = MetaDisposable()
public var currentTextAndEntities: (String, [MessageTextEntity])?
private var theme: ChatPresentationThemeData?
private var wallpaperBackgroundNode: WallpaperBackgroundNode?
private var backgroundContent: WallpaperBubbleBackgroundNode?
private var absolutePosition: (CGRect, CGSize)?
private var item: ChatBotInfoItem?
public init() {
self.offsetContainer = ASDisplayNode()
self.backgroundNode = ASImageNode()
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.displayWithoutProcessing = true
self.imageNode = TransformImageNode()
self.textNode = TextNode()
self.titleNode = TextNode()
super.init(layerBacked: false, dynamicBounce: true, rotated: true)
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
self.addSubnode(self.offsetContainer)
self.offsetContainer.addSubnode(self.backgroundNode)
self.offsetContainer.addSubnode(self.imageNode)
self.offsetContainer.addSubnode(self.titleNode)
self.offsetContainer.addSubnode(self.textNode)
self.wantsTrailingItemSpaceUpdates = true
}
deinit {
self.fetchDisposable.dispose()
}
private func setup(context: AccountContext, videoFile: TelegramMediaFile?) {
guard self.videoNode == nil, let file = videoFile else {
return
}
let videoContent = NativeVideoContent(
id: .message(0, MediaId(namespace: 0, id: Int64.random(in: 0..<Int64.max))),
userLocation: .other,
fileReference: .standalone(media: file),
streamVideo: .none,
loopVideo: true,
enableSound: false,
fetchAutomatically: true,
onlyFullSizeThumbnail: false,
continuePlayingWithoutSoundOnLostAudioSession: false,
storeAfterDownload: nil
)
let videoNode = UniversalVideoNode(context: context, postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded)
videoNode.canAttachContent = true
self.videoNode = videoNode
let cornerRadius = (self.item?.presentationData.chatBubbleCorners.mainRadius ?? 17.0)
(videoNode.decoration as? VideoDecoration)?.updateCorners(ImageCorners(topLeft: .Corner(cornerRadius), topRight: .Corner(cornerRadius), bottomLeft: .Corner(0.0), bottomRight: .Corner(0.0)))
self.offsetContainer.addSubnode(videoNode)
videoNode.play()
}
override public func didLoad() {
super.didLoad()
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.tapActionAtPoint = { [weak self] point in
if let strongSelf = self {
let tapAction = strongSelf.tapActionAtPoint(point, gesture: .tap, isEstimating: true)
switch tapAction.content {
case .none:
break
case .ignore:
return .fail
case .url, .phone, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .theme, .call, .conferenceCall, .openMessage, .timecode, .bankCard, .tooltip, .openPollResults, .copy, .largeEmoji, .customEmoji, .custom:
return .waitForSingleTap
}
}
return .waitForDoubleTap
}
recognizer.highlight = { [weak self] point in
if let strongSelf = self {
strongSelf.updateTouchesAtPoint(point)
}
}
self.view.addGestureRecognizer(recognizer)
}
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
super.updateAbsoluteRect(rect, within: containerSize)
self.absolutePosition = (rect, containerSize)
if let backgroundContent = self.backgroundContent {
var backgroundFrame = backgroundContent.frame
backgroundFrame.origin.x += rect.minX
backgroundFrame.origin.y += containerSize.height - rect.minY
backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate)
}
}
public func asyncLayout() -> (_ item: ChatBotInfoItem, _ width: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
let makeImageLayout = self.imageNode.asyncLayout()
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeTextLayout = TextNode.asyncLayout(self.textNode)
let currentTextAndEntities = self.currentTextAndEntities
let currentTheme = self.theme
let currentItem = self.item
return { [weak self] item, params in
self?.item = item
var updatedBackgroundImage: UIImage?
if currentTheme != item.presentationData.theme {
updatedBackgroundImage = PresentationResourcesChat.chatInfoItemBackgroundImage(item.presentationData.theme.theme, wallpaper: !item.presentationData.theme.wallpaper.isEmpty)
}
var updatedTextAndEntities: (String, [MessageTextEntity])
if let (text, entities) = currentTextAndEntities {
if text == item.text {
updatedTextAndEntities = (text, entities)
} else {
updatedTextAndEntities = (item.text, generateTextEntities(item.text, enabledTypes: .all))
}
} else {
updatedTextAndEntities = (item.text, generateTextEntities(item.text, enabledTypes: .all))
}
let attributedText = stringWithAppliedEntities(updatedTextAndEntities.0, entities: updatedTextAndEntities.1, baseColor: item.presentationData.theme.theme.chat.message.infoPrimaryTextColor, linkColor: item.presentationData.theme.theme.chat.message.infoLinkTextColor, baseFont: messageFont, linkFont: messageFont, boldFont: messageBoldFont, italicFont: messageItalicFont, boldItalicFont: messageBoldItalicFont, fixedFont: messageFixedFont, blockQuoteFont: messageFont, message: nil, adjustQuoteFontSize: true)
let horizontalEdgeInset: CGFloat = 10.0 + params.leftInset
let horizontalContentInset: CGFloat = 12.0
let verticalItemInset: CGFloat = 10.0
let verticalContentInset: CGFloat = 8.0
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: messageBoldFont, textColor: item.presentationData.theme.theme.chat.message.infoPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - horizontalEdgeInset * 2.0 - horizontalContentInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - horizontalEdgeInset * 2.0 - horizontalContentInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let textSpacing: CGFloat = 1.0
let textSize = CGSize(width: max(titleLayout.size.width, textLayout.size.width), height: (titleLayout.size.height + (titleLayout.size.width.isZero ? 0.0 : textSpacing) + textLayout.size.height))
var mediaUpdated = false
if let media = item.photo {
if let currentMedia = currentItem?.photo {
mediaUpdated = !media.isSemanticallyEqual(to: currentMedia)
} else {
mediaUpdated = true
}
}
var updatedImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
var imageSize = CGSize()
var imageDimensions = CGSize()
var imageApply: (() -> Void)?
let imageInset: CGFloat = 1.0 + UIScreenPixel
if let image = item.photo, let dimensions = largestImageRepresentation(image.representations)?.dimensions {
imageDimensions = dimensions.cgSize.aspectFitted(CGSize(width: textSize.width + horizontalContentInset * 2.0 - imageInset * 2.0, height: CGFloat.greatestFiniteMagnitude))
imageSize = imageDimensions
imageSize.height += 4.0
let arguments = TransformImageArguments(corners: ImageCorners(topLeft: .Corner(17.0), topRight: .Corner(17.0), bottomLeft: .Corner(0.0), bottomRight: .Corner(0.0)), imageSize: dimensions.cgSize.aspectFilled(imageDimensions), boundingSize: imageDimensions, intrinsicInsets: UIEdgeInsets(), emptyColor: item.presentationData.theme.theme.list.mediaPlaceholderColor)
imageApply = makeImageLayout(arguments)
if mediaUpdated {
updatedImageSignal = chatMessagePhoto(postbox: item.context.account.postbox, userLocation: .other, photoReference: .standalone(media: image), synchronousLoad: true, highQuality: false)
}
}
if let video = item.video, let dimensions = video.dimensions {
imageDimensions = dimensions.cgSize.aspectFitted(CGSize(width: textSize.width + horizontalContentInset * 2.0 - imageInset * 2.0, height: CGFloat.greatestFiniteMagnitude))
imageSize = imageDimensions
imageSize.height += 4.0
}
let backgroundFrame = CGRect(origin: CGPoint(x: floor((params.width - textSize.width - horizontalContentInset * 2.0) / 2.0), y: verticalItemInset + 4.0), size: CGSize(width: textSize.width + horizontalContentInset * 2.0, height: imageSize.height + textSize.height + verticalContentInset * 2.0))
let titleFrame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + horizontalContentInset, y: backgroundFrame.origin.y + imageSize.height + verticalContentInset), size: titleLayout.size)
let textFrame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + horizontalContentInset, y: backgroundFrame.origin.y + imageSize.height + verticalContentInset + titleLayout.size.height + (titleLayout.size.width.isZero ? 0.0 : textSpacing)), size: textLayout.size)
let imageFrame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + imageInset, y: backgroundFrame.origin.y + imageInset), size: imageDimensions)
let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: imageSize.height + textLayout.size.height + verticalItemInset * 2.0 + verticalContentInset * 2.0 + titleLayout.size.height + (titleLayout.size.width.isZero ? 0.0 : textSpacing) - 3.0), insets: UIEdgeInsets())
return (itemLayout, { _ in
if let strongSelf = self {
strongSelf.theme = item.presentationData.theme
if let updatedBackgroundImage = updatedBackgroundImage {
strongSelf.backgroundNode.image = updatedBackgroundImage
}
strongSelf.controllerInteraction = item.controllerInteraction
strongSelf.currentTextAndEntities = updatedTextAndEntities
if let imageApply = imageApply {
let _ = imageApply()
if let updatedImageSignal = updatedImageSignal {
strongSelf.imageNode.setSignal(updatedImageSignal)
if let image = item.photo {
strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(context: item.context, userLocation: .other, photoReference: .standalone(media: image), displayAtSize: nil, storeToDownloadsPeerId: nil).startStrict())
}
}
strongSelf.imageNode.isHidden = false
} else {
strongSelf.imageNode.isHidden = true
}
strongSelf.imageNode.frame = imageFrame
let _ = titleApply()
let _ = textApply()
strongSelf.offsetContainer.frame = CGRect(origin: CGPoint(), size: itemLayout.contentSize)
strongSelf.backgroundNode.frame = backgroundFrame
strongSelf.titleNode.frame = titleFrame
strongSelf.textNode.frame = textFrame
if item.controllerInteraction.presentationContext.backgroundNode?.hasExtraBubbleBackground() == true {
if strongSelf.backgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
backgroundContent.clipsToBounds = true
strongSelf.backgroundContent = backgroundContent
strongSelf.offsetContainer.insertSubnode(backgroundContent, at: 0)
}
} else {
strongSelf.backgroundContent?.removeFromSupernode()
strongSelf.backgroundContent = nil
}
if let backgroundContent = strongSelf.backgroundContent {
strongSelf.backgroundNode.isHidden = true
backgroundContent.cornerRadius = item.presentationData.chatBubbleCorners.mainRadius
backgroundContent.frame = backgroundFrame
if let (rect, containerSize) = strongSelf.absolutePosition {
var backgroundFrame = backgroundContent.frame
backgroundFrame.origin.x += rect.minX
backgroundFrame.origin.y += containerSize.height - rect.minY
backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate)
}
} else {
strongSelf.backgroundNode.isHidden = false
}
strongSelf.setup(context: item.context, videoFile: item.video)
if let videoNode = strongSelf.videoNode {
videoNode.updateLayout(size: imageFrame.size, transition: .immediate)
videoNode.frame = imageFrame
}
}
})
}
}
override public func updateTrailingItemSpace(_ height: CGFloat, transition: ContainedViewLayoutTransition) {
if height.isLessThanOrEqualTo(0.0) {
transition.updateFrame(node: self.offsetContainer, frame: CGRect(origin: CGPoint(), size: self.offsetContainer.bounds.size))
} else {
transition.updateFrame(node: self.offsetContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: -floorToScreenPixels(height / 2.0)), size: self.offsetContainer.bounds.size))
}
}
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5)
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false)
}
override public func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let result = super.point(inside: point, with: event)
let extra = self.offsetContainer.frame.contains(point)
return result || extra
}
public func updateTouchesAtPoint(_ point: CGPoint?) {
if let item = self.item {
var rects: [CGRect]?
if let point = point {
let textNodeFrame = self.textNode.frame
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - self.offsetContainer.frame.minX - textNodeFrame.minX, y: point.y - self.offsetContainer.frame.minY - textNodeFrame.minY)) {
let possibleNames: [String] = [
TelegramTextAttributes.URL,
TelegramTextAttributes.PeerMention,
TelegramTextAttributes.PeerTextMention,
TelegramTextAttributes.BotCommand,
TelegramTextAttributes.Hashtag
]
for name in possibleNames {
if let _ = attributes[NSAttributedString.Key(rawValue: name)] {
rects = self.textNode.attributeRects(name: name, at: index)
break
}
}
}
}
if let rects = rects {
let linkHighlightingNode: LinkHighlightingNode
if let current = self.linkHighlightingNode {
linkHighlightingNode = current
} else {
linkHighlightingNode = LinkHighlightingNode(color: item.presentationData.theme.theme.chat.message.incoming.linkHighlightColor)
self.linkHighlightingNode = linkHighlightingNode
self.offsetContainer.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode)
}
linkHighlightingNode.frame = self.textNode.frame
linkHighlightingNode.updateRects(rects)
} else if let linkHighlightingNode = self.linkHighlightingNode {
self.linkHighlightingNode = nil
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
linkHighlightingNode?.removeFromSupernode()
})
}
}
}
public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
let textNodeFrame = self.textNode.frame
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - self.offsetContainer.frame.minX - textNodeFrame.minX, y: point.y - self.offsetContainer.frame.minY - textNodeFrame.minY)) {
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
var concealed = true
if let (attributeText, fullText) = self.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
}
return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed)))
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false))
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
return ChatMessageBubbleContentTapAction(content: .textMention(peerName))
} else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String {
return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand))
} else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
return ChatMessageBubbleContentTapAction(content: .hashtag(hashtag.peerName, hashtag.hashtag))
} else {
return ChatMessageBubbleContentTapAction(content: .none)
}
} else {
return ChatMessageBubbleContentTapAction(content: .none)
}
}
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
let tapAction = self.tapActionAtPoint(location, gesture: gesture, isEstimating: false)
switch tapAction.content {
case .none, .ignore:
break
case let .url(url):
self.item?.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url.url, concealed: url.concealed, progress: tapAction.activate?()))
case let .peerMention(peerId, _, _):
if let item = self.item {
let _ = (item.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
if let peer = peer {
self?.item?.controllerInteraction.openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default)
}
})
}
case let .textMention(name):
self.item?.controllerInteraction.openPeerMention(name, tapAction.activate?())
case let .botCommand(command):
self.item?.controllerInteraction.sendBotCommand(nil, command)
case let .hashtag(peerName, hashtag):
self.item?.controllerInteraction.openHashtag(peerName, hashtag)
default:
break
}
case .longTap, .doubleTap:
if let item = self.item, self.backgroundNode.frame.contains(location) {
let tapAction = self.tapActionAtPoint(location, gesture: gesture, isEstimating: false)
switch tapAction.content {
case .none, .ignore:
break
case let .url(url):
item.controllerInteraction.longTap(.url(url.url), ChatControllerInteraction.LongTapParams())
case let .peerMention(peerId, mention, _):
item.controllerInteraction.longTap(.peerMention(peerId, mention), ChatControllerInteraction.LongTapParams())
case let .textMention(name):
item.controllerInteraction.longTap(.mention(name), ChatControllerInteraction.LongTapParams())
case let .botCommand(command):
item.controllerInteraction.longTap(.command(command), ChatControllerInteraction.LongTapParams())
case let .hashtag(_, hashtag):
item.controllerInteraction.longTap(.hashtag(hashtag), ChatControllerInteraction.LongTapParams())
default:
break
}
}
default:
break
}
}
default:
break
}
}
}
private final class VideoDecoration: UniversalVideoDecoration {
public let backgroundNode: ASDisplayNode? = nil
public let contentContainerNode: ASDisplayNode
public let foregroundNode: ASDisplayNode? = nil
private var contentNode: (ASDisplayNode & UniversalVideoContentNode)?
private var validLayout: (size: CGSize, actualSize: CGSize)?
public init() {
self.contentContainerNode = ASDisplayNode()
}
public func updateContentNode(_ contentNode: (UniversalVideoContentNode & ASDisplayNode)?) {
if self.contentNode !== contentNode {
let previous = self.contentNode
self.contentNode = contentNode
if let previous = previous {
if previous.supernode === self.contentContainerNode {
previous.removeFromSupernode()
}
}
if let contentNode = contentNode {
if contentNode.supernode !== self.contentContainerNode {
self.contentContainerNode.addSubnode(contentNode)
if let validLayout = self.validLayout {
contentNode.frame = CGRect(origin: CGPoint(), size: validLayout.size)
contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate)
}
}
}
}
}
public func updateCorners(_ corners: ImageCorners) {
self.contentContainerNode.clipsToBounds = true
if isRoundEqualCorners(corners) {
self.contentContainerNode.cornerRadius = corners.topLeft.radius
} else {
let boundingSize: CGSize = CGSize(width: max(corners.topLeft.radius, corners.bottomLeft.radius) + max(corners.topRight.radius, corners.bottomRight.radius), height: max(corners.topLeft.radius, corners.topRight.radius) + max(corners.bottomLeft.radius, corners.bottomRight.radius))
let size: CGSize = CGSize(width: boundingSize.width + corners.extendedEdges.left + corners.extendedEdges.right, height: boundingSize.height + corners.extendedEdges.top + corners.extendedEdges.bottom)
let arguments = TransformImageArguments(corners: corners, imageSize: size, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())
guard let context = DrawingContext(size: size, clear: true) else {
return
}
context.withContext { ctx in
ctx.setFillColor(UIColor.black.cgColor)
ctx.fill(arguments.drawingRect)
}
addCorners(context, arguments: arguments)
if let maskImage = context.generateImage() {
let mask = CALayer()
mask.contents = maskImage.cgImage
mask.contentsScale = maskImage.scale
mask.contentsCenter = CGRect(x: max(corners.topLeft.radius, corners.bottomLeft.radius) / maskImage.size.width, y: max(corners.topLeft.radius, corners.topRight.radius) / maskImage.size.height, width: (maskImage.size.width - max(corners.topLeft.radius, corners.bottomLeft.radius) - max(corners.topRight.radius, corners.bottomRight.radius)) / maskImage.size.width, height: (maskImage.size.height - max(corners.topLeft.radius, corners.topRight.radius) - max(corners.bottomLeft.radius, corners.bottomRight.radius)) / maskImage.size.height)
self.contentContainerNode.layer.mask = mask
self.contentContainerNode.layer.mask?.frame = self.contentContainerNode.bounds
}
}
}
public func updateClippingFrame(_ frame: CGRect, completion: (() -> Void)?) {
self.contentContainerNode.layer.animate(from: NSValue(cgRect: self.contentContainerNode.bounds), to: NSValue(cgRect: frame), keyPath: "bounds", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
})
if let maskLayer = self.contentContainerNode.layer.mask {
maskLayer.animate(from: NSValue(cgRect: self.contentContainerNode.bounds), to: NSValue(cgRect: frame), keyPath: "bounds", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
})
maskLayer.animate(from: NSValue(cgPoint: maskLayer.position), to: NSValue(cgPoint: CGPoint(x: frame.midX, y: frame.midY)), keyPath: "position", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
})
}
if let contentNode = self.contentNode {
contentNode.layer.animate(from: NSValue(cgPoint: contentNode.layer.position), to: NSValue(cgPoint: CGPoint(x: frame.midX, y: frame.midY)), keyPath: "position", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
completion?()
})
}
}
public func updateContentNodeSnapshot(_ snapshot: UIView?) {
}
public func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, actualSize)
let bounds = CGRect(origin: CGPoint(), size: size)
if let backgroundNode = self.backgroundNode {
transition.updateFrame(node: backgroundNode, frame: bounds)
}
if let foregroundNode = self.foregroundNode {
transition.updateFrame(node: foregroundNode, frame: bounds)
}
transition.updateFrame(node: self.contentContainerNode, frame: bounds)
if let maskLayer = self.contentContainerNode.layer.mask {
transition.updateFrame(layer: maskLayer, frame: bounds)
}
if let contentNode = self.contentNode {
transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size))
contentNode.updateLayout(size: size, actualSize: actualSize, transition: transition)
}
}
public func setStatus(_ status: Signal<MediaPlayerStatus?, NoError>) {
}
public func tap() {
}
}
@@ -0,0 +1,28 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatButtonKeyboardInputNode",
module_name = "ChatButtonKeyboardInputNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/AsyncDisplayKit",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/ChatPresentationInterfaceState",
"//submodules/WallpaperBackgroundNode",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
"//submodules/TelegramUI/Components/ChatInputNode",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,438 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import AccountContext
import ChatPresentationInterfaceState
import WallpaperBackgroundNode
import ChatControllerInteraction
import ChatInputNode
private final class ChatButtonKeyboardInputButtonNode: HighlightTrackingButtonNode {
var button: ReplyMarkupButton? {
didSet {
self.updateIcon()
}
}
private let backgroundContainerNode: ASDisplayNode
private var backgroundNode: WallpaperBubbleBackgroundNode?
private let backgroundColorNode: ASDisplayNode
private let backgroundAdditionalColorNode: ASDisplayNode
private let highlightNode: ASImageNode
private let textNode: ImmediateTextNode
private var iconNode: ASImageNode?
private var theme: PresentationTheme?
init() {
self.backgroundContainerNode = ASDisplayNode()
self.backgroundContainerNode.clipsToBounds = true
self.backgroundContainerNode.allowsGroupOpacity = true
self.backgroundContainerNode.isUserInteractionEnabled = false
self.backgroundContainerNode.cornerRadius = 10.0
self.backgroundContainerNode.layer.cornerCurve = .continuous
self.backgroundColorNode = ASDisplayNode()
self.backgroundColorNode.cornerRadius = 10.0
self.backgroundColorNode.layer.cornerCurve = .continuous
self.backgroundAdditionalColorNode = ASDisplayNode()
self.backgroundAdditionalColorNode.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.1)
self.backgroundAdditionalColorNode.isHidden = true
self.highlightNode = ASImageNode()
self.highlightNode.isUserInteractionEnabled = false
self.textNode = ImmediateTextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.textAlignment = .center
self.textNode.maximumNumberOfLines = 2
super.init()
self.accessibilityTraits = [.button]
self.addSubnode(self.backgroundContainerNode)
self.backgroundContainerNode.addSubnode(self.backgroundColorNode)
self.backgroundContainerNode.addSubnode(self.backgroundAdditionalColorNode)
self.addSubnode(self.textNode)
self.backgroundContainerNode.addSubnode(self.highlightNode)
self.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted, !strongSelf.bounds.width.isZero {
let scale = (strongSelf.bounds.width - 10.0) / strongSelf.bounds.width
strongSelf.layer.animateScale(from: 1.0, to: scale, duration: 0.15, removeOnCompletion: false)
strongSelf.backgroundContainerNode.layer.removeAnimation(forKey: "opacity")
strongSelf.backgroundContainerNode.alpha = 0.6
} else if let presentationLayer = strongSelf.layer.presentation() {
strongSelf.layer.animateScale(from: CGFloat((presentationLayer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0), to: 1.0, duration: 0.25, removeOnCompletion: false)
strongSelf.backgroundContainerNode.alpha = 1.0
strongSelf.backgroundContainerNode.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2)
}
}
}
}
override func setAttributedTitle(_ title: NSAttributedString, for state: UIControl.State) {
self.textNode.attributedText = title
self.accessibilityLabel = title.string
}
private var absoluteRect: (CGRect, CGSize)?
func update(rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) {
self.absoluteRect = (rect, containerSize)
if let backgroundNode = self.backgroundNode {
var backgroundFrame = backgroundNode.frame
backgroundFrame.origin.x += rect.minX
backgroundFrame.origin.y += rect.minY
backgroundNode.update(rect: backgroundFrame, within: containerSize, transition: transition)
}
}
private func updateIcon() {
guard let theme = self.theme else {
return
}
var iconImage: UIImage?
if let button = self.button {
switch button.action {
case .openWebView:
iconImage = PresentationResourcesChat.chatKeyboardActionButtonWebAppIconImage(theme)
default:
iconImage = nil
}
}
if iconImage != nil {
if self.iconNode == nil {
let iconNode = ASImageNode()
iconNode.contentMode = .center
self.iconNode = iconNode
self.addSubnode(iconNode)
}
self.iconNode?.image = iconImage
} else if let iconNode = self.iconNode {
iconNode.removeFromSupernode()
self.iconNode = nil
}
self.setNeedsLayout()
}
func updateTheme(theme: PresentationTheme, wallpaperBackgroundNode: WallpaperBackgroundNode?) {
if theme !== self.theme {
self.theme = theme
self.highlightNode.image = PresentationResourcesChat.chatInputButtonPanelButtonHighlightImage(theme)
self.updateIcon()
}
self.backgroundColorNode.backgroundColor = theme.chat.inputButtonPanel.buttonFillColor
if let alpha = self.backgroundColorNode.backgroundColor?.alpha, alpha < 1.0 {
self.backgroundColorNode.layer.compositingFilter = "softLightBlendMode"
self.backgroundAdditionalColorNode.isHidden = false
} else {
self.backgroundColorNode.layer.compositingFilter = nil
self.backgroundAdditionalColorNode.isHidden = true
}
if wallpaperBackgroundNode?.hasExtraBubbleBackground() == true {
if self.backgroundNode == nil, let backgroundContent = wallpaperBackgroundNode?.makeBubbleBackground(for: .free) {
self.backgroundNode = backgroundContent
self.backgroundContainerNode.insertSubnode(backgroundContent, at: 0)
self.setNeedsLayout()
}
} else {
self.backgroundNode?.removeFromSupernode()
self.backgroundNode = nil
}
}
override func layout() {
super.layout()
self.backgroundContainerNode.frame = self.bounds
self.backgroundColorNode.frame = CGRect(origin: .zero, size: CGSize(width: self.bounds.width, height: self.bounds.height - 1.0))
self.backgroundAdditionalColorNode.frame = self.backgroundColorNode.frame
self.backgroundNode?.frame = self.backgroundColorNode.frame
self.highlightNode.frame = self.bounds
if let (rect, containerSize) = self.absoluteRect {
self.update(rect: rect, within: containerSize, transition: .immediate)
}
if let iconNode = self.iconNode {
iconNode.frame = CGRect(x: self.frame.width - 16.0, y: 4.0, width: 12.0, height: 12.0)
}
let textSize = self.textNode.updateLayout(CGSize(width: self.bounds.width - 16.0, height: self.bounds.height))
self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((self.bounds.width - textSize.width) / 2.0), y: floorToScreenPixels((self.bounds.height - textSize.height) / 2.0)), size: textSize)
}
}
public final class ChatButtonKeyboardInputNode: ChatInputNode {
private let context: AccountContext
private let controllerInteraction: ChatControllerInteraction
private let scrollNode: ASScrollNode
private var backgroundNode: WallpaperBubbleBackgroundNode?
private let backgroundColorNode: ASDisplayNode
private var buttonNodes: [ChatButtonKeyboardInputButtonNode] = []
private var message: Message?
private var theme: PresentationTheme?
public init(context: AccountContext, controllerInteraction: ChatControllerInteraction) {
self.context = context
self.controllerInteraction = controllerInteraction
self.scrollNode = ASScrollNode()
self.backgroundColorNode = ASDisplayNode()
super.init()
self.addSubnode(self.backgroundColorNode)
self.addSubnode(self.scrollNode)
self.scrollNode.view.delaysContentTouches = false
self.scrollNode.view.canCancelContentTouches = true
self.scrollNode.view.alwaysBounceHorizontal = false
self.scrollNode.view.alwaysBounceVertical = false
}
override public func didLoad() {
super.didLoad()
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
}
}
private var absoluteRect: (CGRect, CGSize)?
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) {
self.absoluteRect = (rect, containerSize)
if let backgroundNode = self.backgroundNode {
var backgroundFrame = backgroundNode.frame
backgroundFrame.origin.x += rect.minX
backgroundFrame.origin.y += rect.minY
backgroundNode.update(rect: backgroundFrame, within: containerSize, transition: transition)
}
for buttonNode in self.buttonNodes {
var buttonFrame = buttonNode.frame
buttonFrame.origin.x += rect.minX
buttonFrame.origin.y += rect.minY
buttonNode.update(rect: buttonFrame, within: containerSize, transition: transition)
}
}
override public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, layoutMetrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) {
if self.backgroundNode == nil {
if let backgroundNode = self.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
self.backgroundNode = backgroundNode
self.insertSubnode(backgroundNode, at: 0)
}
}
let updatedTheme = self.theme !== interfaceState.theme
if updatedTheme {
self.theme = interfaceState.theme
self.backgroundColorNode.backgroundColor = interfaceState.theme.chat.inputButtonPanel.panelBackgroundColor
}
var validatedMarkup: ReplyMarkupMessageAttribute?
if let message = interfaceState.keyboardButtonsMessage {
for attribute in message.attributes {
if let replyMarkup = attribute as? ReplyMarkupMessageAttribute {
if !replyMarkup.rows.isEmpty {
validatedMarkup = replyMarkup
}
break
}
}
}
self.message = interfaceState.keyboardButtonsMessage
if let markup = validatedMarkup {
let verticalInset: CGFloat = 16.0
let sideInset: CGFloat = 16.0 + leftInset
var buttonHeight: CGFloat = 43.0
let columnSpacing: CGFloat = 6.0
let rowSpacing: CGFloat = 5.0
var panelHeight = standardInputHeight
let previousRowsHeight = self.scrollNode.view.contentSize.height
let rowsHeight = verticalInset + CGFloat(markup.rows.count) * buttonHeight + CGFloat(max(0, markup.rows.count - 1)) * rowSpacing + verticalInset
if !markup.flags.contains(.fit) && rowsHeight < panelHeight {
buttonHeight = floor((panelHeight - bottomInset - verticalInset * 2.0 - CGFloat(max(0, markup.rows.count - 1)) * rowSpacing) / CGFloat(markup.rows.count))
}
var verticalOffset = verticalInset
var buttonIndex = 0
for row in markup.rows {
let buttonWidth = floor(((width - sideInset - sideInset) + columnSpacing - CGFloat(row.buttons.count) * columnSpacing) / CGFloat(row.buttons.count))
var columnIndex = 0
for button in row.buttons {
let buttonNode: ChatButtonKeyboardInputButtonNode
if buttonIndex < self.buttonNodes.count {
buttonNode = self.buttonNodes[buttonIndex]
} else {
buttonNode = ChatButtonKeyboardInputButtonNode()
buttonNode.titleNode.maximumNumberOfLines = 2
buttonNode.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: [.touchUpInside])
self.scrollNode.addSubnode(buttonNode)
self.buttonNodes.append(buttonNode)
}
buttonNode.updateTheme(theme: interfaceState.theme, wallpaperBackgroundNode: self.controllerInteraction.presentationContext.backgroundNode)
buttonIndex += 1
if buttonNode.button != button || updatedTheme {
buttonNode.button = button
buttonNode.setAttributedTitle(NSAttributedString(string: button.title, font: Font.regular(16.0), textColor: interfaceState.theme.chat.inputButtonPanel.buttonTextColor, paragraphAlignment: .center), for: [])
}
buttonNode.frame = CGRect(origin: CGPoint(x: sideInset + CGFloat(columnIndex) * (buttonWidth + columnSpacing), y: verticalOffset), size: CGSize(width: buttonWidth, height: buttonHeight))
columnIndex += 1
}
verticalOffset += buttonHeight + rowSpacing
}
for i in (buttonIndex ..< self.buttonNodes.count).reversed() {
self.buttonNodes[i].removeFromSupernode()
self.buttonNodes.remove(at: i)
}
if markup.flags.contains(.fit) {
panelHeight = min(panelHeight + bottomInset, rowsHeight + bottomInset)
}
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight)))
self.scrollNode.view.contentSize = CGSize(width: width, height: rowsHeight)
self.scrollNode.view.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0)
if previousRowsHeight != rowsHeight {
self.scrollNode.view.setContentOffset(CGPoint(), animated: false)
}
if let backgroundNode = self.backgroundNode {
backgroundNode.frame = CGRect(origin: .zero, size: CGSize(width: width, height: panelHeight))
}
self.backgroundColorNode.frame = CGRect(origin: .zero, size: CGSize(width: width, height: panelHeight))
if let (rect, containerSize) = self.absoluteRect {
self.updateAbsoluteRect(rect, within: containerSize, transition: transition)
}
return (panelHeight, 0.0)
} else {
return (0.0, 0.0)
}
}
@objc private func buttonPressed(_ button: ASButtonNode) {
if let button = button as? ChatButtonKeyboardInputButtonNode, let markupButton = button.button {
var dismissIfOnce = false
switch markupButton.action {
case .text:
self.controllerInteraction.sendMessage(markupButton.title)
dismissIfOnce = true
case let .url(url):
self.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: true, progress: Promise()))
case .requestMap:
self.controllerInteraction.shareCurrentLocation()
case .requestPhone:
self.controllerInteraction.shareAccountContact()
case .openWebApp:
if let message = self.message {
self.controllerInteraction.requestMessageActionCallback(message, nil, true, false, nil)
}
case let .callback(requiresPassword, data):
if let message = self.message {
self.controllerInteraction.requestMessageActionCallback(message, data, false, requiresPassword, nil)
}
case let .switchInline(samePeer, query, _):
if let message = message {
var botPeer: Peer?
var found = false
for attribute in message.attributes {
if let attribute = attribute as? InlineBotMessageAttribute, let peerId = attribute.peerId {
botPeer = message.peers[peerId]
found = true
}
}
if !found {
botPeer = message.author
}
var peer: Peer?
if samePeer {
peer = message.peers[message.id.peerId]
} else {
peer = botPeer
}
if let peer = peer, let botPeer = botPeer, let addressName = botPeer.addressName {
self.controllerInteraction.openPeer(EnginePeer(peer), .chat(textInputState: ChatTextInputState(inputText: NSAttributedString(string: "@\(addressName) \(query)")), subject: nil, peekData: nil), nil, .default)
}
}
case .payment:
break
case let .urlAuth(url, buttonId):
if let message = self.message {
self.controllerInteraction.requestMessageActionUrlAuth(url, .message(id: message.id, buttonId: buttonId))
}
case let .setupPoll(isQuiz):
self.controllerInteraction.openPollCreation(isQuiz)
case let .openUserProfile(peerId):
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
guard let self, let peer else {
return
}
self.controllerInteraction.openPeer(peer, .info(nil), nil, .default)
})
case let .openWebView(url, simple):
self.controllerInteraction.openWebView(markupButton.title, url, simple, .generic)
case let .requestPeer(peerType, buttonId, maxQuantity):
if let message = self.message {
self.controllerInteraction.openRequestedPeerSelection(message.id, peerType, buttonId, maxQuantity)
}
case let .copyText(payload):
self.controllerInteraction.copyText(payload)
}
if dismissIfOnce {
if let message = self.message {
for attribute in message.attributes {
if let attribute = attribute as? ReplyMarkupMessageAttribute {
if attribute.flags.contains(.once) {
self.controllerInteraction.dismissReplyMarkupMessage(message)
}
break
}
}
}
}
}
}
}
@@ -0,0 +1,37 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatChannelSubscriberInputPanelNode",
module_name = "ChatChannelSubscriberInputPanelNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/TelegramCore",
"//submodules/Postbox",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/AlertUI",
"//submodules/PresentationDataUtils",
"//submodules/UndoUI",
"//submodules/ChatPresentationInterfaceState",
"//submodules/TelegramUI/Components/Chat/ChatInputPanelNode",
"//submodules/AccountContext",
"//submodules/TelegramUI/Components/PeerManagement/OldChannelsController",
"//submodules/TooltipUI",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/TelegramUI/Components/GlassControls",
"//submodules/Components/BundleIconComponent",
"//submodules/Components/MultilineTextComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,661 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import Postbox
import SwiftSignalKit
import TelegramPresentationData
import AlertUI
import PresentationDataUtils
import UndoUI
import ChatPresentationInterfaceState
import ChatInputPanelNode
import AccountContext
import OldChannelsController
import TooltipUI
import TelegramNotices
import GlassBackgroundComponent
import ComponentFlow
import ComponentDisplayAdapters
import GlassControls
import BundleIconComponent
import MultilineTextComponent
private enum SubscriberAction: Equatable, Hashable {
case join
case joinGroup
case applyToJoin
case kicked
case muteNotifications
case unmuteNotifications
case unpinMessages(Int)
case hidePinnedMessages
case openChannel
case openGroup
case openChat
}
private func titleAndColorForAction(_ action: SubscriberAction, theme: PresentationTheme, strings: PresentationStrings) -> (String, UIColor) {
switch action {
case .join:
return (strings.Channel_JoinChannel, theme.chat.inputPanel.panelControlAccentColor)
case .joinGroup:
return (strings.Group_JoinGroup, theme.chat.inputPanel.panelControlAccentColor)
case .applyToJoin:
return (strings.Group_ApplyToJoin, theme.chat.inputPanel.panelControlAccentColor)
case .kicked:
return (strings.Channel_JoinChannel, theme.chat.inputPanel.panelControlDisabledColor)
case .muteNotifications:
return (strings.Conversation_Mute, theme.chat.inputPanel.panelControlAccentColor)
case .unmuteNotifications:
return (strings.Conversation_Unmute, theme.chat.inputPanel.panelControlAccentColor)
case .unpinMessages:
return (strings.Chat_PanelUnpinAllMessages, theme.chat.inputPanel.panelControlAccentColor)
case .hidePinnedMessages:
return (strings.Chat_PanelHidePinnedMessages, theme.chat.inputPanel.panelControlAccentColor)
case .openChannel:
return (strings.SavedMessages_OpenChannel, theme.chat.inputPanel.panelControlAccentColor)
case .openGroup:
return (strings.SavedMessages_OpenGroup, theme.chat.inputPanel.panelControlAccentColor)
case .openChat:
return (strings.SavedMessages_OpenChat, theme.chat.inputPanel.panelControlAccentColor)
}
}
private func actionForPeer(context: AccountContext, peer: Peer, interfaceState: ChatPresentationInterfaceState, isJoining: Bool, isMuted: Bool) -> SubscriberAction? {
if case let .replyThread(message) = interfaceState.chatLocation, message.peerId == context.account.peerId {
if let peer = interfaceState.savedMessagesTopicPeer {
if case let .channel(channel) = peer {
if case .broadcast = channel.info {
return .openChannel
} else {
return .openGroup
}
} else if case .legacyGroup = peer {
return .openGroup
}
}
return .openChat
} else if case .pinnedMessages = interfaceState.subject {
var canManagePin = false
if let channel = peer as? TelegramChannel {
canManagePin = channel.hasPermission(.pinMessages)
} else if let group = peer as? TelegramGroup {
switch group.role {
case .creator, .admin:
canManagePin = true
default:
if let defaultBannedRights = group.defaultBannedRights {
canManagePin = !defaultBannedRights.flags.contains(.banPinMessages)
} else {
canManagePin = true
}
}
} else if let _ = peer as? TelegramUser, interfaceState.explicitelyCanPinMessages {
canManagePin = true
}
if canManagePin {
return .unpinMessages(max(1, interfaceState.pinnedMessage?.totalCount ?? 1))
} else {
return .hidePinnedMessages
}
} else {
if let channel = peer as? TelegramChannel {
if case .broadcast = channel.info, isJoining {
if isMuted {
return .unmuteNotifications
} else {
return .muteNotifications
}
}
switch channel.participationStatus {
case .kicked:
return .kicked
case .left:
if case .group = channel.info {
if channel.flags.contains(.requestToJoin) {
return .applyToJoin
} else {
if channel.flags.contains(.isForum) {
return .join
} else {
return .joinGroup
}
}
} else {
return .join
}
case .member:
if isMuted {
return .unmuteNotifications
} else {
return .muteNotifications
}
}
} else {
if isMuted {
return .unmuteNotifications
} else {
return .muteNotifications
}
}
}
}
private let badgeFont = Font.regular(14.0)
public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
private let panelContainer = UIView()
private let panel = ComponentView<Empty>()
/*private let buttonBackgroundView: GlassBackgroundView
private let button: HighlightableButton
private let buttonTitle: ImmediateTextNode
private let buttonTintTitle: ImmediateTextNode
private let helpButtonBackgroundView: GlassBackgroundView
private let helpButton: HighlightableButton
private let helpButtonIconView: UIImageView
private let giftButtonBackgroundView: GlassBackgroundView
private let giftButton: HighlightableButton
private let giftButtonIconView: UIImageView
private let suggestedPostButtonBackgroundView: GlassBackgroundView
private let suggestedPostButton: HighlightableButton
private let suggestedPostButtonIconView: UIImageView*/
private var action: SubscriberAction?
private let actionDisposable = MetaDisposable()
private let badgeDisposable = MetaDisposable()
private var isJoining: Bool = false
private var presentationInterfaceState: ChatPresentationInterfaceState?
private var layoutData: (CGFloat, CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, CGFloat, Bool, LayoutMetrics)?
public override init() {
/*self.button = HighlightableButton()
self.buttonBackgroundView = GlassBackgroundView()
self.buttonBackgroundView.isUserInteractionEnabled = false
self.buttonTitle = ImmediateTextNode()
self.buttonTitle.isUserInteractionEnabled = false
self.buttonTintTitle = ImmediateTextNode()
self.buttonBackgroundView.contentView.addSubview(self.buttonTitle.view)
self.buttonBackgroundView.maskContentView.addSubview(self.buttonTintTitle.view)
self.buttonBackgroundView.contentView.addSubview(self.button)
self.helpButton = HighlightableButton()
self.helpButtonBackgroundView = GlassBackgroundView()
self.helpButtonBackgroundView.isUserInteractionEnabled = false
self.helpButtonIconView = GlassBackgroundView.ContentImageView()
self.helpButtonBackgroundView.contentView.addSubview(self.helpButtonIconView)
self.helpButtonBackgroundView.contentView.addSubview(self.helpButton)
self.helpButtonBackgroundView.isHidden = true
self.giftButton = HighlightableButton()
self.giftButtonBackgroundView = GlassBackgroundView()
self.giftButtonBackgroundView.isUserInteractionEnabled = false
self.giftButtonIconView = GlassBackgroundView.ContentImageView()
self.giftButtonBackgroundView.contentView.addSubview(self.giftButtonIconView)
self.giftButtonBackgroundView.contentView.addSubview(self.giftButton)
self.giftButtonBackgroundView.isHidden = true
self.suggestedPostButton = HighlightableButton()
self.suggestedPostButtonBackgroundView = GlassBackgroundView()
self.suggestedPostButtonBackgroundView.isUserInteractionEnabled = false
self.suggestedPostButtonIconView = GlassBackgroundView.ContentImageView()
self.suggestedPostButtonBackgroundView.contentView.addSubview(self.suggestedPostButtonIconView)
self.suggestedPostButtonBackgroundView.contentView.addSubview(self.suggestedPostButton)
self.suggestedPostButtonBackgroundView.isHidden = true*/
super.init()
/*self.view.addSubview(self.buttonBackgroundView)
self.view.addSubview(self.helpButtonBackgroundView)
self.view.addSubview(self.giftButtonBackgroundView)
self.view.addSubview(self.suggestedPostButtonBackgroundView)
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
self.helpButton.addTarget(self, action: #selector(self.helpPressed), for: .touchUpInside)
self.giftButton.addTarget(self, action: #selector(self.giftPressed), for: .touchUpInside)
self.suggestedPostButton.addTarget(self, action: #selector(self.suggestedPostPressed), for: .touchUpInside)*/
self.view.addSubview(self.panelContainer)
}
deinit {
self.actionDisposable.dispose()
self.badgeDisposable.dispose()
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.bounds.contains(point) {
return nil
}
return super.hitTest(point, with: event)
}
@objc private func giftPressed() {
self.interfaceInteraction?.openPremiumGift()
}
@objc private func helpPressed() {
self.interfaceInteraction?.presentGigagroupHelp()
}
@objc private func suggestedPostPressed() {
self.interfaceInteraction?.openMonoforum()
}
@objc private func buttonPressed() {
guard let context = self.context, let action = self.action, let presentationInterfaceState = self.presentationInterfaceState, let peer = presentationInterfaceState.renderedPeer?.peer else {
return
}
switch action {
case .join, .joinGroup, .applyToJoin:
self.isJoining = true
if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, isSecondary, metrics) = self.layoutData, let presentationInterfaceState = self.presentationInterfaceState {
let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, maxOverlayHeight: maxOverlayHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: presentationInterfaceState, metrics: metrics, force: true)
}
self.actionDisposable.set((context.peerChannelMemberCategoriesContextsManager.join(engine: context.engine, peerId: peer.id, hash: nil)
|> afterDisposed { [weak self] in
Queue.mainQueue().async {
if let strongSelf = self {
strongSelf.isJoining = false
}
}
}).startStrict(error: { [weak self] error in
guard let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState, let peer = presentationInterfaceState.renderedPeer?.peer else {
return
}
let text: String
switch error {
case .inviteRequestSent:
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
strongSelf.interfaceInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .inviteRequestSent(title: presentationInterfaceState.strings.Group_RequestToJoinSent, text: presentationInterfaceState.strings.Group_RequestToJoinSentDescriptionGroup ), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), nil)
return
case .tooMuchJoined:
strongSelf.interfaceInteraction?.getNavigationController()?.pushViewController(oldChannelsController(context: context, intent: .join, completed: { value in
if value {
self?.buttonPressed()
}
}))
return
case .tooMuchUsers:
text = presentationInterfaceState.strings.Conversation_UsersTooMuchError
case .generic:
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
text = presentationInterfaceState.strings.Channel_ErrorAccessDenied
} else {
text = presentationInterfaceState.strings.Group_ErrorAccessDenied
}
}
strongSelf.interfaceInteraction?.presentController(textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationInterfaceState.strings.Common_OK, action: {})]), nil)
}))
case .kicked:
break
case .muteNotifications, .unmuteNotifications:
if let context = self.context, let presentationInterfaceState = self.presentationInterfaceState, let peer = presentationInterfaceState.renderedPeer?.peer {
self.actionDisposable.set(context.engine.peers.togglePeerMuted(peerId: peer.id, threadId: nil).startStrict())
}
case .hidePinnedMessages, .unpinMessages:
self.interfaceInteraction?.unpinAllMessages()
case .openChannel, .openGroup, .openChat:
if let presentationInterfaceState = self.presentationInterfaceState, let savedMessagesTopicPeer = presentationInterfaceState.savedMessagesTopicPeer {
self.interfaceInteraction?.navigateToChat(savedMessagesTopicPeer.id)
}
}
}
override public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat {
return self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, maxOverlayHeight: maxOverlayHeight, isSecondary: isSecondary, transition: transition, interfaceState: interfaceState, metrics: metrics, force: false)
}
private var displayedGiftOrSuggestTooltip = false
private func presentGiftOrSuggestTooltip() {
guard let context = self.context, !self.displayedGiftOrSuggestTooltip, let parentController = self.interfaceInteraction?.chatController() else {
return
}
self.displayedGiftOrSuggestTooltip = true
let _ = (combineLatest(queue: .mainQueue(),
ApplicationSpecificNotice.getChannelSendGiftTooltip(accountManager: context.sharedContext.accountManager),
ApplicationSpecificNotice.getChannelSuggestTooltip(accountManager: context.sharedContext.accountManager)
|> deliverOnMainQueue)).start(next: { [weak self] giftCount, suggestCount in
guard let self else {
return
}
/*#if DEBUG
var giftCount = giftCount
var suggestCount = suggestCount
if "".isEmpty {
giftCount = 2
suggestCount = 0
}
#endif*/
let giftItemView = (self.panel.view as? GlassControlPanelComponent.View)?.leftItemView?.itemView(id: AnyHashable("gift"))
let suggestPostItemView = (self.panel.view as? GlassControlPanelComponent.View)?.leftItemView?.itemView(id: AnyHashable("suggestPost"))
if giftCount < 2, let giftItemView {
let _ = ApplicationSpecificNotice.incrementChannelSendGiftTooltip(accountManager: context.sharedContext.accountManager).start()
Queue.mainQueue().after(0.4, { [weak giftItemView] in
guard let giftItemView else {
return
}
let absoluteFrame = giftItemView.convert(giftItemView.bounds, to: parentController.view)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY), size: CGSize())
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let text: String = presentationData.strings.Chat_SendGiftTooltip
let tooltipController = TooltipScreen(
account: context.account,
sharedContext: context.sharedContext,
text: .plain(text: text),
balancedTextLayout: false,
style: .wide,
arrowStyle: .small,
icon: nil,
location: .point(location, .bottom),
displayDuration: .default,
inset: 8.0,
shouldDismissOnTouch: { _, _ in
return .ignore
}
)
self.interfaceInteraction?.presentControllerInCurrent(tooltipController, nil)
})
} else if suggestCount < 2, let suggestPostItemView {
let _ = ApplicationSpecificNotice.incrementChannelSuggestTooltip(accountManager: context.sharedContext.accountManager).start()
Queue.mainQueue().after(0.4, { [weak suggestPostItemView] in
guard let suggestPostItemView else {
return
}
let absoluteFrame = suggestPostItemView.convert(suggestPostItemView.bounds, to: parentController.view)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY), size: CGSize())
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let _ = presentationData
let text: String = presentationData.strings.Chat_ChannelMessagesHint
let tooltipController = TooltipScreen(
account: context.account,
sharedContext: context.sharedContext,
text: .plain(text: text),
textBadge: presentationData.strings.Chat_ChannelMessagesHintBadge.isEmpty ? nil : presentationData.strings.Chat_ChannelMessagesHintBadge,
balancedTextLayout: false,
style: .wide,
arrowStyle: .small,
icon: nil,
location: .point(location, .bottom),
displayDuration: .default,
inset: 8.0,
shouldDismissOnTouch: { _, _ in
return .ignore
}
)
self.interfaceInteraction?.presentControllerInCurrent(tooltipController, nil)
})
}
})
}
private func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, force: Bool) -> CGFloat {
let isFirstTime = self.layoutData == nil
self.layoutData = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, isSecondary, metrics)
var transition = transition
if !isFirstTime && !transition.isAnimated {
transition = .animated(duration: 0.4, curve: .spring)
}
self.presentationInterfaceState = interfaceState
var centerAction: (title: String, isAccent: Bool)?
if let context = self.context, let peer = interfaceState.renderedPeer?.peer, let action = actionForPeer(context: context, peer: peer, interfaceState: interfaceState, isJoining: self.isJoining, isMuted: interfaceState.peerIsMuted) {
self.action = action
let (title, _) = titleAndColorForAction(action, theme: interfaceState.theme, strings: interfaceState.strings)
var isAccent = false
switch self.action {
case .join, .joinGroup, .applyToJoin:
isAccent = true
default:
break
}
centerAction = (title, isAccent)
}
var displayGift = false
var displaySuggestPost = false
var displayHelp = false
if let peer = interfaceState.renderedPeer?.peer as? TelegramChannel {
if case .broadcast = peer.info, interfaceState.starGiftsAvailable {
displayGift = true
}
if case let .broadcast(broadcastInfo) = peer.info, broadcastInfo.flags.contains(.hasMonoforum) {
displaySuggestPost = true
}
if peer.flags.contains(.isGigagroup), self.action == .muteNotifications || self.action == .unmuteNotifications {
displayHelp = true
}
}
var leftInset = leftInset + 8.0
var rightInset = rightInset + 8.0
if bottomInset <= 32.0 {
leftInset += 18.0
rightInset += 18.0
}
var leftPanelItems: [GlassControlGroupComponent.Item] = []
if displaySuggestPost {
leftPanelItems.append(GlassControlGroupComponent.Item(
id: "suggestPost",
content: .icon("Chat/Input/Accessory Panels/SuggestPost"),
action: { [weak self] in
self?.suggestedPostPressed()
}
))
}
if displayGift {
leftPanelItems.append(GlassControlGroupComponent.Item(
id: "gift",
content: .icon("Chat/Input/Accessory Panels/Gift"),
action: { [weak self] in
self?.giftPressed()
}
))
}
if displayHelp {
leftPanelItems.append(GlassControlGroupComponent.Item(
id: "help",
content: .icon("Chat/Input/Accessory Panels/Help"),
action: { [weak self] in
self?.helpPressed()
}
))
}
var centerPanelItem: GlassControlPanelComponent.Item?
if let centerAction {
centerPanelItem = GlassControlPanelComponent.Item(
items: [GlassControlGroupComponent.Item(
id: 0,
content: .text(centerAction.title),
action: { [weak self] in
self?.buttonPressed()
}
)],
background: centerAction.isAccent ? .activeTint : .panel
)
}
var rightPanelItems: [GlassControlGroupComponent.Item] = []
rightPanelItems.append(GlassControlGroupComponent.Item(
id: "search",
content: .icon("Chat List/SearchIcon"),
action: { [weak self] in
guard let self else {
return
}
self.interfaceInteraction?.beginMessageSearch(.everything, "")
}
))
let panelHeight = defaultHeight(metrics: metrics)
let _ = isFirstTime
let panelFrame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: width - leftInset - rightInset, height: panelHeight))
let _ = self.panel.update(
transition: ComponentTransition(transition),
component: AnyComponent(GlassControlPanelComponent(
theme: interfaceState.theme,
leftItem: leftPanelItems.isEmpty ? nil : GlassControlPanelComponent.Item(
items: leftPanelItems,
background: .panel
),
centralItem: centerPanelItem,
rightItem: rightPanelItems.isEmpty ? nil : GlassControlPanelComponent.Item(
items: rightPanelItems,
background: .panel
)
)),
environment: {},
containerSize: panelFrame.size
)
if let panelView = self.panel.view {
if panelView.superview == nil {
self.panelContainer.addSubview(panelView)
}
transition.updateFrame(view: self.panelContainer, frame: panelFrame)
transition.updateFrame(view: panelView, frame: CGRect(origin: CGPoint(), size: panelFrame.size))
}
/*if self.presentationInterfaceState != interfaceState || force {
let previousState = self.presentationInterfaceState
self.presentationInterfaceState = interfaceState
if previousState?.theme !== interfaceState.theme {
self.helpButtonIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/Help"), color: .white)?.withRenderingMode(.alwaysTemplate)
self.helpButtonIconView.tintColor = interfaceState.theme.chat.inputPanel.panelControlColor
self.suggestedPostButtonIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/SuggestPost"), color: .white)?.withRenderingMode(.alwaysTemplate)
self.suggestedPostButtonIconView.tintColor = interfaceState.theme.chat.inputPanel.panelControlColor
self.giftButtonIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/Gift"), color: .white)?.withRenderingMode(.alwaysTemplate)
self.giftButtonIconView.tintColor = interfaceState.theme.chat.inputPanel.panelControlColor
}
if let context = self.context, let peer = interfaceState.renderedPeer?.peer, previousState?.renderedPeer?.peer == nil || !peer.isEqual(previousState!.renderedPeer!.peer!) || previousState?.theme !== interfaceState.theme || previousState?.strings !== interfaceState.strings || previousState?.peerIsMuted != interfaceState.peerIsMuted || previousState?.pinnedMessage != interfaceState.pinnedMessage || force {
if let action = actionForPeer(context: context, peer: peer, interfaceState: interfaceState, isJoining: self.isJoining, isMuted: interfaceState.peerIsMuted) {
let previousAction = self.action
self.action = action
let (title, _) = titleAndColorForAction(action, theme: interfaceState.theme, strings: interfaceState.strings)
let _ = previousAction
let titleColor: UIColor
if case .join = self.action {
titleColor = interfaceState.theme.chat.inputPanel.actionControlForegroundColor
} else {
titleColor = interfaceState.theme.chat.inputPanel.panelControlColor
}
self.buttonTitle.attributedText = NSAttributedString(string: title, font: Font.semibold(15.0), textColor: titleColor)
self.buttonTintTitle.attributedText = NSAttributedString(string: title, font: Font.semibold(15.0), textColor: .black)
self.button.accessibilityLabel = title
} else {
self.action = nil
}
}
}
let panelHeight = defaultHeight(metrics: metrics)
if let peer = interfaceState.renderedPeer?.peer as? TelegramChannel {
if case let .broadcast(broadcastInfo) = peer.info, interfaceState.starGiftsAvailable {
if self.giftButton.isHidden && !isFirstTime {
self.giftButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.giftButton.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
}
self.giftButtonBackgroundView.isHidden = false
self.helpButtonBackgroundView.isHidden = true
self.suggestedPostButtonBackgroundView.isHidden = !broadcastInfo.flags.contains(.hasMonoforum)
self.presentGiftOrSuggestTooltip()
} else if case let .broadcast(broadcastInfo) = peer.info, broadcastInfo.flags.contains(.hasMonoforum) {
self.giftButtonBackgroundView.isHidden = true
self.helpButtonBackgroundView.isHidden = true
self.suggestedPostButtonBackgroundView.isHidden = false
self.presentGiftOrSuggestTooltip()
} else if peer.flags.contains(.isGigagroup), self.action == .muteNotifications || self.action == .unmuteNotifications {
self.giftButtonBackgroundView.isHidden = true
self.helpButtonBackgroundView.isHidden = false
self.suggestedPostButtonBackgroundView.isHidden = true
} else {
self.giftButtonBackgroundView.isHidden = true
self.helpButtonBackgroundView.isHidden = true
self.suggestedPostButtonBackgroundView.isHidden = true
}
} else {
self.giftButtonBackgroundView.isHidden = true
self.helpButtonBackgroundView.isHidden = true
self.suggestedPostButtonBackgroundView.isHidden = true
}
let buttonTitleSize = self.buttonTitle.updateLayout(CGSize(width: width, height: panelHeight))
let _ = self.buttonTintTitle.updateLayout(CGSize(width: width, height: panelHeight))
let buttonSize = CGSize(width: buttonTitleSize.width + 16.0 * 2.0, height: 40.0)
let buttonFrame = CGRect(origin: CGPoint(x: floor((width - buttonSize.width) / 2.0), y: floor((panelHeight - buttonSize.height) * 0.5)), size: buttonSize)
transition.updateFrame(view: self.buttonBackgroundView, frame: buttonFrame)
transition.updateFrame(view: self.button, frame: CGRect(origin: CGPoint(), size: buttonFrame.size))
let buttonTintColor: GlassBackgroundView.TintColor
if case .join = self.action {
buttonTintColor = .init(kind: .custom, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7), innerColor: interfaceState.theme.chat.inputPanel.actionControlFillColor)
} else {
buttonTintColor = .init(kind: .panel, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7))
}
self.buttonBackgroundView.update(size: buttonFrame.size, cornerRadius: buttonFrame.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: buttonTintColor, isInteractive: true, transition: ComponentTransition(transition))
self.buttonTitle.frame = CGRect(origin: CGPoint(x: floor((buttonFrame.width - buttonTitleSize.width) * 0.5), y: floor((buttonFrame.height - buttonTitleSize.height) * 0.5)), size: buttonTitleSize)
self.buttonTintTitle.frame = self.buttonTitle.frame
let giftButtonFrame = CGRect(x: width - rightInset - 40.0 - 8.0, y: floor((panelHeight - 40.0) * 0.5), width: 40.0, height: 40.0)
transition.updateFrame(view: self.giftButtonBackgroundView, frame: giftButtonFrame)
if let image = self.giftButtonIconView.image {
transition.updateFrame(view: self.giftButtonIconView, frame: image.size.centered(in: CGRect(origin: CGPoint(), size: giftButtonFrame.size)))
}
transition.updateFrame(view: self.giftButton, frame: CGRect(origin: CGPoint(), size: giftButtonFrame.size))
self.giftButtonBackgroundView.update(size: giftButtonFrame.size, cornerRadius: giftButtonFrame.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), isInteractive: true, transition: ComponentTransition(transition))
let helpButtonFrame = CGRect(x: width - rightInset - 8.0 - 40.0, y: floor((panelHeight - 40.0) * 0.5), width: 40.0, height: 40.0)
transition.updateFrame(view: self.helpButtonBackgroundView, frame: helpButtonFrame)
if let image = self.helpButtonIconView.image {
transition.updateFrame(view: self.helpButtonIconView, frame: image.size.centered(in: CGRect(origin: CGPoint(), size: helpButtonFrame.size)))
}
transition.updateFrame(view: self.helpButton, frame: CGRect(origin: CGPoint(), size: helpButtonFrame.size))
self.helpButtonBackgroundView.update(size: helpButtonFrame.size, cornerRadius: helpButtonFrame.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), isInteractive: true, transition: ComponentTransition(transition))
let suggestedPostButtonFrame = CGRect(x: leftInset + 8.0, y: floor((panelHeight - 40.0) * 0.5), width: 40.0, height: 40.0)
transition.updateFrame(view: self.suggestedPostButtonBackgroundView, frame: suggestedPostButtonFrame)
if let image = self.suggestedPostButtonIconView.image {
transition.updateFrame(view: self.suggestedPostButtonIconView, frame: image.size.centered(in: CGRect(origin: CGPoint(), size: suggestedPostButtonFrame.size)))
}
transition.updateFrame(view: self.suggestedPostButton, frame: CGRect(origin: CGPoint(), size: suggestedPostButtonFrame.size))
self.suggestedPostButtonBackgroundView.update(size: suggestedPostButtonFrame.size, cornerRadius: suggestedPostButtonFrame.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), isInteractive: true, transition: ComponentTransition(transition))*/
return panelHeight
}
override public func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat {
return defaultHeight(metrics: metrics)
}
}
@@ -0,0 +1,28 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatContextResultPeekContent",
module_name = "ChatContextResultPeekContent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/AsyncDisplayKit",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/PhotoResources",
"//submodules/AppBundle",
"//submodules/ContextUI",
"//submodules/SoftwareVideo",
"//submodules/TelegramUI/Components/BatchVideoRendering",
"//submodules/TelegramUI/Components/GifVideoLayer",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,249 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import AVFoundation
import PhotoResources
import AppBundle
import ContextUI
import SoftwareVideo
import BatchVideoRendering
import GifVideoLayer
import AccountContext
public final class ChatContextResultPeekContent: PeekControllerContent {
public let context: AccountContext
public let contextResult: ChatContextResult
public let menu: [ContextMenuItem]
public let batchVideoContext: BatchVideoRenderingContext
public init(context: AccountContext, contextResult: ChatContextResult, menu: [ContextMenuItem], batchVideoContext: BatchVideoRenderingContext) {
self.context = context
self.contextResult = contextResult
self.menu = menu
self.batchVideoContext = batchVideoContext
}
public func presentation() -> PeekControllerContentPresentation {
return .contained
}
public func menuActivation() -> PeerControllerMenuActivation {
return .drag
}
public func menuItems() -> [ContextMenuItem] {
return self.menu
}
public func node() -> PeekControllerContentNode & ASDisplayNode {
return ChatContextResultPeekNode(context: self.context, contextResult: self.contextResult, batchVideoContext: self.batchVideoContext)
}
public func topAccessoryNode() -> ASDisplayNode? {
let arrowNode = ASImageNode()
if let image = UIImage(bundleImageName: "Peek/Arrow") {
arrowNode.image = image
arrowNode.frame = CGRect(origin: CGPoint(), size: image.size)
}
return arrowNode
}
public func fullScreenAccessoryNode(blurView: UIVisualEffectView) -> (PeekControllerAccessoryNode & ASDisplayNode)? {
return nil
}
public func isEqual(to: PeekControllerContent) -> Bool {
if let to = to as? ChatContextResultPeekContent {
return self.contextResult == to.contextResult
} else {
return false
}
}
}
private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerContentNode {
private let context: AccountContext
private let contextResult: ChatContextResult
private let batchVideoContext: BatchVideoRenderingContext
private let imageNodeBackground: ASDisplayNode
private let imageNode: TransformImageNode
private var videoLayer: GifVideoLayer?
private var currentImageResource: TelegramMediaResource?
private var currentVideoFile: TelegramMediaFile?
private var ticking: Bool = false {
didSet {
if self.ticking != oldValue {
self.videoLayer?.shouldBeAnimating = self.ticking
}
}
}
init(context: AccountContext, contextResult: ChatContextResult, batchVideoContext: BatchVideoRenderingContext) {
self.context = context
self.contextResult = contextResult
self.batchVideoContext = batchVideoContext
self.imageNodeBackground = ASDisplayNode()
self.imageNodeBackground.isLayerBacked = true
self.imageNodeBackground.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
self.imageNode = TransformImageNode()
self.imageNode.contentAnimations = [.subsequentUpdates]
self.imageNode.isLayerBacked = !smartInvertColorsEnabled()
self.imageNode.displaysAsynchronously = false
super.init()
self.addSubnode(self.imageNodeBackground)
self.imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates]
self.addSubnode(self.imageNode)
}
deinit {
}
func ready() -> Signal<Bool, NoError> {
return .single(true)
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let imageLayout = self.imageNode.asyncLayout()
let currentImageResource = self.currentImageResource
let currentVideoFile = self.currentVideoFile
var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
var imageResource: TelegramMediaResource?
var videoFileReference: FileMediaReference?
var imageDimensions: CGSize?
switch self.contextResult {
case let .externalReference(externalReference):
if let content = externalReference.content {
imageResource = content.resource
} else if let thumbnail = externalReference.thumbnail {
imageResource = thumbnail.resource
}
imageDimensions = externalReference.content?.dimensions?.cgSize
if let content = externalReference.content, externalReference.type == "gif", let thumbnailResource = imageResource
, let dimensions = content.dimensions {
videoFileReference = .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []))
imageResource = nil
}
case let .internalReference(internalReference):
if let image = internalReference.image {
if let largestRepresentation = largestImageRepresentation(image.representations) {
imageDimensions = largestRepresentation.dimensions.cgSize
}
imageResource = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 200, height: 100))?.resource
} else if let file = internalReference.file {
if let dimensions = file.dimensions {
imageDimensions = dimensions.cgSize
} else if let largestRepresentation = largestImageRepresentation(file.previewRepresentations) {
imageDimensions = largestRepresentation.dimensions.cgSize
}
imageResource = smallestImageRepresentation(file.previewRepresentations)?.resource
}
if let file = internalReference.file {
if file.isVideo && file.isAnimated {
videoFileReference = .standalone(media: file)
imageResource = nil
}
}
}
let fittedImageDimensions: CGSize
let croppedImageDimensions: CGSize
if let imageDimensions = imageDimensions {
fittedImageDimensions = imageDimensions.fitted(CGSize(width: size.width, height: size.height))
} else {
fittedImageDimensions = CGSize(width: min(size.width, size.height), height: min(size.width, size.height))
}
croppedImageDimensions = fittedImageDimensions
var imageApply: (() -> Void)?
if let _ = imageResource {
let imageCorners = ImageCorners()
let arguments = TransformImageArguments(corners: imageCorners, imageSize: fittedImageDimensions, boundingSize: croppedImageDimensions, intrinsicInsets: UIEdgeInsets())
imageApply = imageLayout(arguments)
}
var updatedImageResource = false
if let currentImageResource = currentImageResource, let imageResource = imageResource {
if !currentImageResource.isEqual(to: imageResource) {
updatedImageResource = true
}
} else if (currentImageResource != nil) != (imageResource != nil) {
updatedImageResource = true
}
var updatedVideoFile = false
if let currentVideoFile = currentVideoFile, let videoFileReference = videoFileReference {
if !currentVideoFile.isEqual(to: videoFileReference.media) {
updatedVideoFile = true
}
} else if (currentVideoFile != nil) != (videoFileReference != nil) {
updatedVideoFile = true
}
if updatedImageResource {
if let imageResource = imageResource {
let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: Int32(fittedImageDimensions.width * 2.0), height: Int32(fittedImageDimensions.height * 2.0)), resource: imageResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)
let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])
updateImageSignal = chatMessagePhoto(postbox: self.context.account.postbox, userLocation: .other, photoReference: .standalone(media: tmpImage))
} else {
updateImageSignal = .complete()
}
}
self.currentImageResource = imageResource
self.currentVideoFile = videoFileReference?.media
if let imageApply = imageApply {
if let updateImageSignal = updateImageSignal {
self.imageNode.setSignal(updateImageSignal)
}
self.imageNode.frame = CGRect(origin: CGPoint(), size: croppedImageDimensions)
self.imageNodeBackground.frame = CGRect(origin: CGPoint(), size: croppedImageDimensions)
imageApply()
}
if updatedVideoFile {
if let videoLayer = self.videoLayer {
self.videoLayer = nil
videoLayer.removeFromSuperlayer()
}
if let videoFileReference {
let videoLayer = GifVideoLayer(
context: self.context,
batchVideoContext: self.batchVideoContext,
userLocation: .other,
file: videoFileReference,
synchronousLoad: false
)
self.videoLayer = videoLayer
self.layer.addSublayer(videoLayer)
}
}
if let videoLayer = self.videoLayer {
videoLayer.frame = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height))
}
if !self.ticking {
self.ticking = true
}
return croppedImageDimensions
}
}
@@ -0,0 +1,40 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatEmptyNode",
module_name = "ChatEmptyNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/AppBundle",
"//submodules/LocalizedPeerData",
"//submodules/TelegramStringFormatting",
"//submodules/AccountContext",
"//submodules/ChatPresentationInterfaceState",
"//submodules/WallpaperBackgroundNode",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/TelegramUI/Components/Chat/ChatLoadingNode",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/Markdown",
"//submodules/ReactionSelectionNode",
"//submodules/TelegramUI/Components/Chat/ChatMediaInputStickerGridItem",
"//submodules/PremiumUI",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/Components/BundleIconComponent",
],
visibility = [
"//visibility:public",
],
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatHistoryEntry",
module_name = "ChatHistoryEntry",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/MergeLists",
"//submodules/TemporaryCachedPeerDataManager",
"//submodules/AccountContext",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,318 @@
import Postbox
import TelegramCore
import TelegramPresentationData
import MergeLists
import TemporaryCachedPeerDataManager
import AccountContext
public enum ChatMessageEntryContentType {
case generic
case largeEmoji
case animatedEmoji
}
public struct ChatMessageEntryAttributes: Equatable {
public var rank: CachedChannelAdminRank?
public var isContact: Bool
public var contentTypeHint: ChatMessageEntryContentType
public var updatingMedia: ChatUpdatingMessageMedia?
public var isPlaying: Bool
public var isCentered: Bool
public var authorStoryStats: PeerStoryStats?
public var displayContinueThreadFooter: Bool
public init(rank: CachedChannelAdminRank?, isContact: Bool, contentTypeHint: ChatMessageEntryContentType, updatingMedia: ChatUpdatingMessageMedia?, isPlaying: Bool, isCentered: Bool, authorStoryStats: PeerStoryStats?, displayContinueThreadFooter: Bool) {
self.rank = rank
self.isContact = isContact
self.contentTypeHint = contentTypeHint
self.updatingMedia = updatingMedia
self.isPlaying = isPlaying
self.isCentered = isCentered
self.authorStoryStats = authorStoryStats
self.displayContinueThreadFooter = displayContinueThreadFooter
}
public init() {
self.rank = nil
self.isContact = false
self.contentTypeHint = .generic
self.updatingMedia = nil
self.isPlaying = false
self.isCentered = false
self.authorStoryStats = nil
self.displayContinueThreadFooter = false
}
}
public enum ChatInfoData: Equatable {
case botInfo(title: String, text: String, photo: TelegramMediaImage?, video: TelegramMediaFile?)
case userInfo(peer: EnginePeer, verification: PeerVerification?, registrationDate: String?, phoneCountry: String?, groupsInCommonCount: Int32)
case newThreadInfo
}
public enum ChatHistoryEntry: Identifiable, Comparable {
case MessageEntry(Message, ChatPresentationData, Bool, MessageHistoryEntryLocation?, ChatHistoryMessageSelection, ChatMessageEntryAttributes)
case MessageGroupEntry(Int64, [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes, MessageHistoryEntryLocation?)], ChatPresentationData)
case UnreadEntry(MessageIndex, ChatPresentationData)
case ReplyCountEntry(MessageIndex, Bool, Int, ChatPresentationData)
case ChatInfoEntry(ChatInfoData, ChatPresentationData)
case SearchEntry(PresentationTheme, PresentationStrings)
public var stableId: UInt64 {
switch self {
case let .MessageEntry(message, _, _, _, _, attributes):
let type: UInt64
switch attributes.contentTypeHint {
case .generic:
type = 2
case .largeEmoji:
type = 3
case .animatedEmoji:
type = 4
}
return UInt64(message.stableId) | ((type << 40))
case let .MessageGroupEntry(groupInfo, _, _):
return UInt64(bitPattern: groupInfo) | ((UInt64(2) << 40))
case .UnreadEntry:
return UInt64(4) << 40
case .ReplyCountEntry:
return UInt64(5) << 40
case .ChatInfoEntry:
return UInt64(6) << 40
case .SearchEntry:
return UInt64(7) << 40
}
}
public var index: MessageIndex {
switch self {
case let .MessageEntry(message, _, _, _, _, _):
return message.index
case let .MessageGroupEntry(_, messages, _):
return messages[messages.count - 1].0.index
case let .UnreadEntry(index, _):
return index
case let .ReplyCountEntry(index, _, _, _):
return index
case let .ChatInfoEntry(infoData, _):
switch infoData {
case .newThreadInfo:
return MessageIndex.absoluteUpperBound()
default:
return MessageIndex.absoluteLowerBound()
}
case .SearchEntry:
return MessageIndex.absoluteLowerBound()
}
}
public var firstIndex: MessageIndex {
switch self {
case let .MessageEntry(message, _, _, _, _, _):
return message.index
case let .MessageGroupEntry(_, messages, _):
return messages[0].0.index
case let .UnreadEntry(index, _):
return index
case let .ReplyCountEntry(index, _, _, _):
return index
case let .ChatInfoEntry(infoData, _):
switch infoData {
case .newThreadInfo:
return MessageIndex.absoluteUpperBound()
default:
return MessageIndex.absoluteLowerBound()
}
case .SearchEntry:
return MessageIndex.absoluteLowerBound()
}
}
public var timestamp: Int32? {
switch self {
case let .MessageEntry(message, _, _, _, _, _):
return message.timestamp
case let .MessageGroupEntry(_, messages, _):
return messages[0].0.timestamp
default:
return nil
}
}
public static func ==(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool {
switch lhs {
case let .MessageEntry(lhsMessage, lhsPresentationData, lhsRead, _, lhsSelection, lhsAttributes):
switch rhs {
case let .MessageEntry(rhsMessage, rhsPresentationData, rhsRead, _, rhsSelection, rhsAttributes) where lhsMessage.index == rhsMessage.index && lhsMessage.flags == rhsMessage.flags && lhsRead == rhsRead:
if lhsPresentationData !== rhsPresentationData {
return false
}
if lhsMessage.stableVersion != rhsMessage.stableVersion {
return false
}
if lhsMessage.peers.count != rhsMessage.peers.count {
return false
}
for (id, peer) in lhsMessage.peers {
if let otherPeer = rhsMessage.peers[id] {
if !peer.isEqual(otherPeer) {
return false
}
}
}
if lhsMessage.media.count != rhsMessage.media.count {
return false
}
for i in 0 ..< lhsMessage.media.count {
if !lhsMessage.media[i].isEqual(to: rhsMessage.media[i]) {
return false
}
}
if lhsMessage.associatedMessages.count != rhsMessage.associatedMessages.count {
return false
}
if !lhsMessage.associatedMessages.isEmpty {
for (id, message) in lhsMessage.associatedMessages {
if let otherMessage = rhsMessage.associatedMessages[id] {
if otherMessage.stableVersion != message.stableVersion {
return false
}
}
}
}
if lhsMessage.associatedStories.count != rhsMessage.associatedStories.count {
return false
}
if !lhsMessage.associatedStories.isEmpty {
for (id, story) in lhsMessage.associatedStories {
if let otherStory = rhsMessage.associatedStories[id] {
if story != otherStory {
return false
}
} else {
return false
}
}
}
if lhsSelection != rhsSelection {
return false
}
if lhsAttributes != rhsAttributes {
return false
}
return true
default:
return false
}
case let .MessageGroupEntry(lhsGroupInfo, lhsMessages, lhsPresentationData):
if case let .MessageGroupEntry(rhsGroupInfo, rhsMessages, rhsPresentationData) = rhs, lhsGroupInfo == rhsGroupInfo, lhsPresentationData === rhsPresentationData, lhsMessages.count == rhsMessages.count {
for i in 0 ..< lhsMessages.count {
let (lhsMessage, lhsRead, lhsSelection, lhsAttributes, lhsLocation) = lhsMessages[i]
let (rhsMessage, rhsRead, rhsSelection, rhsAttributes, rhsLocation) = rhsMessages[i]
if lhsMessage.id != rhsMessage.id {
return false
}
if lhsMessage.timestamp != rhsMessage.timestamp {
return false
}
if lhsMessage.flags != rhsMessage.flags {
return false
}
if lhsRead != rhsRead {
return false
}
if lhsSelection != rhsSelection {
return false
}
if lhsPresentationData !== rhsPresentationData {
return false
}
if lhsMessage.stableVersion != rhsMessage.stableVersion {
return false
}
if lhsMessage.media.count != rhsMessage.media.count {
return false
}
for i in 0 ..< lhsMessage.media.count {
if !lhsMessage.media[i].isEqual(to: rhsMessage.media[i]) {
return false
}
}
if lhsMessage.associatedMessages.count != rhsMessage.associatedMessages.count {
return false
}
if !lhsMessage.associatedMessages.isEmpty {
for (id, message) in lhsMessage.associatedMessages {
if let otherMessage = rhsMessage.associatedMessages[id] {
if otherMessage.stableVersion != message.stableVersion {
return false
}
}
}
}
if lhsMessage.associatedStories.count != rhsMessage.associatedStories.count {
return false
}
if !lhsMessage.associatedStories.isEmpty {
for (id, story) in lhsMessage.associatedStories {
if let otherStory = rhsMessage.associatedStories[id] {
if story != otherStory {
return false
}
} else {
return false
}
}
}
if lhsAttributes != rhsAttributes {
return false
}
if lhsLocation != rhsLocation {
return false
}
}
return true
} else {
return false
}
case let .UnreadEntry(lhsIndex, lhsPresentationData):
if case let .UnreadEntry(rhsIndex, rhsPresentationData) = rhs, lhsIndex == rhsIndex, lhsPresentationData === rhsPresentationData {
return true
} else {
return false
}
case let .ReplyCountEntry(lhsIndex, lhsIsComments, lhsCount, lhsPresentationData):
if case let .ReplyCountEntry(rhsIndex, rhsIsComments, rhsCount, rhsPresentationData) = rhs, lhsIndex == rhsIndex, lhsIsComments == rhsIsComments, lhsCount == rhsCount, lhsPresentationData === rhsPresentationData {
return true
} else {
return false
}
case let .ChatInfoEntry(lhsData, lhsPresentationData):
if case let .ChatInfoEntry(rhsData, rhsPresentationData) = rhs, lhsData == rhsData, lhsPresentationData === rhsPresentationData {
return true
} else {
return false
}
case let .SearchEntry(lhsTheme, lhsStrings):
if case let .SearchEntry(rhsTheme, rhsStrings) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings {
return true
} else {
return false
}
}
}
public static func <(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool {
let lhsIndex = lhs.index
let rhsIndex = rhs.index
if lhsIndex == rhsIndex {
return lhs.stableId < rhs.stableId
} else {
return lhsIndex < rhsIndex
}
}
}
@@ -0,0 +1,30 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatHistorySearchContainerNode",
module_name = "ChatHistorySearchContainerNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/MergeLists",
"//submodules/AccountContext",
"//submodules/SearchUI",
"//submodules/TelegramUIPreferences",
"//submodules/ListMessageItem",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemView",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,396 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import MergeLists
import AccountContext
import SearchUI
import TelegramUIPreferences
import ListMessageItem
import ChatControllerInteraction
import ChatMessageItemView
private extension ListMessageItemInteraction {
convenience init(controllerInteraction: ChatControllerInteraction) {
self.init(openMessage: { message, mode -> Bool in
return controllerInteraction.openMessage(message, OpenMessageParams(mode: mode))
}, openMessageContextMenu: { message, bool, node, rect, gesture in
controllerInteraction.openMessageContextMenu(message, bool, node, rect, gesture, nil)
}, toggleMessagesSelection: { messageId, selected in
controllerInteraction.toggleMessagesSelection(messageId, selected)
}, openUrl: { url, param1, param2, message in
controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: param1, external: param2, message: message))
}, openInstantPage: { message, data in
controllerInteraction.openInstantPage(message, data)
}, longTap: { action, message in
controllerInteraction.longTap(action, ChatControllerInteraction.LongTapParams(message: message))
}, getHiddenMedia: {
return controllerInteraction.hiddenMedia
})
}
}
private enum ChatHistorySearchEntryStableId: Hashable {
case messageId(MessageId)
}
private enum ChatHistorySearchEntry: Comparable, Identifiable {
case message(Message, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationFontSize)
var stableId: ChatHistorySearchEntryStableId {
switch self {
case let .message(message, _, _, _, _):
return .messageId(message.id)
}
}
static func ==(lhs: ChatHistorySearchEntry, rhs: ChatHistorySearchEntry) -> Bool {
switch lhs {
case let .message(lhsMessage, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsFontSize):
if case let .message(rhsMessage, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsFontSize) = rhs {
if lhsMessage.id != rhsMessage.id {
return false
}
if lhsMessage.stableVersion != rhsMessage.stableVersion {
return false
}
if lhsTheme !== rhsTheme {
return false
}
if lhsStrings !== rhsStrings {
return false
}
if lhsDateTimeFormat != rhsDateTimeFormat {
return false
}
if lhsFontSize != rhsFontSize {
return false
}
return true
} else {
return false
}
}
}
static func <(lhs: ChatHistorySearchEntry, rhs: ChatHistorySearchEntry) -> Bool {
switch lhs {
case let .message(lhsMessage, _, _, _, _):
if case let .message(rhsMessage, _, _, _, _) = rhs {
return lhsMessage.index < rhsMessage.index
} else {
return false
}
}
}
func item(context: AccountContext, peerId: PeerId, interaction: ChatControllerInteraction) -> ListViewItem {
switch self {
case let .message(message, theme, strings, dateTimeFormat, fontSize):
return ListMessageItem(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: theme, wallpaper: .builtin(WallpaperSettings())), fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: .firstLast, disableAnimations: false, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0.0, auxiliaryRadius: 0.0, mergeBubbleCorners: false)), context: context, chatLocation: .peer(id: peerId), interaction: ListMessageItemInteraction(controllerInteraction: interaction), message: message, selection: .none, displayHeader: true)
}
}
}
private struct ChatHistorySearchContainerTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let query: String
let displayingResults: Bool
}
private func chatHistorySearchContainerPreparedTransition(from fromEntries: [ChatHistorySearchEntry], to toEntries: [ChatHistorySearchEntry], query: String, displayingResults: Bool, context: AccountContext, peerId: PeerId, interaction: ChatControllerInteraction) -> ChatHistorySearchContainerTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, peerId: peerId, interaction: interaction), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, peerId: peerId, interaction: interaction), directionHint: nil) }
return ChatHistorySearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, query: query, displayingResults: displayingResults)
}
public final class ChatHistorySearchContainerNode: SearchDisplayControllerContentNode {
private let context: AccountContext
private let dimNode: ASDisplayNode
private let listNode: ListView
private let emptyResultsTitleNode: ImmediateTextNode
private let emptyResultsTextNode: ImmediateTextNode
private var containerLayout: (ContainerViewLayout, CGFloat)?
private var currentEntries: [ChatHistorySearchEntry]?
public var currentMessages: [MessageId: Message]?
private var currentQuery: String?
private let searchQuery = Promise<String?>()
private let searchQueryDisposable = MetaDisposable()
private let searchDisposable = MetaDisposable()
private let _isSearching = ValuePromise<Bool>(false, ignoreRepeated: true)
override public var isSearching: Signal<Bool, NoError> {
return self._isSearching.get()
}
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationFontSize)>
private var enqueuedTransitions: [(ChatHistorySearchContainerTransition, Bool)] = []
override public var hasDim: Bool {
return true
}
public init(context: AccountContext, peerId: PeerId, threadId: Int64?, tagMask: MessageTags, interfaceInteraction: ChatControllerInteraction) {
self.context = context
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationData = presentationData
self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings, self.presentationData.dateTimeFormat, self.presentationData.listsFontSize))
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5)
self.listNode = ListView()
self.listNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
}
self.emptyResultsTitleNode = ImmediateTextNode()
self.emptyResultsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.SharedMedia_SearchNoResults, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.freeTextColor)
self.emptyResultsTitleNode.textAlignment = .center
self.emptyResultsTitleNode.isHidden = true
self.emptyResultsTextNode = ImmediateTextNode()
self.emptyResultsTextNode.maximumNumberOfLines = 0
self.emptyResultsTextNode.textAlignment = .center
self.emptyResultsTextNode.isHidden = true
super.init()
self.backgroundColor = nil
self.isOpaque = false
self.addSubnode(self.dimNode)
self.addSubnode(self.listNode)
self.addSubnode(self.emptyResultsTitleNode)
self.addSubnode(self.emptyResultsTextNode)
self.listNode.isHidden = true
let themeAndStringsPromise = self.themeAndStringsPromise
let previousEntriesValue = Atomic<[ChatHistorySearchEntry]?>(value: nil)
self.searchQueryDisposable.set((self.searchQuery.get()
|> deliverOnMainQueue).startStrict(next: { [weak self] query in
if let strongSelf = self {
let signal: Signal<([ChatHistorySearchEntry], [MessageId: Message])?, NoError>
if let query = query, !query.isEmpty {
let foundRemoteMessages: Signal<[Message], NoError> = context.engine.messages.searchMessages(location: .peer(peerId: peerId, fromId: nil, tags: tagMask, reactions: nil, threadId: threadId, minDate: nil, maxDate: nil), query: query, state: nil)
|> map { $0.0.messages }
|> delay(0.2, queue: Queue.concurrentDefaultQueue())
signal = combineLatest(foundRemoteMessages, themeAndStringsPromise.get())
|> map { messages, themeAndStrings -> ([ChatHistorySearchEntry], [MessageId: Message])? in
if messages.isEmpty {
return ([], [:])
} else {
return (messages.map { message -> ChatHistorySearchEntry in
return .message(message, themeAndStrings.0, themeAndStrings.1, themeAndStrings.2, themeAndStrings.3)
}, Dictionary(messages.map { ($0.id, $0) }, uniquingKeysWith: { lhs, _ in lhs }))
}
}
strongSelf._isSearching.set(true)
} else {
signal = .single(nil)
strongSelf._isSearching.set(false)
}
strongSelf.searchDisposable.set((signal
|> deliverOnMainQueue).startStrict(next: { entriesAndMessages in
if let strongSelf = self {
let previousEntries = previousEntriesValue.swap(entriesAndMessages?.0)
let firstTime = previousEntries == nil
let transition = chatHistorySearchContainerPreparedTransition(from: previousEntries ?? [], to: entriesAndMessages?.0 ?? [], query: query ?? "", displayingResults: entriesAndMessages?.0 != nil, context: context, peerId: peerId, interaction: interfaceInteraction)
strongSelf.currentEntries = entriesAndMessages?.0
strongSelf.currentMessages = entriesAndMessages?.1
strongSelf.enqueueTransition(transition, firstTime: firstTime)
strongSelf._isSearching.set(false)
}
}))
}
}))
self.listNode.beganInteractiveDragging = { [weak self] _ in
self?.dismissInput?()
}
self.presentationDataDisposable = context.sharedContext.presentationData.startStrict(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.themeAndStringsPromise.set(.single((presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.listsFontSize)))
strongSelf.emptyResultsTitleNode.attributedText = NSAttributedString(string: presentationData.strings.SharedMedia_SearchNoResults, font: Font.semibold(17.0), textColor: presentationData.theme.list.freeTextColor, paragraphAlignment: .center)
if let (layout, navigationBarHeight) = strongSelf.containerLayout {
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
}
})
}
deinit {
self.presentationDataDisposable?.dispose()
self.searchQueryDisposable.dispose()
self.searchDisposable.dispose()
}
override public func didLoad() {
super.didLoad()
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
}
override public func searchTextUpdated(text: String) {
if text.isEmpty {
self.searchQuery.set(.single(nil))
} else {
self.searchQuery.set(.single(text))
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
let firstValidLayout = self.containerLayout == nil
self.containerLayout = (layout, navigationBarHeight)
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
let topInset = navigationBarHeight
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset)))
let padding: CGFloat = 16.0
let emptyTitleSize = self.emptyResultsTitleNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude))
let emptyTextSize = self.emptyResultsTextNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude))
let insets = layout.insets(options: [.input])
let emptyTextSpacing: CGFloat = 8.0
let emptyTotalHeight = emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing
let emptyTitleY = navigationBarHeight + floorToScreenPixels((layout.size.height - navigationBarHeight - max(insets.bottom, layout.intrinsicInsets.bottom) - emptyTotalHeight) / 2.0)
transition.updateFrame(node: self.emptyResultsTitleNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTitleSize.width) / 2.0, y: emptyTitleY), size: emptyTitleSize))
transition.updateFrame(node: self.emptyResultsTextNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTextSize.width) / 2.0, y: emptyTitleY + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize))
self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: topInset, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right), duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
if firstValidLayout {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
private func enqueueTransition(_ transition: ChatHistorySearchContainerTransition, firstTime: Bool) {
self.enqueuedTransitions.append((transition, firstTime))
if self.containerLayout != nil {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
private func dequeueTransition() {
if let (transition, firstTime) = self.enqueuedTransitions.first {
self.enqueuedTransitions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions()
options.insert(.PreferSynchronousDrawing)
if firstTime {
} else {
}
let displayingResults = transition.displayingResults
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
if let strongSelf = self {
if displayingResults != !strongSelf.listNode.isHidden || strongSelf.currentQuery != transition.query {
strongSelf.currentQuery = transition.query
strongSelf.listNode.isHidden = !displayingResults
strongSelf.dimNode.isHidden = displayingResults
strongSelf.backgroundColor = displayingResults ? strongSelf.presentationData.theme.list.plainBackgroundColor : nil
strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.SharedMedia_SearchNoResultsDescription(transition.query).string, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor)
let emptyResults = displayingResults && strongSelf.currentEntries?.isEmpty ?? false
strongSelf.emptyResultsTitleNode.isHidden = !emptyResults
strongSelf.emptyResultsTextNode.isHidden = !emptyResults
if let (layout, navigationBarHeight) = strongSelf.containerLayout {
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
}
}
})
}
}
@objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.cancel?()
}
}
public func messageForGallery(_ id: MessageId) -> Message? {
if let currentEntries = self.currentEntries {
for entry in currentEntries {
switch entry {
case let .message(message, _, _, _, _):
if message.id == id {
return message
}
}
}
}
return nil
}
public func updateHiddenMedia() {
self.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
itemNode.updateHiddenMedia()
} else if let itemNode = itemNode as? ListMessageNode {
itemNode.updateHiddenMedia()
}
}
}
public func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
var transitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
self.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
if let result = itemNode.transitionNode(id: messageId, media: media, adjustRect: false) {
transitionNode = result
}
} else if let itemNode = itemNode as? ListMessageNode {
if let result = itemNode.transitionNode(id: messageId, media: media, adjustRect: false) {
transitionNode = result
}
}
}
return transitionNode
}
}
@@ -0,0 +1,36 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatInlineSearchResultsListComponent",
module_name = "ChatInlineSearchResultsListComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AsyncDisplayKit",
"//submodules/ComponentFlow",
"//submodules/Display",
"//submodules/TelegramCore",
"//submodules/Postbox",
"//submodules/AccountContext",
"//submodules/ChatListUI",
"//submodules/MergeLists",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/UIKitRuntimeUtils",
"//submodules/ChatPresentationInterfaceState",
"//submodules/ContactsPeerItem",
"//submodules/ItemListUI",
"//submodules/ChatListSearchItemHeader",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/Components/MultilineTextComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatInputAccessoryPanel",
module_name = "ChatInputAccessoryPanel",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,60 @@
import Foundation
import UIKit
import TelegramPresentationData
import TelegramUIPreferences
import GlassBackgroundComponent
public final class ChatInputAccessoryPanelEnvironment: Equatable {
public let theme: PresentationTheme
public let strings: PresentationStrings
public let nameDisplayOrder: PresentationPersonNameOrder
public let dateTimeFormat: PresentationDateTimeFormat
public init(
theme: PresentationTheme,
strings: PresentationStrings,
nameDisplayOrder: PresentationPersonNameOrder,
dateTimeFormat: PresentationDateTimeFormat
) {
self.theme = theme
self.strings = strings
self.nameDisplayOrder = nameDisplayOrder
self.dateTimeFormat = dateTimeFormat
}
public static func ==(lhs: ChatInputAccessoryPanelEnvironment, rhs: ChatInputAccessoryPanelEnvironment) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.nameDisplayOrder != rhs.nameDisplayOrder {
return false
}
if lhs.dateTimeFormat != rhs.dateTimeFormat {
return false
}
return true
}
}
public final class ChatInputAccessoryPanelTransitionData {
public let titleView: UIView
public let textView: UIView
public let lineView: UIView
public let imageView: UIView?
public init(titleView: UIView, textView: UIView, lineView: UIView, imageView: UIView?) {
self.titleView = titleView
self.textView = textView
self.lineView = lineView
self.imageView = imageView
}
}
public protocol ChatInputAccessoryPanelView: UIView {
var contentTintView: UIView { get }
var storedFrameBeforeDismissed: CGRect? { get set }
var transitionData: ChatInputAccessoryPanelTransitionData? { get }
}
@@ -0,0 +1,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatInputAutocompletePanel",
module_name = "ChatInputAutocompletePanel",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,44 @@
import Foundation
import UIKit
import TelegramPresentationData
import TelegramUIPreferences
import GlassBackgroundComponent
public final class ChatInputAutocompletePanelEnvironment: Equatable {
public let theme: PresentationTheme
public let strings: PresentationStrings
public let nameDisplayOrder: PresentationPersonNameOrder
public let dateTimeFormat: PresentationDateTimeFormat
public init(
theme: PresentationTheme,
strings: PresentationStrings,
nameDisplayOrder: PresentationPersonNameOrder,
dateTimeFormat: PresentationDateTimeFormat
) {
self.theme = theme
self.strings = strings
self.nameDisplayOrder = nameDisplayOrder
self.dateTimeFormat = dateTimeFormat
}
public static func ==(lhs: ChatInputAutocompletePanelEnvironment, rhs: ChatInputAutocompletePanelEnvironment) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.nameDisplayOrder != rhs.nameDisplayOrder {
return false
}
if lhs.dateTimeFormat != rhs.dateTimeFormat {
return false
}
return true
}
}
public protocol ChatInputAutocompletePanelView: UIView {
var contentTintView: UIView { get }
}
@@ -0,0 +1,25 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatInputContextPanelNode",
module_name = "ChatInputContextPanelNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/AccountContext",
"//submodules/ChatPresentationInterfaceState",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,44 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import ChatPresentationInterfaceState
import ChatControllerInteraction
public enum ChatInputContextPanelPlacement {
case overPanels
case overTextInput
}
open class ChatInputContextPanelNode: ASDisplayNode {
public let context: AccountContext
open var interfaceInteraction: ChatPanelInterfaceInteraction?
open var placement: ChatInputContextPanelPlacement = .overPanels
open var theme: PresentationTheme
open var strings: PresentationStrings
open var fontSize: PresentationFontSize
public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, chatPresentationContext: ChatPresentationContext) {
self.context = context
self.theme = theme
self.strings = strings
self.fontSize = fontSize
super.init()
}
open func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) {
}
open func animateOut(completion: @escaping () -> Void) {
completion()
}
open var topItemFrame: CGRect? {
return nil
}
}
@@ -0,0 +1,32 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatInputMessageAccessoryPanel",
module_name = "ChatInputMessageAccessoryPanel",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/TelegramPresentationData",
"//submodules/ComponentFlow",
"//submodules/AccountContext",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramUI/Components/Chat/ChatInputAccessoryPanel",
"//submodules/Display",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/MultilineTextWithEntitiesComponent",
"//submodules/TelegramStringFormatting",
"//submodules/PhotoResources",
"//submodules/TextFormat",
"//submodules/TelegramUI/Components/CompositeTextNode",
"//submodules/ChatInterfaceState",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,929 @@
import Foundation
import UIKit
import TelegramPresentationData
import ChatInputAccessoryPanel
import AccountContext
import TelegramCore
import SwiftSignalKit
import ComponentFlow
import Display
import GlassBackgroundComponent
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import TelegramStringFormatting
import PhotoResources
import TextFormat
import CompositeTextNode
import ChatInterfaceState
private func generateCloseIcon() -> UIImage {
return generateImage(CGSize(width: 12.0, height: 12.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.copy)
context.setStrokeColor(UIColor.white.cgColor)
context.setLineWidth(2.0)
context.setLineCap(.round)
context.move(to: CGPoint(x: 1.0, y: 1.0))
context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - 1.0))
context.strokePath()
context.move(to: CGPoint(x: size.width - 1.0, y: 1.0))
context.addLine(to: CGPoint(x: 1.0, y: size.height - 1.0))
context.strokePath()
})!.withRenderingMode(.alwaysTemplate)
}
private func textStringForForwardedMessage(_ message: EngineMessage, strings: PresentationStrings) -> (text: String, entities: [MessageTextEntity], isMedia: Bool) {
for media in message.media {
switch media {
case _ as TelegramMediaImage:
return (strings.Message_Photo, [], true)
case let file as TelegramMediaFile:
if file.isVideoSticker || file.isAnimatedSticker {
return (strings.Message_Sticker, [], true)
}
var fileName: String = strings.Message_File
for attribute in file.attributes {
switch attribute {
case .Sticker:
return (strings.Message_Sticker, [], true)
case let .FileName(name):
fileName = name
case let .Audio(isVoice, _, title, performer, _):
if isVoice {
return (strings.Message_Audio, [], true)
} else {
if let title = title, let performer = performer, !title.isEmpty, !performer.isEmpty {
return (title + "" + performer, [], true)
} else if let title = title, !title.isEmpty {
return (title, [], true)
} else if let performer = performer, !performer.isEmpty {
return (performer, [], true)
} else {
return (strings.Message_Audio, [], true)
}
}
case .Video:
if file.isAnimated {
return (strings.Message_Animation, [], true)
} else {
return (strings.Message_Video, [], true)
}
default:
break
}
}
return (fileName, [], true)
case _ as TelegramMediaContact:
return (strings.Message_Contact, [], true)
case let game as TelegramMediaGame:
return (game.title, [], true)
case _ as TelegramMediaMap:
return (strings.Message_Location, [], true)
case _ as TelegramMediaAction:
return ("", [], true)
case _ as TelegramMediaPoll:
return (strings.ForwardedPolls(1), [], true)
case let todo as TelegramMediaTodo:
return (todo.text, [], true)
case let dice as TelegramMediaDice:
return (dice.emoji, [], true)
case let invoice as TelegramMediaInvoice:
return (invoice.title, [], true)
default:
break
}
}
return (message.text, message._asMessage().textEntitiesAttribute?.entities ?? [], false)
}
public final class ChatInputMessageAccessoryPanel: Component {
public typealias EnvironmentType = ChatInputAccessoryPanelEnvironment
public enum Contents: Equatable {
public final class Reply: Equatable {
public let id: EngineMessage.Id
public let quote: EngineMessageReplyQuote?
public let todoItemId: Int32?
public let message: EngineMessage?
public init(id: EngineMessage.Id, quote: EngineMessageReplyQuote?, todoItemId: Int32?, message: EngineMessage?) {
self.id = id
self.quote = quote
self.todoItemId = todoItemId
self.message = message
}
public static func ==(lhs: Reply, rhs: Reply) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.quote != rhs.quote {
return false
}
if lhs.todoItemId != rhs.todoItemId {
return false
}
if lhs.message?.id != rhs.message?.id {
return false
}
if lhs.message?.stableVersion != rhs.message?.stableVersion {
return false
}
return true
}
}
public final class Edit: Equatable {
public let id: EngineMessage.Id
public let message: EngineMessage?
public init(id: EngineMessage.Id, message: EngineMessage?) {
self.id = id
self.message = message
}
public static func ==(lhs: Edit, rhs: Edit) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.message?.id != rhs.message?.id {
return false
}
if lhs.message?.stableVersion != rhs.message?.stableVersion {
return false
}
return true
}
}
public final class Forward: Equatable {
public let messageIds: [EngineMessage.Id]
public let forwardOptionsState: ChatInterfaceForwardOptionsState?
public init(messageIds: [EngineMessage.Id], forwardOptionsState: ChatInterfaceForwardOptionsState?) {
self.messageIds = messageIds
self.forwardOptionsState = forwardOptionsState
}
public static func ==(lhs: Forward, rhs: Forward) -> Bool {
if lhs.messageIds != rhs.messageIds {
return false
}
if lhs.forwardOptionsState != rhs.forwardOptionsState {
return false
}
return true
}
}
public final class LinkPreview: Equatable {
public let url: String
public let webpage: TelegramMediaWebpage
public init(url: String, webpage: TelegramMediaWebpage) {
self.url = url
self.webpage = webpage
}
public static func ==(lhs: LinkPreview, rhs: LinkPreview) -> Bool {
if lhs.url != rhs.url {
return false
}
if lhs.webpage != rhs.webpage {
return false
}
return true
}
}
public final class SuggestPost: Equatable {
public let state: ChatInterfaceState.PostSuggestionState
public init(state: ChatInterfaceState.PostSuggestionState) {
self.state = state
}
public static func ==(lhs: SuggestPost, rhs: SuggestPost) -> Bool {
if lhs.state != rhs.state {
return false
}
return true
}
}
case reply(Reply)
case edit(Edit)
case forward(Forward)
case linkPreview(LinkPreview)
case suggestPost(SuggestPost)
}
let context: AccountContext
let contents: Contents
let chatPeerId: EnginePeer.Id?
let action: ((UIView) -> Void)?
let dismiss: (UIView) -> Void
public init(
context: AccountContext,
contents: Contents,
chatPeerId: EnginePeer.Id?,
action: ((UIView) -> Void)?,
dismiss: @escaping (UIView) -> Void
) {
self.context = context
self.contents = contents
self.chatPeerId = chatPeerId
self.action = action
self.dismiss = dismiss
}
public static func ==(lhs: ChatInputMessageAccessoryPanel, rhs: ChatInputMessageAccessoryPanel) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.contents != rhs.contents {
return false
}
if lhs.chatPeerId != rhs.chatPeerId {
return false
}
if (lhs.action == nil) != (rhs.action == nil) {
return false
}
return true
}
public final class View: UIView, ChatInputAccessoryPanelView {
private let closeButton: HighlightTrackingButton
private let closeButtonIcon: GlassBackgroundView.ContentImageView
private let lineView: UIImageView
private let titleNode: CompositeTextNode
private let text = ComponentView<Empty>()
private let tintText = ComponentView<Empty>()
public let contentTintView: UIView
private var isUpdating: Bool = false
private var component: ChatInputMessageAccessoryPanel?
private weak var state: EmptyComponentState?
private var environment: EnvironmentType?
private var messages: [EngineMessage] = []
private var contentDisposable: Disposable?
private var inlineTextStarImage: UIImage?
private var inlineTextTonImage: (UIImage, UIColor)?
public var transitionData: ChatInputAccessoryPanelTransitionData? {
guard let textView = self.text.view else {
return nil
}
return ChatInputAccessoryPanelTransitionData(
titleView: self.titleNode.view,
textView: textView,
lineView: self.lineView,
imageView: nil
)
}
public var storedFrameBeforeDismissed: CGRect?
override public init(frame: CGRect) {
self.contentTintView = UIView()
self.closeButton = HighlightTrackingButton()
self.closeButtonIcon = GlassBackgroundView.ContentImageView()
self.lineView = UIImageView()
self.titleNode = CompositeTextNode()
super.init(frame: frame)
self.addSubview(self.lineView)
self.addSubview(self.titleNode.view)
self.addSubview(self.closeButtonIcon)
self.contentTintView.addSubview(self.closeButtonIcon.tintMask)
self.addSubview(self.closeButton)
self.closeButton.addTarget(self, action: #selector(self.closeButtonPressed), for: .touchUpInside)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.contentDisposable?.dispose()
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
guard let component = self.component else {
return
}
if case .ended = recognizer.state {
component.action?(self)
}
}
@objc private func closeButtonPressed() {
guard let component = self.component else {
return
}
component.dismiss(self)
}
public func update(component: ChatInputMessageAccessoryPanel, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let environment = environment[EnvironmentType.self].value
let messageIdsFromComponent: (ChatInputMessageAccessoryPanel) -> [EngineMessage.Id] = { component in
let messageIds: [EngineMessage.Id]
switch component.contents {
case let .edit(edit):
messageIds = [edit.id]
case let .reply(reply):
messageIds = [reply.id]
case let .forward(forward):
messageIds = forward.messageIds
case .linkPreview, .suggestPost:
messageIds = []
}
return messageIds
}
let messageIds = messageIdsFromComponent(component)
if self.component == nil || self.component.flatMap(messageIdsFromComponent) != messageIds {
self.contentDisposable?.dispose()
if !messageIds.isEmpty {
self.contentDisposable = (component.context.engine.data.subscribe(
EngineDataList(messageIds.map { id in
return TelegramEngine.EngineData.Item.Messages.Message(id: id)
})
)
|> deliverOnMainQueue).startStrict(next: { [weak self] messages in
guard let self else {
return
}
self.messages = messages.compactMap { $0 }
if !self.isUpdating {
self.state?.updated(transition: .immediate, isLocal: true)
}
})
}
}
self.component = component
self.state = state
self.environment = environment
if self.closeButtonIcon.image == nil {
self.closeButtonIcon.image = generateCloseIcon()
}
if self.lineView.image == nil {
self.lineView.image = generateImage(CGSize(width: 2.0, height: 3.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.white.cgColor)
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 1.0).cgPath)
context.fillPath()
})?.withRenderingMode(.alwaysTemplate).stretchableImage(withLeftCapWidth: 0, topCapHeight: 1)
}
let size = CGSize(width: availableSize.width, height: 52.0)
let containerInsets = UIEdgeInsets(top: 8.0, left: 12.0, bottom: 6.0, right: 0.0)
let lineSize = CGSize(width: 2.0, height: size.height - containerInsets.top - containerInsets.bottom)
let lineFrame = CGRect(origin: CGPoint(x: containerInsets.left, y: containerInsets.top), size: lineSize)
transition.setFrame(view: self.lineView, frame: lineFrame)
self.lineView.tintColor = environment.theme.chat.inputPanel.panelControlAccentColor
let closeButtonSize = CGSize(width: 44.0, height: 44.0)
let closeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - closeButtonSize.width, y: floor((size.height - closeButtonSize.height) * 0.5)), size: closeButtonSize)
transition.setFrame(view: self.closeButton, frame: closeButtonFrame)
if let image = self.closeButtonIcon.image {
let closeButtonIconFrame = image.size.centered(in: closeButtonFrame)
transition.setFrame(view: self.closeButtonIcon, frame: closeButtonIconFrame)
}
self.closeButtonIcon.tintColor = environment.theme.chat.inputPanel.inputControlColor
let secondaryTextColor = environment.theme.chat.inputPanel.inputPlaceholderColor
var textString: NSAttributedString
var isPhoto = false
if self.messages.count == 1, let message = self.messages.first {
var text = ""
let effectiveMessage = message
//TODO:release media
/*if let currentEditMediaReference = self.currentEditMediaReference {
effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media])
}*/
let (attributedText, _, _) = descriptionStringForMessage(
contentSettings: component.context.currentContentSettings.with { $0 },
message: effectiveMessage,
strings: environment.strings,
nameDisplayOrder: environment.nameDisplayOrder,
dateTimeFormat: environment.dateTimeFormat,
accountPeerId: component.context.account.peerId
)
text = attributedText.string
var updatedMediaReference: AnyMediaReference?
var imageDimensions: CGSize?
if !message._asMessage().containsSecretMedia {
var candidateMediaReference: AnyMediaReference?
for media in message.media {
if media is TelegramMediaImage || media is TelegramMediaFile {
candidateMediaReference = .message(message: MessageReference(message._asMessage()), media: media)
break
}
}
if let imageReference = candidateMediaReference?.concrete(TelegramMediaImage.self) {
updatedMediaReference = imageReference.abstract
if let representation = largestRepresentationForPhoto(imageReference.media) {
imageDimensions = representation.dimensions.cgSize
}
} else if let fileReference = candidateMediaReference?.concrete(TelegramMediaFile.self) {
updatedMediaReference = fileReference.abstract
if !fileReference.media.isInstantVideo, let representation = largestImageRepresentation(fileReference.media.previewRepresentations), !fileReference.media.isSticker {
imageDimensions = representation.dimensions.cgSize
}
}
}
/*let imageNodeLayout = self.imageNode.asyncLayout()
var applyImage: (() -> Void)?
if let imageDimensions = imageDimensions {
let boundingSize = CGSize(width: 35.0, height: 35.0)
applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: 2.0), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()))
}
var mediaUpdated = false
if let updatedMediaReference = updatedMediaReference, let previousMediaReference = self.previousMediaReference {
mediaUpdated = !updatedMediaReference.media.isEqual(to: previousMediaReference.media)
} else if (updatedMediaReference != nil) != (self.previousMediaReference != nil) {
mediaUpdated = true
}
self.previousMediaReference = updatedMediaReference*/
let hasSpoiler = message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute })
var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
let _ = updateImageSignal
if let updatedMediaReference = updatedMediaReference, imageDimensions != nil {
if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) {
updateImageSignal = chatMessagePhotoThumbnail(account: component.context.account, userLocation: MediaResourceUserLocation.peer(message.id.peerId), photoReference: imageReference, blurred: hasSpoiler)
isPhoto = true
} else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) {
if fileReference.media.isVideo {
updateImageSignal = chatMessageVideoThumbnail(account: component.context.account, userLocation: MediaResourceUserLocation.peer(message.id.peerId), fileReference: fileReference, blurred: hasSpoiler)
} else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) {
updateImageSignal = chatWebpageSnippetFile(account: component.context.account, userLocation: MediaResourceUserLocation.peer(message.id.peerId), mediaReference: fileReference.abstract, representation: iconImageRepresentation)
}
}
} else {
updateImageSignal = .single({ _ in return nil })
}
let isMedia: Bool
let isText: Bool
/*if let currentEditMediaReference = self.currentEditMediaReference {
effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media])
}*/
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
switch messageContentKind(contentSettings: component.context.currentContentSettings.with { $0 }, message: effectiveMessage, strings: environment.strings, nameDisplayOrder: environment.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: component.context.account.peerId) {
case .text:
isMedia = false
isText = true
default:
isMedia = effectiveMessage.text.isEmpty
isText = false
}
let textFont = Font.regular(14.0)
let messageText: NSAttributedString
if isText {
let entities = (message._asMessage().textEntitiesAttribute?.entities ?? []).filter { entity in
switch entity.type {
case .Spoiler, .CustomEmoji:
return true
default:
return false
}
}
let textColor = environment.theme.chat.inputPanel.primaryTextColor
if entities.count > 0 {
messageText = stringWithAppliedEntities(trimToLineCount(message.text, lineCount: 1), entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message._asMessage())
} else {
messageText = NSAttributedString(string: text, font: textFont, textColor: isMedia ? secondaryTextColor : environment.theme.chat.inputPanel.primaryTextColor)
}
} else {
messageText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: isMedia ? secondaryTextColor : environment.theme.chat.inputPanel.primaryTextColor)
}
textString = messageText
} else {
textString = NSAttributedString()
}
var titleText: [CompositeTextNode.Component] = []
switch component.contents {
case .edit:
let canEditMedia: Bool
//TODO:release
/*if let message = self.message, !messageMediaEditingOptions(message: message).isEmpty {
canEditMedia = true
} else {
canEditMedia = false
}*/
canEditMedia = !"".isEmpty
let titleStringValue: String
if let message = self.messages.first, message.id.namespace == Namespaces.Message.QuickReplyCloud {
titleStringValue = environment.strings.Conversation_EditingQuickReplyPanelTitle
} else if canEditMedia {
titleStringValue = isPhoto ? environment.strings.Conversation_EditingPhotoPanelTitle : environment.strings.Conversation_EditingCaptionPanelTitle
} else {
titleStringValue = environment.strings.Conversation_EditingMessagePanelTitle
}
titleText = [.text(NSAttributedString(string: titleStringValue, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))]
case let .reply(reply):
if let peer = self.messages.first?.peers[reply.id.peerId] as? TelegramChannel, case .broadcast = peer.info {
let icon: UIImage?
icon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/PanelTextChannelIcon"), color: environment.theme.chat.inputPanel.panelControlAccentColor)
if let icon {
let rawString: PresentationStrings.FormattedString
if reply.quote != nil {
rawString = environment.strings.Chat_ReplyPanel_ReplyToQuoteBy(peer.debugDisplayTitle)
} else {
rawString = environment.strings.Chat_ReplyPanel_ReplyTo(peer.debugDisplayTitle)
}
if let nameRange = rawString.ranges.first {
titleText = []
let rawNsString = rawString.string as NSString
if nameRange.range.lowerBound != 0 {
titleText.append(.text(NSAttributedString(string: rawNsString.substring(with: NSRange(location: 0, length: nameRange.range.lowerBound)), font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor)))
}
titleText.append(.icon(icon))
titleText.append(.text(NSAttributedString(string: peer.debugDisplayTitle, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor)))
if nameRange.range.upperBound != rawNsString.length {
titleText.append(.text(NSAttributedString(string: rawNsString.substring(with: NSRange(location: nameRange.range.upperBound, length: rawNsString.length - nameRange.range.upperBound)), font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor)))
}
} else {
titleText.append(.text(NSAttributedString(string: rawString.string, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor)))
}
}
} else {
var authorName = ""
if let forwardInfo = self.messages.first?._asMessage().forwardInfo, forwardInfo.flags.contains(.isImported) {
if let author = forwardInfo.author {
authorName = EnginePeer(author).displayTitle(strings: environment.strings, displayOrder: environment.nameDisplayOrder)
} else if let authorSignature = forwardInfo.authorSignature {
authorName = authorSignature
}
} else if let author = self.messages.first?._asMessage().effectiveAuthor {
authorName = EnginePeer(author).displayTitle(strings: environment.strings, displayOrder: environment.nameDisplayOrder)
}
if let _ = reply.todoItemId {
let string = environment.strings.Chat_ReplyPanel_ReplyToTodoItem
titleText = [.text(NSAttributedString(string: string, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))]
} else if let _ = reply.quote {
let string = environment.strings.Chat_ReplyPanel_ReplyToQuoteBy(authorName).string
titleText = [.text(NSAttributedString(string: string, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))]
} else {
let string = environment.strings.Conversation_ReplyMessagePanelTitle(authorName).string
titleText = [.text(NSAttributedString(string: string, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))]
}
if reply.id.peerId != component.chatPeerId {
if let peer = self.messages.first?.peers[reply.id.peerId], (peer is TelegramChannel || peer is TelegramGroup) {
let icon: UIImage?
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
icon = UIImage(bundleImageName: "Chat/Input/Accessory Panels/PanelTextChannelIcon")
} else {
icon = UIImage(bundleImageName: "Chat/Input/Accessory Panels/PanelTextGroupIcon")
}
if let iconImage = generateTintedImage(image: icon, color: environment.theme.chat.inputPanel.panelControlAccentColor) {
titleText.append(.icon(iconImage))
titleText.append(.text(NSAttributedString(string: peer.debugDisplayTitle, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor)))
}
}
}
if let message = self.messages.first {
let textFont = Font.regular(14.0)
if let quote = reply.quote {
let textColor = environment.theme.chat.inputPanel.primaryTextColor
textString = stringWithAppliedEntities(trimToLineCount(quote.text, lineCount: 1), entities: quote.entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message._asMessage())
} else if let todoItemId = reply.todoItemId, let todo = message.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo, let todoItem = todo.items.first(where: { $0.id == todoItemId }) {
let textColor = environment.theme.chat.inputPanel.primaryTextColor
textString = stringWithAppliedEntities(trimToLineCount(todoItem.text, lineCount: 1), entities: todoItem.entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message._asMessage())
}
}
}
case let .forward(forward):
var title = ""
var authors = ""
var uniquePeerIds = Set<EnginePeer.Id>()
var text = NSMutableAttributedString(string: "")
for message in self.messages {
if let author = message.forwardInfo?.author ?? message._asMessage().effectiveAuthor, !uniquePeerIds.contains(author.id) {
uniquePeerIds.insert(author.id)
if !authors.isEmpty {
authors.append(", ")
}
if author.id == component.context.account.peerId {
authors.append(environment.strings.DialogList_You)
} else {
authors.append(EnginePeer(author).compactDisplayTitle)
}
}
}
if self.messages.count == 1 {
title = environment.strings.Conversation_ForwardOptions_ForwardTitleSingle
let (string, entities, _) = textStringForForwardedMessage(messages[0], strings: environment.strings)
text = NSMutableAttributedString(attributedString: NSAttributedString(string: "\(authors): ", font: Font.regular(14.0), textColor: secondaryTextColor))
let additionalText = NSMutableAttributedString(attributedString: NSAttributedString(string: string, font: Font.regular(14.0), textColor: secondaryTextColor))
for entity in entities {
switch entity.type {
case let .CustomEmoji(_, fileId):
let range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
if range.lowerBound >= 0 && range.upperBound <= additionalText.length {
additionalText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: messages[0].associatedMedia[EngineMedia.Id(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile), range: range)
}
default:
break
}
}
text.append(additionalText)
} else {
title = environment.strings.Conversation_ForwardOptions_ForwardTitle(Int32(messages.count))
text = NSMutableAttributedString(attributedString: NSAttributedString(string: environment.strings.Conversation_ForwardFrom(authors).string, font: Font.regular(14.0), textColor: secondaryTextColor))
}
if forward.forwardOptionsState?.hideNames == true {
text = NSMutableAttributedString(attributedString: NSAttributedString(string: environment.strings.Conversation_ForwardOptions_SenderNamesRemoved, font: Font.regular(14.0), textColor: secondaryTextColor))
}
titleText = [.text(NSAttributedString(string: title, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))]
textString = text
case let .linkPreview(linkPreview):
var authorName = ""
var text = ""
switch linkPreview.webpage.content {
case .Pending:
authorName = environment.strings.Channel_NotificationLoading
text = linkPreview.url
case let .Loaded(content):
if let contentText = content.text {
text = contentText
} else {
if let file = content.file, let mediaKind = mediaContentKind(EngineMedia(file)) {
if content.type == "telegram_background" {
text = environment.strings.Message_Wallpaper
} else if content.type == "telegram_theme" {
text = environment.strings.Message_Theme
} else {
text = stringForMediaKind(mediaKind, strings: environment.strings).0.string
}
} else if content.type == "telegram_theme" {
text = environment.strings.Message_Theme
} else if content.type == "video" {
text = stringForMediaKind(.video, strings: environment.strings).0.string
} else if content.type == "telegram_story" {
text = stringForMediaKind(.story, strings: environment.strings).0.string
} else if let _ = content.image {
text = stringForMediaKind(.image, strings: environment.strings).0.string
}
}
if let title = content.title {
authorName = title
} else if let websiteName = content.websiteName {
authorName = websiteName
} else {
authorName = content.displayUrl
}
}
titleText = [.text(NSAttributedString(string: authorName, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor))]
textString = NSAttributedString(string: text, font: Font.regular(14.0), textColor: environment.theme.chat.inputPanel.primaryTextColor)
case let .suggestPost(suggestPost):
if suggestPost.state.editingOriginalMessageId != nil {
titleText.append(.text(NSAttributedString(string: environment.strings.Chat_PostSuggestion_Suggest_InputEditTitle, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor)))
} else {
titleText.append(.text(NSAttributedString(string: environment.strings.Chat_PostSuggestion_Suggest_InputTitle, font: Font.medium(14.0), textColor: environment.theme.chat.inputPanel.panelControlAccentColor)))
}
let textFont = Font.regular(14.0)
if let price = suggestPost.state.price, price.amount != .zero {
let currencySymbol: String
let amountString: String
switch price.currency {
case .stars:
currencySymbol = "#"
amountString = "\(price.amount)"
case .ton:
currencySymbol = "$"
amountString = formatTonAmountText(price.amount.value, dateTimeFormat: environment.dateTimeFormat)
}
if let timestamp = suggestPost.state.timestamp {
let timeString = humanReadableStringForTimestamp(strings: environment.strings, dateTimeFormat: environment.dateTimeFormat, timestamp: timestamp, alwaysShowTime: true, allowYesterday: false, format: HumanReadableStringFormat(
dateFormatString: { value in
return PresentationStrings.FormattedString(string: environment.strings.SuggestPost_SetTimeFormat_Date(value).string, ranges: [])
},
tomorrowFormatString: { value in
return PresentationStrings.FormattedString(string: environment.strings.SuggestPost_SetTimeFormat_TomorrowAt(value).string, ranges: [])
},
todayFormatString: { value in
return PresentationStrings.FormattedString(string: environment.strings.SuggestPost_SetTimeFormat_TodayAt(value).string, ranges: [])
},
yesterdayFormatString: { value in
return PresentationStrings.FormattedString(string: environment.strings.SuggestPost_SetTimeFormat_TodayAt(value).string, ranges: [])
}
)).string
textString = NSAttributedString(string: "\(currencySymbol)\(amountString) 📅 \(timeString)", font: textFont, textColor: environment.theme.chat.inputPanel.primaryTextColor)
} else {
textString = NSAttributedString(string: environment.strings.Chat_PostSuggestion_Suggest_InputSubtitleAnytime("\(currencySymbol)\(amountString)").string, font: textFont, textColor: environment.theme.chat.inputPanel.primaryTextColor)
}
} else {
textString = NSAttributedString(string: environment.strings.Chat_PostSuggestion_Suggest_InputSubtitleEmpty, font: textFont, textColor: environment.theme.chat.inputPanel.primaryTextColor)
}
let mutableTextString = NSMutableAttributedString(attributedString: textString)
for currency in [.stars, .ton] as [CurrencyAmount.Currency] {
var inlineTextStarImage: UIImage?
if let current = self.inlineTextStarImage {
inlineTextStarImage = current
} else {
if let image = UIImage(bundleImageName: "Premium/Stars/StarSmall") {
let starInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
inlineTextStarImage = generateImage(CGSize(width: starInsets.left + image.size.width + starInsets.right, height: image.size.height), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
defer {
UIGraphicsPopContext()
}
image.draw(at: CGPoint(x: starInsets.left, y: starInsets.top))
})?.withRenderingMode(.alwaysOriginal)
self.inlineTextStarImage = inlineTextStarImage
}
}
var inlineTextTonImage: UIImage?
if let current = self.inlineTextTonImage, current.1 == environment.theme.list.itemAccentColor {
inlineTextTonImage = current.0
} else {
if let image = UIImage(bundleImageName: "Ads/TonMedium") {
let tonInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
let inlineTextTonImageValue = generateTintedImage(image: generateImage(CGSize(width: tonInsets.left + image.size.width + tonInsets.right, height: image.size.height), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
defer {
UIGraphicsPopContext()
}
image.draw(at: CGPoint(x: tonInsets.left, y: tonInsets.top))
}), color: environment.theme.list.itemAccentColor)!.withRenderingMode(.alwaysOriginal)
inlineTextTonImage = inlineTextTonImageValue
self.inlineTextTonImage = (inlineTextTonImageValue, environment.theme.list.itemAccentColor)
}
}
let currencySymbol: String
let currencyImage: UIImage?
switch currency {
case .stars:
currencySymbol = "#"
currencyImage = inlineTextStarImage
case .ton:
currencySymbol = "$"
currencyImage = inlineTextTonImage
}
if let range = mutableTextString.string.range(of: currencySymbol), let currencyImage {
final class RunDelegateData {
let ascent: CGFloat
let descent: CGFloat
let width: CGFloat
init(ascent: CGFloat, descent: CGFloat, width: CGFloat) {
self.ascent = ascent
self.descent = descent
self.width = width
}
}
let runDelegateData = RunDelegateData(
ascent: Font.regular(14.0).ascender,
descent: Font.regular(14.0).descender,
width: currencyImage.size.width + 2.0
)
var callbacks = CTRunDelegateCallbacks(
version: kCTRunDelegateCurrentVersion,
dealloc: { dataRef in
Unmanaged<RunDelegateData>.fromOpaque(dataRef).release()
},
getAscent: { dataRef in
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
return data.takeUnretainedValue().ascent
},
getDescent: { dataRef in
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
return data.takeUnretainedValue().descent
},
getWidth: { dataRef in
let data = Unmanaged<RunDelegateData>.fromOpaque(dataRef)
return data.takeUnretainedValue().width
}
)
if let runDelegate = CTRunDelegateCreate(&callbacks, Unmanaged.passRetained(runDelegateData).toOpaque()) {
mutableTextString.addAttribute(NSAttributedString.Key(kCTRunDelegateAttributeName as String), value: runDelegate, range: NSRange(range, in: mutableTextString.string))
}
mutableTextString.addAttribute(.attachment, value: currencyImage, range: NSRange(range, in: mutableTextString.string))
mutableTextString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff), range: NSRange(range, in: mutableTextString.string))
mutableTextString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: mutableTextString.string))
}
textString = mutableTextString
}
}
let textInsets = UIEdgeInsets(top: 10.0, left: 8.0, bottom: 0.0, right: 44.0)
self.titleNode.components = titleText
let titleSize = self.titleNode.update(constrainedSize: CGSize(width: availableSize.width - lineFrame.maxX - textInsets.left - textInsets.right, height: 100.0))
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(textString),
)),
environment: {},
containerSize: CGSize(width: availableSize.width - lineFrame.maxX - textInsets.left - textInsets.right, height: 100.0)
)
let tintTextString = NSMutableAttributedString(attributedString: textString)
tintTextString.addAttribute(.foregroundColor, value: UIColor.black, range: NSRange(location: 0, length: tintTextString.length))
let _ = self.tintText.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(tintTextString),
)),
environment: {},
containerSize: CGSize(width: availableSize.width - lineFrame.maxX - textInsets.left - textInsets.right, height: 100.0)
)
let titleTextSpacing: CGFloat = 1.0
let titleFrame = CGRect(origin: CGPoint(x: lineFrame.maxX + textInsets.left, y: textInsets.top), size: titleSize)
let textFrame = CGRect(origin: CGPoint(x: lineFrame.maxX + textInsets.left, y: titleFrame.maxY + titleTextSpacing), size: textSize)
transition.setFrame(view: self.titleNode.view, frame: titleFrame)
if let textView = self.text.view, let tintTextView = self.tintText.view {
if textView.superview == nil {
textView.layer.anchorPoint = CGPoint()
self.addSubview(textView)
tintTextView.layer.anchorPoint = CGPoint()
self.contentTintView.addSubview(tintTextView)
}
transition.setPosition(view: textView, position: textFrame.origin)
textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
transition.setPosition(view: tintTextView, position: textFrame.origin)
tintTextView.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
}
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,24 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatInputPanelNode",
module_name = "ChatInputPanelNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/AccountContext",
"//submodules/ChatPresentationInterfaceState",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,45 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import AccountContext
import ChatPresentationInterfaceState
import ChatControllerInteraction
public protocol ChatInputPanelViewForOverlayContent: UIView {
func maybeDismissContent(point: CGPoint)
}
open class ChatInputPanelNode: ASDisplayNode {
open var context: AccountContext?
open var chatControllerInteraction: ChatControllerInteraction?
open var interfaceInteraction: ChatPanelInterfaceInteraction?
open var prevInputPanelNode: ChatInputPanelNode?
open var viewForOverlayContent: ChatInputPanelViewForOverlayContent?
open func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) {
}
open func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat {
return 0.0
}
open func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat {
return 0.0
}
open func defaultHeight(metrics: LayoutMetrics) -> CGFloat {
if case .regular = metrics.widthClass, case .regular = metrics.heightClass {
return 40.0
} else {
return 40.0
}
}
open func canHandleTransition(from prevInputPanelNode: ChatInputPanelNode?) -> Bool {
return false
}
}
@@ -0,0 +1,24 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatInputTextNode",
module_name = "ChatInputTextNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/AppBundle",
"//submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl",
"//submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView",
"//submodules/AccountContext",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,23 @@
objc_library(
name = "ChatInputTextViewImpl",
enable_modules = True,
module_name = "ChatInputTextViewImpl",
srcs = glob([
"Sources/**/*.m",
"Sources/**/*.c",
"Sources/**/*.h",
], allow_empty=True),
hdrs = glob([
"PublicHeaders/**/*.h",
]),
includes = [
"PublicHeaders",
],
sdk_frameworks = [
"Foundation",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,29 @@
#ifndef ChatInputTextViewImpl_h
#define ChatInputTextViewImpl_h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface ChatInputTextViewImplTargetForAction: NSObject
@property (nonatomic, strong, readonly) id _Nullable target;
- (instancetype _Nonnull)initWithTarget:(id _Nullable)target;
@end
@interface ChatInputTextViewImpl : UITextView
@property (nonatomic, copy) bool (^ _Nullable shouldCopy)();
@property (nonatomic, copy) bool (^ _Nullable shouldPaste)();
@property (nonatomic, copy) bool (^ _Nullable shouldRespondToAction)(SEL _Nullable);
@property (nonatomic, copy) ChatInputTextViewImplTargetForAction * _Nullable (^ _Nullable targetForAction)(SEL _Nullable);
@property (nonatomic, copy) bool (^ _Nullable shouldReturn)();
@property (nonatomic, copy) void (^ _Nullable backspaceWhileEmpty)();
@property (nonatomic, copy) void (^ _Nullable dropAutocorrectioniOS16)();
- (instancetype _Nonnull)initWithFrame:(CGRect)frame textContainer:(NSTextContainer * _Nullable)textContainer disableTiling:(bool)disableTiling;
@end
#endif /* Lottie_h */
@@ -0,0 +1,161 @@
#import <ChatInputTextViewImpl/ChatInputTextViewImpl.h>
@implementation ChatInputTextViewImplTargetForAction
- (instancetype)initWithTarget:(id _Nullable)target {
self = [super init];
if (self != nil) {
_target = target;
}
return self;
}
@end
@interface ChatInputTextViewImpl () <UIGestureRecognizerDelegate> {
UIGestureRecognizer *_tapRecognizer;
}
@end
@implementation ChatInputTextViewImpl
- (instancetype _Nonnull)initWithFrame:(CGRect)frame textContainer:(NSTextContainer * _Nullable)textContainer disableTiling:(bool)disableTiling {
self = [super initWithFrame:frame textContainer:textContainer];
if (self != nil) {
if (disableTiling) {
SEL selector = NSSelectorFromString(@"_disableTiledViews");
if (selector && [self respondsToSelector:selector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:selector];
#pragma clang diagnostic pop
}
}
if (@available(iOS 17.0, *)) {
} else {
_tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(workaroundTapGesture:)];
_tapRecognizer.cancelsTouchesInView = false;
_tapRecognizer.delaysTouchesBegan = false;
_tapRecognizer.delaysTouchesEnded = false;
_tapRecognizer.delegate = self;
[self addGestureRecognizer:_tapRecognizer];
}
}
return self;
}
- (BOOL)touchesShouldCancelInContentView:(UIView *)view {
return false;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return true;
}
- (void)workaroundTapGesture:(UITapGestureRecognizer *)recognizer {
if (recognizer.state == UIGestureRecognizerStateEnded) {
static Class promptClass = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
promptClass = NSClassFromString([[NSString alloc] initWithFormat:@"%@AutocorrectInlinePrompt", @"UI"]);
});
UIView *result = [self hitTest:[recognizer locationInView:self] withEvent:nil];
if (result != nil && [result class] == promptClass) {
if (_dropAutocorrectioniOS16) {
_dropAutocorrectioniOS16();
}
}
}
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
if (_targetForAction) {
ChatInputTextViewImplTargetForAction *result = _targetForAction(action);
if (result) {
return result.target != nil;
}
}
if (_shouldRespondToAction) {
if (!_shouldRespondToAction(action)) {
return false;
}
}
if (action == @selector(paste:)) {
NSArray *items = [UIMenuController sharedMenuController].menuItems;
if (((UIMenuItem *)items.firstObject).action == @selector(toggleBoldface:)) {
return false;
}
return true;
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
static SEL promptForReplaceSelector;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
promptForReplaceSelector = NSSelectorFromString(@"_promptForReplace:");
});
if (action == promptForReplaceSelector) {
return false;
}
#pragma clang diagnostic pop
if (action == @selector(toggleUnderline:)) {
return false;
}
return [super canPerformAction:action withSender:sender];
}
- (id)targetForAction:(SEL)action withSender:(id)__unused sender {
if (_targetForAction) {
ChatInputTextViewImplTargetForAction *result = _targetForAction(action);
if (result) {
return result.target;
}
}
return [super targetForAction:action withSender:sender];
}
- (void)copy:(id)sender {
if (_shouldCopy == nil || _shouldCopy()) {
[super copy:sender];
}
}
- (void)paste:(id)sender {
if (_shouldPaste == nil || _shouldPaste()) {
[super paste:sender];
}
}
- (NSArray *)keyCommands {
UIKeyCommand *plainReturn = [UIKeyCommand keyCommandWithInput:@"\r" modifierFlags:kNilOptions action:@selector(handlePlainReturn:)];
return @[
plainReturn
];
}
- (void)handlePlainReturn:(id)__unused sender {
if (_shouldReturn) {
_shouldReturn();
}
}
- (void)deleteBackward {
bool notify = self.text.length == 0;
[super deleteBackward];
if (notify) {
if (_backspaceWhileEmpty) {
_backspaceWhileEmpty();
}
}
}
@end
@@ -0,0 +1,22 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatInstantVideoMessageDurationNode",
module_name = "ChatInstantVideoMessageDurationNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Display",
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//submodules/AnimatedCountLabelNode",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,221 @@
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
import Display
import UniversalMediaPlayer
import AnimatedCountLabelNode
private let textFont = Font.with(size: 11.0, design: .regular, weight: .regular, traits: [.monospacedNumbers])
private struct ChatInstantVideoMessageDurationNodeState: Equatable {
let hours: Int32?
let minutes: Int32?
let seconds: Int32?
init() {
self.hours = nil
self.minutes = nil
self.seconds = nil
}
init(hours: Int32, minutes: Int32, seconds: Int32) {
self.hours = hours
self.minutes = minutes
self.seconds = seconds
}
static func ==(lhs: ChatInstantVideoMessageDurationNodeState, rhs: ChatInstantVideoMessageDurationNodeState) -> Bool {
if lhs.hours != rhs.hours || lhs.minutes != rhs.minutes || lhs.seconds != rhs.seconds {
return false
}
return true
}
}
private final class ChatInstantVideoMessageDurationNodeParameters: NSObject {
let state: ChatInstantVideoMessageDurationNodeState
let isSeen: Bool
let textColor: UIColor
init(state: ChatInstantVideoMessageDurationNodeState, isSeen: Bool, textColor: UIColor) {
self.state = state
self.isSeen = isSeen
self.textColor = textColor
super.init()
}
}
public final class ChatInstantVideoMessageDurationNode: ASImageNode {
private var textColor: UIColor
public var defaultDuration: Double? {
didSet {
if self.defaultDuration != oldValue {
self.updateTimestamp()
self.updateContents()
}
}
}
public var isSeen: Bool = false {
didSet {
if self.isSeen != oldValue {
self.updateContents()
}
}
}
private var updateTimer: SwiftSignalKit.Timer?
private var statusValue: MediaPlayerStatus? {
didSet {
if self.statusValue != oldValue {
if let statusValue = statusValue, case .playing = statusValue.status {
self.ensureHasTimer()
} else {
self.stopTimer()
}
self.updateTimestamp()
}
}
}
private var state = ChatInstantVideoMessageDurationNodeState() {
didSet {
if self.state != oldValue {
self.updateContents()
}
}
}
private var statusDisposable: Disposable?
private var statusValuePromise = Promise<MediaPlayerStatus?>()
public var status: Signal<MediaPlayerStatus?, NoError>? {
didSet {
if let status = self.status {
self.statusValuePromise.set(status)
} else {
self.statusValuePromise.set(.never())
}
}
}
public var size: CGSize = CGSize()
public var sizeUpdated: ((CGSize) -> Void)?
public init(textColor: UIColor) {
self.textColor = textColor
super.init()
self.isOpaque = false
self.contentsScale = UIScreenScale
self.contentMode = .topRight
self.statusDisposable = (self.statusValuePromise.get()
|> deliverOnMainQueue).startStrict(next: { [weak self] status in
if let strongSelf = self {
strongSelf.statusValue = status
}
})
}
deinit {
self.statusDisposable?.dispose()
self.updateTimer?.invalidate()
}
public func updateTheme(textColor: UIColor) {
if !self.textColor.isEqual(textColor) {
self.textColor = textColor
self.updateContents()
}
}
private func ensureHasTimer() {
if self.updateTimer == nil {
let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
self?.updateTimestamp()
}, queue: Queue.mainQueue())
self.updateTimer = timer
timer.start()
}
}
private func stopTimer() {
self.updateTimer?.invalidate()
self.updateTimer = nil
}
public func updateTimestamp() {
if let statusValue = self.statusValue, Double(0.0).isLess(than: statusValue.duration) {
let timestampSeconds: Double
if !statusValue.generationTimestamp.isZero {
timestampSeconds = statusValue.timestamp + (CACurrentMediaTime() - statusValue.generationTimestamp)
} else {
timestampSeconds = statusValue.timestamp
}
let timestamp = Int32(timestampSeconds)
self.state = ChatInstantVideoMessageDurationNodeState(hours: timestamp / (60 * 60), minutes: timestamp % (60 * 60) / 60, seconds: timestamp % 60)
} else if let defaultDuration = self.defaultDuration {
let timestamp = Int32(defaultDuration)
self.state = ChatInstantVideoMessageDurationNodeState(hours: timestamp / (60 * 60), minutes: timestamp % (60 * 60) / 60, seconds: timestamp % 60)
} else {
self.state = ChatInstantVideoMessageDurationNodeState()
}
}
private func updateContents() {
let image = self.generateContents(withParameters: self.getParameters(), isCancelled: { return false })
let previousSize = self.image?.size
self.image = image
if let image = image, previousSize != image.size {
self.size = image.size
self.sizeUpdated?(image.size)
}
}
private func getParameters() -> NSObjectProtocol? {
return ChatInstantVideoMessageDurationNodeParameters(state: self.state, isSeen: self.isSeen, textColor: self.textColor)
}
private func generateContents(withParameters: Any?, isCancelled: () -> Bool) -> UIImage? {
guard let parameters = withParameters as? ChatInstantVideoMessageDurationNodeParameters else {
return nil
}
let text: String
if let hours = parameters.state.hours, let minutes = parameters.state.minutes, let seconds = parameters.state.seconds {
if hours != 0 {
text = String(format: "%d:%02d:%02d", hours, minutes, seconds)
} else {
text = String(format: "%d:%02d", minutes, seconds)
}
} else {
text = "-:--"
}
let string = NSAttributedString(string: text, font: textFont, textColor: parameters.textColor)
let textRect = string.boundingRect(with: CGSize(width: 200.0, height: 100.0), options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil)
let unseenInset: CGFloat = (parameters.isSeen ? 0.0 : 10.0)
let imageSize = CGSize(width: ceil(textRect.width) + 10.0 + unseenInset, height: 18.0)
return generateImage(imageSize, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
context.setBlendMode(.normal)
if !parameters.isSeen {
context.setFillColor(parameters.textColor.cgColor)
let diameter: CGFloat = 4.0
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - size.height + floor((size.height - diameter) / 2.0), y: floor((size.height - diameter) / 2.0)), size: CGSize(width: diameter, height: diameter)))
}
UIGraphicsPushContext(context)
string.draw(at: CGPoint(x: floor((size.width - unseenInset - textRect.size.width) / 2.0) + textRect.origin.x, y: 2.0 + textRect.origin.y + UIScreenPixel))
UIGraphicsPopContext()
})
}
}
@@ -0,0 +1,34 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatLoadingNode",
module_name = "ChatLoadingNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Display",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/ActivityIndicator",
"//submodules/WallpaperBackgroundNode",
"//submodules/ShimmerEffect",
"//submodules/ChatPresentationInterfaceState",
"//submodules/AccountContext",
"//submodules/TelegramUI/Components/Chat/ChatMessageItem",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemView",
"//submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemImpl",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,543 @@
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
import Display
import Postbox
import TelegramCore
import TelegramPresentationData
import ActivityIndicator
import WallpaperBackgroundNode
import ShimmerEffect
import ChatPresentationInterfaceState
import AccountContext
import ChatMessageItem
import ChatMessageItemView
import ChatMessageStickerItemNode
import ChatMessageInstantVideoItemNode
import ChatMessageAnimatedStickerItemNode
import ChatMessageBubbleItemNode
import ChatMessageItemImpl
public final class ChatLoadingNode: ASDisplayNode {
private let backgroundNode: NavigationBackgroundNode
private let activityIndicator: ActivityIndicator
private let offset: CGPoint
public init(context: AccountContext, theme: PresentationTheme, chatWallpaper: TelegramWallpaper, bubbleCorners: PresentationChatBubbleCorners) {
self.backgroundNode = NavigationBackgroundNode(color: selectDateFillStaticColor(theme: theme, wallpaper: chatWallpaper), enableBlur: context.sharedContext.energyUsageSettings.fullTranslucency && dateFillNeedsBlur(theme: theme, wallpaper: chatWallpaper))
let serviceColor = serviceMessageColorComponents(theme: theme, wallpaper: chatWallpaper)
self.activityIndicator = ActivityIndicator(type: .custom(serviceColor.primaryText, 22.0, 2.0, false), speed: .regular)
if serviceColor.primaryText != .white {
self.offset = CGPoint(x: 0.5, y: 0.5)
} else {
self.offset = CGPoint()
}
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.activityIndicator)
}
public func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) {
let displayRect = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top - insets.bottom))
let backgroundSize: CGFloat = 30.0
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: displayRect.minX + floor((displayRect.width - backgroundSize) / 2.0), y: displayRect.minY + floor((displayRect.height - backgroundSize) / 2.0)), size: CGSize(width: backgroundSize, height: backgroundSize)))
self.backgroundNode.update(size: self.backgroundNode.bounds.size, cornerRadius: self.backgroundNode.bounds.height / 2.0, transition: transition)
let activitySize = self.activityIndicator.measure(size)
transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: displayRect.minX + floor((displayRect.width - activitySize.width) / 2.0) + self.offset.x, y: displayRect.minY + floor((displayRect.height - activitySize.height) / 2.0) + self.offset.y), size: activitySize))
}
public var progressFrame: CGRect {
return self.backgroundNode.frame
}
}
private let avatarSize = CGSize(width: 38.0, height: 38.0)
private let avatarImage = generateFilledCircleImage(diameter: avatarSize.width, color: .white)
private let avatarBorderImage = generateCircleImage(diameter: avatarSize.width, lineWidth: 1.0 - UIScreenPixel, color: .white)
public final class ChatLoadingPlaceholderMessageContainer {
public var avatarNode: ASImageNode?
public var avatarBorderNode: ASImageNode?
public let bubbleNode: ASImageNode
public let bubbleBorderNode: ASImageNode
public var parentView: UIView? {
return self.bubbleNode.supernode?.view
}
public var frame: CGRect {
return self.bubbleNode.frame
}
public init(bubbleImage: UIImage?, bubbleBorderImage: UIImage?) {
self.bubbleNode = ASImageNode()
self.bubbleNode.displaysAsynchronously = false
self.bubbleNode.image = bubbleImage
self.bubbleBorderNode = ASImageNode()
self.bubbleBorderNode.displaysAsynchronously = false
self.bubbleBorderNode.image = bubbleBorderImage
}
public func setup(maskNode: ASDisplayNode, borderMaskNode: ASDisplayNode) {
maskNode.addSubnode(self.bubbleNode)
borderMaskNode.addSubnode(self.bubbleBorderNode)
}
public func animateWith(_ listItemNode: ListViewItemNode, delay: Double, transition: ContainedViewLayoutTransition) {
listItemNode.allowsGroupOpacity = true
listItemNode.alpha = 1.0
listItemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: delay, completion: { _ in
listItemNode.allowsGroupOpacity = false
})
if let bubbleItemNode = listItemNode as? ChatMessageBubbleItemNode {
bubbleItemNode.animateFromLoadingPlaceholder(delay: delay, transition: transition)
} else if let stickerItemNode = listItemNode as? ChatMessageStickerItemNode {
stickerItemNode.animateFromLoadingPlaceholder(delay: delay, transition: transition)
} else if let stickerItemNode = listItemNode as? ChatMessageAnimatedStickerItemNode {
stickerItemNode.animateFromLoadingPlaceholder(delay: delay, transition: transition)
} else if let videoItemNode = listItemNode as? ChatMessageInstantVideoItemNode {
videoItemNode.animateFromLoadingPlaceholder(delay: delay, transition: transition)
}
}
public func update(size: CGSize, isSidebarOpen: Bool, hasAvatar: Bool, rect: CGRect, transition: ContainedViewLayoutTransition) {
var avatarOffset: CGFloat = 0.0
if hasAvatar && self.avatarNode == nil {
let avatarNode = ASImageNode()
avatarNode.displaysAsynchronously = false
avatarNode.image = avatarImage
self.bubbleNode.supernode?.addSubnode(avatarNode)
self.avatarNode = avatarNode
let avatarBorderNode = ASImageNode()
avatarBorderNode.displaysAsynchronously = false
avatarBorderNode.image = avatarBorderImage
self.bubbleBorderNode.supernode?.addSubnode(avatarBorderNode)
self.avatarBorderNode = avatarBorderNode
}
if let avatarNode = self.avatarNode, let avatarBorderNode = self.avatarBorderNode {
var avatarFrame = CGRect(origin: CGPoint(x: rect.minX + 3.0, y: rect.maxY + 1.0 - avatarSize.height), size: avatarSize)
if isSidebarOpen {
avatarFrame.origin.x -= avatarFrame.width * 0.5
avatarFrame.origin.y += avatarFrame.height * 0.5
}
transition.updatePosition(node: avatarNode, position: avatarFrame.center)
transition.updateBounds(node: avatarNode, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
transition.updateTransformScale(node: avatarNode, scale: isSidebarOpen ? 0.001 : 1.0)
transition.updateAlpha(node: avatarNode, alpha: isSidebarOpen ? 0.0 : 1.0)
transition.updatePosition(node: avatarBorderNode, position: avatarFrame.center)
transition.updateBounds(node: avatarBorderNode, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
transition.updateTransformScale(node: avatarBorderNode, scale: isSidebarOpen ? 0.001 : 1.0)
transition.updateAlpha(node: avatarBorderNode, alpha: isSidebarOpen ? 0.0 : 1.0)
if !isSidebarOpen {
avatarOffset += avatarSize.width - 1.0
}
}
let bubbleFrame = CGRect(origin: CGPoint(x: rect.minX + 3.0 + avatarOffset, y: rect.origin.y), size: CGSize(width: rect.width, height: rect.height))
transition.updateFrame(node: self.bubbleNode, frame: bubbleFrame)
transition.updateFrame(node: self.bubbleBorderNode, frame: bubbleFrame)
}
}
public final class ChatLoadingPlaceholderNode: ASDisplayNode {
private weak var backgroundNode: WallpaperBackgroundNode?
private let context: AccountContext
private let maskNode: ASDisplayNode
private let borderMaskNode: ASDisplayNode
private let containerNode: ASDisplayNode
private var backgroundContent: WallpaperBubbleBackgroundNode?
private let backgroundColorNode: ASDisplayNode
private let effectNode: ShimmerEffectForegroundNode
private let borderNode: ASDisplayNode
private let borderEffectNode: ShimmerEffectForegroundNode
private let messageContainers: [ChatLoadingPlaceholderMessageContainer]
private var absolutePosition: (CGRect, CGSize)?
private var validLayout: (CGSize, Bool, UIEdgeInsets, LayoutMetrics)?
public init(context: AccountContext, theme: PresentationTheme, chatWallpaper: TelegramWallpaper, bubbleCorners: PresentationChatBubbleCorners, backgroundNode: WallpaperBackgroundNode) {
self.context = context
self.backgroundNode = backgroundNode
self.maskNode = ASDisplayNode()
self.borderMaskNode = ASDisplayNode()
let bubbleImage = messageBubbleImage(maxCornerRadius: bubbleCorners.mainRadius, minCornerRadius: bubbleCorners.auxiliaryRadius, incoming: true, fillColor: .white, strokeColor: .clear, neighbors: .none, theme: theme.chat, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true)
let bubbleBorderImage = messageBubbleImage(maxCornerRadius: bubbleCorners.mainRadius, minCornerRadius: bubbleCorners.auxiliaryRadius, incoming: true, fillColor: .clear, strokeColor: .red, neighbors: .none, theme: theme.chat, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true, onlyOutline: true)
var messageContainers: [ChatLoadingPlaceholderMessageContainer] = []
for _ in 0 ..< 14 {
let container = ChatLoadingPlaceholderMessageContainer(bubbleImage: bubbleImage, bubbleBorderImage: bubbleBorderImage)
container.setup(maskNode: self.maskNode, borderMaskNode: self.borderMaskNode)
messageContainers.append(container)
}
self.messageContainers = messageContainers
self.containerNode = ASDisplayNode()
self.borderNode = ASDisplayNode()
self.backgroundColorNode = ASDisplayNode()
self.backgroundColorNode.backgroundColor = selectDateFillStaticColor(theme: theme, wallpaper: chatWallpaper)
self.effectNode = ShimmerEffectForegroundNode()
self.effectNode.layer.compositingFilter = "screenBlendMode"
self.borderEffectNode = ShimmerEffectForegroundNode()
self.borderEffectNode.layer.compositingFilter = "screenBlendMode"
super.init()
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.backgroundColorNode)
if context.sharedContext.energyUsageSettings.fullTranslucency {
self.containerNode.addSubnode(self.effectNode)
}
self.addSubnode(self.borderNode)
if context.sharedContext.energyUsageSettings.fullTranslucency {
self.borderNode.addSubnode(self.borderEffectNode)
}
}
override public func didLoad() {
super.didLoad()
self.containerNode.view.mask = self.maskNode.view
self.borderNode.view.mask = self.borderMaskNode.view
if self.context.sharedContext.energyUsageSettings.fullTranslucency {
Queue.mainQueue().after(0.3) { [weak self] in
guard let self else {
return
}
if !self.didAnimateOut {
self.backgroundNode?.updateIsLooping(true)
}
}
}
}
private var bottomInset: (Int, CGFloat)?
public func setup(_ historyNode: ListView, updating: Bool = false) {
let listNode = historyNode
var listItemNodes: [ASDisplayNode] = []
var count = 0
var inset: CGFloat = 0.0
listNode.forEachVisibleItemNode { itemNode in
inset += itemNode.frame.height
count += 1
listItemNodes.append(itemNode)
}
if updating {
let heightNorm = listNode.bounds.height - listNode.insets.top
listNode.forEachItemHeaderNode { itemNode in
var animateScale = true
if itemNode is ChatMessageAvatarHeaderNode {
animateScale = false
}
let delayFactor = itemNode.frame.minY / heightNorm
let delay = Double(delayFactor * 0.2)
itemNode.allowsGroupOpacity = true
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay, completion: { [weak itemNode] _ in
itemNode?.allowsGroupOpacity = false
})
if animateScale {
itemNode.layer.animateScale(from: 0.94, to: 1.0, duration: 0.4, delay: delay, timingFunction: kCAMediaTimingFunctionSpring)
}
}
}
if count > 0 {
self.bottomInset = (count, inset)
}
if updating {
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring)
transition.animateOffsetAdditive(node: self.maskNode, offset: -inset)
transition.animateOffsetAdditive(node: self.borderMaskNode, offset: -inset)
for listItemNode in listItemNodes {
var incoming = false
if let itemNode = listItemNode as? ChatMessageItemView, let item = itemNode.item, item.message.effectivelyIncoming(item.context.account.peerId) {
incoming = true
}
transition.animatePositionAdditive(node: listItemNode, offset: CGPoint(x: incoming ? 30.0 : -30.0, y: -30.0))
transition.animateTransformScale(node: listItemNode, from: CGPoint(x: 0.85, y: 0.85))
listItemNode.allowsGroupOpacity = true
listItemNode.alpha = 1.0
listItemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { _ in
listItemNode.allowsGroupOpacity = false
})
}
}
self.maskNode.bounds = self.maskNode.bounds.offsetBy(dx: 0.0, dy: inset)
self.borderMaskNode.bounds = self.borderMaskNode.bounds.offsetBy(dx: 0.0, dy: inset)
}
private var didAnimateOut = false
public func animateOut(_ historyNode: ListView, completion: @escaping () -> Void = {}) {
guard let (size, isSidebarOpen, _, _) = self.validLayout else {
return
}
let listNode = historyNode
self.didAnimateOut = true
self.backgroundNode?.updateIsLooping(false)
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring)
var lastFrame: CGRect?
let heightNorm = listNode.bounds.height - listNode.insets.top
var index = 0
var skipCount = self.bottomInset?.0 ?? 0
listNode.forEachVisibleItemNode { itemNode in
guard index < self.messageContainers.count, let listItemNode = itemNode as? ListViewItemNode else {
return
}
let delayFactor = listItemNode.frame.minY / heightNorm
let delay = Double(delayFactor * 0.1)
if skipCount > 0 {
skipCount -= 1
return
}
if let itemNode = itemNode as? ChatUnreadItemNode {
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: 0.0)
return
}
if let itemNode = itemNode as? ChatReplyCountItemNode {
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: 0.0)
return
}
let messageContainer = self.messageContainers[index]
messageContainer.animateWith(listItemNode, delay: delay, transition: transition)
lastFrame = messageContainer.frame
index += 1
}
skipCount = self.bottomInset?.0 ?? 0
listNode.forEachItemHeaderNode { itemNode in
var animateScale = true
if itemNode is ChatMessageAvatarHeaderNode {
animateScale = false
if skipCount > 0 {
return
}
}
if itemNode is ChatMessageDateHeaderNode {
if skipCount > 0 {
skipCount -= 1
return
}
}
let delayFactor = itemNode.frame.minY / heightNorm
let delay = Double(delayFactor * 0.2)
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay)
if animateScale {
itemNode.layer.animateScale(from: 0.94, to: 1.0, duration: 0.4, delay: delay, timingFunction: kCAMediaTimingFunctionSpring)
}
}
self.alpha = 0.0
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in
completion()
})
if let lastFrame = lastFrame, index < self.messageContainers.count {
var offset = lastFrame.minY
for k in index ..< self.messageContainers.count {
let messageContainer = self.messageContainers[k]
let messageSize = messageContainer.frame.size
messageContainer.update(size: size, isSidebarOpen: isSidebarOpen, hasAvatar: self.chatType != .channel && self.chatType != .user, rect: CGRect(origin: CGPoint(x: 0.0, y: offset - messageSize.height), size: messageSize), transition: transition)
offset -= messageSize.height
}
}
}
public func addContentOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
self.maskNode.bounds = self.maskNode.bounds.offsetBy(dx: 0.0, dy: -offset)
self.borderMaskNode.bounds = self.borderMaskNode.bounds.offsetBy(dx: 0.0, dy: -offset)
transition.animateOffsetAdditive(node: self.maskNode, offset: offset)
transition.animateOffsetAdditive(node: self.borderMaskNode, offset: offset)
if let (rect, containerSize) = self.absolutePosition {
self.update(rect: rect, within: containerSize)
}
}
public func update(rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition = .immediate) {
self.absolutePosition = (rect, containerSize)
if let backgroundContent = self.backgroundContent {
var backgroundFrame = backgroundContent.frame
backgroundFrame.origin.x += rect.minX
backgroundFrame.origin.y += rect.minY
backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: transition)
}
}
public enum ChatType: Equatable {
case generic
case user
case group
case channel
}
private var chatType: ChatType = .channel
public func updatePresentationInterfaceState(renderedPeer: RenderedPeer?, chatLocation: ChatLocation) {
var chatType: ChatType = .channel
if let peer = renderedPeer?.peer {
if peer is TelegramUser {
chatType = .user
} else if peer is TelegramGroup {
chatType = .group
} else if let channel = peer as? TelegramChannel {
if channel.isMonoForum {
if let mainChannel = renderedPeer?.chatOrMonoforumMainPeer as? TelegramChannel, mainChannel.hasPermission(.manageDirect) {
if chatLocation.threadId == nil {
chatType = .group
} else {
chatType = .user
}
} else {
chatType = .user
}
} else {
if case .group = channel.info {
chatType = .group
} else {
chatType = .channel
}
}
}
}
if self.chatType != chatType {
self.chatType = chatType
if let (size, isSidebarOpen, insets, metrics) = self.validLayout {
self.updateLayout(size: size, isSidebarOpen: isSidebarOpen, insets: insets, metrics: metrics, transition: .immediate)
}
}
}
public func updateLayout(size: CGSize, isSidebarOpen: Bool, insets: UIEdgeInsets, metrics: LayoutMetrics, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, isSidebarOpen, insets, metrics)
let bounds = CGRect(origin: .zero, size: size)
transition.updateFrame(node: self.maskNode, frame: bounds)
transition.updateFrame(node: self.borderMaskNode, frame: bounds)
transition.updateFrame(node: self.containerNode, frame: bounds)
transition.updateFrame(node: self.borderNode, frame: bounds)
transition.updateFrame(node: self.backgroundColorNode, frame: bounds)
transition.updateFrame(node: self.effectNode, frame: bounds)
transition.updateFrame(node: self.borderEffectNode, frame: bounds)
self.effectNode.updateAbsoluteRect(bounds, within: bounds.size)
self.borderEffectNode.updateAbsoluteRect(bounds, within: bounds.size)
self.effectNode.update(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.14), horizontal: true, effectSize: 280.0, globalTimeOffset: false, duration: 1.6)
self.borderEffectNode.update(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.35), horizontal: true, effectSize: 320.0, globalTimeOffset: false, duration: 1.6)
let shortHeight: CGFloat = 71.0
let tallHeight: CGFloat = 93.0
var width = size.width
if case .regular = metrics.widthClass, abs(size.width - size.height) < 0.2 * size.height {
width *= 0.7
}
let dimensions: [CGSize] = [
CGSize(width: floorToScreenPixels(0.47 * width), height: tallHeight),
CGSize(width: floorToScreenPixels(0.58 * width), height: tallHeight),
CGSize(width: floorToScreenPixels(0.69 * width), height: tallHeight),
CGSize(width: floorToScreenPixels(0.47 * width), height: tallHeight),
CGSize(width: floorToScreenPixels(0.58 * width), height: shortHeight),
CGSize(width: floorToScreenPixels(0.36 * width), height: tallHeight),
CGSize(width: floorToScreenPixels(0.47 * width), height: tallHeight),
CGSize(width: floorToScreenPixels(0.36 * width), height: shortHeight),
CGSize(width: floorToScreenPixels(0.58 * width), height: tallHeight),
CGSize(width: floorToScreenPixels(0.69 * width), height: tallHeight),
CGSize(width: floorToScreenPixels(0.58 * width), height: tallHeight),
CGSize(width: floorToScreenPixels(0.36 * width), height: shortHeight),
CGSize(width: floorToScreenPixels(0.47 * width), height: tallHeight),
CGSize(width: floorToScreenPixels(0.58 * width), height: tallHeight)
].map {
if self.chatType == .channel {
return CGSize(width: floor($0.width * 1.3), height: floor($0.height * 1.8))
} else {
return $0
}
}
var offset: CGFloat = 5.0
var index = 0
for messageContainer in self.messageContainers {
let messageSize = dimensions[index % 14]
messageContainer.update(size: bounds.size, isSidebarOpen: isSidebarOpen, hasAvatar: self.chatType != .channel && self.chatType != .user, rect: CGRect(origin: CGPoint(x: insets.left, y: bounds.size.height - insets.bottom - offset - messageSize.height), size: messageSize), transition: transition)
offset += messageSize.height
index += 1
}
if self.backgroundNode?.hasExtraBubbleBackground() == true {
self.backgroundColorNode.isHidden = true
} else {
self.backgroundColorNode.isHidden = true
}
if let backgroundNode = self.backgroundNode, let backgroundContent = backgroundNode.makeBubbleBackground(for: .free) {
if self.backgroundContent == nil {
self.backgroundContent = backgroundContent
self.containerNode.insertSubnode(backgroundContent, at: 0)
}
} else {
self.backgroundContent?.removeFromSupernode()
self.backgroundContent = nil
}
if let backgroundContent = self.backgroundContent {
transition.updateFrame(node: backgroundContent, frame: bounds)
if let (rect, containerSize) = self.absolutePosition {
self.update(rect: rect, within: containerSize)
}
}
}
}
@@ -0,0 +1,30 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMediaInputStickerGridItem",
module_name = "ChatMediaInputStickerGridItem",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AsyncDisplayKit",
"//submodules/Postbox",
"//submodules/TelegramPresentationData",
"//submodules/StickerResources",
"//submodules/AccountContext",
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
"//submodules/ShimmerEffect",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
"//submodules/ChatPresentationInterfaceState",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,470 @@
import Foundation
import UIKit
import Display
import TelegramCore
import SwiftSignalKit
import AsyncDisplayKit
import Postbox
import TelegramPresentationData
import StickerResources
import AccountContext
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import ShimmerEffect
import ChatControllerInteraction
import ChatPresentationInterfaceState
public enum ChatMediaInputStickerGridSectionAccessory {
case none
case setup
case clear
}
public final class ChatMediaInputStickerGridSection: GridSection {
public let collectionId: ItemCollectionId
public let collectionInfo: StickerPackCollectionInfo?
public let accessory: ChatMediaInputStickerGridSectionAccessory
public let interaction: ChatMediaInputNodeInteraction
public let theme: PresentationTheme
public let height: CGFloat = 26.0
public var hashValue: Int {
return self.collectionId.hashValue
}
public init(collectionId: ItemCollectionId, collectionInfo: StickerPackCollectionInfo?, accessory: ChatMediaInputStickerGridSectionAccessory, theme: PresentationTheme, interaction: ChatMediaInputNodeInteraction) {
self.collectionId = collectionId
self.collectionInfo = collectionInfo
self.accessory = accessory
self.theme = theme
self.interaction = interaction
}
public func isEqual(to: GridSection) -> Bool {
if let to = to as? ChatMediaInputStickerGridSection {
return self.collectionId == to.collectionId && self.theme === to.theme
} else {
return false
}
}
public func node() -> ASDisplayNode {
return ChatMediaInputStickerGridSectionNode(collectionInfo: self.collectionInfo, accessory: self.accessory, theme: self.theme, interaction: self.interaction)
}
}
private let sectionTitleFont = Font.medium(12.0)
public final class ChatMediaInputStickerGridSectionNode: ASDisplayNode {
public let titleNode: ASTextNode
public let setupNode: HighlightableButtonNode?
public let interaction: ChatMediaInputNodeInteraction
public let accessory: ChatMediaInputStickerGridSectionAccessory
public init(collectionInfo: StickerPackCollectionInfo?, accessory: ChatMediaInputStickerGridSectionAccessory, theme: PresentationTheme, interaction: ChatMediaInputNodeInteraction) {
self.interaction = interaction
self.titleNode = ASTextNode()
self.titleNode.isUserInteractionEnabled = false
self.accessory = accessory
switch accessory {
case .none:
self.setupNode = nil
case .setup:
let setupNode = HighlightableButtonNode()
setupNode.setImage(PresentationResourcesChat.chatInputMediaPanelGridSetupImage(theme), for: [])
self.setupNode = setupNode
case .clear:
let setupNode = HighlightableButtonNode()
setupNode.setImage(PresentationResourcesChat.chatInputMediaPanelGridDismissImage(theme, color: theme.chat.inputMediaPanel.stickersSectionTextColor), for: [])
self.setupNode = setupNode
}
super.init()
self.addSubnode(self.titleNode)
self.titleNode.attributedText = NSAttributedString(string: collectionInfo?.title.uppercased() ?? "", font: sectionTitleFont, textColor: theme.chat.inputMediaPanel.stickersSectionTextColor)
self.titleNode.maximumNumberOfLines = 1
self.titleNode.truncationMode = .byTruncatingTail
self.setupNode.flatMap(self.addSubnode)
self.setupNode?.addTarget(self, action: #selector(self.setupPressed), forControlEvents: .touchUpInside)
}
override public func layout() {
super.layout()
let bounds = self.bounds
let titleSize = self.titleNode.measure(CGSize(width: bounds.size.width - 24.0, height: CGFloat.greatestFiniteMagnitude))
self.titleNode.frame = CGRect(origin: CGPoint(x: 12.0, y: 9.0), size: titleSize)
if let setupNode = self.setupNode {
setupNode.frame = CGRect(origin: CGPoint(x: bounds.width - 12.0 - 16.0, y: 3.0), size: CGSize(width: 16.0, height: 26.0))
}
}
@objc private func setupPressed() {
switch self.accessory {
case .setup:
self.interaction.openPeerSpecificSettings()
case .clear:
self.interaction.clearRecentlyUsedStickers()
default:
break
}
}
}
public final class ChatMediaInputStickerGridItem: GridItem {
public let context: AccountContext
public let index: ItemCollectionViewEntryIndex
public let stickerItem: StickerPackItem
public let selected: () -> Void
public let interfaceInteraction: ChatControllerInteraction?
public let inputNodeInteraction: ChatMediaInputNodeInteraction
public let theme: PresentationTheme
public let large: Bool
public let isLocked: Bool
public let section: GridSection?
public init(context: AccountContext, collectionId: ItemCollectionId, stickerPackInfo: StickerPackCollectionInfo?, index: ItemCollectionViewEntryIndex, stickerItem: StickerPackItem, canManagePeerSpecificPack: Bool?, interfaceInteraction: ChatControllerInteraction?, inputNodeInteraction: ChatMediaInputNodeInteraction, hasAccessory: Bool, theme: PresentationTheme, large: Bool = false, isLocked: Bool = false, selected: @escaping () -> Void) {
self.context = context
self.index = index
self.stickerItem = stickerItem
self.interfaceInteraction = interfaceInteraction
self.inputNodeInteraction = inputNodeInteraction
self.theme = theme
self.large = large
self.isLocked = isLocked
self.selected = selected
let accessory: ChatMediaInputStickerGridSectionAccessory
accessory = .none
self.section = ChatMediaInputStickerGridSection(collectionId: collectionId, collectionInfo: stickerPackInfo, accessory: accessory, theme: theme, interaction: inputNodeInteraction)
}
public func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
let node = ChatMediaInputStickerGridItemNode()
node.interfaceInteraction = self.interfaceInteraction
node.inputNodeInteraction = self.inputNodeInteraction
node.selected = self.selected
return node
}
public func update(node: GridItemNode) {
guard let node = node as? ChatMediaInputStickerGridItemNode else {
assertionFailure()
return
}
node.interfaceInteraction = self.interfaceInteraction
node.inputNodeInteraction = self.inputNodeInteraction
node.selected = self.selected
}
}
public final class ChatMediaInputStickerGridItemNode: GridItemNode {
private var currentState: (AccountContext, StickerPackItem, CGSize)?
private var currentSize: CGSize?
public let imageNode: TransformImageNode
public private(set) var animationNode: AnimatedStickerNode?
public private(set) var placeholderNode: StickerShimmerEffectNode?
private var lockBackground: UIVisualEffectView?
private var lockTintView: UIView?
private var lockIconNode: ASImageNode?
public var isLocked: Bool?
private var didSetUpAnimationNode = false
private var item: ChatMediaInputStickerGridItem?
private let stickerFetchedDisposable = MetaDisposable()
public var currentIsPreviewing = false
override public var isVisibleInGrid: Bool {
didSet {
self.updateVisibility()
}
}
private var isPanelVisible = false
private var isPlaying = false
public var interfaceInteraction: ChatControllerInteraction?
public var inputNodeInteraction: ChatMediaInputNodeInteraction?
public var selected: (() -> Void)?
public var stickerPackItem: StickerPackItem? {
return self.currentState?.1
}
override public init() {
self.imageNode = TransformImageNode()
self.placeholderNode = StickerShimmerEffectNode()
self.placeholderNode?.isUserInteractionEnabled = false
super.init()
self.addSubnode(self.imageNode)
if let placeholderNode = self.placeholderNode {
self.addSubnode(placeholderNode)
}
var firstTime = true
self.imageNode.imageUpdated = { [weak self] image in
guard let strongSelf = self else {
return
}
if image != nil {
strongSelf.removePlaceholder(animated: !firstTime)
}
firstTime = false
}
}
deinit {
self.stickerFetchedDisposable.dispose()
}
private func removePlaceholder(animated: Bool) {
if let placeholderNode = self.placeholderNode {
self.placeholderNode = nil
if !animated {
placeholderNode.removeFromSupernode()
} else {
placeholderNode.allowsGroupOpacity = true
placeholderNode.alpha = 0.0
placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak placeholderNode] _ in
placeholderNode?.removeFromSupernode()
placeholderNode?.allowsGroupOpacity = false
})
}
}
}
override public func didLoad() {
super.didLoad()
self.imageNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:))))
}
override public func updateLayout(item: GridItem, size: CGSize, isVisible: Bool, synchronousLoads: Bool) {
guard let item = item as? ChatMediaInputStickerGridItem else {
return
}
let sideSize: CGFloat = size.width - 10.0
let boundingSize = CGSize(width: sideSize, height: sideSize)
self.item = item
if self.currentState == nil || self.currentState!.0 !== item.context || self.currentState!.1 != item.stickerItem || self.isLocked != item.isLocked {
if !item.inputNodeInteraction.displayStickerPlaceholder {
self.removePlaceholder(animated: false)
}
if let dimensions = item.stickerItem.file.dimensions {
let parsedStickerItemFile = item.stickerItem.file._parse()
if item.stickerItem.file.isAnimatedSticker || item.stickerItem.file.isVideoSticker {
if self.animationNode == nil {
let animationNode = DefaultAnimatedStickerNodeImpl()
animationNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:))))
self.animationNode = animationNode
animationNode.started = { [weak self] in
self?.imageNode.isHidden = true
}
if let placeholderNode = self.placeholderNode {
self.insertSubnode(animationNode, belowSubnode: placeholderNode)
} else {
self.addSubnode(animationNode)
}
}
let dimensions = item.stickerItem.file.dimensions ?? PixelDimensions(width: 512, height: 512)
let fittedSize = item.large ? CGSize(width: 384.0, height: 384.0) : CGSize(width: 160.0, height: 160.0)
if item.stickerItem.file.isVideoSticker {
self.imageNode.setSignal(chatMessageSticker(account: item.context.account, userLocation: .other, file: parsedStickerItemFile, small: false, synchronousLoad: synchronousLoads && isVisible))
} else {
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: item.context.account.postbox, userLocation: .other, file: parsedStickerItemFile, small: false, size: dimensions.cgSize.aspectFitted(fittedSize)))
}
self.updateVisibility()
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: item.context.account, userLocation: .other, fileReference: stickerPackFileReference(parsedStickerItemFile), resource: parsedStickerItemFile.resource).startStrict())
} else {
if let animationNode = self.animationNode {
animationNode.visibility = false
self.animationNode = nil
animationNode.removeFromSupernode()
self.imageNode.isHidden = false
self.didSetUpAnimationNode = false
}
self.imageNode.setSignal(chatMessageSticker(account: item.context.account, userLocation: .other, file: parsedStickerItemFile, small: !item.large, synchronousLoad: synchronousLoads && isVisible))
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: item.context.account, userLocation: .other, fileReference: stickerPackFileReference(parsedStickerItemFile), resource: chatMessageStickerResource(file: parsedStickerItemFile, small: !item.large)).startStrict())
}
self.currentState = (item.context, item.stickerItem, dimensions.cgSize)
self.setNeedsLayout()
}
self.isLocked = item.isLocked
if item.isLocked {
let lockBackground: UIVisualEffectView
let lockIconNode: ASImageNode
if let currentBackground = self.lockBackground, let currentIcon = self.lockIconNode {
lockBackground = currentBackground
lockIconNode = currentIcon
} else {
let effect: UIBlurEffect
if #available(iOS 10.0, *) {
effect = UIBlurEffect(style: .regular)
} else {
effect = UIBlurEffect(style: .light)
}
lockBackground = UIVisualEffectView(effect: effect)
lockBackground.clipsToBounds = true
lockBackground.isUserInteractionEnabled = false
lockIconNode = ASImageNode()
lockIconNode.displaysAsynchronously = false
lockIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/SmallLock"), color: .white)
let lockTintView = UIView()
lockTintView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.15)
lockBackground.contentView.addSubview(lockTintView)
self.lockBackground = lockBackground
self.lockTintView = lockTintView
self.lockIconNode = lockIconNode
self.view.addSubview(lockBackground)
self.addSubnode(lockIconNode)
}
} else if let lockBackground = self.lockBackground, let lockTintView = self.lockTintView, let lockIconNode = self.lockIconNode {
self.lockBackground = nil
self.lockTintView = nil
self.lockIconNode = nil
lockBackground.removeFromSuperview()
lockTintView.removeFromSuperview()
lockIconNode.removeFromSupernode()
}
}
if self.currentSize != size {
self.currentSize = size
if let (_, _, mediaDimensions) = self.currentState {
let imageSize = mediaDimensions.aspectFitted(boundingSize)
let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize)
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
if self.imageNode.supernode === self {
self.imageNode.frame = imageFrame
}
if let animationNode = self.animationNode {
if animationNode.supernode === self {
animationNode.frame = imageFrame
}
animationNode.updateLayout(size: imageSize)
}
}
}
if let placeholderNode = self.placeholderNode {
let placeholderFrame = CGRect(origin: CGPoint(x: floor((size.width - boundingSize.width) / 2.0), y: floor((size.height - boundingSize.height) / 2.0)), size: boundingSize)
if placeholderNode.supernode === self {
placeholderNode.frame = placeholderFrame
}
let theme = item.theme
placeholderNode.update(backgroundColor: theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0), foregroundColor: theme.chat.inputMediaPanel.stickersSectionTextColor.blitOver(theme.chat.inputMediaPanel.stickersBackgroundColor, alpha: 0.15), shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3), data: item.stickerItem.file.immediateThumbnailData, size: placeholderFrame.size, enableEffect: true)
}
if let lockBackground = self.lockBackground, let lockTintView = self.lockTintView, let lockIconNode = self.lockIconNode {
let lockSize = CGSize(width: 24.0, height: 24.0)
let lockBackgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - lockSize.width) / 2.0), y: size.height - lockSize.height - 2.0), size: lockSize)
lockBackground.frame = lockBackgroundFrame
lockBackground.layer.cornerRadius = lockSize.width / 2.0
if #available(iOS 13.0, *) {
lockBackground.layer.cornerCurve = .circular
}
lockTintView.frame = CGRect(origin: CGPoint(), size: lockBackgroundFrame.size)
if let icon = lockIconNode.image {
lockIconNode.frame = CGRect(origin: CGPoint(x: lockBackgroundFrame.minX + floorToScreenPixels((lockBackgroundFrame.width - icon.size.width) / 2.0), y: lockBackgroundFrame.minY + floorToScreenPixels((lockBackgroundFrame.height - icon.size.height) / 2.0)), size: icon.size)
}
}
}
override public func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) {
if let placeholderNode = self.placeholderNode {
placeholderNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: absoluteRect.minX + placeholderNode.frame.minX, y: absoluteRect.minY + placeholderNode.frame.minY), size: placeholderNode.frame.size), within: containerSize)
}
}
@objc private func imageNodeTap(_ recognizer: UITapGestureRecognizer) {
if self.imageNode.layer.animation(forKey: "opacity") != nil {
return
}
if let interfaceInteraction = self.interfaceInteraction, let (_, item, _) = self.currentState, case .ended = recognizer.state {
if let isLocked = self.isLocked, isLocked {
} else {
let _ = interfaceInteraction.sendSticker(.standalone(media: item.file._parse()), false, false, nil, false, self.view, self.bounds, nil, [])
self.imageNode.layer.animateAlpha(from: 0.5, to: 1.0, duration: 1.0)
}
}
}
public func transitionNode() -> ASDisplayNode? {
return self.imageNode
}
public func updateIsPanelVisible(_ isPanelVisible: Bool) {
if self.isPanelVisible != isPanelVisible {
self.isPanelVisible = isPanelVisible
self.updateVisibility()
}
}
public func updateVisibility() {
guard let item = self.item else {
return
}
let isPlaying = self.isPanelVisible && self.isVisibleInGrid && (item.context.sharedContext.energyUsageSettings.loopStickers)
if self.isPlaying != isPlaying {
self.isPlaying = isPlaying
self.animationNode?.visibility = isPlaying
if let item = self.item, isPlaying, !self.didSetUpAnimationNode {
self.didSetUpAnimationNode = true
if let animationNode = self.animationNode {
let dimensions = item.stickerItem.file.dimensions ?? PixelDimensions(width: 512, height: 512)
let fitSize = item.large ? CGSize(width: 384.0, height: 384.0) : CGSize(width: 160.0, height: 160.0)
let fittedDimensions = dimensions.cgSize.aspectFitted(fitSize)
animationNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: item.stickerItem.file._parse().resource, isVideo: item.stickerItem.file.isVideoSticker), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
}
}
}
}
public func updatePreviewing(animated: Bool) {
var isPreviewing = false
if let (_, item, _) = self.currentState, let interaction = self.inputNodeInteraction {
isPreviewing = interaction.previewedStickerPackItemFile?.id == item.file.id
}
if self.currentIsPreviewing != isPreviewing {
self.currentIsPreviewing = isPreviewing
if isPreviewing {
self.layer.sublayerTransform = CATransform3DMakeScale(0.8, 0.8, 1.0)
if animated {
self.layer.animateSpring(from: 1.0 as NSNumber, to: 0.8 as NSNumber, keyPath: "sublayerTransform.scale", duration: 0.4)
}
} else {
self.layer.sublayerTransform = CATransform3DIdentity
if animated {
self.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "sublayerTransform.scale", duration: 0.5)
}
}
}
}
}
@@ -0,0 +1,42 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageActionBubbleContentNode",
module_name = "ChatMessageActionBubbleContentNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/AccountContext",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/TextFormat",
"//submodules/LocalizedPeerData",
"//submodules/UrlEscaping",
"//submodules/PhotoResources",
"//submodules/TelegramStringFormatting",
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//submodules/TelegramUniversalVideoContent",
"//submodules/GalleryUI",
"//submodules/WallpaperBackgroundNode",
"//submodules/InvisibleInkDustNode",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/Markdown",
"//submodules/ComponentFlow",
"//submodules/ReactionSelectionNode",
"//submodules/Components/MultilineTextComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,992 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import AccountContext
import TelegramPresentationData
import TelegramUIPreferences
import TextFormat
import LocalizedPeerData
import UrlEscaping
import PhotoResources
import TelegramStringFormatting
import UniversalMediaPlayer
import TelegramUniversalVideoContent
import GalleryUI
import WallpaperBackgroundNode
import InvisibleInkDustNode
import TextNodeWithEntities
import ChatMessageBubbleContentNode
import ChatMessageItemCommon
import Markdown
import ComponentFlow
import ReactionSelectionNode
import MultilineTextComponent
private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: Message, messageCount: Int? = nil, accountPeerId: PeerId, forForumOverview: Bool) -> NSAttributedString? {
return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: EngineMessage(message), messageCount: messageCount, accountPeerId: accountPeerId, forChatList: false, forForumOverview: forForumOverview)
}
public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
public var expandHighlightingNode: LinkHighlightingNode?
public var titleNode: TextNode?
public let labelNode: TextNodeWithEntities
private var dustNode: InvisibleInkDustNode?
public var backgroundNode: WallpaperBubbleBackgroundNode?
public var backgroundColorNode: ASDisplayNode
public let backgroundMaskNode: ASImageNode
public var linkHighlightingNode: LinkHighlightingNode?
private var buyStarsTitle: TextNode?
private var buyStarsButton: HighlightTrackingButton?
private var buttonStarsNode: PremiumStarsNode?
private let mediaBackgroundNode: ASImageNode
fileprivate var imageNode: TransformImageNode?
fileprivate var videoNode: UniversalVideoNode?
private var videoContent: NativeVideoContent?
private var videoStartTimestamp: Double?
private let fetchDisposable = MetaDisposable()
private var leadingIconView: UIImageView?
private var cachedMaskBackgroundImage: (CGPoint, UIImage, [CGRect])?
private var absoluteRect: (CGRect, CGSize)?
override public var disablesClipping: Bool {
return true
}
override public var visibility: ListViewItemNodeVisibility {
didSet {
if oldValue != self.visibility {
switch self.visibility {
case .none:
self.labelNode.visibilityRect = nil
//self.spoilerTextNode?.visibilityRect = nil
case let .visible(_, subRect):
var subRect = subRect
subRect.origin.x = 0.0
subRect.size.width = 10000.0
self.labelNode.visibilityRect = subRect
//self.spoilerTextNode?.visibilityRect = subRect
}
}
}
}
required public init() {
self.labelNode = TextNodeWithEntities()
self.labelNode.textNode.isUserInteractionEnabled = false
self.labelNode.textNode.displaysAsynchronously = false
self.backgroundColorNode = ASDisplayNode()
self.backgroundMaskNode = ASImageNode()
self.mediaBackgroundNode = ASImageNode()
self.mediaBackgroundNode.displaysAsynchronously = false
self.mediaBackgroundNode.displayWithoutProcessing = true
super.init()
self.addSubnode(self.labelNode.textNode)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.fetchDisposable.dispose()
}
override public func didLoad() {
super.didLoad()
}
override public func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
if let imageNode = self.imageNode, self.item?.message.id == messageId {
return (imageNode, imageNode.bounds, { [weak self] in
guard let strongSelf = self, let imageNode = strongSelf.imageNode else {
return (nil, nil)
}
let resultView = imageNode.view.snapshotContentTree(unhide: true)
if let resultView = resultView, strongSelf.mediaBackgroundNode.supernode != nil, let backgroundView = strongSelf.mediaBackgroundNode.view.snapshotContentTree(unhide: true) {
let backgroundContainer = UIView()
backgroundContainer.addSubview(backgroundView)
backgroundContainer.frame = CGRect(origin: CGPoint(x: -2.0, y: -2.0), size: CGSize(width: resultView.frame.width + 4.0, height: resultView.frame.height + 4.0))
backgroundView.frame = backgroundContainer.bounds
let viewWithBackground = UIView()
viewWithBackground.addSubview(backgroundContainer)
viewWithBackground.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: resultView.frame.size)
resultView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: resultView.frame.size)
viewWithBackground.addSubview(resultView)
return (viewWithBackground, backgroundContainer)
}
return (resultView, nil)
})
} else {
return nil
}
}
override public func updateHiddenMedia(_ media: [Media]?) -> Bool {
var mediaHidden = false
var currentMedia: Media?
if let item = item {
mediaLoop: for media in item.message.media {
if let media = media as? TelegramMediaAction {
switch media.action {
case let .photoUpdated(image):
currentMedia = image
break mediaLoop
default:
break
}
}
}
}
if let currentMedia = currentMedia, let media = media {
for item in media {
if item.isSemanticallyEqual(to: currentMedia) {
mediaHidden = true
break
}
}
}
self.imageNode?.isHidden = mediaHidden
self.mediaBackgroundNode.isHidden = mediaHidden
return mediaHidden
}
@objc private func buyStarsPressed() {
if let item = self.item {
item.controllerInteraction.openStarsPurchase(nil)
}
}
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeLabelLayout = TextNodeWithEntities.asyncLayout(self.labelNode)
let makeBuyStarsTitleLayout = TextNode.asyncLayout(self.buyStarsTitle)
let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage
return { item, layoutConstants, _, _, _, _ in
var isDetached = false
if let _ = item.message.paidStarsAttribute {
isDetached = true
}
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center, isDetached: isDetached)
let backgroundImage = PresentationResourcesChat.chatActionPhotoBackgroundImage(item.presentationData.theme.theme, wallpaper: !item.presentationData.theme.wallpaper.isEmpty)
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
var forForumOverview = false
if item.chatLocation.threadId == nil {
forForumOverview = true
}
var messageCount: Int = 1
if case let .group(messages) = item.content {
messageCount = messages.count
}
let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: item.message, messageCount: messageCount, accountPeerId: item.context.account.peerId, forForumOverview: forForumOverview)
var image: TelegramMediaImage?
var suggestedPost: TelegramMediaActionType.SuggestedPostApprovalStatus?
var leadingIcon: UIImage?
var isStory = false
for media in item.message.media {
if let action = media as? TelegramMediaAction {
switch action.action {
case let .photoUpdated(img):
image = img
case let .suggestedPostApprovalStatus(status):
suggestedPost = status
case let .todoCompletions(completed, _):
if !completed.isEmpty {
leadingIcon = PresentationResourcesChat.chatServiceMessageTodoCompletedIcon(item.presentationData.theme.theme)
} else {
leadingIcon = PresentationResourcesChat.chatServiceMessageTodoIncompletedIcon(item.presentationData.theme.theme)
}
case .todoAppendTasks:
leadingIcon = PresentationResourcesChat.chatServiceMessageTodoAppendedIcon(item.presentationData.theme.theme)
default:
break
}
} else if media is TelegramMediaStory {
leadingIcon = PresentationResourcesChat.chatExpiredStoryIndicatorIcon(item.presentationData.theme.theme, type: .free)
isStory = true
}
}
var isUser = true
if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, peer.isMonoForum, let linkedMonoforumId = peer.linkedMonoforumId, let mainChannel = item.message.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect) {
isUser = false
}
let imageSize = CGSize(width: 212.0, height: 212.0)
var updatedAttributedString = attributedString
if leadingIcon != nil, let attributedString {
let mutableString = NSMutableAttributedString(attributedString: attributedString)
mutableString.insert(NSAttributedString(string: isStory ? " " : " ", font: Font.regular(13.0), textColor: .clear), at: 0)
updatedAttributedString = mutableString
}
var textAlignment: NSTextAlignment = .center
let primaryTextColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
if let suggestedPost {
textAlignment = .left
let channelName: String
if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, peer.isMonoForum, let linkedMonoforumId = peer.linkedMonoforumId, let mainChannel = item.message.peers[linkedMonoforumId] as? TelegramChannel {
channelName = EnginePeer(mainChannel).compactDisplayTitle
} else {
channelName = " "
}
switch suggestedPost {
case let .approved(timestamp, amount):
let timeString = humanReadableStringForTimestamp(strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat, timestamp: timestamp ?? 0, alwaysShowTime: true, allowYesterday: false, format: HumanReadableStringFormat(
dateFormatString: { value in
return PresentationStrings.FormattedString(string: item.presentationData.strings.SuggestPost_SetTimeFormat_Date(value).string.lowercased(), ranges: [])
},
tomorrowFormatString: { value in
return PresentationStrings.FormattedString(string: item.presentationData.strings.SuggestPost_SetTimeFormat_TomorrowAt(value).string.lowercased(), ranges: [])
},
todayFormatString: { value in
return PresentationStrings.FormattedString(string: item.presentationData.strings.SuggestPost_SetTimeFormat_TodayAt(value).string.lowercased(), ranges: [])
},
yesterdayFormatString: { value in
return PresentationStrings.FormattedString(string: item.presentationData.strings.SuggestPost_SetTimeFormat_TodayAt(value).string.lowercased(), ranges: [])
}
)).string
var pricePart = ""
if let amount, amount.amount != .zero {
let amountString: String
switch amount.currency {
case .stars:
amountString = item.presentationData.strings.Chat_PostApproval_DetailStatus_StarsAmount(Int32((amount.amount.value == 1 && amount.amount.nanos == 0) ? 1 : 100)).replacingOccurrences(of: "#", with: "\(amount.amount)")
case .ton:
amountString = item.presentationData.strings.Chat_PostApproval_DetailStatus_TonAmount(Int32((amount.amount.value == 1 * 1_000_000_000) ? 1 : 100)).replacingOccurrences(of: "#", with: "\(formatTonAmountText(amount.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat, maxDecimalPositions: 3))")
}
switch amount.currency {
case .stars:
if isUser {
pricePart = "\n\n" + item.presentationData.strings.Chat_PostApproval_Message_UserAgreementPriceStars(amountString, channelName).string
} else {
pricePart = "\n\n" + item.presentationData.strings.Chat_PostApproval_Message_AdminAgreementPriceStars(amountString, channelName).string
}
case .ton:
if isUser {
pricePart = "\n\n" + item.presentationData.strings.Chat_PostApproval_Message_UserAgreementPriceTon(amountString, channelName).string
} else {
pricePart = "\n\n" + item.presentationData.strings.Chat_PostApproval_Message_AdminAgreementPriceTon(amountString, channelName).string
}
}
}
let rawString: String
if let timestamp {
if Int32(Date().timeIntervalSince1970) >= timestamp {
if isUser {
rawString = item.presentationData.strings.Chat_PostApproval_Message_UserAgreementPast(channelName, timeString).string + pricePart
} else {
rawString = item.presentationData.strings.Chat_PostApproval_Message_AdminAgreementPast(channelName, timeString).string + pricePart
}
} else {
if isUser {
rawString = item.presentationData.strings.Chat_PostApproval_Message_UserAgreementFuture(channelName, timeString).string + pricePart
} else {
rawString = item.presentationData.strings.Chat_PostApproval_Message_AdminAgreementFuture(channelName, timeString).string + pricePart
}
}
} else {
if isUser {
rawString = item.presentationData.strings.Chat_PostApproval_Message_UserAgreementNoTime(channelName).string + pricePart
} else {
rawString = item.presentationData.strings.Chat_PostApproval_Message_AdminAgreementNoTime(channelName).string + pricePart
}
}
updatedAttributedString = parseMarkdownIntoAttributedString(rawString, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: primaryTextColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor),
linkAttribute: { url in
return ("URL", url)
}
))
case let .rejected(reason, comment):
let rawString: String
if !item.message.effectivelyIncoming(item.context.account.peerId) {
switch reason {
case .generic:
if let comment {
rawString = item.presentationData.strings.Chat_PostApproval_Message_AdminDeclinedComment(comment).string
} else {
rawString = item.presentationData.strings.Chat_PostApproval_Message_AdminDeclined
}
case .lowBalance:
rawString = ""
}
} else {
switch reason {
case .generic:
if let comment {
rawString = "\"\(comment)\""
} else {
rawString = ""
}
case .lowBalance:
rawString = ""
}
}
textAlignment = .center
updatedAttributedString = parseMarkdownIntoAttributedString(rawString, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: primaryTextColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor),
linkAttribute: { url in
return ("URL", url)
}
))
}
}
var titleLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if let suggestedPost {
let channelName: String
if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, peer.isMonoForum, let linkedMonoforumId = peer.linkedMonoforumId, let mainChannel = item.message.peers[linkedMonoforumId] as? TelegramChannel {
channelName = EnginePeer(mainChannel).compactDisplayTitle
} else {
channelName = " "
}
let rawString: String
var smallFont = false
switch suggestedPost {
case .approved:
rawString = item.presentationData.strings.Chat_PostApproval_Message_TitleApproved
case let .rejected(reason, comment):
if !item.message.effectivelyIncoming(item.context.account.peerId) {
switch reason {
case .generic:
if comment != nil {
rawString = item.presentationData.strings.Chat_PostApproval_Message_AdminTitleRejectedComment
} else {
rawString = item.presentationData.strings.Chat_PostApproval_Message_AdminTitleRejected
smallFont = true
}
case .lowBalance:
rawString = item.presentationData.strings.Chat_PostApproval_Message_AdminTitleFailedFunds
smallFont = true
}
} else {
switch reason {
case .generic:
if comment != nil {
rawString = item.presentationData.strings.Chat_PostApproval_Message_UserTitleRejectedComment(channelName).string
} else {
rawString = item.presentationData.strings.Chat_PostApproval_Message_UserTitleRejected(channelName).string
smallFont = true
}
case .lowBalance:
rawString = item.presentationData.strings.Chat_PostApproval_Message_UserTitleFailedFunds
smallFont = true
}
}
}
let baseFontSize: CGFloat = smallFont ? 13.0 : 15.0
let titleString = parseMarkdownIntoAttributedString(rawString, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(baseFontSize), textColor: primaryTextColor),
bold: MarkdownAttributeSet(font: Font.bold(baseFontSize), textColor: primaryTextColor),
link: MarkdownAttributeSet(font: Font.semibold(baseFontSize), textColor: primaryTextColor),
linkAttribute: { url in
return ("URL", url)
}
))
titleLayoutAndApply = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
}
let (labelLayout, apply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: updatedAttributedString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: textAlignment, cutout: nil, insets: UIEdgeInsets()))
var labelRects = labelLayout.linesRects()
if labelRects.count > 1 {
let sortedIndices = (0 ..< labelRects.count).sorted(by: { labelRects[$0].width > labelRects[$1].width })
for i in 0 ..< sortedIndices.count {
let index = sortedIndices[i]
for j in -1 ... 1 {
if j != 0 && index + j >= 0 && index + j < sortedIndices.count {
if abs(labelRects[index + j].width - labelRects[index].width) < 40.0 {
labelRects[index + j].size.width = max(labelRects[index + j].width, labelRects[index].width)
labelRects[index].size.width = labelRects[index + j].size.width
}
}
}
}
}
for i in 0 ..< labelRects.count {
labelRects[i] = labelRects[i].insetBy(dx: -7.0, dy: floor((labelRects[i].height - 22.0) / 2.0))
labelRects[i].size.height = 22.0
labelRects[i].origin.x = floor((labelLayout.size.width - labelRects[i].width) / 2.0)
}
let backgroundMaskImage: (CGPoint, UIImage)?
var backgroundMaskUpdated = false
if suggestedPost != nil {
backgroundMaskImage = nil
if cachedMaskBackgroundImage != nil {
backgroundMaskUpdated = true
}
} else {
if let (currentOffset, currentImage, currentRects) = cachedMaskBackgroundImage, currentRects == labelRects {
backgroundMaskImage = (currentOffset, currentImage)
} else {
backgroundMaskImage = LinkHighlightingNode.generateImage(color: .white, inset: 0.0, innerRadius: 11.0, outerRadius: 11.0, rects: labelRects, useModernPathCalculation: false)
backgroundMaskUpdated = true
}
}
var backgroundSize = CGSize(width: labelLayout.size.width, height: labelLayout.size.height)
if let _ = image {
backgroundSize.width = imageSize.width + 2.0
backgroundSize.height += imageSize.height + 10.0
}
let titleSpacing: CGFloat = 14.0
var contentInsets = UIEdgeInsets()
var contentOuterInsets = UIEdgeInsets()
if let titleLayoutAndApply {
backgroundSize.width = max(backgroundSize.width, titleLayoutAndApply.0.size.width)
if labelLayout.size.width != 0.0 {
backgroundSize.height += titleSpacing
}
backgroundSize.height += titleLayoutAndApply.0.size.height
contentInsets = UIEdgeInsets(top: 12.0, left: 16.0, bottom: 12.0, right: 16.0)
contentOuterInsets = UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0)
backgroundSize.width += contentInsets.left + contentInsets.right
backgroundSize.height += contentInsets.top + contentInsets.bottom
} else {
backgroundSize.width += 8.0 + 8.0
backgroundSize.height += 4.0
}
var hasBuyStarsButton = false
if item.message.effectivelyIncoming(item.context.account.peerId), let suggestedPost, case let .rejected(reason, _) = suggestedPost, case .lowBalance = reason {
hasBuyStarsButton = true
}
var buyStarsTitleLayoutAndApply: (TextNodeLayout, () -> TextNode)?
var buyStarsButtonSize: CGSize?
if hasBuyStarsButton {
let serviceColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
let buyStarsTitleLayoutAndApplyValue = makeBuyStarsTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Chat_PostApproval_Message_BuyStars, font: Font.semibold(15.0), textColor: serviceColor.primaryText), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: textAlignment, cutout: nil, insets: UIEdgeInsets()))
buyStarsTitleLayoutAndApply = buyStarsTitleLayoutAndApplyValue
let buyStarsButtonSizeValue = CGSize(width: buyStarsTitleLayoutAndApplyValue.0.size.width + 20.0 * 2.0, height: buyStarsTitleLayoutAndApplyValue.0.size.height + 8.0 * 2.0)
buyStarsButtonSize = buyStarsButtonSizeValue
backgroundSize.width = max(backgroundSize.width, buyStarsButtonSizeValue.width + 8.0 * 2.0)
backgroundSize.height += 15.0 + buyStarsButtonSizeValue.height
}
return (backgroundSize.width, { boundingWidth in
return (CGSize(width: boundingWidth, height: backgroundSize.height + contentOuterInsets.top + contentOuterInsets.bottom), { [weak self] animation, synchronousLoads, _ in
if let strongSelf = self {
strongSelf.item = item
let maskPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: imageSize), cornerRadius: 15.5)
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((boundingWidth - imageSize.width) / 2.0), y: labelLayout.size.height + 12.0), size: imageSize)
if let image = image {
let imageNode: TransformImageNode
if let current = strongSelf.imageNode {
imageNode = current
} else {
imageNode = TransformImageNode()
let shape = CAShapeLayer()
shape.path = maskPath.cgPath
imageNode.layer.mask = shape
strongSelf.imageNode = imageNode
strongSelf.insertSubnode(imageNode, at: 0)
strongSelf.insertSubnode(strongSelf.mediaBackgroundNode, at: 0)
}
strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(context: item.context, userLocation: .peer(item.message.id.peerId), photoReference: .message(message: MessageReference(item.message), media: image), displayAtSize: nil, storeToDownloadsPeerId: nil).startStrict())
let updateImageSignal = chatMessagePhoto(postbox: item.context.account.postbox, userLocation: .peer(item.message.id.peerId), photoReference: .message(message: MessageReference(item.message), media: image), synchronousLoad: synchronousLoads)
imageNode.setSignal(updateImageSignal, attemptSynchronously: synchronousLoads)
let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets())
let apply = imageNode.asyncLayout()(arguments)
apply()
imageNode.frame = imageFrame
strongSelf.mediaBackgroundNode.frame = imageFrame.insetBy(dx: -2.0, dy: -2.0)
} else if let imageNode = strongSelf.imageNode {
strongSelf.mediaBackgroundNode.removeFromSupernode()
imageNode.removeFromSupernode()
strongSelf.imageNode = nil
}
strongSelf.mediaBackgroundNode.image = backgroundImage
if let image = image, let video = image.videoRepresentations.last, let id = image.id?.id {
let videoFileReference = FileMediaReference.message(message: MessageReference(item.message), media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: image.representations, videoThumbnails: [], immediateThumbnailData: image.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []))
let videoContent = NativeVideoContent(id: .profileVideo(id, "action"), userLocation: .peer(item.message.id.peerId), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, storeAfterDownload: nil)
if videoContent.id != strongSelf.videoContent?.id {
let mediaManager = item.context.sharedContext.mediaManager
let videoNode = UniversalVideoNode(context: item.context, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay)
videoNode.isUserInteractionEnabled = false
videoNode.ownsContentNodeUpdated = { [weak self] owns in
if let strongSelf = self {
strongSelf.videoNode?.isHidden = !owns
}
}
strongSelf.videoContent = videoContent
strongSelf.videoNode = videoNode
videoNode.updateLayout(size: imageSize, transition: .immediate)
videoNode.frame = imageFrame
let shape = CAShapeLayer()
shape.path = maskPath.cgPath
videoNode.layer.mask = shape
strongSelf.addSubnode(videoNode)
videoNode.canAttachContent = true
if let videoStartTimestamp = video.startTimestamp {
videoNode.seek(videoStartTimestamp)
} else {
videoNode.seek(0.0)
}
videoNode.play()
}
} else if let videoNode = strongSelf.videoNode {
strongSelf.videoContent = nil
strongSelf.videoNode = nil
videoNode.removeFromSupernode()
}
let _ = apply(TextNodeWithEntities.Arguments(
context: item.context,
cache: item.controllerInteraction.presentationContext.animationCache,
renderer: item.controllerInteraction.presentationContext.animationRenderer,
placeholderColor: item.presentationData.theme.theme.chat.message.freeform.withWallpaper.reactionInactiveBackground,
attemptSynchronous: synchronousLoads
))
var labelFrame: CGRect
let contentFrame: CGRect
if let (titleLayout, titleApply) = titleLayoutAndApply {
contentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((boundingWidth - backgroundSize.width) * 0.5), y: contentOuterInsets.top), size: backgroundSize)
let titleFrame = CGRect(origin: CGPoint(x: contentFrame.minX + floor((contentFrame.width - titleLayout.size.width) * 0.5), y: contentFrame.minY + contentInsets.top), size: titleLayout.size)
labelFrame = CGRect(origin: CGPoint(x: contentFrame.minX + contentInsets.left, y: titleFrame.maxY + titleSpacing), size: labelLayout.size)
if textAlignment == .center {
labelFrame.origin.x = contentFrame.minX + floor((contentFrame.width - labelFrame.width) * 0.5)
}
let titleNode = titleApply()
if strongSelf.titleNode !== titleNode {
strongSelf.titleNode?.removeFromSupernode()
strongSelf.titleNode = titleNode
strongSelf.addSubnode(titleNode)
titleNode.anchorPoint = CGPoint()
titleNode.frame = titleFrame
} else {
animation.animator.updatePosition(layer: titleNode.layer, position: titleFrame.origin, completion: nil)
titleNode.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
}
} else {
labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((boundingWidth - labelLayout.size.width) / 2.0) - 1.0, y: image != nil ? 2.0 : floorToScreenPixels((backgroundSize.height - labelLayout.size.height) / 2.0) - 1.0), size: labelLayout.size)
contentFrame = labelFrame
}
if hasBuyStarsButton, let (buyStarsTitleLayout, buyStarsTitleApply) = buyStarsTitleLayoutAndApply, let buyStarsButtonSize {
let buyStarsButton: HighlightTrackingButton
if let current = strongSelf.buyStarsButton {
buyStarsButton = current
} else {
buyStarsButton = HighlightTrackingButton()
buyStarsButton.clipsToBounds = true
strongSelf.buyStarsButton = buyStarsButton
strongSelf.view.addSubview(buyStarsButton)
buyStarsButton.highligthedChanged = { [weak buyStarsButton] highlighted in
guard let buyStarsButton else {
return
}
if highlighted {
buyStarsButton.layer.removeAnimation(forKey: "opacity")
buyStarsButton.alpha = 0.6
} else {
buyStarsButton.alpha = 1.0
buyStarsButton.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
buyStarsButton.addTarget(strongSelf, action: #selector(strongSelf.buyStarsPressed), for: .touchUpInside)
}
let buttonStarsNode: PremiumStarsNode
if let current = strongSelf.buttonStarsNode {
buttonStarsNode = current
} else {
buttonStarsNode = PremiumStarsNode()
buttonStarsNode.isUserInteractionEnabled = false
strongSelf.buttonStarsNode = buttonStarsNode
buyStarsButton.addSubview(buttonStarsNode.view)
}
let buyStarsTitle = buyStarsTitleApply()
if buyStarsTitle !== strongSelf.buyStarsTitle {
buyStarsTitle.isUserInteractionEnabled = false
strongSelf.buyStarsTitle?.view.removeFromSuperview()
}
strongSelf.buyStarsTitle = buyStarsTitle
buyStarsButton.addSubview(buyStarsTitle.view)
let buttonTitleSize = buyStarsTitleLayout.size
let buttonFrame = CGRect(origin: CGPoint(x: contentFrame.minX + floor((contentFrame.width - buyStarsButtonSize.width) * 0.5), y: labelFrame.minY - 2.0), size: buyStarsButtonSize)
buyStarsButton.frame = buttonFrame
buyStarsButton.layer.cornerRadius = buttonFrame.height * 0.5
buyStarsTitle.frame = CGRect(origin: CGPoint(x: floor((buyStarsButtonSize.width - buttonTitleSize.width) * 0.5), y: floor((buyStarsButtonSize.height - buttonTitleSize.height) * 0.5)), size: buttonTitleSize)
buyStarsButton.backgroundColor = item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12)
buttonStarsNode.frame = CGRect(origin: CGPoint(), size: buyStarsButtonSize)
} else {
if let buyStarsTitle = strongSelf.buyStarsTitle {
strongSelf.buyStarsTitle = nil
buyStarsTitle.view.removeFromSuperview()
}
if let buyStarsButton = strongSelf.buyStarsButton {
strongSelf.buyStarsButton = nil
buyStarsButton.removeFromSuperview()
}
if let buttonStarsNode = strongSelf.buttonStarsNode {
strongSelf.buttonStarsNode = nil
buttonStarsNode.view.removeFromSuperview()
}
}
if let leadingIcon {
let leadingIconView: UIImageView
if let current = strongSelf.leadingIconView {
leadingIconView = current
} else {
leadingIconView = UIImageView()
strongSelf.leadingIconView = leadingIconView
strongSelf.view.addSubview(leadingIconView)
}
leadingIconView.image = leadingIcon
if let lineRect = labelLayout.linesRects().first, let iconImage = leadingIconView.image {
let iconSize = iconImage.size
var iconFrame = CGRect(origin: CGPoint(x: lineRect.minX + labelFrame.minX - 1.0, y: labelFrame.minY), size: iconSize)
if !isStory {
iconFrame.origin.x += 3.0
}
leadingIconView.frame = iconFrame
}
} else if let leadingIconView = strongSelf.leadingIconView {
strongSelf.leadingIconView = nil
leadingIconView.removeFromSuperview()
}
animation.animator.updateFrame(layer: strongSelf.labelNode.textNode.layer, frame: labelFrame, completion: nil)
strongSelf.backgroundColorNode.backgroundColor = selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
if !labelLayout.spoilers.isEmpty {
let dustColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
let dustNode: InvisibleInkDustNode
if let current = strongSelf.dustNode {
dustNode = current
} else {
dustNode = InvisibleInkDustNode(textNode: nil, enableAnimations: item.context.sharedContext.energyUsageSettings.fullTranslucency)
dustNode.isUserInteractionEnabled = false
strongSelf.dustNode = dustNode
strongSelf.insertSubnode(dustNode, aboveSubnode: strongSelf.labelNode.textNode)
}
dustNode.frame = labelFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 1.0)
dustNode.update(size: dustNode.frame.size, color: dustColor, textColor: dustColor, rects: labelLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: labelLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) })
} else if let dustNode = strongSelf.dustNode {
dustNode.removeFromSupernode()
strongSelf.dustNode = nil
}
let baseBackgroundFrame = labelFrame.offsetBy(dx: 0.0, dy: -11.0)
if var rect = strongSelf.labelNode.textNode.cachedLayout?.allAttributeRects(name: TelegramTextAttributes.Button).first?.1 {
rect = rect.insetBy(dx: -2.0, dy: 2.0).offsetBy(dx: 0.0, dy: 1.0 - UIScreenPixel)
let highlightNode: LinkHighlightingNode
if let current = strongSelf.expandHighlightingNode {
highlightNode = current
} else {
highlightNode = LinkHighlightingNode(color: UIColor(rgb: 0x000000, alpha: 0.1))
highlightNode.outerRadius = 7.5
strongSelf.insertSubnode(highlightNode, belowSubnode: strongSelf.labelNode.textNode)
strongSelf.expandHighlightingNode = highlightNode
}
highlightNode.frame = strongSelf.labelNode.textNode.frame
highlightNode.updateRects([rect])
} else {
strongSelf.expandHighlightingNode?.removeFromSupernode()
strongSelf.expandHighlightingNode = nil
}
if suggestedPost != nil {
let backgroundFrame = contentFrame
if strongSelf.backgroundNode == nil {
if let backgroundNode = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
strongSelf.backgroundNode = backgroundNode
backgroundNode.addSubnode(strongSelf.backgroundColorNode)
strongSelf.insertSubnode(backgroundNode, at: 0)
}
}
strongSelf.backgroundColorNode.isHidden = true
if let backgroundNode = strongSelf.backgroundNode {
backgroundNode.clipsToBounds = true
backgroundNode.cornerRadius = min(backgroundFrame.height * 0.5, 22.0)
backgroundNode.view.mask = nil
animation.animator.updateFrame(layer: backgroundNode.layer, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: backgroundFrame.size), completion: nil)
if let (rect, size) = strongSelf.absoluteRect {
strongSelf.updateAbsoluteRect(rect, within: size)
}
strongSelf.backgroundMaskNode.frame = CGRect(origin: CGPoint(), size: backgroundFrame.size)
strongSelf.backgroundMaskNode.layer.layerTintColor = nil
} else {
animation.animator.updateFrame(layer: strongSelf.backgroundMaskNode.layer, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: backgroundFrame.size), completion: nil)
strongSelf.backgroundMaskNode.layer.layerTintColor = selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).cgColor
}
strongSelf.backgroundMaskNode.image = nil
animation.animator.updateFrame(layer: strongSelf.backgroundColorNode.layer, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size), completion: nil)
strongSelf.cachedMaskBackgroundImage = nil
switch strongSelf.visibility {
case .none:
strongSelf.labelNode.visibilityRect = nil
//strongSelf.spoilerTextNode?.visibilityRect = nil
case let .visible(_, subRect):
var subRect = subRect
subRect.origin.x = 0.0
subRect.size.width = 10000.0
strongSelf.labelNode.visibilityRect = subRect
//strongSelf.spoilerTextNode?.visibilityRect = subRect
}
} else if let (offset, image) = backgroundMaskImage {
if strongSelf.backgroundNode == nil {
if let backgroundNode = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
strongSelf.backgroundNode = backgroundNode
backgroundNode.addSubnode(strongSelf.backgroundColorNode)
strongSelf.insertSubnode(backgroundNode, at: 0)
}
}
strongSelf.backgroundColorNode.isHidden = true
if backgroundMaskUpdated {
if let backgroundNode = strongSelf.backgroundNode {
if labelRects.count == 1 {
backgroundNode.clipsToBounds = true
backgroundNode.cornerRadius = min(32.0, labelRects[0].height / 2.0)
backgroundNode.view.mask = nil
} else {
backgroundNode.clipsToBounds = false
backgroundNode.cornerRadius = 0.0
backgroundNode.view.mask = strongSelf.backgroundMaskNode.view
}
}
}
if let backgroundNode = strongSelf.backgroundNode {
animation.animator.updateFrame(layer: backgroundNode.layer, frame: CGRect(origin: CGPoint(x: baseBackgroundFrame.minX + offset.x, y: baseBackgroundFrame.minY + offset.y), size: image.size), completion: nil)
if let (rect, size) = strongSelf.absoluteRect {
strongSelf.updateAbsoluteRect(rect, within: size)
}
strongSelf.backgroundMaskNode.frame = CGRect(origin: CGPoint(), size: image.size)
strongSelf.backgroundMaskNode.layer.layerTintColor = nil
} else {
animation.animator.updateFrame(layer: strongSelf.backgroundMaskNode.layer, frame: CGRect(origin: CGPoint(x: baseBackgroundFrame.minX + offset.x, y: baseBackgroundFrame.minY + offset.y), size: image.size), completion: nil)
strongSelf.backgroundMaskNode.layer.layerTintColor = selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).cgColor
}
strongSelf.backgroundMaskNode.image = image
animation.animator.updateFrame(layer: strongSelf.backgroundColorNode.layer, frame: CGRect(origin: CGPoint(), size: image.size), completion: nil)
strongSelf.cachedMaskBackgroundImage = (offset, image, labelRects)
switch strongSelf.visibility {
case .none:
strongSelf.labelNode.visibilityRect = nil
//strongSelf.spoilerTextNode?.visibilityRect = nil
case let .visible(_, subRect):
var subRect = subRect
subRect.origin.x = 0.0
subRect.size.width = 10000.0
strongSelf.labelNode.visibilityRect = subRect
//strongSelf.spoilerTextNode?.visibilityRect = subRect
}
}
}
})
})
})
}
}
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
self.absoluteRect = (rect, containerSize)
if let backgroundNode = self.backgroundNode {
var backgroundFrame = backgroundNode.frame
backgroundFrame.origin.x += rect.minX
backgroundFrame.origin.y += rect.minY
backgroundNode.update(rect: backgroundFrame, within: containerSize, transition: .immediate)
}
}
override public func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) {
if let backgroundNode = self.backgroundNode {
backgroundNode.offset(value: value, animationCurve: animationCurve, duration: duration)
}
}
override public func applyAbsoluteOffsetSpring(value: CGFloat, duration: Double, damping: CGFloat) {
if let backgroundNode = self.backgroundNode {
backgroundNode.offsetSpring(value: value, duration: duration, damping: damping)
}
}
override public func updateTouchesAtPoint(_ point: CGPoint?) {
if let item = self.item {
var rects: [(CGRect, CGRect)]?
let textNodeFrame = self.labelNode.textNode.frame
if let point = point {
if let (index, attributes) = self.labelNode.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY - 10.0)) {
let possibleNames: [String] = [
TelegramTextAttributes.URL,
TelegramTextAttributes.PeerMention,
TelegramTextAttributes.PeerTextMention,
TelegramTextAttributes.BotCommand,
TelegramTextAttributes.Hashtag
]
for name in possibleNames {
if let _ = attributes[NSAttributedString.Key(rawValue: name)] {
rects = self.labelNode.textNode.lineAndAttributeRects(name: name, at: index)
break
}
}
}
}
if let rects = rects {
var mappedRects: [CGRect] = []
for i in 0 ..< rects.count {
let lineRect = rects[i].0
var itemRect = rects[i].1
itemRect.origin.x = floor((textNodeFrame.size.width - lineRect.width) / 2.0) + itemRect.origin.x
mappedRects.append(itemRect)
}
let linkHighlightingNode: LinkHighlightingNode
if let current = self.linkHighlightingNode {
linkHighlightingNode = current
} else {
let serviceColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
linkHighlightingNode = LinkHighlightingNode(color: serviceColor.linkHighlight)
linkHighlightingNode.useModernPathCalculation = false
linkHighlightingNode.inset = 2.5
self.linkHighlightingNode = linkHighlightingNode
self.insertSubnode(linkHighlightingNode, belowSubnode: self.labelNode.textNode)
}
linkHighlightingNode.frame = self.labelNode.textNode.frame.offsetBy(dx: 0.0, dy: 1.5)
linkHighlightingNode.updateRects(mappedRects)
} else if let linkHighlightingNode = self.linkHighlightingNode {
self.linkHighlightingNode = nil
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
linkHighlightingNode?.removeFromSupernode()
})
}
}
}
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
guard let item = self.item else {
return ChatMessageBubbleContentTapAction(content: .none)
}
let textNodeFrame = self.labelNode.textNode.frame
if let (index, attributes) = self.labelNode.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY - 10.0)), gesture == .tap {
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
var concealed = true
if let (attributeText, fullText) = self.labelNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
}
return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed)))
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
if peerMention.peerId == item.context.account.peerId, let action = item.message.media.first as? TelegramMediaAction, case .customText = action.action {
return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false))
} else {
return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: true))
}
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
return ChatMessageBubbleContentTapAction(content: .textMention(peerName))
} else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String {
return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand))
} else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
return ChatMessageBubbleContentTapAction(content: .hashtag(hashtag.peerName, hashtag.hashtag))
}
}
if let imageNode = self.imageNode, imageNode.frame.contains(point) {
return ChatMessageBubbleContentTapAction(content: .openMessage)
}
if let buyStarsButton = self.buyStarsButton, buyStarsButton.frame.contains(point) {
return ChatMessageBubbleContentTapAction(content: .ignore)
}
if let backgroundNode = self.backgroundNode, backgroundNode.frame.contains(point) {
if let item = self.item, item.message.media.contains(where: { $0 is TelegramMediaStory }) {
return ChatMessageBubbleContentTapAction(content: .none)
} else {
return ChatMessageBubbleContentTapAction(content: .openMessage)
}
} else {
return ChatMessageBubbleContentTapAction(content: .none)
}
}
}
@@ -0,0 +1,27 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageActionButtonsNode",
module_name = "ChatMessageActionButtonsNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AsyncDisplayKit",
"//submodules/TelegramCore",
"//submodules/Postbox",
"//submodules/Display",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/WallpaperBackgroundNode",
"//submodules/UrlHandling",
"//submodules/TelegramUI/Components/TextLoadingEffect",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,705 @@
import Foundation
import UIKit
import AsyncDisplayKit
import TelegramCore
import Postbox
import Display
import TelegramPresentationData
import AccountContext
import WallpaperBackgroundNode
import UrlHandling
import SwiftSignalKit
import TextLoadingEffect
private let titleFont = Font.medium(16.0)
private extension UIBezierPath {
convenience init(roundRect rect: CGRect, topLeftRadius: CGFloat = 0.0, topRightRadius: CGFloat = 0.0, bottomLeftRadius: CGFloat = 0.0, bottomRightRadius: CGFloat = 0.0) {
self.init()
let path = CGMutablePath()
let topLeft = rect.origin
let topRight = CGPoint(x: rect.maxX, y: rect.minY)
let bottomRight = CGPoint(x: rect.maxX, y: rect.maxY)
let bottomLeft = CGPoint(x: rect.minX, y: rect.maxY)
if topLeftRadius != .zero {
path.move(to: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y))
} else {
path.move(to: CGPoint(x: topLeft.x, y: topLeft.y))
}
if topRightRadius != .zero {
path.addLine(to: CGPoint(x: topRight.x-topRightRadius, y: topRight.y))
path.addCurve(to: CGPoint(x: topRight.x, y: topRight.y+topRightRadius), control1: CGPoint(x: topRight.x, y: topRight.y), control2:CGPoint(x: topRight.x, y: topRight.y + topRightRadius))
} else {
path.addLine(to: CGPoint(x: topRight.x, y: topRight.y))
}
if bottomRightRadius != .zero {
path.addLine(to: CGPoint(x: bottomRight.x, y: bottomRight.y-bottomRightRadius))
path.addCurve(to: CGPoint(x: bottomRight.x-bottomRightRadius, y: bottomRight.y), control1: CGPoint(x: bottomRight.x, y: bottomRight.y), control2: CGPoint(x: bottomRight.x-bottomRightRadius, y: bottomRight.y))
} else {
path.addLine(to: CGPoint(x: bottomRight.x, y: bottomRight.y))
}
if bottomLeftRadius != .zero {
path.addLine(to: CGPoint(x: bottomLeft.x+bottomLeftRadius, y: bottomLeft.y))
path.addCurve(to: CGPoint(x: bottomLeft.x, y: bottomLeft.y-bottomLeftRadius), control1: CGPoint(x: bottomLeft.x, y: bottomLeft.y), control2: CGPoint(x: bottomLeft.x, y: bottomLeft.y-bottomLeftRadius))
} else {
path.addLine(to: CGPoint(x: bottomLeft.x, y: bottomLeft.y))
}
if topLeftRadius != .zero {
path.addLine(to: CGPoint(x: topLeft.x, y: topLeft.y+topLeftRadius))
path.addCurve(to: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y) , control1: CGPoint(x: topLeft.x, y: topLeft.y) , control2: CGPoint(x: topLeft.x+topLeftRadius, y: topLeft.y))
} else {
path.addLine(to: CGPoint(x: topLeft.x, y: topLeft.y))
}
path.closeSubpath()
self.cgPath = path
}
}
private final class ChatMessageActionButtonNode: ASDisplayNode {
private var backgroundBlurView: PortalView?
private var titleNode: TextNode?
private var iconNode: ASImageNode?
private var buttonView: HighlightTrackingButton?
private var wallpaperBackgroundNode: WallpaperBackgroundNode?
private var backgroundContent: WallpaperBubbleBackgroundNode?
private var backgroundColorNode: ASDisplayNode?
private var maskPath: CGPath?
private var loadingEffectView: TextLoadingEffectView?
private var absolutePosition: (CGRect, CGSize)?
private var button: ReplyMarkupButton?
var pressed: ((ReplyMarkupButton, Promise<Bool>) -> Void)?
var longTapped: ((ReplyMarkupButton) -> Void)?
var longTapRecognizer: UILongPressGestureRecognizer?
private let accessibilityArea: AccessibilityAreaNode
private var progressDisposable: Disposable?
override init() {
self.accessibilityArea = AccessibilityAreaNode()
self.accessibilityArea.accessibilityTraits = .button
super.init()
self.addSubnode(self.accessibilityArea)
self.accessibilityArea.activate = { [weak self] in
self?.buttonPressed()
return true
}
}
deinit {
self.progressDisposable?.dispose()
}
override func didLoad() {
super.didLoad()
let buttonView = HighlightTrackingButton(frame: self.bounds)
buttonView.addTarget(self, action: #selector(self.buttonPressed), for: [.touchUpInside])
self.buttonView = buttonView
buttonView.isAccessibilityElement = false
self.view.addSubview(buttonView)
buttonView.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
//strongSelf.backgroundBlurNode.layer.removeAnimation(forKey: "opacity")
//strongSelf.backgroundBlurNode.alpha = 0.55
if let backgroundBlurView = strongSelf.backgroundBlurView {
backgroundBlurView.view.layer.removeAnimation(forKey: "opacity")
backgroundBlurView.view.alpha = 0.55
}
strongSelf.backgroundContent?.layer.removeAnimation(forKey: "opacity")
strongSelf.backgroundContent?.alpha = 0.55
} else {
//strongSelf.backgroundBlurNode.alpha = 1.0
//strongSelf.backgroundBlurNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
if let backgroundBlurView = strongSelf.backgroundBlurView {
backgroundBlurView.view.alpha = 1.0
backgroundBlurView.view.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
}
strongSelf.backgroundContent?.alpha = 1.0
strongSelf.backgroundContent?.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
}
}
}
let longTapRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.longTapGesture(_:)))
longTapRecognizer.minimumPressDuration = 0.3
buttonView.addGestureRecognizer(longTapRecognizer)
self.longTapRecognizer = longTapRecognizer
}
@objc func buttonPressed() {
if let button = self.button, let pressed = self.pressed {
let progressPromise = Promise<Bool>()
pressed(button, progressPromise)
self.progressDisposable?.dispose()
self.progressDisposable = (progressPromise.get()
|> deliverOnMainQueue).startStrict(next: { [weak self] isLoading in
guard let self else {
return
}
self.updateIsLoading(isLoading: isLoading)
})
}
}
private func updateIsLoading(isLoading: Bool) {
if isLoading {
if self.loadingEffectView == nil {
let loadingEffectView = TextLoadingEffectView(frame: CGRect())
self.loadingEffectView = loadingEffectView
if let iconNode = self.iconNode, iconNode.view.superview != nil {
self.view.insertSubview(loadingEffectView, belowSubview: iconNode.view)
} else if let titleNode = self.titleNode, titleNode.view.superview != nil {
self.view.insertSubview(loadingEffectView, belowSubview: titleNode.view)
} else {
self.view.addSubview(loadingEffectView)
}
if let buttonView = self.buttonView, let maskPath = self.maskPath {
let loadingFrame = buttonView.frame
loadingEffectView.frame = loadingFrame
loadingEffectView.update(color: UIColor(white: 1.0, alpha: 1.0), rect: CGRect(origin: CGPoint(), size: loadingFrame.size), path: maskPath)
}
}
} else {
if let loadingEffectView {
self.loadingEffectView = nil
loadingEffectView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak loadingEffectView] _ in
loadingEffectView?.removeFromSuperview()
})
}
}
}
@objc func longTapGesture(_ recognizer: UILongPressGestureRecognizer) {
if let button = self.button, let longTapped = self.longTapped, recognizer.state == .began {
longTapped(button)
}
}
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
self.absolutePosition = (rect, containerSize)
if let backgroundContent = self.backgroundContent {
var backgroundFrame = backgroundContent.frame
backgroundFrame.origin.x += rect.minX
backgroundFrame.origin.y += rect.minY
backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate)
}
}
class func asyncLayout(_ maybeNode: ChatMessageActionButtonNode?) -> (_ context: AccountContext, _ theme: ChatPresentationThemeData, _ bubbleCorners: PresentationChatBubbleCorners, _ strings: PresentationStrings, _ backgroundNode: WallpaperBackgroundNode?, _ message: Message, _ button: ReplyMarkupButton, _ customInfo: ChatMessageActionButtonsNode.CustomInfo?, _ constrainedWidth: CGFloat, _ position: MessageBubbleActionButtonPosition) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonNode))) {
let titleLayout = TextNode.asyncLayout(maybeNode?.titleNode)
return { context, theme, bubbleCorners, strings, backgroundNode, message, button, customInfo, constrainedWidth, position in
let incoming = message.effectivelyIncoming(context.account.peerId)
let graphics = PresentationResourcesChat.additionalGraphics(theme.theme, wallpaper: theme.wallpaper, bubbleCorners: bubbleCorners)
let messageTheme = incoming ? theme.theme.chat.message.incoming : theme.theme.chat.message.outgoing
let titleColor = bubbleVariableColor(variableColor: messageTheme.actionButtonsTextColor, wallpaper: theme.wallpaper)
var isStarsPayment = false
let iconImage: UIImage?
var tintColor: UIColor?
if let customIcon = customInfo?.icon {
switch customIcon {
case .suggestedPostReject:
iconImage = PresentationResourcesChat.messageButtonsPostReject(theme.theme)
case .suggestedPostApprove:
iconImage = PresentationResourcesChat.messageButtonsPostApprove(theme.theme)
case .suggestedPostEdit:
iconImage = PresentationResourcesChat.messageButtonsPostEdit(theme.theme)
case .actionArrow:
iconImage = PresentationResourcesChat.chatBubbleArrowFreeImage(theme.theme)
}
tintColor = titleColor
} else {
switch button.action {
case .text:
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingMessageIconImage : graphics.chatBubbleActionButtonOutgoingMessageIconImage
case let .url(value):
var isApp = false
if isTelegramMeLink(value), let internalUrl = parseFullInternalUrl(sharedContext: context.sharedContext, context: context, url: value) {
if case .peer(_, .appStart) = internalUrl {
isApp = true
} else if case .peer(_, .attachBotStart) = internalUrl {
isApp = true
} else if case .startAttach = internalUrl {
isApp = true
}
}
if isApp {
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingWebAppIconImage : graphics.chatBubbleActionButtonOutgoingWebAppIconImage
} else if value.lowercased().contains("?startgroup=") {
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingAddToChatIconImage : graphics.chatBubbleActionButtonOutgoingAddToChatIconImage
} else {
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingLinkIconImage : graphics.chatBubbleActionButtonOutgoingLinkIconImage
}
case .urlAuth:
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingLinkIconImage : graphics.chatBubbleActionButtonOutgoingLinkIconImage
case .requestPhone:
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingPhoneIconImage : graphics.chatBubbleActionButtonOutgoingPhoneIconImage
case .requestMap:
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingLocationIconImage : graphics.chatBubbleActionButtonOutgoingLocationIconImage
case .switchInline:
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingShareIconImage : graphics.chatBubbleActionButtonOutgoingShareIconImage
case .payment:
if button.title.contains("⭐️") {
isStarsPayment = true
iconImage = nil
} else {
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingPaymentIconImage : graphics.chatBubbleActionButtonOutgoingPaymentIconImage
}
case .openUserProfile:
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingProfileIconImage : graphics.chatBubbleActionButtonOutgoingProfileIconImage
case .openWebView:
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingWebAppIconImage : graphics.chatBubbleActionButtonOutgoingWebAppIconImage
case .copyText:
iconImage = incoming ? graphics.chatBubbleActionButtonIncomingCopyIconImage : graphics.chatBubbleActionButtonOutgoingCopyIconImage
default:
iconImage = nil
}
}
let sideInset: CGFloat = 8.0
let minimumSideInset: CGFloat = 4.0 + (iconImage?.size.width ?? 0.0)
var title = button.title
if case .payment = button.action {
for media in message.media {
if let invoice = media as? TelegramMediaInvoice {
if invoice.receiptMessageId != nil {
title = strings.Message_ReplyActionButtonShowReceipt
}
}
}
}
let attributedTitle: NSAttributedString
if isStarsPayment {
let updatedTitle = title.replacingOccurrences(of: "⭐️", with: " # ")
let buttonAttributedString = NSMutableAttributedString(string: updatedTitle, font: titleFont, textColor: titleColor, paragraphAlignment: .center)
if let range = buttonAttributedString.string.range(of: "#"), let starImage = UIImage(bundleImageName: "Item List/PremiumIcon") {
buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string))
buttonAttributedString.addAttribute(.foregroundColor, value: titleColor, range: NSRange(range, in: buttonAttributedString.string))
buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string))
}
attributedTitle = buttonAttributedString
} else {
attributedTitle = NSAttributedString(string: title, font: titleFont, textColor: titleColor)
}
var customIconSpaceWidth: CGFloat = 0.0
if let iconImage, customInfo?.icon != nil {
customIconSpaceWidth = 3.0 + iconImage.size.width
}
let (titleSize, titleApply) = titleLayout(TextNodeLayoutArguments(attributedString: attributedTitle, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(44.0, constrainedWidth - minimumSideInset - minimumSideInset - customIconSpaceWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets(top: 1.0, left: 0.0, bottom: 1.0, right: 0.0)))
let contentWidth = titleSize.size.width + sideInset + sideInset + customIconSpaceWidth
return (contentWidth, { width in
return (CGSize(width: width, height: 42.0), { animation in
var animation = animation
let node: ChatMessageActionButtonNode
if let maybeNode = maybeNode {
node = maybeNode
} else {
node = ChatMessageActionButtonNode()
animation = .None
}
node.wallpaperBackgroundNode = backgroundNode
node.button = button
switch button.action {
case .url:
node.longTapRecognizer?.isEnabled = true
default:
node.longTapRecognizer?.isEnabled = false
}
//animation.animator.updateFrame(layer: node.backgroundBlurNode.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: max(0.0, width), height: 42.0)), completion: nil)
if node.backgroundBlurView == nil {
if let backgroundBlurView = backgroundNode?.makeFreeBackground() {
node.backgroundBlurView = backgroundBlurView
node.view.insertSubview(backgroundBlurView.view, at: 0)
}
}
if let backgroundBlurView = node.backgroundBlurView {
animation.animator.updateFrame(layer: backgroundBlurView.view.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: max(0.0, width), height: 42.0)), completion: nil)
}
/*node.backgroundBlurNode.update(size: node.backgroundBlurNode.bounds.size, cornerRadius: 0.0, animator: animation.animator)
node.backgroundBlurNode.updateColor(color: selectDateFillStaticColor(theme: theme.theme, wallpaper: theme.wallpaper), enableBlur: context.sharedContext.energyUsageSettings.fullTranslucency && dateFillNeedsBlur(theme: theme.theme, wallpaper: theme.wallpaper), transition: .immediate)*/
if backgroundNode?.hasExtraBubbleBackground() == true {
if node.backgroundContent == nil, let backgroundContent = backgroundNode?.makeBubbleBackground(for: .free) {
backgroundContent.clipsToBounds = true
backgroundContent.allowsGroupOpacity = true
node.backgroundContent = backgroundContent
node.insertSubnode(backgroundContent, at: 0)
let backgroundColorNode = ASDisplayNode()
backgroundColorNode.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.08)
backgroundContent.addSubnode(backgroundColorNode)
node.backgroundColorNode = backgroundColorNode
}
} else {
node.backgroundContent?.removeFromSupernode()
node.backgroundContent = nil
node.backgroundColorNode?.removeFromSupernode()
node.backgroundColorNode = nil
}
node.cornerRadius = bubbleCorners.auxiliaryRadius
node.clipsToBounds = true
if let backgroundContent = node.backgroundContent {
//node.backgroundBlurNode.isHidden = true
node.backgroundBlurView?.view.isHidden = true
animation.animator.updateFrame(layer: backgroundContent.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: max(0.0, width), height: 42.0)), completion: nil)
node.backgroundColorNode?.frame = backgroundContent.bounds
if let (rect, containerSize) = node.absolutePosition {
var backgroundFrame = backgroundContent.frame
backgroundFrame.origin.x += rect.minX
backgroundFrame.origin.y += rect.minY
backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate)
}
} else {
node.backgroundBlurView?.view.isHidden = false
}
let rect = CGRect(origin: CGPoint(), size: CGSize(width: max(0.0, width), height: 42.0))
let maskPath: CGPath?
var needsMask = true
switch position {
case .bottomSingle:
maskPath = UIBezierPath(roundRect: rect, topLeftRadius: bubbleCorners.auxiliaryRadius, topRightRadius: bubbleCorners.auxiliaryRadius, bottomLeftRadius: bubbleCorners.mainRadius, bottomRightRadius: bubbleCorners.mainRadius).cgPath
case .bottomLeft:
maskPath = UIBezierPath(roundRect: rect, topLeftRadius: bubbleCorners.auxiliaryRadius, topRightRadius: bubbleCorners.auxiliaryRadius, bottomLeftRadius: bubbleCorners.mainRadius, bottomRightRadius: bubbleCorners.auxiliaryRadius).cgPath
case .bottomRight:
maskPath = UIBezierPath(roundRect: rect, topLeftRadius: bubbleCorners.auxiliaryRadius, topRightRadius: bubbleCorners.auxiliaryRadius, bottomLeftRadius: bubbleCorners.auxiliaryRadius, bottomRightRadius: bubbleCorners.mainRadius).cgPath
default:
needsMask = false
maskPath = UIBezierPath(roundRect: rect, topLeftRadius: bubbleCorners.auxiliaryRadius, topRightRadius: bubbleCorners.auxiliaryRadius, bottomLeftRadius: bubbleCorners.auxiliaryRadius, bottomRightRadius: bubbleCorners.auxiliaryRadius).cgPath
}
let currentMaskPath = (node.layer.mask as? CAShapeLayer)?.path
node.maskPath = maskPath
let effectiveMaskPath = needsMask ? maskPath : nil
if currentMaskPath != effectiveMaskPath {
if let effectiveMaskPath = effectiveMaskPath {
if let shapeLayer = node.layer.mask as? SimpleShapeLayer {
animation.animator.updateShapeLayerPath(layer: shapeLayer, path: effectiveMaskPath, completion: nil)
} else {
let shapeLayer = SimpleShapeLayer()
shapeLayer.path = effectiveMaskPath
node.layer.mask = shapeLayer
}
} else {
node.layer.mask = nil
}
}
if iconImage != nil {
if node.iconNode == nil {
let iconNode = ASImageNode()
iconNode.contentMode = .center
node.iconNode = iconNode
node.addSubnode(iconNode)
}
node.iconNode?.image = iconImage
node.iconNode?.customTintColor = tintColor
} else if node.iconNode != nil {
node.iconNode?.removeFromSupernode()
node.iconNode = nil
}
let titleNode = titleApply()
if node.titleNode !== titleNode {
node.titleNode = titleNode
node.addSubnode(titleNode)
titleNode.isUserInteractionEnabled = false
}
var titleFrame = CGRect(origin: CGPoint(x: floor((width - titleSize.size.width) / 2.0), y: floor((42.0 - titleSize.size.height) / 2.0) + 1.0), size: titleSize.size)
if let image = node.iconNode?.image, customInfo?.icon != nil {
if customInfo?.icon == .actionArrow {
titleFrame.origin.x = floorToScreenPixels((width - titleSize.size.width - image.size.width + 1.0) * 0.5) - 0.0
} else {
titleFrame.origin.x = floorToScreenPixels((width - titleSize.size.width - image.size.width - 3.0) * 0.5) + 3.0 + image.size.width
}
}
titleNode.layer.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
animation.animator.updatePosition(layer: titleNode.layer, position: CGPoint(x: titleFrame.midX, y: titleFrame.midY), completion: nil)
if let buttonView = node.buttonView {
buttonView.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: 42.0))
}
if let iconNode = node.iconNode {
let iconFrame: CGRect
if customInfo?.icon != nil, let image = iconNode.image {
if customInfo?.icon == .actionArrow {
iconFrame = CGRect(x: titleFrame.maxX + 4.0, y: titleFrame.minY + floorToScreenPixels((titleFrame.height - image.size.height) * 0.5) - 1.0, width: image.size.width, height: image.size.height)
} else {
iconFrame = CGRect(x: titleFrame.minX - 3.0 - image.size.width, y: titleFrame.minY + floorToScreenPixels((titleFrame.height - image.size.height) * 0.5) - 1.0, width: image.size.width, height: image.size.height)
}
} else {
iconFrame = CGRect(x: width - 16.0, y: 4.0, width: 12.0, height: 12.0)
}
animation.animator.updateFrame(layer: iconNode.layer, frame: iconFrame, completion: nil)
}
if let (rect, size) = node.absolutePosition {
node.updateAbsoluteRect(rect, within: size)
}
node.accessibilityArea.accessibilityLabel = title
node.accessibilityArea.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: 42.0))
if let buttonView = node.buttonView {
let isEnabled = customInfo?.isEnabled ?? true
if buttonView.isEnabled != isEnabled {
buttonView.isEnabled = isEnabled
if let backgroundBlurView = node.backgroundBlurView {
backgroundBlurView.view.alpha = isEnabled ? 1.0 : 0.55
}
node.backgroundContent?.alpha = isEnabled ? 1.0 : 0.55
}
}
return node
})
})
}
}
}
public final class ChatMessageActionButtonsNode: ASDisplayNode {
public enum CustomIcon {
case suggestedPostApprove
case suggestedPostReject
case suggestedPostEdit
case actionArrow
}
public struct CustomInfo {
var isEnabled: Bool
var icon: CustomIcon?
public init(isEnabled: Bool, icon: CustomIcon?) {
self.isEnabled = isEnabled
self.icon = icon
}
}
private var buttonNodes: [ChatMessageActionButtonNode] = []
private var buttonPressedWrapper: ((ReplyMarkupButton, Promise<Bool>) -> Void)?
private var buttonLongTappedWrapper: ((ReplyMarkupButton) -> Void)?
public var buttonPressed: ((ReplyMarkupButton, Promise<Bool>) -> Void)?
public var buttonLongTapped: ((ReplyMarkupButton) -> Void)?
private var absolutePosition: (CGRect, CGSize)?
override public init() {
super.init()
self.buttonPressedWrapper = { [weak self] button, promise in
if let buttonPressed = self?.buttonPressed {
buttonPressed(button, promise)
}
}
self.buttonLongTappedWrapper = { [weak self] button in
if let buttonLongTapped = self?.buttonLongTapped {
buttonLongTapped(button)
}
}
}
public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
self.absolutePosition = (rect, containerSize)
for button in self.buttonNodes {
var buttonFrame = button.frame
buttonFrame.origin.x += rect.minX
buttonFrame.origin.y += rect.minY
button.updateAbsoluteRect(buttonFrame, within: containerSize)
}
}
public class func asyncLayout(_ maybeNode: ChatMessageActionButtonsNode?) -> (_ context: AccountContext, _ theme: ChatPresentationThemeData, _ chatBubbleCorners: PresentationChatBubbleCorners, _ strings: PresentationStrings, _ backgroundNode: WallpaperBackgroundNode?, _ replyMarkup: ReplyMarkupMessageAttribute, _ customInfos: [MemoryBuffer: CustomInfo], _ message: Message, _ constrainedWidth: CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)) {
let currentButtonLayouts = maybeNode?.buttonNodes.map { ChatMessageActionButtonNode.asyncLayout($0) } ?? []
return { context, theme, chatBubbleCorners, strings, backgroundNode, replyMarkup, customInfos, message, constrainedWidth in
let buttonHeight: CGFloat = 42.0
let buttonSpacing: CGFloat = 2.0
var overallMinimumRowWidth: CGFloat = 0.0
var finalizeRowLayouts: [[((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonNode))]] = []
var rowIndex = 0
var buttonIndex = 0
for row in replyMarkup.rows {
var maximumRowButtonWidth: CGFloat = 0.0
let maximumButtonWidth: CGFloat = max(1.0, floor((constrainedWidth - CGFloat(max(0, row.buttons.count - 1)) * buttonSpacing) / CGFloat(row.buttons.count)))
var finalizeRowButtonLayouts: [((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonNode))] = []
var rowButtonIndex = 0
for button in row.buttons {
var customInfo: CustomInfo?
if case let .callback(_, data) = button.action {
customInfo = customInfos[data]
}
let buttonPosition: MessageBubbleActionButtonPosition
if rowIndex == replyMarkup.rows.count - 1 {
if row.buttons.count == 1 {
buttonPosition = .bottomSingle
} else if rowButtonIndex == 0 {
buttonPosition = .bottomLeft
} else if rowButtonIndex == row.buttons.count - 1 {
buttonPosition = .bottomRight
} else {
buttonPosition = .middle
}
} else {
buttonPosition = .middle
}
let prepareButtonLayout: (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonNode)))
if buttonIndex < currentButtonLayouts.count {
prepareButtonLayout = currentButtonLayouts[buttonIndex](context, theme, chatBubbleCorners, strings, backgroundNode, message, button, customInfo, maximumButtonWidth, buttonPosition)
} else {
prepareButtonLayout = ChatMessageActionButtonNode.asyncLayout(nil)(context, theme, chatBubbleCorners, strings, backgroundNode, message, button, customInfo, maximumButtonWidth, buttonPosition)
}
maximumRowButtonWidth = max(maximumRowButtonWidth, prepareButtonLayout.minimumWidth)
finalizeRowButtonLayouts.append(prepareButtonLayout.layout)
buttonIndex += 1
rowButtonIndex += 1
}
overallMinimumRowWidth = max(overallMinimumRowWidth, maximumRowButtonWidth * CGFloat(row.buttons.count) + buttonSpacing * max(0.0, CGFloat(row.buttons.count - 1)))
finalizeRowLayouts.append(finalizeRowButtonLayouts)
rowIndex += 1
}
return (min(constrainedWidth, overallMinimumRowWidth), { constrainedWidth in
var buttonFramesAndApply: [(CGRect, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonNode)] = []
var verticalRowOffset: CGFloat = 0.0
verticalRowOffset += buttonSpacing * 0.5
var rowIndex = 0
for finalizeRowButtonLayouts in finalizeRowLayouts {
let actualButtonWidth: CGFloat = max(1.0, floor((constrainedWidth - CGFloat(max(0, finalizeRowButtonLayouts.count - 1)) * buttonSpacing) / CGFloat(finalizeRowButtonLayouts.count)))
var horizontalButtonOffset: CGFloat = 0.0
for finalizeButtonLayout in finalizeRowButtonLayouts {
let (buttonSize, buttonApply) = finalizeButtonLayout(actualButtonWidth)
let buttonFrame = CGRect(origin: CGPoint(x: horizontalButtonOffset, y: verticalRowOffset), size: buttonSize)
buttonFramesAndApply.append((buttonFrame, buttonApply))
horizontalButtonOffset += buttonSize.width + buttonSpacing
}
verticalRowOffset += buttonHeight + buttonSpacing
rowIndex += 1
}
if verticalRowOffset > 0.0 {
verticalRowOffset = max(0.0, verticalRowOffset - buttonSpacing)
}
return (CGSize(width: constrainedWidth, height: verticalRowOffset), { animation in
let node: ChatMessageActionButtonsNode
if let maybeNode = maybeNode {
node = maybeNode
} else {
node = ChatMessageActionButtonsNode()
}
var updatedButtons: [ChatMessageActionButtonNode] = []
var index = 0
for (buttonFrame, buttonApply) in buttonFramesAndApply {
let buttonNode = buttonApply(animation)
updatedButtons.append(buttonNode)
if buttonNode.supernode == nil {
buttonNode.pressed = node.buttonPressedWrapper
buttonNode.longTapped = node.buttonLongTappedWrapper
buttonNode.frame = buttonFrame
node.addSubnode(buttonNode)
} else {
animation.animator.updateFrame(layer: buttonNode.layer, frame: buttonFrame, completion: nil)
}
index += 1
}
var buttonsUpdated = false
if node.buttonNodes.count != updatedButtons.count {
buttonsUpdated = true
} else {
for i in 0 ..< updatedButtons.count {
if updatedButtons[i] !== node.buttonNodes[i] {
buttonsUpdated = true
break
}
}
}
if buttonsUpdated {
for currentButton in node.buttonNodes {
if !updatedButtons.contains(currentButton) {
currentButton.removeFromSupernode()
}
}
}
node.buttonNodes = updatedButtons
if let (rect, size) = node.absolutePosition {
node.updateAbsoluteRect(rect, within: size)
}
return node
})
})
}
}
}
@@ -0,0 +1,61 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageAnimatedStickerItemNode",
module_name = "ChatMessageAnimatedStickerItemNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/TextFormat",
"//submodules/AccountContext",
"//submodules/MediaResources",
"//submodules/StickerResources",
"//submodules/ContextUI",
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
"//submodules/Emoji",
"//submodules/Markdown",
"//submodules/ManagedAnimationNode",
"//submodules/SlotMachineAnimationNode",
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//submodules/ShimmerEffect",
"//submodules/WallpaperBackgroundNode",
"//submodules/LocalMediaResources",
"//submodules/AppBundle",
"//submodules/ChatPresentationInterfaceState",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
"//submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItem",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemView",
"//submodules/TelegramUI/Components/Chat/ChatMessageSwipeToReplyNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageSelectionNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageDeliveryFailedNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageShareButton",
"//submodules/TelegramUI/Components/Chat/ChatMessageThreadInfoNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode",
"//submodules/TelegramUI/Components/Chat/ChatSwipeToReplyRecognizer",
"//submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode",
"//submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode",
"//submodules/TelegramUI/Components/Chat/MessageHaptics",
"//submodules/TelegramUI/Components/Chat/ChatMessageTransitionNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageSuggestedPostInfoNode",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageAttachedContentButtonNode",
module_name = "ChatMessageAttachedContentButtonNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/TelegramPresentationData",
"//submodules/ChatPresentationInterfaceState",
"//submodules/ShimmerEffect",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,191 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import ChatPresentationInterfaceState
import ShimmerEffect
private let buttonFont = Font.semibold(14.0)
private let sharedBackgroundImage = generateStretchableFilledCircleImage(radius: 6.0, color: UIColor.white)?.withRenderingMode(.alwaysTemplate)
public final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode {
private let textNode: TextNode
private var iconView: UIImageView?
private var shimmerEffectNode: ShimmerEffectForegroundNode?
private var backgroundView: UIImageView?
private var regularIconImage: UIImage?
public var pressed: (() -> Void)?
private var titleColor: UIColor?
public init() {
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
super.init()
self.addSubnode(self.textNode)
self.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
let scale = (strongSelf.bounds.width - 10.0) / strongSelf.bounds.width
strongSelf.layer.animateScale(from: 1.0, to: scale, duration: 0.15, removeOnCompletion: false)
} else {
if let presentationLayer = strongSelf.layer.presentation() {
strongSelf.layer.animateScale(from: CGFloat((presentationLayer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0), to: 1.0, duration: 0.25, removeOnCompletion: false)
}
}
}
}
self.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
}
@objc private func buttonPressed() {
self.pressed?()
}
public func startShimmering() {
guard let titleColor = self.titleColor else {
return
}
let shimmerEffectNode: ShimmerEffectForegroundNode
if let current = self.shimmerEffectNode {
shimmerEffectNode = current
} else {
shimmerEffectNode = ShimmerEffectForegroundNode()
shimmerEffectNode.cornerRadius = 6.0
self.insertSubnode(shimmerEffectNode, at: 0)
self.shimmerEffectNode = shimmerEffectNode
}
shimmerEffectNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
let backgroundFrame = self.bounds
shimmerEffectNode.frame = backgroundFrame
shimmerEffectNode.updateAbsoluteRect(CGRect(origin: .zero, size: backgroundFrame.size), within: backgroundFrame.size)
shimmerEffectNode.update(backgroundColor: .clear, foregroundColor: titleColor.withAlphaComponent(0.3), horizontal: true, effectSize: nil, globalTimeOffset: false, duration: nil)
}
public func stopShimmering() {
guard let shimmerEffectNode = self.shimmerEffectNode else {
return
}
self.shimmerEffectNode = nil
shimmerEffectNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak shimmerEffectNode] _ in
shimmerEffectNode?.removeFromSupernode()
})
}
public typealias AsyncLayout = (_ width: CGFloat, _ sideInset: CGFloat?, _ iconImage: UIImage?, _ cornerIcon: Bool, _ title: String, _ titleColor: UIColor, _ inProgress: Bool, _ drawBackground: Bool) -> (CGFloat, (CGFloat, CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageAttachedContentButtonNode))
public static func asyncLayout(_ current: ChatMessageAttachedContentButtonNode?) -> AsyncLayout {
let previousRegularIconImage = current?.regularIconImage
let maybeMakeTextLayout = (current?.textNode).flatMap(TextNode.asyncLayout)
return { width, sideInset, iconImage, cornerIcon, title, titleColor, inProgress, drawBackground in
let targetNode: ChatMessageAttachedContentButtonNode
if let current = current {
targetNode = current
} else {
targetNode = ChatMessageAttachedContentButtonNode()
}
let makeTextLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode)
if let maybeMakeTextLayout = maybeMakeTextLayout {
makeTextLayout = maybeMakeTextLayout
} else {
makeTextLayout = TextNode.asyncLayout(targetNode.textNode)
}
var updatedRegularIconImage: UIImage?
if iconImage !== previousRegularIconImage {
updatedRegularIconImage = iconImage
}
var iconWidth: CGFloat = 0.0
if let iconImage = iconImage {
iconWidth = iconImage.size.width + 5.0
}
let labelInset: CGFloat = sideInset ?? 8.0
let (textSize, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: buttonFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(1.0, width - labelInset * 2.0 - iconWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .left, cutout: nil, insets: UIEdgeInsets()))
return (textSize.size.width + labelInset * 2.0, { refinedWidth, refinedHeight in
let size = CGSize(width: refinedWidth, height: refinedHeight)
return (size, { animation in
targetNode.accessibilityLabel = title
targetNode.titleColor = titleColor
let iconView: UIImageView
if let current = targetNode.iconView {
iconView = current
} else {
iconView = UIImageView()
targetNode.iconView = iconView
targetNode.view.addSubview(iconView)
}
iconView.tintColor = titleColor
if let updatedRegularIconImage = updatedRegularIconImage {
targetNode.regularIconImage = updatedRegularIconImage
if !targetNode.textNode.isHidden {
iconView.image = updatedRegularIconImage.withRenderingMode(.alwaysTemplate)
}
}
let _ = textApply()
let backgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: refinedWidth, height: size.height))
var textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((refinedWidth - textSize.size.width) / 2.0), y: floorToScreenPixels((backgroundFrame.height - textSize.size.height) / 2.0)), size: textSize.size)
if drawBackground {
textFrame.origin.y += 1.0
}
if let image = iconView.image {
let iconFrame: CGRect
if cornerIcon {
iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - image.size.width - 5.0, y: 5.0), size: image.size)
} else {
textFrame.origin.x += floor(image.size.width / 2.0)
iconFrame = CGRect(origin: CGPoint(x: textFrame.minX - image.size.width - 5.0, y: textFrame.minY + floorToScreenPixels((textFrame.height - image.size.height) * 0.5)), size: image.size)
}
animation.animator.updateFrame(layer: iconView.layer, frame: iconFrame, completion: nil)
}
targetNode.textNode.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
animation.animator.updatePosition(layer: targetNode.textNode.layer, position: textFrame.center, completion: nil)
if drawBackground {
let backgroundView: UIImageView
if let current = targetNode.backgroundView {
backgroundView = current
animation.animator.updateFrame(layer: backgroundView.layer, frame: backgroundFrame, completion: nil)
} else {
backgroundView = UIImageView()
backgroundView.image = sharedBackgroundImage
targetNode.backgroundView = backgroundView
targetNode.view.insertSubview(backgroundView, at: 0)
backgroundView.frame = backgroundFrame
}
backgroundView.tintColor = titleColor.withMultipliedAlpha(0.1)
} else if let backgroundView = targetNode.backgroundView {
targetNode.backgroundView = nil
backgroundView.removeFromSuperview()
}
return targetNode
})
})
}
}
}
@@ -0,0 +1,51 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageAttachedContentNode",
module_name = "ChatMessageAttachedContentNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Postbox",
"//submodules/Display",
"//submodules/AsyncDisplayKit",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/TextFormat",
"//submodules/AccountContext",
"//submodules/UrlEscaping",
"//submodules/PhotoResources",
"//submodules/WebsiteType",
"//submodules/ChatMessageInteractiveMediaBadge",
"//submodules/GalleryData",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/TelegramUI/Components/AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
"//submodules/ShimmerEffect",
"//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode",
"//submodules/TelegramUI/Components/Chat/ChatHistoryEntry",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode",
"//submodules/TelegramUI/Components/WallpaperPreviewMedia",
"//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode",
"//submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,34 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageBirthdateSuggestionContentNode",
module_name = "ChatMessageBirthdateSuggestionContentNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/AccountContext",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/TextFormat",
"//submodules/LocalizedPeerData",
"//submodules/TelegramStringFormatting",
"//submodules/WallpaperBackgroundNode",
"//submodules/ReactionSelectionNode",
"//submodules/Markdown",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,390 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import AccountContext
import TelegramPresentationData
import TelegramUIPreferences
import TextFormat
import TelegramStringFormatting
import WallpaperBackgroundNode
import Markdown
import ChatMessageBubbleContentNode
import ChatMessageItemCommon
import ChatControllerInteraction
import AnimatedStickerNode
import TelegramAnimatedStickerNode
public class ChatMessageBirthdateSuggestionContentNode: ChatMessageBubbleContentNode {
private var mediaBackgroundContent: WallpaperBubbleBackgroundNode?
private let mediaBackgroundNode: NavigationBackgroundNode
private let animationNode: AnimatedStickerNode
private let subtitleNode: TextNode
private let dayTitleNode: TextNode
private let dayValueNode: TextNode
private let monthTitleNode: TextNode
private let monthValueNode: TextNode
private let yearTitleNode: TextNode
private let yearValueNode: TextNode
private let buttonNode: HighlightTrackingButtonNode
private let buttonTitleNode: TextNode
private var absoluteRect: (CGRect, CGSize)?
required public init() {
self.mediaBackgroundNode = NavigationBackgroundNode(color: .clear)
self.mediaBackgroundNode.clipsToBounds = true
self.mediaBackgroundNode.cornerRadius = 27.0
self.animationNode = DefaultAnimatedStickerNodeImpl()
self.subtitleNode = TextNode()
self.subtitleNode.isUserInteractionEnabled = false
self.subtitleNode.displaysAsynchronously = false
self.dayTitleNode = TextNode()
self.dayTitleNode.isUserInteractionEnabled = false
self.dayTitleNode.displaysAsynchronously = false
self.dayValueNode = TextNode()
self.dayValueNode.isUserInteractionEnabled = false
self.dayValueNode.displaysAsynchronously = false
self.monthTitleNode = TextNode()
self.monthTitleNode.isUserInteractionEnabled = false
self.monthTitleNode.displaysAsynchronously = false
self.monthValueNode = TextNode()
self.monthValueNode.isUserInteractionEnabled = false
self.monthValueNode.displaysAsynchronously = false
self.yearTitleNode = TextNode()
self.yearTitleNode.isUserInteractionEnabled = false
self.yearTitleNode.displaysAsynchronously = false
self.yearValueNode = TextNode()
self.yearValueNode.isUserInteractionEnabled = false
self.yearValueNode.displaysAsynchronously = false
self.buttonNode = HighlightTrackingButtonNode()
self.buttonNode.clipsToBounds = true
self.buttonNode.cornerRadius = 17.0
self.buttonTitleNode = TextNode()
self.buttonTitleNode.isUserInteractionEnabled = false
self.buttonTitleNode.displaysAsynchronously = false
super.init()
self.addSubnode(self.mediaBackgroundNode)
self.addSubnode(self.animationNode)
self.addSubnode(self.subtitleNode)
self.addSubnode(self.dayTitleNode)
self.addSubnode(self.dayValueNode)
self.addSubnode(self.monthTitleNode)
self.addSubnode(self.monthValueNode)
self.addSubnode(self.yearTitleNode)
self.addSubnode(self.yearValueNode)
self.addSubnode(self.buttonNode)
self.addSubnode(self.buttonTitleNode)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.buttonNode.layer.removeAnimation(forKey: "opacity")
strongSelf.buttonNode.alpha = 0.4
strongSelf.buttonTitleNode.layer.removeAnimation(forKey: "opacity")
strongSelf.buttonTitleNode.alpha = 0.4
} else {
strongSelf.buttonNode.alpha = 1.0
strongSelf.buttonNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.buttonTitleNode.alpha = 1.0
strongSelf.buttonTitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func buttonPressed() {
guard let item = self.item else {
return
}
let _ = item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: .default))
}
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode)
let makeDayTitleLayout = TextNode.asyncLayout(self.dayTitleNode)
let makeDayValueLayout = TextNode.asyncLayout(self.dayValueNode)
let makeMonthTitleLayout = TextNode.asyncLayout(self.monthTitleNode)
let makeMonthValueLayout = TextNode.asyncLayout(self.monthValueNode)
let makeYearTitleLayout = TextNode.asyncLayout(self.yearTitleNode)
let makeYearValueLayout = TextNode.asyncLayout(self.yearValueNode)
let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode)
return { item, layoutConstants, _, _, _, _ in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center)
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
let width: CGFloat = 186.0
var day: Int32 = 1
var month: Int32 = 1
var year: Int32?
if let action = item.message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .suggestedBirthday(birthday) = action.action {
day = birthday.day
month = birthday.month
year = birthday.year
}
let primaryTextColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
let subtitleColor = primaryTextColor.withAlphaComponent(item.presentationData.theme.theme.overallDarkAppearance ? 0.7 : 0.8)
let peerName = item.message.peers[item.message.id.peerId].flatMap { EnginePeer($0).compactDisplayTitle } ?? ""
let text: String
let fromYou = item.message.author?.id == item.context.account.peerId
if fromYou {
text = item.presentationData.strings.Conversation_SuggestedBirthdateTextYou(peerName).string
} else {
text = item.presentationData.strings.Conversation_SuggestedBirthdateText(peerName).string
}
let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor)
let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: primaryTextColor)
let subtitle = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in
return nil
}), textAlignment: .center)
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitle, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let titleFont = Font.regular(13.0)
let valueFont = Font.semibold(13.0)
let (dayTitleLayout, dayTitleApply) = makeDayTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Conversation_SuggestedBirthdate_Day, font: titleFont, textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (dayValueLayout, dayValueApply) = makeDayValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "\(day)", font: valueFont, textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (monthTitleLayout, monthTitleApply) = makeMonthTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Conversation_SuggestedBirthdate_Month, font: titleFont, textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (monthValueLayout, monthValueApply) = makeMonthValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: stringForMonth(strings: item.presentationData.strings, month: month - 1), font: valueFont, textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (yearTitleLayout, yearTitleApply) = makeYearTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Conversation_SuggestedBirthdate_Year, font: titleFont, textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (yearValueLayout, yearValueApply) = makeYearValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: year.flatMap { "\($0)" } ?? "", font: valueFont, textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Conversation_SuggestedBirthdate_View, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
var backgroundSize = CGSize(width: width, height: subtitleLayout.size.height + 160.0)
if !fromYou {
backgroundSize.height += 44.0
}
return (backgroundSize.width, { boundingWidth in
return (backgroundSize, { [weak self] animation, synchronousLoads, _ in
if let strongSelf = self {
let isFirstTime = strongSelf.item == nil
strongSelf.item = item
let mediaBackgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - width) / 2.0), y: 0.0), size: backgroundSize)
strongSelf.mediaBackgroundNode.frame = mediaBackgroundFrame
strongSelf.mediaBackgroundNode.updateColor(color: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: item.controllerInteraction.enableFullTranslucency && dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), transition: .immediate)
strongSelf.mediaBackgroundNode.update(size: mediaBackgroundFrame.size, transition: .immediate)
strongSelf.buttonNode.backgroundColor = item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12)
if item.presentationData.theme.theme.overallDarkAppearance {
strongSelf.dayTitleNode.layer.compositingFilter = nil
strongSelf.monthTitleNode.layer.compositingFilter = nil
strongSelf.yearTitleNode.layer.compositingFilter = nil
} else {
strongSelf.dayTitleNode.layer.compositingFilter = "overlayBlendMode"
strongSelf.monthTitleNode.layer.compositingFilter = "overlayBlendMode"
strongSelf.yearTitleNode.layer.compositingFilter = "overlayBlendMode"
}
let _ = subtitleApply()
let _ = dayTitleApply()
let _ = monthTitleApply()
let _ = yearTitleApply()
let _ = dayValueApply()
let _ = monthValueApply()
let _ = yearValueApply()
let _ = buttonTitleApply()
let iconSize = CGSize(width: 80.0, height: 80.0)
let animationFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - iconSize.width) / 2.0), y: mediaBackgroundFrame.minY + 8.0), size: iconSize)
strongSelf.animationNode.frame = animationFrame
if isFirstTime {
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "Cake"), width: 384, height: 384, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
strongSelf.animationNode.visibility = true
}
strongSelf.animationNode.updateLayout(size: iconSize)
let subtitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 96.0), size: subtitleLayout.size)
strongSelf.subtitleNode.frame = subtitleFrame
let titleOriginY = subtitleFrame.maxY + 11.0
let valueOriginY = titleOriginY + 19.0
let minX = mediaBackgroundFrame.minX
let maxX = mediaBackgroundFrame.maxX
let width = mediaBackgroundFrame.width
let dayColWidth = max(dayTitleLayout.size.width, dayValueLayout.size.width)
let monthColWidth = max(monthTitleLayout.size.width, monthValueLayout.size.width)
let yearColW = max(yearTitleLayout.size.width, yearValueLayout.size.width)
func centerX(inLeft left: CGFloat, right: CGFloat, contentWidth: CGFloat) -> CGFloat {
return left + floorToScreenPixels((right - left - contentWidth) * 0.5)
}
if yearValueLayout.size.width > 0.0 {
strongSelf.yearTitleNode.isHidden = false
strongSelf.yearValueNode.isHidden = false
let monthLeft = centerX(inLeft: minX, right: maxX, contentWidth: monthColWidth)
let monthRight = monthLeft + monthColWidth
let dayLeft = centerX(inLeft: minX, right: monthLeft, contentWidth: dayColWidth)
let yearLeft = centerX(inLeft: monthRight, right: maxX, contentWidth: yearColW)
strongSelf.dayTitleNode.frame = CGRect(
origin: CGPoint(x: dayLeft + floorToScreenPixels((dayColWidth - dayTitleLayout.size.width) * 0.5), y: titleOriginY),
size: dayTitleLayout.size
)
strongSelf.dayValueNode.frame = CGRect(
origin: CGPoint(x: dayLeft + floorToScreenPixels((dayColWidth - dayValueLayout.size.width) * 0.5), y: valueOriginY),
size: dayValueLayout.size
)
strongSelf.monthTitleNode.frame = CGRect(
origin: CGPoint(x: monthLeft + floorToScreenPixels((monthColWidth - monthTitleLayout.size.width) * 0.5), y: titleOriginY),
size: monthTitleLayout.size
)
strongSelf.monthValueNode.frame = CGRect(
origin: CGPoint(x: monthLeft + floorToScreenPixels((monthColWidth - monthValueLayout.size.width) * 0.5), y: valueOriginY),
size: monthValueLayout.size
)
strongSelf.yearTitleNode.frame = CGRect(
origin: CGPoint(x: yearLeft + floorToScreenPixels((yearColW - yearTitleLayout.size.width) * 0.5), y: titleOriginY),
size: yearTitleLayout.size
)
strongSelf.yearValueNode.frame = CGRect(
origin: CGPoint(x: yearLeft + floorToScreenPixels((yearColW - yearValueLayout.size.width) * 0.5), y: valueOriginY),
size: yearValueLayout.size
)
} else {
strongSelf.yearTitleNode.isHidden = true
strongSelf.yearValueNode.isHidden = true
let spacing: CGFloat = 16.0
let totalWidth = dayColWidth + monthColWidth + spacing
let dayLeft = minX + floorToScreenPixels((width - totalWidth) / 2.0)
let monthLeft = dayLeft + dayColWidth + spacing
strongSelf.dayTitleNode.frame = CGRect(
origin: CGPoint(x: dayLeft + floorToScreenPixels((dayColWidth - dayTitleLayout.size.width) * 0.5), y: titleOriginY),
size: dayTitleLayout.size
)
strongSelf.dayValueNode.frame = CGRect(
origin: CGPoint(x: dayLeft + floorToScreenPixels((dayColWidth - dayValueLayout.size.width) * 0.5), y: valueOriginY),
size: dayValueLayout.size
)
strongSelf.monthTitleNode.frame = CGRect(
origin: CGPoint(x: monthLeft + floorToScreenPixels((monthColWidth - monthTitleLayout.size.width) * 0.5), y: titleOriginY),
size: monthTitleLayout.size
)
strongSelf.monthValueNode.frame = CGRect(
origin: CGPoint(x: monthLeft + floorToScreenPixels((monthColWidth - monthValueLayout.size.width) * 0.5), y: valueOriginY),
size: monthValueLayout.size
)
}
strongSelf.buttonNode.isHidden = fromYou
strongSelf.buttonTitleNode.isHidden = fromYou
let buttonSize = CGSize(width: buttonTitleLayout.size.width + 38.0, height: 34.0)
let buttonFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: mediaBackgroundFrame.maxY - buttonSize.height - 16.0), size: buttonSize)
strongSelf.buttonNode.frame = buttonFrame
let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: floorToScreenPixels(buttonFrame.midY - buttonTitleLayout.size.height / 2.0)), size: buttonTitleLayout.size)
strongSelf.buttonTitleNode.frame = buttonTitleFrame
if item.controllerInteraction.presentationContext.backgroundNode?.hasExtraBubbleBackground() == true {
if strongSelf.mediaBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
strongSelf.mediaBackgroundNode.isHidden = true
backgroundContent.clipsToBounds = true
backgroundContent.allowsGroupOpacity = true
backgroundContent.cornerRadius = 27.0
strongSelf.mediaBackgroundContent = backgroundContent
strongSelf.insertSubnode(backgroundContent, at: 0)
}
strongSelf.mediaBackgroundContent?.frame = mediaBackgroundFrame
} else {
strongSelf.mediaBackgroundNode.isHidden = false
strongSelf.mediaBackgroundContent?.removeFromSupernode()
strongSelf.mediaBackgroundContent = nil
}
if let (rect, size) = strongSelf.absoluteRect {
strongSelf.updateAbsoluteRect(rect, within: size)
}
}
})
})
})
}
}
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
self.absoluteRect = (rect, containerSize)
if let mediaBackgroundContent = self.mediaBackgroundContent {
var backgroundFrame = mediaBackgroundContent.frame
backgroundFrame.origin.x += rect.minX
backgroundFrame.origin.y += rect.minY
mediaBackgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate)
}
}
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.mediaBackgroundNode.frame.contains(point) {
return ChatMessageBubbleContentTapAction(content: .openMessage)
} else {
return ChatMessageBubbleContentTapAction(content: .none)
}
}
}
@@ -0,0 +1,30 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageBubbleContentNode",
module_name = "ChatMessageBubbleContentNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/TelegramUIPreferences",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/ChatMessageBackground",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
"//submodules/TelegramUI/Components/Chat/ChatHistoryEntry",
"//submodules/TelegramUI/Components/Chat/ChatMessageItem",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,164 @@
import Foundation
import UIKit
import Display
import TelegramPresentationData
import ChatMessageItemCommon
public func chatMessageBubbleImageContentCorners(relativeContentPosition position: ChatMessageBubbleContentPosition, normalRadius: CGFloat, mergedRadius: CGFloat, mergedWithAnotherContentRadius: CGFloat, layoutConstants: ChatMessageItemLayoutConstants, chatPresentationData: ChatPresentationData) -> ImageCorners {
let topLeftCorner: ImageCorner
let topRightCorner: ImageCorner
switch position {
case let .linear(top, _):
switch top {
case .Neighbour:
topLeftCorner = .Corner(mergedWithAnotherContentRadius)
topRightCorner = .Corner(mergedWithAnotherContentRadius)
case .BubbleNeighbour:
topLeftCorner = .Corner(mergedRadius)
topRightCorner = .Corner(mergedRadius)
case let .None(mergeStatus):
switch mergeStatus {
case .Left:
topLeftCorner = .Corner(mergedRadius)
topRightCorner = .Corner(normalRadius)
case .None:
topLeftCorner = .Corner(normalRadius)
topRightCorner = .Corner(normalRadius)
case .Right:
topLeftCorner = .Corner(normalRadius)
topRightCorner = .Corner(mergedRadius)
case .Both:
topLeftCorner = .Corner(mergedRadius)
topRightCorner = .Corner(mergedRadius)
}
}
case let .mosaic(position, _):
switch position.topLeft {
case .none:
topLeftCorner = .Corner(normalRadius)
case .merged:
topLeftCorner = .Corner(mergedWithAnotherContentRadius)
case .mergedBubble:
topLeftCorner = .Corner(mergedRadius)
}
switch position.topRight {
case .none:
topRightCorner = .Corner(normalRadius)
case .merged:
topRightCorner = .Corner(mergedWithAnotherContentRadius)
case .mergedBubble:
topRightCorner = .Corner(mergedRadius)
}
}
let bottomLeftCorner: ImageCorner
let bottomRightCorner: ImageCorner
switch position {
case let .linear(_, bottom):
switch bottom {
case .Neighbour:
bottomLeftCorner = .Corner(mergedWithAnotherContentRadius)
bottomRightCorner = .Corner(mergedWithAnotherContentRadius)
case .BubbleNeighbour:
bottomLeftCorner = .Corner(mergedRadius)
bottomRightCorner = .Corner(mergedRadius)
case let .None(mergeStatus):
switch mergeStatus {
case .Left:
bottomLeftCorner = .Corner(mergedRadius)
bottomRightCorner = .Corner(normalRadius)
case .Both:
bottomLeftCorner = .Corner(mergedRadius)
bottomRightCorner = .Corner(mergedRadius)
case let .None(status):
let bubbleInsets: UIEdgeInsets
if case .color = chatPresentationData.theme.wallpaper {
let colors: PresentationThemeBubbleColorComponents
switch status {
case .Incoming:
colors = chatPresentationData.theme.theme.chat.message.incoming.bubble.withoutWallpaper
case .Outgoing:
colors = chatPresentationData.theme.theme.chat.message.outgoing.bubble.withoutWallpaper
case .None:
colors = chatPresentationData.theme.theme.chat.message.incoming.bubble.withoutWallpaper
}
if colors.fill[0] == colors.stroke || colors.stroke.alpha.isZero {
bubbleInsets = UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)
} else {
bubbleInsets = layoutConstants.bubble.strokeInsets
}
} else {
bubbleInsets = layoutConstants.image.bubbleInsets
}
switch status {
case .Incoming:
bottomLeftCorner = .Tail(normalRadius, PresentationResourcesChat.chatBubbleMediaCorner(chatPresentationData.theme.theme, incoming: true, mainRadius: normalRadius, inset: max(0.0, bubbleInsets.left - 1.0))!)
bottomRightCorner = .Corner(normalRadius)
case .Outgoing:
bottomLeftCorner = .Corner(normalRadius)
bottomRightCorner = .Tail(normalRadius, PresentationResourcesChat.chatBubbleMediaCorner(chatPresentationData.theme.theme, incoming: false, mainRadius: normalRadius, inset: max(0.0, bubbleInsets.right - 1.0))!)
case .None:
bottomLeftCorner = .Corner(normalRadius)
bottomRightCorner = .Corner(normalRadius)
}
case .Right:
bottomLeftCorner = .Corner(normalRadius)
bottomRightCorner = .Corner(mergedRadius)
}
}
case let .mosaic(position, _):
switch position.bottomLeft {
case let .none(tail):
if tail {
let bubbleInsets: UIEdgeInsets
if case .color = chatPresentationData.theme.wallpaper {
let colors: PresentationThemeBubbleColorComponents
colors = chatPresentationData.theme.theme.chat.message.incoming.bubble.withoutWallpaper
if colors.fill[0] == colors.stroke || colors.stroke.alpha.isZero {
bubbleInsets = UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)
} else {
bubbleInsets = layoutConstants.bubble.strokeInsets
}
} else {
bubbleInsets = layoutConstants.image.bubbleInsets
}
bottomLeftCorner = .Tail(normalRadius, PresentationResourcesChat.chatBubbleMediaCorner(chatPresentationData.theme.theme, incoming: true, mainRadius: normalRadius, inset: max(0.0, bubbleInsets.left - 1.0))!)
} else {
bottomLeftCorner = .Corner(normalRadius)
}
case .merged:
bottomLeftCorner = .Corner(mergedWithAnotherContentRadius)
case .mergedBubble:
bottomLeftCorner = .Corner(mergedRadius)
}
switch position.bottomRight {
case let .none(tail):
if tail {
let bubbleInsets: UIEdgeInsets
if case .color = chatPresentationData.theme.wallpaper {
let colors: PresentationThemeBubbleColorComponents
colors = chatPresentationData.theme.theme.chat.message.outgoing.bubble.withoutWallpaper
if colors.fill[0] == colors.stroke || colors.stroke.alpha.isZero {
bubbleInsets = UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)
} else {
bubbleInsets = layoutConstants.bubble.strokeInsets
}
} else {
bubbleInsets = layoutConstants.image.bubbleInsets
}
bottomRightCorner = .Tail(normalRadius, PresentationResourcesChat.chatBubbleMediaCorner(chatPresentationData.theme.theme, incoming: false, mainRadius: normalRadius, inset: max(0.0, bubbleInsets.right - 1.0))!)
} else {
bottomRightCorner = .Corner(normalRadius)
}
case .merged:
bottomRightCorner = .Corner(mergedWithAnotherContentRadius)
case .mergedBubble:
bottomRightCorner = .Corner(mergedRadius)
}
}
return ImageCorners(topLeft: topLeftCorner, topRight: topRightCorner, bottomLeft: bottomLeftCorner, bottomRight: bottomRightCorner)
}
@@ -0,0 +1,321 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import TelegramUIPreferences
import TelegramPresentationData
import AccountContext
import ChatMessageBackground
import ChatControllerInteraction
import ChatHistoryEntry
import ChatMessageItem
import ChatMessageItemCommon
import SwiftSignalKit
public enum ChatMessageBubbleContentBackgroundHiding {
case never
case emptyWallpaper
case always
}
public enum ChatMessageBubbleContentAlignment {
case none
case center
}
public struct ChatMessageBubbleContentProperties {
public let hidesSimpleAuthorHeader: Bool
public let headerSpacing: CGFloat
public let hidesBackground: ChatMessageBubbleContentBackgroundHiding
public let forceFullCorners: Bool
public let forceAlignment: ChatMessageBubbleContentAlignment
public let shareButtonOffset: CGPoint?
public let hidesHeaders: Bool
public let avatarOffset: CGFloat?
public let isDetached: Bool
public init(
hidesSimpleAuthorHeader: Bool,
headerSpacing: CGFloat,
hidesBackground: ChatMessageBubbleContentBackgroundHiding,
forceFullCorners: Bool,
forceAlignment: ChatMessageBubbleContentAlignment,
shareButtonOffset: CGPoint? = nil,
hidesHeaders: Bool = false,
avatarOffset: CGFloat? = nil,
isDetached: Bool = false
) {
self.hidesSimpleAuthorHeader = hidesSimpleAuthorHeader
self.headerSpacing = headerSpacing
self.hidesBackground = hidesBackground
self.forceFullCorners = forceFullCorners
self.forceAlignment = forceAlignment
self.shareButtonOffset = shareButtonOffset
self.hidesHeaders = hidesHeaders
self.avatarOffset = avatarOffset
self.isDetached = isDetached
}
}
public enum ChatMessageBubbleNoneMergeStatus {
case Incoming
case Outgoing
case None
}
public enum ChatMessageBubbleMergeStatus {
case None(ChatMessageBubbleNoneMergeStatus)
case Left
case Right
case Both
}
public enum ChatMessageBubbleRelativePosition {
public enum NeighbourType {
case media
case header
case footer
case text
case reactions
}
public enum NeighbourSpacing {
case `default`
case condensed
case overlap(CGFloat)
}
case None(ChatMessageBubbleMergeStatus)
case BubbleNeighbour
case Neighbour(Bool, NeighbourType, NeighbourSpacing)
}
public enum ChatMessageBubbleContentMosaicNeighbor {
case merged
case mergedBubble
case none(tail: Bool)
}
public struct ChatMessageBubbleContentMosaicPosition {
public let topLeft: ChatMessageBubbleContentMosaicNeighbor
public let topRight: ChatMessageBubbleContentMosaicNeighbor
public let bottomLeft: ChatMessageBubbleContentMosaicNeighbor
public let bottomRight: ChatMessageBubbleContentMosaicNeighbor
public init(topLeft: ChatMessageBubbleContentMosaicNeighbor, topRight: ChatMessageBubbleContentMosaicNeighbor, bottomLeft: ChatMessageBubbleContentMosaicNeighbor, bottomRight: ChatMessageBubbleContentMosaicNeighbor) {
self.topLeft = topLeft
self.topRight = topRight
self.bottomLeft = bottomLeft
self.bottomRight = bottomRight
}
}
public enum ChatMessageBubbleContentPosition {
case linear(top: ChatMessageBubbleRelativePosition, bottom: ChatMessageBubbleRelativePosition)
case mosaic(position: ChatMessageBubbleContentMosaicPosition, wide: Bool)
}
public enum ChatMessageBubblePreparePosition {
case linear(top: ChatMessageBubbleRelativePosition, bottom: ChatMessageBubbleRelativePosition)
case mosaic(top: ChatMessageBubbleRelativePosition, bottom: ChatMessageBubbleRelativePosition, index: Int?)
}
public struct ChatMessageBubbleContentTapAction {
public struct Url {
public var url: String
public var concealed: Bool
public var allowInlineWebpageResolution: Bool
public init(
url: String,
concealed: Bool,
allowInlineWebpageResolution: Bool = false
) {
self.url = url
self.concealed = concealed
self.allowInlineWebpageResolution = allowInlineWebpageResolution
}
}
public enum Content {
case none
case url(Url)
case phone(String)
case textMention(String)
case peerMention(peerId: PeerId, mention: String, openProfile: Bool)
case botCommand(String)
case hashtag(String?, String)
case instantPage
case wallpaper
case theme
case call(peerId: PeerId, isVideo: Bool)
case conferenceCall(message: Message)
case openMessage
case timecode(Double, String)
case tooltip(String, ASDisplayNode?, CGRect?)
case bankCard(String)
case ignore
case openPollResults(Data)
case copy(String)
case largeEmoji(String, String?, TelegramMediaFile)
case customEmoji(TelegramMediaFile)
case custom(() -> Void)
}
public var content: Content
public var rects: [CGRect]?
public var hasLongTapAction: Bool
public var activate: (() -> Promise<Bool>?)?
public init(content: Content, rects: [CGRect]? = nil, hasLongTapAction: Bool = true, activate: (() -> Promise<Bool>?)? = nil) {
self.content = content
self.rects = rects
self.hasLongTapAction = hasLongTapAction
self.activate = activate
}
}
public final class ChatMessageBubbleContentItem {
public let context: AccountContext
public let controllerInteraction: ChatControllerInteraction
public let message: Message
public let topMessage: Message
public let content: ChatMessageItemContent
public let read: Bool
public let chatLocation: ChatLocation
public let presentationData: ChatPresentationData
public let associatedData: ChatMessageItemAssociatedData
public let attributes: ChatMessageEntryAttributes
public let isItemPinned: Bool
public let isItemEdited: Bool
public init(context: AccountContext, controllerInteraction: ChatControllerInteraction, message: Message, topMessage: Message, content: ChatMessageItemContent, read: Bool, chatLocation: ChatLocation, presentationData: ChatPresentationData, associatedData: ChatMessageItemAssociatedData, attributes: ChatMessageEntryAttributes, isItemPinned: Bool, isItemEdited: Bool) {
self.context = context
self.controllerInteraction = controllerInteraction
self.message = message
self.topMessage = topMessage
self.content = content
self.read = read
self.chatLocation = chatLocation
self.presentationData = presentationData
self.associatedData = associatedData
self.attributes = attributes
self.isItemPinned = isItemPinned
self.isItemEdited = isItemEdited
}
}
open class ChatMessageBubbleContentNode: ASDisplayNode {
open var supportsMosaic: Bool {
return false
}
open var index: Int?
public weak var itemNode: ChatMessageItemNodeProtocol?
public weak var bubbleBackgroundNode: ChatMessageBackground?
public weak var bubbleBackdropNode: ChatMessageBubbleBackdrop?
open var visibility: ListViewItemNodeVisibility = .none
public var item: ChatMessageBubbleContentItem?
public var updateIsTextSelectionActive: ((Bool) -> Void)?
public var requestInlineUpdate: (() -> Void)?
public var requestFullUpdate: (() -> Void)?
open var disablesClipping: Bool {
return false
}
required public override init() {
super.init()
}
open func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
preconditionFailure()
}
open func animateInsertion(_ currentTimestamp: Double, duration: Double) {
}
open func animateAdded(_ currentTimestamp: Double, duration: Double) {
}
open func animateRemoved(_ currentTimestamp: Double, duration: Double) {
}
open func animateInsertionIntoBubble(_ duration: Double) {
}
open func animateRemovalFromBubble(_ duration: Double, completion: @escaping () -> Void) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
completion()
})
}
open func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
return nil
}
open func updateHiddenMedia(_ media: [Media]?) -> Bool {
return false
}
open func updateSearchTextHighlightState(text: String?, messages: [MessageIndex]?) {
}
open func updateAutomaticMediaDownloadSettings(_ settings: MediaAutoDownloadSettings) {
}
open func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
return nil
}
open func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
return ChatMessageBubbleContentTapAction(content: .none)
}
open func updateTouchesAtPoint(_ point: CGPoint?) {
}
open func updateHighlightedState(animated: Bool) -> Bool {
return false
}
open func willUpdateIsExtractedToContextPreview(_ value: Bool) {
}
open func updateIsExtractedToContextPreview(_ value: Bool) {
}
open func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
}
open func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) {
}
open func applyAbsoluteOffsetSpring(value: CGFloat, duration: Double, damping: CGFloat) {
}
open func unreadMessageRangeUpdated() {
}
open func reactionTargetView(value: MessageReaction.Reaction) -> UIView? {
return nil
}
open func messageEffectTargetView() -> UIView? {
return nil
}
open func targetForStoryTransition(id: StoryId) -> UIView? {
return nil
}
open func getStatusNode() -> ASDisplayNode? {
return nil
}
}
@@ -0,0 +1,101 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageBubbleItemNode",
module_name = "ChatMessageBubbleItemNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/TextFormat",
"//submodules/AccountContext",
"//submodules/TemporaryCachedPeerDataManager",
"//submodules/LocalizedPeerData",
"//submodules/ContextUI",
"//submodules/TelegramUniversalVideoContent",
"//submodules/MosaicLayout",
"//submodules/TextSelectionNode",
"//submodules/PlatformRestrictionMatching",
"//submodules/Emoji",
"//submodules/PersistentStringHash",
"//submodules/GridMessageSelectionNode",
"//submodules/AppBundle",
"//submodules/Markdown",
"//submodules/WallpaperBackgroundNode",
"//submodules/ChatPresentationInterfaceState",
"//submodules/ChatMessageBackground",
"//submodules/TelegramUI/Components/AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
"//submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatHistoryEntry",
"//submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItem",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemView",
"//submodules/TelegramUI/Components/Chat/ChatMessageSwipeToReplyNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageSelectionNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageDeliveryFailedNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageShareButton",
"//submodules/TelegramUI/Components/Chat/ChatMessageThreadInfoNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode",
"//submodules/TelegramUI/Components/Chat/ChatSwipeToReplyRecognizer",
"//submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageCommentFooterContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousDescriptionContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousLinkContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousMessageContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageGameBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageInvoiceBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageBirthdateSuggestionContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageStoryMentionContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageUnlockMediaNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageStarsMediaInfoNode",
"//submodules/UIKitRuntimeUtils",
"//submodules/TelegramUI/Components/Chat/ChatMessageTransitionNode",
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
"//submodules/TelegramUI/Components/LottieMetal",
"//submodules/TelegramStringFormatting",
"//submodules/AvatarNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageSuggestedPostInfoNode",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,29 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageCallBubbleContentNode",
module_name = "ChatMessageCallBubbleContentNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/TelegramCore",
"//submodules/Postbox",
"//submodules/TelegramPresentationData",
"//submodules/AppBundle",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode",
"//submodules/AnimatedAvatarSetNode",
"//submodules/AvatarNode",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,420 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import Postbox
import TelegramPresentationData
import AppBundle
import ChatMessageBubbleContentNode
import ChatMessageItemCommon
import ChatMessageDateAndStatusNode
import SwiftSignalKit
import AnimatedAvatarSetNode
import AvatarNode
private let titleFont: UIFont = Font.medium(16.0)
private let labelFont: UIFont = Font.regular(13.0)
private let avatarFont: UIFont = avatarPlaceholderFont(size: 8.0)
private let incomingGreenIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/CallIncomingArrow"), color: UIColor(rgb: 0x36c033))
private let incomingRedIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/CallIncomingArrow"), color: UIColor(rgb: 0xff4747))
private let outgoingGreenIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/CallOutgoingArrow"), color: UIColor(rgb: 0x36c033))
private let outgoingRedIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/CallOutgoingArrow"), color: UIColor(rgb: 0xff4747))
public class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
private let titleNode: TextNode
private let labelNode: TextNode
private var peopleAvatarsContext: AnimatedAvatarSetContext?
private var peopleAvatarsNode: AnimatedAvatarSetNode?
private var peopleTextNode: TextNode?
private let iconNode: ASImageNode
private let buttonNode: HighlightableButtonNode
private var activeConferenceUpdateTimer: SwiftSignalKit.Timer?
required public init() {
self.titleNode = TextNode()
self.labelNode = TextNode()
self.iconNode = ASImageNode()
self.iconNode.displayWithoutProcessing = true
self.iconNode.displaysAsynchronously = false
self.iconNode.isLayerBacked = true
self.buttonNode = HighlightableButtonNode()
self.buttonNode.isAccessibilityElement = false
super.init()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .topLeft
self.titleNode.contentsScale = UIScreenScale
self.titleNode.displaysAsynchronously = false
self.addSubnode(self.titleNode)
self.labelNode.isUserInteractionEnabled = false
self.labelNode.contentMode = .topLeft
self.labelNode.contentsScale = UIScreenScale
self.labelNode.displaysAsynchronously = false
self.addSubnode(self.labelNode)
self.addSubnode(self.iconNode)
self.addSubnode(self.buttonNode)
self.buttonNode.addTarget(self, action: #selector(self.callButtonPressed), forControlEvents: .touchUpInside)
}
deinit {
self.activeConferenceUpdateTimer?.invalidate()
}
override public func accessibilityActivate() -> Bool {
self.callButtonPressed()
return true
}
override public func didLoad() {
super.didLoad()
self.view.accessibilityElementsHidden = true
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let makePeopleTextLayout = TextNode.asyncLayout(self.peopleTextNode)
return { item, layoutConstants, _, _, _, _ in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
let incoming = item.message.effectivelyIncoming(item.context.account.peerId)
let horizontalInset = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
let textConstrainedSize = CGSize(width: constrainedSize.width - horizontalInset, height: constrainedSize.height)
let avatarsLeftInset: CGFloat = 5.0
let avatarsRightInset: CGFloat = 5.0
let peopleAvatarSize: CGFloat = 16.0
let peopleAvatarSpacing: CGFloat = 10.0
let messageTheme = incoming ? item.presentationData.theme.theme.chat.message.incoming : item.presentationData.theme.theme.chat.message.outgoing
var peopleTextString: String?
var peopleAvatars: [Peer] = []
var titleString: String?
var callDuration: Int32?
var callSuccessful = true
var isVideo = false
var updateConferenceTimerEndTimeout: Int32?
for media in item.message.media {
if let action = media as? TelegramMediaAction, case let .phoneCall(_, discardReason, duration, isVideoValue) = action.action {
isVideo = isVideoValue
callDuration = duration
if let discardReason = discardReason {
switch discardReason {
case .disconnect:
callSuccessful = false
if isVideo {
titleString = item.presentationData.strings.Notification_VideoCallCanceled
} else {
titleString = item.presentationData.strings.Notification_CallCanceled
}
case .missed, .busy:
callSuccessful = false
if incoming {
if isVideo {
titleString = item.presentationData.strings.Notification_VideoCallMissed
} else {
titleString = item.presentationData.strings.Notification_CallMissed
}
} else {
if isVideo {
titleString = item.presentationData.strings.Notification_VideoCallCanceled
} else {
titleString = item.presentationData.strings.Notification_CallCanceled
}
}
case .hangup:
break
}
}
break
} else if let action = media as? TelegramMediaAction, case let .conferenceCall(conferenceCall) = action.action {
isVideo = conferenceCall.flags.contains(.isVideo)
callDuration = conferenceCall.duration
if conferenceCall.otherParticipants.count > 0 {
peopleTextString = item.presentationData.strings.Chat_CallMessage_GroupCallParticipantCount(Int32(conferenceCall.otherParticipants.count + 1))
if let peer = item.message.author {
peopleAvatars.append(peer)
}
for id in conferenceCall.otherParticipants {
if let peer = item.message.peers[id] {
peopleAvatars.append(peer)
}
}
}
let missedTimeout: Int32
#if DEBUG && false
missedTimeout = 5
#else
missedTimeout = 30
#endif
let currentTime = Int32(Date().timeIntervalSince1970)
if conferenceCall.flags.contains(.isMissed) {
titleString = item.presentationData.strings.Chat_CallMessage_DeclinedGroupCall
} else if conferenceCall.duration == nil && item.message.timestamp < currentTime - missedTimeout {
titleString = item.presentationData.strings.Chat_CallMessage_MissedGroupCall
} else {
if incoming {
titleString = item.presentationData.strings.Chat_CallMessage_IncomingGroupCall
} else {
titleString = item.presentationData.strings.Chat_CallMessage_OutgoingGroupCall
}
updateConferenceTimerEndTimeout = (item.message.timestamp + missedTimeout) - currentTime
}
break
}
}
if titleString == nil {
let baseString: String
if incoming {
if isVideo {
baseString = item.presentationData.strings.Notification_VideoCallIncoming
} else {
baseString = item.presentationData.strings.Notification_CallIncoming
}
} else {
if isVideo {
baseString = item.presentationData.strings.Notification_VideoCallOutgoing
} else {
baseString = item.presentationData.strings.Notification_CallOutgoing
}
}
titleString = baseString
}
let attributedTitle = NSAttributedString(string: titleString ?? "", font: titleFont, textColor: messageTheme.primaryTextColor)
var callIcon: UIImage?
if callSuccessful {
if incoming {
callIcon = incomingGreenIcon
} else {
callIcon = outgoingGreenIcon
}
} else {
if incoming {
callIcon = incomingRedIcon
} else {
callIcon = outgoingRedIcon
}
}
var buttonImage: UIImage?
if incoming {
if isVideo {
buttonImage = PresentationResourcesChat.chatBubbleIncomingVideoCallButtonImage(item.presentationData.theme.theme)
} else {
buttonImage = PresentationResourcesChat.chatBubbleIncomingCallButtonImage(item.presentationData.theme.theme)
}
} else {
if isVideo {
buttonImage = PresentationResourcesChat.chatBubbleOutgoingVideoCallButtonImage(item.presentationData.theme.theme)
} else {
buttonImage = PresentationResourcesChat.chatBubbleOutgoingCallButtonImage(item.presentationData.theme.theme)
}
}
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, associatedData: item.associatedData)
var statusText: String
if let callDuration = callDuration, callDuration > 1 {
statusText = item.presentationData.strings.Notification_CallFormat(dateText, callDurationString(strings: item.presentationData.strings, value: callDuration)).string
} else {
statusText = dateText
}
if peopleTextString != nil || !peopleAvatars.isEmpty {
statusText.append(",")
}
let attributedLabel = NSAttributedString(string: statusText, font: labelFont, textColor: messageTheme.fileDurationColor)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedTitle, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: attributedLabel, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
var peopleTextLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if let peopleTextString {
peopleTextLayoutAndApply = makePeopleTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: peopleTextString, font: labelFont, textColor: messageTheme.fileDurationColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
}
let titleSize = titleLayout.size
let labelSize = labelLayout.size
var titleFrame = CGRect(origin: CGPoint(), size: titleSize)
var labelFrame = CGRect(origin: CGPoint(x: 14.0, y: 0.0), size: labelSize)
titleFrame = titleFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top + 4.0)
labelFrame = labelFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top + titleSize.height + 4.0)
var boundingSize: CGSize
var labelsWidth: CGFloat = labelFrame.size.width
var avatarsWidth: CGFloat = 0.0
if !peopleAvatars.isEmpty {
avatarsWidth += avatarsLeftInset
avatarsWidth += 1.0 * peopleAvatarSize + CGFloat(min(3, peopleAvatars.count) - 1) * peopleAvatarSpacing
avatarsWidth += avatarsRightInset
labelsWidth += avatarsWidth
}
if let peopleTextLayoutAndApply {
labelsWidth += peopleTextLayoutAndApply.0.size.width
}
boundingSize = CGSize(width: max(titleFrame.size.width, labelsWidth + 14.0), height: 47.0)
boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom
boundingSize.width += 54.0
return (boundingSize.width, { boundingWidth in
return (boundingSize, { [weak self] animation, _, _ in
if let strongSelf = self {
strongSelf.item = item
let _ = titleApply()
let _ = labelApply()
strongSelf.titleNode.frame = titleFrame
strongSelf.labelNode.frame = labelFrame
if !peopleAvatars.isEmpty {
let peopleAvatarsContext: AnimatedAvatarSetContext
if let current = strongSelf.peopleAvatarsContext {
peopleAvatarsContext = current
} else {
peopleAvatarsContext = AnimatedAvatarSetContext()
strongSelf.peopleAvatarsContext = peopleAvatarsContext
}
let peopleAvatarsNode: AnimatedAvatarSetNode
if let current = strongSelf.peopleAvatarsNode {
peopleAvatarsNode = current
} else {
peopleAvatarsNode = AnimatedAvatarSetNode()
strongSelf.peopleAvatarsNode = peopleAvatarsNode
strongSelf.addSubnode(peopleAvatarsNode)
}
let peopleAvatarsContent = peopleAvatarsContext.update(peers: peopleAvatars.prefix(3).map(EnginePeer.init), animated: false)
let peopleAvatarsSize = peopleAvatarsNode.update(context: item.context, content: peopleAvatarsContent, itemSize: CGSize(width: peopleAvatarSize, height: peopleAvatarSize), customSpacing: peopleAvatarSize - peopleAvatarSpacing, font: avatarFont, animated: false, synchronousLoad: false)
peopleAvatarsNode.frame = CGRect(origin: CGPoint(x: labelFrame.maxX + avatarsLeftInset, y: labelFrame.minY - 1.0), size: peopleAvatarsSize)
} else {
strongSelf.peopleAvatarsContext = nil
if let peopleAvatarsNode = strongSelf.peopleAvatarsNode {
strongSelf.peopleAvatarsNode = nil
peopleAvatarsNode.removeFromSupernode()
}
}
if let peopleTextLayoutAndApply {
let peopleTextNode = peopleTextLayoutAndApply.1()
if strongSelf.peopleTextNode !== peopleTextNode {
strongSelf.peopleTextNode?.removeFromSupernode()
strongSelf.peopleTextNode = peopleTextNode
strongSelf.addSubnode(peopleTextNode)
}
peopleTextNode.frame = CGRect(origin: CGPoint(x: labelFrame.maxX + avatarsWidth, y: labelFrame.minY), size: peopleTextLayoutAndApply.0.size)
}
if let callIcon = callIcon {
if strongSelf.iconNode.image != callIcon {
strongSelf.iconNode.image = callIcon
}
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: titleFrame.minX + 1.0, y: labelFrame.minY + 4.0), size: callIcon.size)
}
if let buttonImage = buttonImage {
strongSelf.buttonNode.setImage(buttonImage, for: [])
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: boundingWidth - buttonImage.size.width - 8.0, y: 15.0), size: buttonImage.size)
}
if let activeConferenceUpdateTimer = strongSelf.activeConferenceUpdateTimer {
activeConferenceUpdateTimer.invalidate()
strongSelf.activeConferenceUpdateTimer = nil
}
if let updateConferenceTimerEndTimeout, updateConferenceTimerEndTimeout >= 0 {
strongSelf.activeConferenceUpdateTimer?.invalidate()
strongSelf.activeConferenceUpdateTimer = SwiftSignalKit.Timer(timeout: Double(updateConferenceTimerEndTimeout) + 0.5, repeat: false, completion: { [weak strongSelf] in
guard let strongSelf else {
return
}
strongSelf.requestInlineUpdate?()
}, queue: .mainQueue())
strongSelf.activeConferenceUpdateTimer?.start()
}
}
})
})
})
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
@objc private func callButtonPressed() {
if let item = self.item {
var isVideo = false
for media in item.message.media {
if let action = media as? TelegramMediaAction, case let .phoneCall(_, _, _, isVideoValue) = action.action {
isVideo = isVideoValue
} else if let action = media as? TelegramMediaAction, case .conferenceCall = action.action {
item.controllerInteraction.openConferenceCall(item.message)
return
}
}
item.controllerInteraction.callPeer(item.message.id.peerId, isVideo)
}
}
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.buttonNode.frame.contains(point) {
return ChatMessageBubbleContentTapAction(content: .ignore)
} else if self.bounds.contains(point), let item = self.item {
var isVideo = false
for media in item.message.media {
if let action = media as? TelegramMediaAction, case let .phoneCall(_, _, _, isVideoValue) = action.action {
isVideo = isVideoValue
} else if let action = media as? TelegramMediaAction, case .conferenceCall = action.action {
return ChatMessageBubbleContentTapAction(content: .conferenceCall(message: item.message))
}
}
return ChatMessageBubbleContentTapAction(content: .call(peerId: item.message.id.peerId, isVideo: isVideo))
} else {
return ChatMessageBubbleContentTapAction(content: .none)
}
}
}
@@ -0,0 +1,28 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageCommentFooterContentNode",
module_name = "ChatMessageCommentFooterContentNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Postbox",
"//submodules/Display",
"//submodules/AsyncDisplayKit",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/RadialStatusNode",
"//submodules/AnimatedCountLabelNode",
"//submodules/AnimatedAvatarSetNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,424 @@
import Foundation
import UIKit
import Postbox
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import RadialStatusNode
import AnimatedCountLabelNode
import AnimatedAvatarSetNode
import ChatMessageBubbleContentNode
import ChatMessageItemCommon
public final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
private let separatorNode: ASDisplayNode
private let countNode: AnimatedCountLabelNode
private let alternativeCountNode: AnimatedCountLabelNode
private let iconNode: ASImageNode
private let arrowNode: ASImageNode
private let buttonNode: HighlightTrackingButtonNode
private let avatarsContext: AnimatedAvatarSetContext
private let avatarsNode: AnimatedAvatarSetNode
private let unreadIconNode: ASImageNode
private var statusNode: RadialStatusNode?
required public init() {
self.separatorNode = ASDisplayNode()
self.separatorNode.isUserInteractionEnabled = false
self.countNode = AnimatedCountLabelNode()
self.alternativeCountNode = AnimatedCountLabelNode()
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
self.iconNode.isUserInteractionEnabled = false
self.unreadIconNode = ASImageNode()
self.unreadIconNode.displaysAsynchronously = false
self.unreadIconNode.displayWithoutProcessing = true
self.unreadIconNode.isUserInteractionEnabled = false
self.arrowNode = ASImageNode()
self.arrowNode.displaysAsynchronously = false
self.arrowNode.displayWithoutProcessing = true
self.arrowNode.isUserInteractionEnabled = false
self.avatarsContext = AnimatedAvatarSetContext()
self.avatarsNode = AnimatedAvatarSetNode()
self.avatarsNode.isUserInteractionEnabled = false
self.buttonNode = HighlightTrackingButtonNode()
super.init()
self.buttonNode.addSubnode(self.separatorNode)
self.buttonNode.addSubnode(self.countNode)
self.buttonNode.addSubnode(self.alternativeCountNode)
self.buttonNode.addSubnode(self.iconNode)
self.buttonNode.addSubnode(self.unreadIconNode)
self.buttonNode.addSubnode(self.arrowNode)
self.buttonNode.addSubnode(self.avatarsNode)
self.addSubnode(self.buttonNode)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
let nodes: [ASDisplayNode] = [
strongSelf.buttonNode
]
for node in nodes {
if highlighted {
node.layer.removeAnimation(forKey: "opacity")
node.alpha = 0.4
} else {
node.alpha = 1.0
node.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
}
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func buttonPressed() {
guard let item = self.item else {
return
}
if item.message.id.peerId.isReplies {
item.controllerInteraction.openReplyThreadOriginalMessage(item.message)
} else {
item.controllerInteraction.openMessageReplies(item.message.id, true, false)
}
}
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let makeCountLayout = self.countNode.asyncLayout()
let makeAlternativeCountLayout = self.alternativeCountNode.asyncLayout()
return { item, layoutConstants, preparePosition, _, constrainedSize, _ in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
let displaySeparator: Bool
let topOffset: CGFloat
let topSeparatorOffset: CGFloat
if case let .linear(top, _) = preparePosition, case .Neighbour(_, .media, _) = top {
displaySeparator = false
topOffset = 2.0
topSeparatorOffset = 0.0
} else {
displaySeparator = true
topOffset = 2.0
topSeparatorOffset = 2.0
}
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
let incoming = item.message.effectivelyIncoming(item.context.account.peerId)
let maxTextWidth = CGFloat.greatestFiniteMagnitude
let horizontalInset = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
var dateReplies = 0
var replyPeers: [Peer] = []
var hasUnseenReplies = false
for attribute in item.message.attributes {
if let attribute = attribute as? ReplyThreadMessageAttribute {
dateReplies = Int(attribute.count)
replyPeers = attribute.latestUsers.compactMap { peerId -> Peer? in
return item.message.peers[peerId]
}
if let maxMessageId = attribute.maxMessageId, let maxReadMessageId = attribute.maxReadMessageId {
hasUnseenReplies = maxMessageId > maxReadMessageId
}
}
}
let messageTheme = incoming ? item.presentationData.theme.theme.chat.message.incoming : item.presentationData.theme.theme.chat.message.outgoing
let textFont = Font.regular(17.0)
let rawSegments: [AnimatedCountLabelNode.Segment]
let rawAlternativeSegments: [AnimatedCountLabelNode.Segment]
var accessibilityLabel = ""
if item.message.id.peerId.isReplies {
rawSegments = [.text(100, NSAttributedString(string: item.presentationData.strings.Conversation_ViewReply, font: textFont, textColor: messageTheme.accentTextColor))]
rawAlternativeSegments = rawSegments
accessibilityLabel = item.presentationData.strings.Conversation_ViewReply
} else if dateReplies > 0 {
var commentsPart = item.presentationData.strings.Conversation_MessageViewComments(Int32(dateReplies))
if commentsPart.contains("[") && commentsPart.contains("]") {
if let startIndex = commentsPart.firstIndex(of: "["), let endIndex = commentsPart.firstIndex(of: "]") {
commentsPart.removeSubrange(startIndex ... endIndex)
}
} else {
commentsPart = commentsPart.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789-,. "))
}
var segments: [AnimatedCountLabelNode.Segment] = []
let textAndRanges = item.presentationData.strings.Conversation_MessageViewCommentsFormat("\(dateReplies)", commentsPart)
let rawText = textAndRanges.string
var textIndex = 0
var latestIndex = 0
for indexAndRange in textAndRanges.ranges {
var lowerSegmentIndex = indexAndRange.range.lowerBound
if indexAndRange.index != 0 {
lowerSegmentIndex = min(lowerSegmentIndex, latestIndex)
} else {
if latestIndex < indexAndRange.range.lowerBound {
let part = String(rawText[rawText.index(rawText.startIndex, offsetBy: latestIndex) ..< rawText.index(rawText.startIndex, offsetBy: indexAndRange.range.lowerBound)])
segments.append(.text(textIndex, NSAttributedString(string: part, font: textFont, textColor: messageTheme.accentTextColor)))
textIndex += 1
}
}
latestIndex = indexAndRange.range.upperBound
let part = String(rawText[rawText.index(rawText.startIndex, offsetBy: lowerSegmentIndex) ..< rawText.index(rawText.startIndex, offsetBy: min(rawText.count, indexAndRange.range.upperBound))])
if indexAndRange.index == 0 {
segments.append(.number(dateReplies, NSAttributedString(string: part, font: textFont, textColor: messageTheme.accentTextColor)))
} else {
segments.append(.text(textIndex, NSAttributedString(string: part, font: textFont, textColor: messageTheme.accentTextColor)))
textIndex += 1
}
}
if latestIndex < rawText.count {
let part = String(rawText[rawText.index(rawText.startIndex, offsetBy: latestIndex)...])
segments.append(.text(textIndex, NSAttributedString(string: part, font: textFont, textColor: messageTheme.accentTextColor)))
textIndex += 1
}
rawSegments = segments
rawAlternativeSegments = rawSegments
accessibilityLabel = rawText
} else {
rawSegments = [.text(100, NSAttributedString(string: item.presentationData.strings.Conversation_MessageLeaveComment, font: textFont, textColor: messageTheme.accentTextColor))]
rawAlternativeSegments = [.text(100, NSAttributedString(string: item.presentationData.strings.Conversation_MessageLeaveCommentShort, font: textFont, textColor: messageTheme.accentTextColor))]
accessibilityLabel = item.presentationData.strings.Conversation_MessageLeaveComment
}
let imageSize: CGFloat = 30.0
let imageSpacing: CGFloat = 20.0
var textLeftInset: CGFloat = 0.0
if replyPeers.isEmpty {
textLeftInset = 41.0
} else {
textLeftInset = 15.0 + imageSize * min(1.0, CGFloat(replyPeers.count)) + (imageSpacing) * max(0.0, min(2.0, CGFloat(replyPeers.count - 1)))
}
let textRightInset: CGFloat = 24.0
let textConstrainedSize = CGSize(width: min(maxTextWidth, constrainedSize.width - horizontalInset - textLeftInset - textRightInset), height: constrainedSize.height)
let textInsets = UIEdgeInsets()//(top: 2.0, left: 2.0, bottom: 5.0, right: 2.0)
let (countLayout, countApply) = makeCountLayout(textConstrainedSize, .zero, rawSegments)
let (alternativeCountLayout, alternativeCountApply) = makeAlternativeCountLayout(textConstrainedSize, .zero, rawAlternativeSegments)
var textFrame = CGRect(origin: CGPoint(x: -textInsets.left + textLeftInset - 2.0, y: -textInsets.top + 5.0 + topOffset), size: countLayout.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))
textFrame = textFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top - 5.0 + UIScreenPixel)
textFrameWithoutInsets = textFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top)
var suggestedBoundingWidth: CGFloat
suggestedBoundingWidth = textFrameWithoutInsets.width
suggestedBoundingWidth += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right + textLeftInset + textRightInset
let iconImage: UIImage?
let iconOffset: CGPoint
if item.message.id.peerId.isReplies {
iconImage = PresentationResourcesChat.chatMessageRepliesIcon(item.presentationData.theme.theme, incoming: incoming)
iconOffset = CGPoint(x: -4.0, y: -4.0)
} else {
iconImage = PresentationResourcesChat.chatMessageCommentsIcon(item.presentationData.theme.theme, incoming: incoming)
iconOffset = CGPoint(x: 0.0, y: -1.0)
}
let arrowImage = PresentationResourcesChat.chatMessageCommentsArrowIcon(item.presentationData.theme.theme, incoming: incoming)
let unreadIconImage = PresentationResourcesChat.chatMessageCommentsUnreadDotIcon(item.presentationData.theme.theme, incoming: incoming)
return (suggestedBoundingWidth, { boundingWidth in
var boundingSize: CGSize
boundingSize = textFrameWithoutInsets.size
boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
boundingSize.height = 40.0 + topOffset
return (boundingSize, { [weak self] animation, synchronousLoad, _ in
if let strongSelf = self {
strongSelf.item = item
let transition: ContainedViewLayoutTransition
if animation.isAnimated {
transition = .animated(duration: 0.2, curve: .easeInOut)
} else {
transition = .immediate
}
strongSelf.countNode.isHidden = countLayout.isTruncated
strongSelf.alternativeCountNode.isHidden = !strongSelf.countNode.isHidden
strongSelf.buttonNode.accessibilityLabel = accessibilityLabel
let _ = countApply(animation.isAnimated)
let _ = alternativeCountApply(animation.isAnimated)
let adjustedTextFrame = textFrame
if strongSelf.countNode.frame.isEmpty {
strongSelf.countNode.frame = adjustedTextFrame
} else {
transition.updateFrameAdditive(node: strongSelf.countNode, frame: adjustedTextFrame)
}
if strongSelf.alternativeCountNode.frame.isEmpty {
strongSelf.alternativeCountNode.frame = CGRect(origin: adjustedTextFrame.origin, size: alternativeCountLayout.size)
} else {
transition.updateFrameAdditive(node: strongSelf.alternativeCountNode, frame: CGRect(origin: adjustedTextFrame.origin, size: alternativeCountLayout.size))
}
let effectiveTextFrame: CGRect
if !strongSelf.alternativeCountNode.isHidden {
effectiveTextFrame = strongSelf.alternativeCountNode.frame
} else {
effectiveTextFrame = strongSelf.countNode.frame
}
if let iconImage = iconImage {
strongSelf.iconNode.image = iconImage
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: 15.0 + iconOffset.x, y: 6.0 + iconOffset.y + topOffset), size: iconImage.size)
}
if let arrowImage = arrowImage {
strongSelf.arrowNode.image = arrowImage
let arrowFrame = CGRect(origin: CGPoint(x: boundingWidth - 33.0, y: 6.0 + topOffset), size: arrowImage.size)
if strongSelf.arrowNode.frame.isEmpty {
strongSelf.arrowNode.frame = arrowFrame
} else {
transition.updateFrameAdditive(node: strongSelf.arrowNode, frame: arrowFrame)
}
if let unreadIconImage = unreadIconImage {
strongSelf.unreadIconNode.image = unreadIconImage
let unreadIconFrame = CGRect(origin: CGPoint(x: effectiveTextFrame.maxX + 4.0, y: effectiveTextFrame.minY + floor((effectiveTextFrame.height - unreadIconImage.size.height) / 2.0) + 1.0), size: unreadIconImage.size)
if strongSelf.unreadIconNode.frame.isEmpty {
strongSelf.unreadIconNode.frame = unreadIconFrame
} else {
transition.updateFrameAdditive(node: strongSelf.unreadIconNode, frame: unreadIconFrame)
}
}
}
if strongSelf.unreadIconNode.alpha.isZero != !hasUnseenReplies {
transition.updateAlpha(node: strongSelf.unreadIconNode, alpha: hasUnseenReplies ? 1.0 : 0.0)
if hasUnseenReplies {
strongSelf.unreadIconNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0)
}
}
let hasActivity = item.controllerInteraction.currentMessageWithLoadingReplyThread == item.message.id
if hasActivity {
strongSelf.arrowNode.isHidden = true
let statusNode: RadialStatusNode
if let current = strongSelf.statusNode {
statusNode = current
} else {
statusNode = RadialStatusNode(backgroundNodeColor: .clear)
strongSelf.statusNode = statusNode
strongSelf.buttonNode.addSubnode(statusNode)
}
let statusSize = CGSize(width: 20.0, height: 20.0)
let statusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusSize.width - 11.0, y: 8.0 + topOffset), size: statusSize)
if statusNode.frame.isEmpty {
statusNode.frame = statusFrame
} else {
transition.updateFrameAdditive(node: statusNode, frame: statusFrame)
}
statusNode.transitionToState(.progress(color: messageTheme.accentTextColor, lineWidth: 1.5, value: nil, cancelEnabled: false, animateRotation: true), animated: false, synchronous: false, completion: {})
} else {
strongSelf.arrowNode.isHidden = false
if let statusNode = strongSelf.statusNode {
strongSelf.statusNode = nil
statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.3, removeOnCompletion: false, completion: { [weak statusNode] _ in
statusNode?.removeFromSupernode()
})
strongSelf.arrowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.3)
}
}
let avatarContent = strongSelf.avatarsContext.update(peers: replyPeers.map(EnginePeer.init), animated: animation.isAnimated)
let avatarsSize = strongSelf.avatarsNode.update(context: item.context, content: avatarContent, animated: animation.isAnimated, synchronousLoad: synchronousLoad)
let iconAlpha: CGFloat = avatarsSize.width.isZero ? 1.0 : 0.0
if iconAlpha.isZero != strongSelf.iconNode.alpha.isZero {
transition.updateAlpha(node: strongSelf.iconNode, alpha: iconAlpha)
if animation.isAnimated {
if iconAlpha.isZero {
} else {
strongSelf.iconNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
}
}
}
let avatarsFrame = CGRect(origin: CGPoint(x: 13.0, y: 3.0 + topOffset), size: avatarsSize)
strongSelf.avatarsNode.frame = avatarsFrame
//strongSelf.avatarsNode.updateLayout(size: avatarsFrame.size)
//strongSelf.avatarsNode.update(context: item.context, peers: replyPeers, synchronousLoad: synchronousLoad, imageSize: imageSize, imageSpacing: imageSpacing, borderWidth: 2.0 - UIScreenPixel)
strongSelf.separatorNode.backgroundColor = messageTheme.polls.separator
strongSelf.separatorNode.isHidden = !displaySeparator
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: layoutConstants.bubble.strokeInsets.left, y: -3.0 + topSeparatorOffset), size: CGSize(width: boundingWidth - layoutConstants.bubble.strokeInsets.left - layoutConstants.bubble.strokeInsets.right, height: UIScreenPixel))
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: boundingWidth, height: boundingSize.height))
strongSelf.buttonNode.isUserInteractionEnabled = item.message.id.namespace == Namespaces.Message.Cloud
strongSelf.buttonNode.alpha = item.message.id.namespace == Namespaces.Message.Cloud ? 1.0 : 0.5
}
})
})
})
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
}
override public func animateInsertionIntoBubble(_ duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.buttonNode.frame.contains(point) {
return ChatMessageBubbleContentTapAction(content: .ignore)
}
return ChatMessageBubbleContentTapAction(content: .none)
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.buttonNode.isUserInteractionEnabled && self.buttonNode.frame.contains(point) {
return self.buttonNode.view
}
return nil
}
}
@@ -0,0 +1,32 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageContactBubbleContentNode",
module_name = "ChatMessageContactBubbleContentNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/AvatarNode",
"//submodules/AccountContext",
"//submodules/PhoneNumberFormat",
"//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
"//submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,613 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import AvatarNode
import AccountContext
import PhoneNumberFormat
import ChatMessageDateAndStatusNode
import ChatMessageBubbleContentNode
import ChatMessageItemCommon
import ChatMessageAttachedContentButtonNode
import ChatControllerInteraction
import MessageInlineBlockBackgroundView
private let avatarFont = avatarPlaceholderFont(size: 16.0)
private let titleFont = Font.semibold(14.0)
private let textFont = Font.regular(14.0)
public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
private var backgroundView: MessageInlineBlockBackgroundView?
private var actionButtonSeparator: SimpleLayer?
private let avatarNode: AvatarNode
private let dateAndStatusNode: ChatMessageDateAndStatusNode
private let titleNode: TextNode
private let textNode: TextNode
private var contact: TelegramMediaContact?
private var contactInfo : String?
private let addButtonNode: ChatMessageAttachedContentButtonNode
private let messageButtonNode: ChatMessageAttachedContentButtonNode
required public init() {
self.avatarNode = AvatarNode(font: avatarFont)
self.dateAndStatusNode = ChatMessageDateAndStatusNode()
self.titleNode = TextNode()
self.textNode = TextNode()
self.addButtonNode = ChatMessageAttachedContentButtonNode()
self.messageButtonNode = ChatMessageAttachedContentButtonNode()
super.init()
self.addSubnode(self.avatarNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.addButtonNode)
self.addSubnode(self.messageButtonNode)
self.addButtonNode.addTarget(self, action: #selector(self.addButtonPressed), forControlEvents: .touchUpInside)
self.messageButtonNode.addTarget(self, action: #selector(self.messageButtonPressed), forControlEvents: .touchUpInside)
self.dateAndStatusNode.reactionSelected = { [weak self] _, value, sourceView in
guard let strongSelf = self, let item = strongSelf.item else {
return
}
item.controllerInteraction.updateMessageReaction(item.topMessage, .reaction(value), false, sourceView)
}
self.dateAndStatusNode.openReactionPreview = { [weak self] gesture, sourceView, value in
guard let strongSelf = self, let item = strongSelf.item else {
gesture?.cancel()
return
}
item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceView, gesture, value)
}
}
override public func accessibilityActivate() -> Bool {
self.addButtonPressed()
return true
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func didLoad() {
super.didLoad()
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.contactTap(_:)))
self.view.addGestureRecognizer(tapRecognizer)
}
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let statusLayout = self.dateAndStatusNode.asyncLayout()
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeTextLayout = TextNode.asyncLayout(self.textNode)
let makeMessageButtonLayout = ChatMessageAttachedContentButtonNode.asyncLayout(self.messageButtonNode)
let makeAddButtonLayout = ChatMessageAttachedContentButtonNode.asyncLayout(self.addButtonNode)
let previousContact = self.contact
let previousContactInfo = self.contactInfo
return { item, layoutConstants, _, _, constrainedSize, _ in
var selectedContact: TelegramMediaContact?
for media in item.message.media {
if let media = media as? TelegramMediaContact {
selectedContact = media;
}
}
var incoming = item.message.effectivelyIncoming(item.context.account.peerId)
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info {
incoming = false
}
var contactPeer: Peer?
if let peerId = selectedContact?.peerId, let peer = item.message.peers[peerId] {
contactPeer = peer
}
let nameColors: PeerNameColors.Colors?
switch contactPeer?.nameColor {
case let .preset(nameColor):
nameColors = item.context.peerNameColors.get(nameColor, dark: item.presentationData.theme.theme.overallDarkAppearance)
case let .collectible(collectibleColor):
nameColors = collectibleColor.peerNameColors(dark: item.presentationData.theme.theme.overallDarkAppearance)
default:
nameColors = nil
}
let messageTheme = incoming ? item.presentationData.theme.theme.chat.message.incoming : item.presentationData.theme.theme.chat.message.outgoing
let mainColor: UIColor
var secondaryColor: UIColor?
var tertiaryColor: UIColor?
if !incoming {
mainColor = messageTheme.accentTextColor
if let _ = nameColors?.secondary {
secondaryColor = .clear
}
if let _ = nameColors?.tertiary {
tertiaryColor = .clear
}
} else {
var authorNameColor: UIColor?
authorNameColor = nameColors?.main
secondaryColor = nameColors?.secondary
tertiaryColor = nameColors?.tertiary
if let authorNameColor {
mainColor = authorNameColor
} else {
mainColor = messageTheme.accentTextColor
}
}
var titleString: NSAttributedString?
var textString: NSAttributedString?
var updatedContactInfo: String?
var canMessage = false
var canAdd = false
var displayName: String = ""
if let selectedContact = selectedContact {
if !selectedContact.firstName.isEmpty && !selectedContact.lastName.isEmpty {
displayName = "\(selectedContact.firstName) \(selectedContact.lastName)"
} else if !selectedContact.firstName.isEmpty {
displayName = selectedContact.firstName
} else {
displayName = selectedContact.lastName
}
if displayName.isEmpty {
displayName = item.presentationData.strings.Message_Contact
}
if selectedContact.peerId != nil {
canMessage = true
}
let info: String
if let previousContact = previousContact, previousContact.isEqual(to: selectedContact), let contactInfo = previousContactInfo {
info = contactInfo
} else {
if let vCard = selectedContact.vCardData, let vCardData = vCard.data(using: .utf8), let contactData = DeviceContactExtendedData(vcard: vCardData) {
if displayName.isEmpty && !contactData.organization.isEmpty {
displayName = contactData.organization
}
let infoLineLimit = 5
var infoComponents: [String] = []
if !contactData.basicData.phoneNumbers.isEmpty {
for phone in contactData.basicData.phoneNumbers {
if infoComponents.count < infoLineLimit {
infoComponents.append(formatPhoneNumber(context: item.context, number: phone.value))
}
}
} else {
infoComponents.append(formatPhoneNumber(context: item.context, number: selectedContact.phoneNumber))
}
if infoComponents.count < infoLineLimit {
for email in contactData.emailAddresses {
if infoComponents.count < infoLineLimit {
infoComponents.append(email.value)
}
}
}
if infoComponents.count < infoLineLimit {
if !contactData.organization.isEmpty && displayName != contactData.organization {
infoComponents.append(contactData.organization)
}
}
info = infoComponents.joined(separator: "\n")
} else {
info = formatPhoneNumber(context: item.context, number: selectedContact.phoneNumber)
}
}
canAdd = !item.associatedData.deviceContactsNumbers.contains(selectedContact.phoneNumber)
updatedContactInfo = info
titleString = NSAttributedString(string: displayName, font: titleFont, textColor: mainColor)
textString = NSAttributedString(string: info, font: textFont, textColor: messageTheme.primaryTextColor)
} else {
updatedContactInfo = nil
}
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
let avatarSize = CGSize(width: 40.0, height: 40.0)
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()))
var edited = false
if item.attributes.updatingMedia != nil {
edited = true
}
var viewCount: Int?
var dateReplies = 0
var starsCount: Int64?
var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message)
if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) {
dateReactionsAndPeers = ([], [])
}
for attribute in item.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 = item.chatLocation {
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info {
dateReplies = Int(attribute.count)
}
} else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel {
starsCount = attribute.stars.value
}
}
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, associatedData: item.associatedData)
let statusType: ChatMessageDateAndStatusType?
if case .customChatContents = item.associatedData.subject {
statusType = nil
} else if item.message.timestamp == 0 {
statusType = nil
} else {
switch position {
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if incoming {
statusType = .BubbleIncoming
} else {
if item.message.flags.contains(.Failed) {
statusType = .BubbleOutgoing(.Failed)
} else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil {
statusType = .BubbleOutgoing(.Sending)
} else {
statusType = .BubbleOutgoing(.Sent(read: item.read))
}
}
default:
statusType = nil
}
}
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))?
let messageEffect = item.message.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects)
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: 1000.0, reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil),
constrainedSize: CGSize(width: constrainedSize.width - sideInsets, height: .greatestFiniteMagnitude),
availableReactions: item.associatedData.availableReactions,
savedMessageTags: item.associatedData.savedMessageTags,
reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
displayAllReactionPeers: item.message.id.peerId.namespace == Namespaces.Peer.CloudUser,
areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId),
areStarReactionsEnabled: item.associatedData.areStarReactionsEnabled,
messageEffect: messageEffect,
replyCount: dateReplies,
starsCount: starsCount,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread,
hasAutoremove: item.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: item.topMessage),
animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
))
}
let avatarPlaceholderColor: UIColor
if incoming {
avatarPlaceholderColor = item.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor
} else {
avatarPlaceholderColor = item.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor
}
let (messageButtonWidth, messageContinueLayout) = makeMessageButtonLayout(constrainedSize.width, 10.0, nil, false, item.presentationData.strings.Conversation_ContactMessage.uppercased(), mainColor, false, false)
let addTitle: String
if !canMessage && !canAdd {
addTitle = item.presentationData.strings.Conversation_ViewContactDetails
} else {
if canMessage {
addTitle = item.presentationData.strings.Conversation_ContactAddContact
} else {
addTitle = item.presentationData.strings.Conversation_ContactAddContactLong
}
}
let (addButtonWidth, addContinueLayout) = makeAddButtonLayout(constrainedSize.width, 10.0, nil, false, addTitle.uppercased(), mainColor, false, false)
let showAddButton = !(!canAdd && canMessage)
let showMessageButton = canMessage
let buttonCount = (showAddButton ? 1 : 0) + (showMessageButton ? 1 : 0)
let maxButtonWidth = max(messageButtonWidth, addButtonWidth)
var maxContentWidth: CGFloat = avatarSize.width + 7.0
if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue {
maxContentWidth = max(maxContentWidth, statusSuggestedWidthAndContinue.0)
}
maxContentWidth = max(maxContentWidth, 7.0 + avatarSize.width + 7.0 + titleLayout.size.width + 7.0)
maxContentWidth = max(maxContentWidth, 7.0 + avatarSize.width + 7.0 + textLayout.size.width + 7.0)
maxContentWidth = max(maxContentWidth, maxButtonWidth * CGFloat(buttonCount))
maxContentWidth = max(maxContentWidth, 220.0)
let contentWidth = maxContentWidth + layoutConstants.text.bubbleInsets.right * 2.0
return (contentWidth, { boundingWidth in
let baseAvatarFrame = CGRect(origin: CGPoint(x: layoutConstants.text.bubbleInsets.right, y: layoutConstants.text.bubbleInsets.top), size: avatarSize)
let lineWidth: CGFloat = 3.0
var buttonCount = 1
if canMessage && canAdd {
buttonCount += 1
}
var buttonWidth = floor((boundingWidth - layoutConstants.text.bubbleInsets.right * 2.0 - lineWidth))
if buttonCount > 1 {
buttonWidth /= CGFloat(buttonCount)
}
let (messageButtonSize, messageButtonApply) = messageContinueLayout(buttonWidth, 33.0)
let (addButtonSize, addButtonApply) = addContinueLayout(buttonWidth, 33.0)
let buttonSpacing: CGFloat = 4.0
let statusSizeAndApply = statusSuggestedWidthAndContinue?.1(boundingWidth - sideInsets)
var layoutSize = CGSize(width: contentWidth, height: 64.0 + textLayout.size.height + addButtonSize.height + buttonSpacing)
if let statusSizeAndApply = statusSizeAndApply {
layoutSize.height += statusSizeAndApply.0.height - 4.0
}
let messageButtonFrame = CGRect(origin: CGPoint(x: layoutConstants.text.bubbleInsets.right + lineWidth, y: layoutSize.height - 24.0 - messageButtonSize.height), size: messageButtonSize)
let addButtonFrame = CGRect(origin: CGPoint(x: layoutConstants.text.bubbleInsets.right + lineWidth + (canMessage ? buttonWidth : 0.0), y: layoutSize.height - 24.0 - addButtonSize.height), size: addButtonSize)
let avatarFrame = baseAvatarFrame.offsetBy(dx: 9.0, dy: 14.0)
var customLetters: [String] = []
if let selectedContact = selectedContact, selectedContact.peerId == nil {
let firstName = selectedContact.firstName
let lastName = selectedContact.lastName
if !firstName.isEmpty && !lastName.isEmpty {
customLetters = [String(firstName[..<firstName.index(after: firstName.startIndex)]).uppercased(), String(lastName[..<lastName.index(after: lastName.startIndex)]).uppercased()]
} else if !firstName.isEmpty {
customLetters = [String(firstName[..<firstName.index(after: firstName.startIndex)]).uppercased()]
} else if !lastName.isEmpty {
customLetters = [String(lastName[..<lastName.index(after: lastName.startIndex)]).uppercased()]
} else if !displayName.isEmpty {
customLetters = [String(displayName[..<displayName.index(after: displayName.startIndex)]).uppercased()]
}
}
return (layoutSize, { [weak self] animation, synchronousLoads, _ in
if let strongSelf = self {
strongSelf.item = item
strongSelf.contact = selectedContact
strongSelf.contactInfo = updatedContactInfo
strongSelf.avatarNode.frame = avatarFrame
let _ = titleApply()
let _ = textApply()
let _ = messageButtonApply(animation)
let _ = addButtonApply(animation)
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: avatarFrame.maxX + 7.0, y: avatarFrame.minY + 1.0), size: titleLayout.size)
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: avatarFrame.maxX + 7.0, y: avatarFrame.minY + 20.0), size: textLayout.size)
strongSelf.addButtonNode.frame = addButtonFrame
strongSelf.addButtonNode.isHidden = !canAdd && canMessage
strongSelf.messageButtonNode.frame = messageButtonFrame
strongSelf.messageButtonNode.isHidden = !canMessage
let backgroundInsets = layoutConstants.text.bubbleInsets
let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top + 5.0), size: CGSize(width: boundingWidth - layoutConstants.text.bubbleInsets.right * 2.0, height: layoutSize.height - 34.0))
if let statusSizeAndApply = statusSizeAndApply {
strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: layoutConstants.text.bubbleInsets.left, y: backgroundFrame.maxY + 3.0), size: statusSizeAndApply.0)
if strongSelf.dateAndStatusNode.supernode == nil {
strongSelf.addSubnode(strongSelf.dateAndStatusNode)
statusSizeAndApply.1(.None)
} else {
statusSizeAndApply.1(animation)
}
} else if strongSelf.dateAndStatusNode.supernode != nil {
strongSelf.dateAndStatusNode.removeFromSupernode()
}
if let _ = titleString {
if strongSelf.titleNode.supernode == nil {
strongSelf.addSubnode(strongSelf.titleNode)
}
if strongSelf.textNode.supernode == nil {
strongSelf.addSubnode(strongSelf.textNode)
}
} else {
if strongSelf.titleNode.supernode != nil {
strongSelf.titleNode.removeFromSupernode()
}
if strongSelf.textNode.supernode != nil {
strongSelf.textNode.removeFromSupernode()
}
}
if let peerId = selectedContact?.peerId, let peer = item.message.peers[peerId] {
strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme.theme, peer: EnginePeer(peer), emptyColor: avatarPlaceholderColor, synchronousLoad: synchronousLoads)
} else {
strongSelf.avatarNode.setCustomLetters(customLetters)
}
if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported) {
strongSelf.dateAndStatusNode.pressed = {
guard let strongSelf = self, let item = strongSelf.item else {
return
}
item.controllerInteraction.displayImportedMessageTooltip(strongSelf.dateAndStatusNode)
}
} else if messageEffect != nil {
strongSelf.dateAndStatusNode.pressed = {
guard let strongSelf = self, let item = strongSelf.item else {
return
}
item.controllerInteraction.playMessageEffect(item.message)
}
} else {
strongSelf.dateAndStatusNode.pressed = nil
}
var pattern: MessageInlineBlockBackgroundView.Pattern?
if let contactPeer, let backgroundEmojiId = contactPeer.backgroundEmojiId {
pattern = MessageInlineBlockBackgroundView.Pattern(
context: item.context,
fileId: backgroundEmojiId,
file: item.message.associatedMedia[MediaId(
namespace: Namespaces.Media.CloudFile,
id: backgroundEmojiId
)] as? TelegramMediaFile
)
}
let patternTopRightPosition = CGPoint()
let backgroundView: MessageInlineBlockBackgroundView
if let current = strongSelf.backgroundView {
backgroundView = current
animation.animator.updateFrame(layer: backgroundView.layer, frame: backgroundFrame, completion: nil)
backgroundView.update(size: backgroundFrame.size, isTransparent: false, primaryColor: mainColor, secondaryColor: secondaryColor, thirdColor: tertiaryColor, backgroundColor: nil, pattern: pattern, patternTopRightPosition: patternTopRightPosition, animation: animation)
} else {
backgroundView = MessageInlineBlockBackgroundView()
strongSelf.backgroundView = backgroundView
backgroundView.frame = backgroundFrame
strongSelf.view.insertSubview(backgroundView, at: 0)
backgroundView.update(size: backgroundFrame.size, isTransparent: false, primaryColor: mainColor, secondaryColor: secondaryColor, thirdColor: tertiaryColor, backgroundColor: nil, pattern: pattern, patternTopRightPosition: patternTopRightPosition, animation: .None)
}
let separatorFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + 9.0, y: backgroundFrame.maxY - 36.0), size: CGSize(width: backgroundFrame.width - 18.0, height: UIScreenPixel))
let actionButtonSeparator: SimpleLayer
if let current = strongSelf.actionButtonSeparator {
actionButtonSeparator = current
animation.animator.updateFrame(layer: actionButtonSeparator, frame: separatorFrame, completion: nil)
} else {
actionButtonSeparator = SimpleLayer()
strongSelf.actionButtonSeparator = actionButtonSeparator
strongSelf.layer.addSublayer(actionButtonSeparator)
actionButtonSeparator.frame = separatorFrame
}
actionButtonSeparator.backgroundColor = mainColor.withMultipliedAlpha(0.2).cgColor
}
})
})
})
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.messageButtonNode.frame.contains(point) {
return ChatMessageBubbleContentTapAction(content: .ignore)
}
if self.addButtonNode.frame.contains(point) {
return ChatMessageBubbleContentTapAction(content: .ignore)
}
if self.dateAndStatusNode.supernode != nil, let _ = self.dateAndStatusNode.hitTest(self.view.convert(point, to: self.dateAndStatusNode.view), with: nil) {
return ChatMessageBubbleContentTapAction(content: .ignore)
}
return ChatMessageBubbleContentTapAction(content: .none)
}
@objc private func contactTap(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
if let item = self.item {
var selectedContact: TelegramMediaContact?
for media in item.message.media {
if let media = media as? TelegramMediaContact {
selectedContact = media
}
}
if let peerId = selectedContact?.peerId, let peer = item.message.peers[peerId] {
item.controllerInteraction.openPeer(EnginePeer(peer), .info(nil), nil, .default)
} else {
let _ = item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: .default))
}
}
}
}
@objc private func addButtonPressed() {
if let item = self.item {
let _ = item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: .default))
}
}
@objc private func messageButtonPressed() {
if let item = self.item {
var selectedContact: TelegramMediaContact?
for media in item.message.media {
if let media = media as? TelegramMediaContact {
selectedContact = media
}
}
if let peerId = selectedContact?.peerId, let peer = item.message.peers[peerId] {
item.controllerInteraction.openPeer(EnginePeer(peer), .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default)
}
}
}
override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? {
if !self.dateAndStatusNode.isHidden {
return self.dateAndStatusNode.reactionView(value: value)
}
return nil
}
override public func messageEffectTargetView() -> UIView? {
if !self.dateAndStatusNode.isHidden {
return self.dateAndStatusNode.messageEffectTargetView()
}
return nil
}
override public 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)
}
}
@@ -0,0 +1,32 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageDateAndStatusNode",
module_name = "ChatMessageDateAndStatusNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramUIPreferences",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/TelegramStringFormatting",
"//submodules/LocalizedPeerData",
"//submodules/Components/ReactionButtonListComponent",
"//submodules/Components/ReactionImageComponent",
"//submodules/TelegramUI/Components/AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,224 @@
import Foundation
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import TelegramStringFormatting
import LocalizedPeerData
import AccountContext
public enum MessageTimestampStatusFormat {
case full
case regular
case minimal
}
private func dateStringForDay(strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, timestamp: Int32) -> String {
var t: time_t = time_t(timestamp)
var timeinfo: tm = tm()
localtime_r(&t, &timeinfo)
let timestampNow = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
var now: time_t = time_t(timestampNow)
var timeinfoNow: tm = tm()
localtime_r(&now, &timeinfoNow)
if timeinfo.tm_year != timeinfoNow.tm_year {
return "\(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat))"
} else {
return "\(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, dateTimeFormat: dateTimeFormat))"
}
}
private func monthAtIndex(_ index: Int, strings: PresentationStrings) -> String {
switch index {
case 0:
return strings.Month_ShortJanuary
case 1:
return strings.Month_ShortFebruary
case 2:
return strings.Month_ShortMarch
case 3:
return strings.Month_ShortApril
case 4:
return strings.Month_ShortMay
case 5:
return strings.Month_ShortJune
case 6:
return strings.Month_ShortJuly
case 7:
return strings.Month_ShortAugust
case 8:
return strings.Month_ShortSeptember
case 9:
return strings.Month_ShortOctober
case 10:
return strings.Month_ShortNovember
case 11:
return strings.Month_ShortDecember
default:
return ""
}
}
public func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Message, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, strings: PresentationStrings, format: MessageTimestampStatusFormat = .regular, associatedData: ChatMessageItemAssociatedData, ignoreAuthor: Bool = false) -> String {
if let adAttribute = message.adAttribute {
switch adAttribute.messageType {
case .sponsored:
return strings.Message_SponsoredLabel
case .recommended:
return strings.Message_RecommendedLabel
}
}
var timestamp: Int32
if let scheduleTime = message.scheduleTime {
timestamp = scheduleTime
} else {
timestamp = message.timestamp
}
var displayFullDate = false
if case .full = format, timestamp > 100000 {
displayFullDate = true
} else if let forwardInfo = message.forwardInfo, message.id.peerId == accountPeerId {
displayFullDate = true
timestamp = forwardInfo.date
}
if let sourceAuthorInfo = message.sourceAuthorInfo, let orignalDate = sourceAuthorInfo.orignalDate {
timestamp = orignalDate
}
var dateText = stringForMessageTimestamp(timestamp: timestamp, dateTimeFormat: dateTimeFormat)
if timestamp == scheduleWhenOnlineTimestamp {
dateText = " "
}
if let repeatPeriod = message.scheduleRepeatPeriod {
let repeatString: String
switch repeatPeriod {
case 60:
repeatString = "1 min"
case 300:
repeatString = "5 min"
case 86400:
repeatString = strings.Message_RepeatPeriod_Daily
case 7 * 86400:
repeatString = strings.Message_RepeatPeriod_Weekly
case 14 * 86400:
repeatString = strings.Message_RepeatPeriod_Biweekly
case 30 * 86400:
repeatString = strings.Message_RepeatPeriod_Monthly
case 91 * 86400:
repeatString = strings.Message_RepeatPeriod_3Months
case 182 * 86400:
repeatString = strings.Message_RepeatPeriod_6Months
case 365 * 86400:
repeatString = strings.Message_RepeatPeriod_Yearly
default:
repeatString = "\(repeatPeriod)s"
}
dateText = strings.Message_RepeatAt(repeatString, dateText).string
}
if message.id.namespace == Namespaces.Message.ScheduledCloud, let _ = message.pendingProcessingAttribute {
return strings.Message_Approximate(dateText).string
}
if displayFullDate {
let dayText: String
let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
var t: time_t = time_t(timestamp)
var timeinfo: tm = tm()
localtime_r(&t, &timeinfo)
var now: time_t = time_t(nowTimestamp)
var timeinfoNow: tm = tm()
localtime_r(&now, &timeinfoNow)
if timeinfo.tm_year == timeinfoNow.tm_year {
if format != .full, timeinfo.tm_yday == timeinfoNow.tm_yday {
dayText = strings.Weekday_Today
} else {
dayText = strings.Date_ChatDateHeader(monthAtIndex(Int(timeinfo.tm_mon), strings: strings), "\(timeinfo.tm_mday)").string
}
} else {
dayText = strings.Date_ChatDateHeaderYear(monthAtIndex(Int(timeinfo.tm_mon), strings: strings), "\(timeinfo.tm_mday)", "\(1900 + timeinfo.tm_year)").string
}
dateText = strings.Message_FullDateFormat(dayText, stringForMessageTimestamp(timestamp: timestamp, dateTimeFormat: dateTimeFormat)).string
}
else if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported) {
dateText = strings.Message_ImportedDateFormat(dateStringForDay(strings: strings, dateTimeFormat: dateTimeFormat, timestamp: forwardInfo.date), stringForMessageTimestamp(timestamp: forwardInfo.date, dateTimeFormat: dateTimeFormat), dateText).string
}
var authorTitle: String?
if let author = message.author as? TelegramUser {
if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info {
if let channel = message.peers[message.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info, message.author?.id != channel.id, info.flags.contains(.messagesShouldHaveProfiles) {
} else {
authorTitle = EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder)
}
} else if let forwardInfo = message.forwardInfo, forwardInfo.sourceMessageId?.peerId.namespace == Namespaces.Peer.CloudChannel {
authorTitle = forwardInfo.authorSignature
}
} else {
if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info {
if let channel = message.peers[message.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info, message.author?.id != channel.id, info.flags.contains(.messagesShouldHaveProfiles) {
} else {
for attribute in message.attributes {
if let attribute = attribute as? AuthorSignatureMessageAttribute {
authorTitle = attribute.signature
break
}
}
}
}
if message.id.peerId != accountPeerId {
for attribute in message.attributes {
if let attribute = attribute as? SourceReferenceMessageAttribute {
if let forwardInfo = message.forwardInfo {
if forwardInfo.author?.id == attribute.messageId.peerId {
if authorTitle == nil {
authorTitle = forwardInfo.authorSignature
}
}
}
break
}
}
}
}
if authorTitle == nil {
for attribute in message.attributes {
if let attribute = attribute as? InlineBusinessBotMessageAttribute {
if let title = attribute.title {
authorTitle = title
} else if let peerId = attribute.peerId, let peer = message.peers[peerId] {
authorTitle = peer.debugDisplayTitle
}
}
}
}
if let subject = associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info {
authorTitle = nil
}
if ignoreAuthor {
authorTitle = nil
}
if case .minimal = format {
} else {
if let authorTitle = authorTitle, !authorTitle.isEmpty {
dateText = "\(authorTitle), \(dateText)"
}
}
return dateText
}
@@ -0,0 +1,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageDeliveryFailedNode",
module_name = "ChatMessageDeliveryFailedNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/TelegramPresentationData",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,40 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
public final class ChatMessageDeliveryFailedNode: ASImageNode {
private let tapped: () -> Void
private var theme: PresentationTheme?
public init(tapped: @escaping () -> Void) {
self.tapped = tapped
super.init()
self.displaysAsynchronously = false
self.displayWithoutProcessing = true
self.isUserInteractionEnabled = true
}
override public func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.tapped()
}
}
public func updateLayout(theme: PresentationTheme) -> CGSize {
if self.theme !== theme {
self.theme = theme
self.image = PresentationResourcesChat.chatBubbleDeliveryFailedIcon(theme)
}
return CGSize(width: 22.0, height: 22.0)
}
}
@@ -0,0 +1,25 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageEventLogPreviousDescriptionContentNode",
module_name = "ChatMessageEventLogPreviousDescriptionContentNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Postbox",
"//submodules/Display",
"//submodules/AsyncDisplayKit",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramCore",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,118 @@
import Foundation
import UIKit
import Postbox
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import ChatMessageBubbleContentNode
import ChatMessageItemCommon
import ChatMessageAttachedContentNode
public final class ChatMessageEventLogPreviousDescriptionContentNode: ChatMessageBubbleContentNode {
private let contentNode: ChatMessageAttachedContentNode
override public var visibility: ListViewItemNodeVisibility {
didSet {
self.contentNode.visibility = visibility
}
}
required public init() {
self.contentNode = ChatMessageAttachedContentNode()
super.init()
self.addSubnode(self.contentNode)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let contentNodeLayout = self.contentNode.asyncLayout()
return { item, layoutConstants, preparePosition, _, constrainedSize, _ in
var messageEntities: [MessageTextEntity]?
for attribute in item.message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
messageEntities = attribute.entities
break
}
}
let title: String = item.presentationData.strings.Channel_AdminLog_MessagePreviousDescription
let text: String
if item.message.text.isEmpty {
text = item.presentationData.strings.Channel_AdminLog_EmptyMessageText
} else {
text = item.message.text
}
let mediaAndFlags: ([Media], ChatMessageAttachedContentNodeMediaFlags)? = nil
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(id: item.message.id.peerId), title, nil, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize, item.controllerInteraction.presentationContext.animationCache, item.controllerInteraction.presentationContext.animationRenderer)
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
return (contentProperties, nil, initialWidth, { constrainedSize, position in
let (refinedWidth, finalizeLayout) = continueLayout(constrainedSize, position)
return (refinedWidth, { boundingWidth in
let (size, apply) = finalizeLayout(boundingWidth)
return (size, { [weak self] animation, synchronousLoads, applyInfo in
if let strongSelf = self {
strongSelf.item = item
apply(animation, synchronousLoads, applyInfo)
strongSelf.contentNode.frame = CGRect(origin: CGPoint(), size: size)
}
})
})
})
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
}
override public func animateInsertionIntoBubble(_ duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.bounds.contains(point) {
/*if let webPage = self.webPage, case let .Loaded(content) = webPage.content {
if content.instantPage != nil {
return .instantPage
}
}*/
}
return ChatMessageBubbleContentTapAction(content: .none)
}
override public func updateHiddenMedia(_ media: [Media]?) -> Bool {
return self.contentNode.updateHiddenMedia(media)
}
override public func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
if self.item?.message.id != messageId {
return nil
}
return self.contentNode.transitionNode(media: media)
}
}
@@ -0,0 +1,25 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageEventLogPreviousLinkContentNode",
module_name = "ChatMessageEventLogPreviousLinkContentNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Postbox",
"//submodules/Display",
"//submodules/AsyncDisplayKit",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramCore",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,111 @@
import Foundation
import UIKit
import Postbox
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import ChatMessageBubbleContentNode
import ChatMessageItemCommon
import ChatMessageAttachedContentNode
public final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubbleContentNode {
private let contentNode: ChatMessageAttachedContentNode
override public var visibility: ListViewItemNodeVisibility {
didSet {
self.contentNode.visibility = visibility
}
}
required public init() {
self.contentNode = ChatMessageAttachedContentNode()
super.init()
self.addSubnode(self.contentNode)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let contentNodeLayout = self.contentNode.asyncLayout()
return { item, layoutConstants, preparePosition, _, constrainedSize, _ in
var messageEntities: [MessageTextEntity]?
for attribute in item.message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
messageEntities = attribute.entities
break
}
}
let title: String = item.message.text.contains("\n") ? item.presentationData.strings.Channel_AdminLog_MessagePreviousLinks : item.presentationData.strings.Channel_AdminLog_MessagePreviousLink
let text: String = item.message.text
let mediaAndFlags: ([Media], ChatMessageAttachedContentNodeMediaFlags)? = nil
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(id: item.message.id.peerId), title, nil, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize, item.controllerInteraction.presentationContext.animationCache, item.controllerInteraction.presentationContext.animationRenderer)
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
return (contentProperties, nil, initialWidth, { constrainedSize, position in
let (refinedWidth, finalizeLayout) = continueLayout(constrainedSize, position)
return (refinedWidth, { boundingWidth in
let (size, apply) = finalizeLayout(boundingWidth)
return (size, { [weak self] animation, synchronousLoads, applyInfo in
if let strongSelf = self {
strongSelf.item = item
apply(animation, synchronousLoads, applyInfo)
strongSelf.contentNode.frame = CGRect(origin: CGPoint(), size: size)
}
})
})
})
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
}
override public func animateInsertionIntoBubble(_ duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.bounds.contains(point) {
/*if let webPage = self.webPage, case let .Loaded(content) = webPage.content {
if content.instantPage != nil {
return .instantPage
}
}*/
}
return ChatMessageBubbleContentTapAction(content: .none)
}
override public func updateHiddenMedia(_ media: [Media]?) -> Bool {
return self.contentNode.updateHiddenMedia(media)
}
override public func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
if self.item?.message.id != messageId {
return nil
}
return self.contentNode.transitionNode(media: media)
}
}
@@ -0,0 +1,25 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageEventLogPreviousMessageContentNode",
module_name = "ChatMessageEventLogPreviousMessageContentNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Postbox",
"//submodules/Display",
"//submodules/AsyncDisplayKit",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramCore",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,119 @@
import Foundation
import UIKit
import Postbox
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import ChatMessageBubbleContentNode
import ChatMessageItemCommon
import ChatMessageAttachedContentNode
public final class ChatMessageEventLogPreviousMessageContentNode: ChatMessageBubbleContentNode {
private let contentNode: ChatMessageAttachedContentNode
override public var visibility: ListViewItemNodeVisibility {
didSet {
self.contentNode.visibility = visibility
}
}
required public init() {
self.contentNode = ChatMessageAttachedContentNode()
super.init()
self.addSubnode(self.contentNode)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let contentNodeLayout = self.contentNode.asyncLayout()
return { item, layoutConstants, preparePosition, _, constrainedSize, _ in
var messageEntities: [MessageTextEntity]?
for attribute in item.message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
messageEntities = attribute.entities
break
}
}
let title: String = item.presentationData.strings.Channel_AdminLog_MessagePreviousMessage
let text: String
if item.message.text.isEmpty {
text = item.presentationData.strings.Channel_AdminLog_EmptyMessageText
} else {
text = item.message.text
}
let mediaAndFlags: ([Media], ChatMessageAttachedContentNodeMediaFlags)? = nil
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(id: item.message.id.peerId), title, nil, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize, item.controllerInteraction.presentationContext.animationCache, item.controllerInteraction.presentationContext.animationRenderer)
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
return (contentProperties, nil, initialWidth, { constrainedSize, position in
let (refinedWidth, finalizeLayout) = continueLayout(constrainedSize, position)
return (refinedWidth, { boundingWidth in
let (size, apply) = finalizeLayout(boundingWidth)
return (size, { [weak self] animation, synchronousLoads, applyInfo in
if let strongSelf = self {
strongSelf.item = item
apply(animation, synchronousLoads, applyInfo)
strongSelf.contentNode.frame = CGRect(origin: CGPoint(), size: size)
}
})
})
})
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
}
override public func animateInsertionIntoBubble(_ duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.bounds.contains(point) {
let contentNodeFrame = self.contentNode.frame
return self.contentNode.tapActionAtPoint(point.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY), gesture: gesture, isEstimating: isEstimating)
}
return ChatMessageBubbleContentTapAction(content: .none)
}
override public func updateTouchesAtPoint(_ point: CGPoint?) {
let contentNodeFrame = self.contentNode.frame
self.contentNode.updateTouchesAtPoint(point.flatMap { $0.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY) })
}
override public func updateHiddenMedia(_ media: [Media]?) -> Bool {
return self.contentNode.updateHiddenMedia(media)
}
override public func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
if self.item?.message.id != messageId {
return nil
}
return self.contentNode.transitionNode(media: media)
}
}
@@ -0,0 +1,33 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageFactCheckBubbleContentNode",
module_name = "ChatMessageFactCheckBubbleContentNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Postbox",
"//submodules/Display",
"//submodules/AsyncDisplayKit",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/TelegramStringFormatting",
"//submodules/TextFormat",
"//submodules/Geocoding",
"//submodules/UrlEscaping",
"//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView",
"//submodules/TextSelectionNode",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,726 @@
import Foundation
import UIKit
import Postbox
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramStringFormatting
import TextFormat
import ChatMessageDateAndStatusNode
import ChatMessageBubbleContentNode
import ChatMessageItemCommon
import MessageInlineBlockBackgroundView
import TextSelectionNode
import Geocoding
import UrlEscaping
private func generateMaskImage() -> UIImage? {
return generateImage(CGSize(width: 140, height: 30), rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setFillColor(UIColor.white.cgColor)
context.fill(CGRect(origin: .zero, size: size))
var locations: [CGFloat] = [0.0, 0.5, 1.0]
let colors: [CGColor] = [UIColor.white.cgColor, UIColor.white.withAlphaComponent(0.0).cgColor, UIColor.white.withAlphaComponent(0.0).cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.setBlendMode(.copy)
context.clip(to: CGRect(origin: CGPoint(x: 10.0, y: 8.0), size: CGSize(width: 130.0, height: 22.0)))
context.drawLinearGradient(gradient, start: CGPoint(x: 10.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions())
})?.resizableImage(withCapInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 22.0, right: 130.0))
}
public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode {
private var backgroundView: MessageInlineBlockBackgroundView?
private var titleNode: TextNode
private var titleBadgeLabel: TextNode
private var titleBadgeButton: HighlightTrackingButtonNode?
private let textClippingNode: ASDisplayNode
private let textNode: TextNode
private let additionalTextNode: TextNode
private var linkHighlightingNode: LinkHighlightingNode?
private var textSelectionNode: TextSelectionNode?
private let lineNode: ASDisplayNode
private var maskView: UIImageView?
private var maskOverlayView: UIView?
private var expandIcon: ASImageNode
private let statusNode: ChatMessageDateAndStatusNode
private var isExpanded: Bool = false
private var appliedIsExpanded: Bool = false
private var countryName: String?
required public init() {
self.titleNode = TextNode()
self.titleBadgeLabel = TextNode()
self.textClippingNode = ASDisplayNode()
self.textNode = TextNode()
self.additionalTextNode = TextNode()
self.expandIcon = ASImageNode()
self.statusNode = ChatMessageDateAndStatusNode()
self.lineNode = ASDisplayNode()
super.init()
self.textClippingNode.clipsToBounds = true
self.addSubnode(self.textClippingNode)
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .topLeft
self.titleNode.contentsScale = UIScreenScale
self.titleNode.displaysAsynchronously = false
self.addSubnode(self.titleNode)
self.textNode.isUserInteractionEnabled = false
self.textNode.contentMode = .topLeft
self.textNode.contentsScale = UIScreenScale
self.textNode.displaysAsynchronously = false
self.textClippingNode.addSubnode(self.textNode)
self.additionalTextNode.isUserInteractionEnabled = false
self.additionalTextNode.contentMode = .topLeft
self.additionalTextNode.contentsScale = UIScreenScale
self.additionalTextNode.displaysAsynchronously = false
self.textClippingNode.addSubnode(self.additionalTextNode)
self.textClippingNode.addSubnode(self.lineNode)
self.titleBadgeLabel.isUserInteractionEnabled = false
self.titleBadgeLabel.contentMode = .topLeft
self.titleBadgeLabel.contentsScale = UIScreenScale
self.titleBadgeLabel.displaysAsynchronously = false
self.addSubnode(self.titleBadgeLabel)
self.expandIcon.displaysAsynchronously = false
self.addSubnode(self.expandIcon)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func didLoad() {
self.maskView = UIImageView()
let maskOverlayView = UIView()
maskOverlayView.alpha = 0.0
maskOverlayView.backgroundColor = .white
self.maskOverlayView = maskOverlayView
self.maskView?.addSubview(maskOverlayView)
}
@objc private func badgePressed() {
guard let item = self.item, let countryName = self.countryName else {
return
}
item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_FactCheck_Description(countryName).string, true, self.titleBadgeButton, nil)
}
@objc private func expandPressed() {
self.isExpanded = !self.isExpanded
guard let item = self.item else{
return
}
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false)
}
public override func willUpdateIsExtractedToContextPreview(_ value: Bool) {
if !value {
if let textSelectionNode = self.textSelectionNode {
self.textSelectionNode = nil
textSelectionNode.highlightAreaNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
textSelectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak textSelectionNode] _ in
textSelectionNode?.highlightAreaNode.removeFromSupernode()
textSelectionNode?.removeFromSupernode()
})
}
}
}
public override func updateIsExtractedToContextPreview(_ value: Bool) {
if value {
if self.textSelectionNode == nil, let item = self.item, let rootNode = item.controllerInteraction.chatControllerNode() {
let selectionColor: UIColor = item.presentationData.theme.theme.chat.message.incoming.textSelectionColor
let knobColor: UIColor = item.presentationData.theme.theme.chat.message.incoming.textSelectionKnobColor
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: knobColor, isDark: item.presentationData.theme.theme.overallDarkAppearance), strings: item.presentationData.strings, textNode: self.textNode, updateIsActive: { [weak self] value in
self?.updateIsTextSelectionActive?(value)
}, present: { [weak self] c, a in
self?.item?.controllerInteraction.presentGlobalOverlayController(c, a)
}, rootNode: { [weak rootNode] in
return rootNode
}, performAction: { [weak self] text, action in
guard let strongSelf = self, let item = strongSelf.item else {
return
}
item.controllerInteraction.performTextSelectionAction(item.message, true, text, action)
})
textSelectionNode.enableQuote = false
self.textSelectionNode = textSelectionNode
self.addSubnode(textSelectionNode)
self.insertSubnode(textSelectionNode.highlightAreaNode, belowSubnode: self.textClippingNode)
textSelectionNode.frame = self.textClippingNode.view.convert(self.textNode.frame, to: self.view)
textSelectionNode.highlightAreaNode.frame = textSelectionNode.frame
}
} else {
if let textSelectionNode = self.textSelectionNode {
self.textSelectionNode = nil
self.updateIsTextSelectionActive?(false)
textSelectionNode.highlightAreaNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
textSelectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak textSelectionNode] _ in
textSelectionNode?.highlightAreaNode.removeFromSupernode()
textSelectionNode?.removeFromSupernode()
})
}
}
}
public override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if let titleBadgeButton = self.titleBadgeButton, titleBadgeButton.frame.contains(point) {
return ChatMessageBubbleContentTapAction(content: .ignore)
}
if self.statusNode.supernode != nil, let _ = self.statusNode.hitTest(self.view.convert(point, to: self.statusNode.view), with: nil) {
return ChatMessageBubbleContentTapAction(content: .ignore)
}
let textNodeFrame = self.textClippingNode.frame
if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: false)))
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false))
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
return ChatMessageBubbleContentTapAction(content: .textMention(peerName))
} else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String {
return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand))
} else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
return ChatMessageBubbleContentTapAction(content: .hashtag(hashtag.peerName, hashtag.hashtag))
}
}
if let backgroundView = self.backgroundView, backgroundView.frame.contains(point), case .tap = gesture {
return ChatMessageBubbleContentTapAction(content: .custom({ [weak self] in
self?.expandPressed()
}), hasLongTapAction: false)
}
return ChatMessageBubbleContentTapAction(content: .none)
}
public override func updateTouchesAtPoint(_ point: CGPoint?) {
guard let item = self.item else {
return
}
var rects: [CGRect]?
if let point = point {
let textNodeFrame = self.textClippingNode.frame
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
let possibleNames: [String] = [
TelegramTextAttributes.URL,
TelegramTextAttributes.PeerMention,
TelegramTextAttributes.PeerTextMention,
TelegramTextAttributes.BotCommand,
TelegramTextAttributes.Hashtag,
TelegramTextAttributes.BankCard
]
for name in possibleNames {
if let _ = attributes[NSAttributedString.Key(rawValue: name)] {
rects = self.textNode.attributeRects(name: name, at: index)
break
}
}
}
}
if let rects {
let linkHighlightingNode: LinkHighlightingNode
if let current = self.linkHighlightingNode {
linkHighlightingNode = current
} else {
linkHighlightingNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.linkHighlightColor : item.presentationData.theme.theme.chat.message.outgoing.linkHighlightColor)
self.linkHighlightingNode = linkHighlightingNode
self.insertSubnode(linkHighlightingNode, belowSubnode: self.textClippingNode)
}
linkHighlightingNode.frame = self.textClippingNode.frame
linkHighlightingNode.updateRects(rects)
} else if let linkHighlightingNode = self.linkHighlightingNode {
self.linkHighlightingNode = nil
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
linkHighlightingNode?.removeFromSupernode()
})
}
}
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let titleLayout = TextNode.asyncLayout(self.titleNode)
let titleBadgeLayout = TextNode.asyncLayout(self.titleBadgeLabel)
let textLayout = TextNode.asyncLayout(self.textNode)
let additionalTextLayout = TextNode.asyncLayout(self.additionalTextNode)
let measureTextLayout = TextNode.asyncLayout(nil)
let statusLayout = self.statusNode.asyncLayout()
let currentIsExpanded = self.isExpanded
let currentCountryName = self.countryName
return { item, layoutConstants, _, _, _, _ in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
let message = item.message
let incoming = item.message.effectivelyIncoming(item.context.account.peerId)
let maxTextWidth = CGFloat.greatestFiniteMagnitude
let horizontalInset = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
let textConstrainedSize = CGSize(width: min(maxTextWidth, constrainedSize.width - (horizontalInset - 2.0) * 2.0), height: constrainedSize.height)
var edited = false
if item.attributes.updatingMedia != nil {
edited = true
}
var viewCount: Int?
var rawText = ""
var rawEntities: [MessageTextEntity] = []
var dateReplies = 0
var starsCount: Int64?
var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message)
if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) {
dateReactionsAndPeers = ([], [])
}
for attribute in item.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? FactCheckMessageAttribute, case let .Loaded(text, entities, _) = attribute.content {
rawText = text
rawEntities = entities
} else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = item.chatLocation {
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info {
dateReplies = Int(attribute.count)
}
} else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel {
starsCount = attribute.stars.value
}
}
let dateFormat: MessageTimestampStatusFormat
if item.presentationData.isPreview {
dateFormat = .full
} else if let subject = item.associatedData.subject, case .messageOptions = subject {
dateFormat = .minimal
} else {
dateFormat = .regular
}
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: dateFormat, associatedData: item.associatedData)
let statusType: ChatMessageDateAndStatusType?
if case .customChatContents = item.associatedData.subject {
statusType = nil
} else {
switch position {
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if incoming {
statusType = .BubbleIncoming
} else {
if message.flags.contains(.Failed) {
statusType = .BubbleOutgoing(.Failed)
} else if message.flags.isSending && !message.isSentOrAcknowledged {
statusType = .BubbleOutgoing(.Sending)
} else {
statusType = .BubbleOutgoing(.Sent(read: item.read))
}
}
default:
statusType = nil
}
}
let messageTheme = incoming ? item.presentationData.theme.theme.chat.message.incoming : item.presentationData.theme.theme.chat.message.outgoing
let fontSize = floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)
let textFont = Font.regular(fontSize)
let textBoldFont = Font.semibold(fontSize)
let textItalicFont = Font.italic(fontSize)
let textBoldItalicFont = Font.semiboldItalic(fontSize)
let textFixedFont = Font.regular(fontSize)
let textBlockQuoteFont = Font.regular(fontSize)
let badgeFont = Font.regular(floor(item.presentationData.fontSize.baseDisplaySize * 11.0 / 17.0))
let attributedText = stringWithAppliedEntities(rawText, entities: rawEntities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFixedFont, blockQuoteFont: textBlockQuoteFont, message: nil)
let textInsets = UIEdgeInsets(top: 2.0, left: 0.0, bottom: 5.0, right: 0.0)
var backgroundInsets = UIEdgeInsets()
backgroundInsets.left += layoutConstants.text.bubbleInsets.left
backgroundInsets.right += layoutConstants.text.bubbleInsets.right
let mainColor = messageTheme.scamColor
let (titleLayout, titleApply) = titleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Message_FactCheck, font: textBoldFont, textColor: mainColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets, lineColor: mainColor))
let titleBadgePadding: CGFloat = 5.0
let titleBadgeSpacing: CGFloat = 5.0
let titleBadgeString = NSAttributedString(string: item.presentationData.strings.Message_FactCheck_WhatIsThis, font: badgeFont, textColor: mainColor)
let (titleBadgeLayout, titleBadgeApply) = titleBadgeLayout(TextNodeLayoutArguments(attributedString: titleBadgeString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize))
let countryName: String
if let currentCountryName {
countryName = currentCountryName
} else {
if let attribute = item.message.factCheckAttribute, case let .Loaded(_, _, countryIdValue) = attribute.content {
let locale = localeWithStrings(item.presentationData.strings)
countryName = displayCountryName(countryIdValue, locale: locale)
} else {
countryName = ""
}
}
let finalAttributedText = stringWithAppliedEntities(rawText, entities: rawEntities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFixedFont, blockQuoteFont: textBlockQuoteFont, message: nil) as! NSMutableAttributedString
finalAttributedText.append(NSAttributedString(string: "__", font: textFont, textColor: .clear))
let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: finalAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets, lineColor: messageTheme.accentControlColor))
let additionalAttributedText = NSMutableAttributedString(string: item.presentationData.strings.Conversation_FactCheck_InnerDescription(countryName).string, font: badgeFont, textColor: mainColor)
additionalAttributedText.append(NSAttributedString(string: "__", font: badgeFont, textColor: .clear))
let (additionalTextLayout, additionalTextApply) = additionalTextLayout(TextNodeLayoutArguments(attributedString: additionalAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, lineSpacing: 0.0, cutout: nil, insets: textInsets, lineColor: messageTheme.accentControlColor))
var canExpand = false
var clippedTextHeight: CGFloat = textLayout.size.height
if textLayout.numberOfLines > 4 {
let (measuredTextLayout, _) = measureTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 4, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets, lineColor: messageTheme.accentControlColor))
canExpand = true
if !currentIsExpanded {
clippedTextHeight = measuredTextLayout.size.height
}
}
var titleFrame = CGRect(origin: CGPoint(x: -textInsets.left, y: -textInsets.top), size: titleLayout.size)
titleFrame = titleFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left * 2.0 - 2.0, dy: layoutConstants.text.bubbleInsets.top - 3.0)
var titleFrameWithoutInsets = CGRect(origin: CGPoint(x: titleFrame.origin.x + textInsets.left, y: titleFrame.origin.y + textInsets.top), size: CGSize(width: titleFrame.width - textInsets.left - textInsets.right, height: titleFrame.height - textInsets.top - textInsets.bottom))
titleFrameWithoutInsets = titleFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top)
let topInset: CGFloat = 5.0
let textSpacing: CGFloat = 3.0
let textFrame = CGRect(origin: CGPoint(x: titleFrame.origin.x, y: -textInsets.top + titleFrameWithoutInsets.height + textSpacing), 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: clippedTextHeight - textInsets.top - textInsets.bottom))
textFrameWithoutInsets = textFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top)
let additionalTextFrame = CGRect(origin: CGPoint(x: titleFrame.origin.x, y: textFrame.maxY), size: additionalTextLayout.size)
var additionalTextFrameWithoutInsets = CGRect(origin: CGPoint(x: additionalTextFrame.origin.x + textInsets.left, y: additionalTextFrame.origin.y + textInsets.top), size: CGSize(width: additionalTextFrame.width - textInsets.left - textInsets.right, height: additionalTextFrame.height - textInsets.top - textInsets.bottom))
additionalTextFrameWithoutInsets = additionalTextFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top)
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> 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 && !item.presentationData.isPreview,
impressionCount: !item.presentationData.isPreview ? viewCount : nil,
dateText: dateText,
type: statusType,
layoutInput: .trailingContent(contentWidth: nil, reactionSettings: item.presentationData.isPreview ? nil : ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions), preferAdditionalInset: false)),
constrainedSize: textConstrainedSize,
availableReactions: item.associatedData.availableReactions,
savedMessageTags: item.associatedData.savedMessageTags,
reactions: dateReactionsAndPeers.reactions,
reactionPeers: dateReactionsAndPeers.peers,
displayAllReactionPeers: item.message.id.peerId.namespace == Namespaces.Peer.CloudUser,
areReactionsTags: item.topMessage.areReactionsTags(accountPeerId: item.context.account.peerId),
areStarReactionsEnabled: item.associatedData.areStarReactionsEnabled,
messageEffect: item.topMessage.messageEffect(availableMessageEffects: item.associatedData.availableMessageEffects),
replyCount: dateReplies,
starsCount: starsCount,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread,
hasAutoremove: item.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: item.topMessage),
animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
))
}
var suggestedBoundingWidth: CGFloat = max(textFrameWithoutInsets.width, titleFrameWithoutInsets.width + titleBadgeLayout.size.width + titleBadgeSpacing + titleBadgePadding * 2.0)
if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue {
suggestedBoundingWidth = max(suggestedBoundingWidth, statusSuggestedWidthAndContinue.0)
}
suggestedBoundingWidth = max(suggestedBoundingWidth, additionalTextFrameWithoutInsets.width)
let sideInsets = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
suggestedBoundingWidth += (sideInsets - 2.0) * 2.0
return (suggestedBoundingWidth, { boundingWidth in
var boundingSize: CGSize
let statusSizeAndApply = statusSuggestedWidthAndContinue?.1(boundingWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right)
var contentHeight = titleFrameWithoutInsets.height + textSpacing + textFrameWithoutInsets.size.height
if canExpand && !currentIsExpanded {
} else {
contentHeight += textSpacing * 2.0 + 1.0 + additionalTextFrameWithoutInsets.height
}
contentHeight += textSpacing
boundingSize = CGSize(width: boundingWidth, height: topInset + contentHeight - textSpacing)
if let statusSizeAndApply = statusSizeAndApply {
boundingSize.height += statusSizeAndApply.0.height
}
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, _, info in
if let strongSelf = self {
info?.setInvertOffsetDirection()
let isFirstTime = strongSelf.item == nil
let themeUpdated = strongSelf.item?.presentationData.theme.theme !== item.presentationData.theme.theme
strongSelf.item = item
strongSelf.countryName = countryName
let backgroundView: MessageInlineBlockBackgroundView
if let current = strongSelf.backgroundView {
backgroundView = current
} else {
backgroundView = MessageInlineBlockBackgroundView()
strongSelf.view.insertSubview(backgroundView, at: 0)
strongSelf.backgroundView = backgroundView
}
if themeUpdated {
strongSelf.lineNode.backgroundColor = mainColor.withAlphaComponent(0.15)
}
var isExpandedUpdated = false
if strongSelf.appliedIsExpanded != currentIsExpanded {
strongSelf.appliedIsExpanded = currentIsExpanded
info?.setInvertOffsetDirection()
isExpandedUpdated = true
animation.transition.updateTransformRotation(node: strongSelf.expandIcon, angle: currentIsExpanded ? .pi : 0.0)
if let maskOverlayView = strongSelf.maskOverlayView {
animation.transition.updateAlpha(layer: maskOverlayView.layer, alpha: currentIsExpanded ? 1.0 : 0.0)
}
}
let cachedLayout = strongSelf.textNode.cachedLayout
if case .System = animation, !isExpandedUpdated {
if let cachedLayout = cachedLayout {
if !cachedLayout.areLinesEqual(to: textLayout) {
if let textContents = strongSelf.textNode.contents {
let fadeNode = ASDisplayNode()
fadeNode.displaysAsynchronously = false
fadeNode.contents = textContents
fadeNode.frame = strongSelf.textNode.frame
fadeNode.isLayerBacked = true
strongSelf.textClippingNode.addSubnode(fadeNode)
fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak fadeNode] _ in
fadeNode?.removeFromSupernode()
})
strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
}
}
}
}
if themeUpdated {
strongSelf.expandIcon.image = generateImage(CGSize(width: 15.0, height: 9.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(mainColor.cgColor)
context.setLineWidth(2.0 - UIScreenPixel)
context.setLineCap(.round)
context.setLineJoin(.round)
context.beginPath()
context.move(to: CGPoint(x: 1.0 + UIScreenPixel, y: 1.0))
context.addLine(to: CGPoint(x: size.width / 2.0, y: size.height - 2.0))
context.addLine(to: CGPoint(x: size.width - 1.0 - UIScreenPixel, y: 1.0))
context.strokePath()
})
}
let _ = titleApply()
strongSelf.titleNode.frame = titleFrame.offsetBy(dx: 0.0, dy: topInset)
let _ = titleBadgeApply()
let _ = textApply()
strongSelf.textNode.frame = CGRect(origin: .zero, size: textFrame.size)
let _ = additionalTextApply()
strongSelf.additionalTextNode.frame = CGRect(origin: CGPoint(x: 0.0, y: textFrame.height - textInsets.bottom + textSpacing + 1.0), size: additionalTextFrame.size)
let clippingTextFrame = CGRect(origin: textFrame.origin.offsetBy(dx: 0.0, dy: topInset), size: CGSize(width: boundingWidth, height: contentHeight - titleFrame.height + textSpacing))
var titleLineWidth: CGFloat = 0.0
if let firstLine = titleLayout.linesRects().first {
titleLineWidth = firstLine.width
} else {
titleLineWidth = titleFrame.width
}
let titleBadgeFrame = CGRect(origin: CGPoint(x: titleFrame.minX + titleLineWidth + titleBadgeSpacing + titleBadgePadding, y: topInset + floorToScreenPixels(titleFrame.midY - titleBadgeLayout.size.height / 2.0) - 1.0), size: titleBadgeLayout.size)
let badgeBackgroundFrame = titleBadgeFrame.insetBy(dx: -titleBadgePadding, dy: -1.0 + UIScreenPixel)
strongSelf.titleBadgeLabel.frame = titleBadgeFrame
let titleBadgeButton: HighlightTrackingButtonNode
if let current = strongSelf.titleBadgeButton {
titleBadgeButton = current
titleBadgeButton.bounds = CGRect(origin: .zero, size: badgeBackgroundFrame.size)
animation.animator.updatePosition(layer: titleBadgeButton.layer, position: badgeBackgroundFrame.center, completion: nil)
} else {
titleBadgeButton = HighlightTrackingButtonNode()
titleBadgeButton.addTarget(self, action: #selector(strongSelf.badgePressed), forControlEvents: .touchUpInside)
titleBadgeButton.frame = badgeBackgroundFrame
titleBadgeButton.highligthedChanged = { [weak self, weak titleBadgeButton] highlighted in
if let strongSelf = self, let titleBadgeButton {
if highlighted {
titleBadgeButton.layer.removeAnimation(forKey: "opacity")
titleBadgeButton.alpha = 0.4
strongSelf.titleBadgeLabel.layer.removeAnimation(forKey: "opacity")
strongSelf.titleBadgeLabel.alpha = 0.4
} else {
titleBadgeButton.alpha = 1.0
titleBadgeButton.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.titleBadgeLabel.alpha = 1.0
strongSelf.titleBadgeLabel.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
strongSelf.titleBadgeButton = titleBadgeButton
strongSelf.addSubnode(titleBadgeButton)
}
titleBadgeButton.isHidden = item.presentationData.isPreview
strongSelf.titleBadgeLabel.isHidden = item.presentationData.isPreview
if themeUpdated || titleBadgeButton.backgroundImage(for: .normal) == nil {
titleBadgeButton.setBackgroundImage(generateFilledCircleImage(diameter: badgeBackgroundFrame.height, color: mainColor.withMultipliedAlpha(0.1))?.stretchableImage(withLeftCapWidth: Int(badgeBackgroundFrame.height / 2), topCapHeight: Int(badgeBackgroundFrame.height / 2)), for: .normal)
}
let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top + topInset), size: CGSize(width: boundingWidth - backgroundInsets.left - backgroundInsets.right, height: contentHeight))
if isFirstTime {
strongSelf.textClippingNode.frame = clippingTextFrame
} else {
animation.animator.updateFrame(layer: strongSelf.textClippingNode.layer, frame: clippingTextFrame, completion: nil)
}
if let maskView = strongSelf.maskView, let maskOverlayView = strongSelf.maskOverlayView {
animation.animator.updateFrame(layer: maskView.layer, frame: CGRect(origin: .zero, size: CGSize(width: boundingWidth, height: clippingTextFrame.size.height)), completion: nil)
animation.animator.updateFrame(layer: maskOverlayView.layer, frame: CGRect(origin: .zero, size: CGSize(width: boundingWidth, height: clippingTextFrame.size.height)), completion: nil)
}
if isFirstTime {
backgroundView.frame = backgroundFrame
} else {
animation.animator.updateFrame(layer: backgroundView.layer, frame: backgroundFrame, completion: nil)
}
backgroundView.update(size: backgroundFrame.size, isTransparent: false, primaryColor: mainColor, secondaryColor: nil, thirdColor: nil, backgroundColor: nil, pattern: nil, patternTopRightPosition: nil, animation: isFirstTime ? .None : animation)
animation.animator.updateFrame(layer: strongSelf.lineNode.layer, frame: CGRect(origin: CGPoint(x: 0.0, y: textFrame.height - textSpacing + 1.0), size: CGSize(width: backgroundFrame.width - 9.0 - 6.0, height: 1.0 - UIScreenPixel)), completion: nil)
if canExpand {
let wasHidden = strongSelf.expandIcon.isHidden
strongSelf.expandIcon.isHidden = false
if strongSelf.maskView?.image == nil {
strongSelf.maskView?.image = generateMaskImage()
}
strongSelf.textClippingNode.view.mask = strongSelf.maskView
var expandIconFrame: CGRect = .zero
if let icon = strongSelf.expandIcon.image {
expandIconFrame = CGRect(origin: CGPoint(x: boundingWidth - icon.size.width - 19.0, y: backgroundFrame.maxY - icon.size.height - 6.0), size: icon.size)
if wasHidden || isFirstTime {
strongSelf.expandIcon.position = expandIconFrame.center
} else {
animation.animator.updatePosition(layer: strongSelf.expandIcon.layer, position: expandIconFrame.center, completion: nil)
}
strongSelf.expandIcon.bounds = CGRect(origin: .zero, size: expandIconFrame.size)
}
} else {
strongSelf.expandIcon.isHidden = true
strongSelf.textClippingNode.view.mask = nil
}
if let textSelectionNode = strongSelf.textSelectionNode {
let shouldUpdateLayout = textSelectionNode.frame.size != textFrame.size
textSelectionNode.frame = strongSelf.textClippingNode.view.convert(strongSelf.textNode.frame, to: strongSelf.view)
textSelectionNode.highlightAreaNode.frame = textSelectionNode.frame
if shouldUpdateLayout {
textSelectionNode.updateLayout()
}
}
if let statusSizeAndApply = statusSizeAndApply {
strongSelf.statusNode.reactionSelected = { [weak strongSelf] _, value, sourceView in
guard let strongSelf, let item = strongSelf.item else {
return
}
item.controllerInteraction.updateMessageReaction(item.topMessage, .reaction(value), false, sourceView)
}
strongSelf.statusNode.openReactionPreview = { [weak strongSelf] gesture, sourceNode, value in
guard let strongSelf, let item = strongSelf.item else {
gesture?.cancel()
return
}
item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceNode, gesture, value)
}
let statusFrame = CGRect(origin: CGPoint(x: boundingWidth - layoutConstants.text.bubbleInsets.right - statusSizeAndApply.0.width, y: backgroundFrame.maxY + 4.0), size: statusSizeAndApply.0)
if isFirstTime {
strongSelf.statusNode.frame = statusFrame
} else {
animation.animator.updateFrame(layer: strongSelf.statusNode.layer, frame: statusFrame, completion: nil)
}
if strongSelf.statusNode.supernode == nil {
strongSelf.addSubnode(strongSelf.statusNode)
statusSizeAndApply.1(.None)
} else {
statusSizeAndApply.1(animation)
}
} else if strongSelf.statusNode.supernode != nil {
strongSelf.statusNode.removeFromSupernode()
}
}
})
})
})
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double) {
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
}
@@ -0,0 +1,30 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageFileBubbleContentNode",
module_name = "ChatMessageFileBubbleContentNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/TelegramUIPreferences",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/AudioTranscriptionButtonComponent",
"//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,264 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramUIPreferences
import ComponentFlow
import AudioTranscriptionButtonComponent
import ChatMessageDateAndStatusNode
import ChatMessageBubbleContentNode
import ChatMessageItemCommon
import ChatMessageInteractiveFileNode
import ChatControllerInteraction
public class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
public let interactiveFileNode: ChatMessageInteractiveFileNode
override public var visibility: ListViewItemNodeVisibility {
didSet {
var wasVisible = false
if case .visible = oldValue {
wasVisible = true
}
var isVisible = false
if case .visible = self.visibility {
isVisible = true
}
if wasVisible != isVisible {
self.interactiveFileNode.visibility = isVisible
}
}
}
required public init() {
self.interactiveFileNode = ChatMessageInteractiveFileNode()
super.init()
self.addSubnode(self.interactiveFileNode)
self.interactiveFileNode.toggleSelection = { [weak self] value in
if let strongSelf = self, let item = strongSelf.item {
item.controllerInteraction.toggleMessagesSelection([item.message.id], value)
}
}
self.interactiveFileNode.activateLocalContent = { [weak self] in
if let strongSelf = self, let item = strongSelf.item {
let _ = item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: .default))
}
}
self.interactiveFileNode.requestUpdateLayout = { [weak self] _ in
if let strongSelf = self, let item = strongSelf.item {
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false)
}
}
self.interactiveFileNode.displayImportedTooltip = { [weak self] sourceNode in
if let strongSelf = self, let item = strongSelf.item {
let _ = item.controllerInteraction.displayImportedMessageTooltip(sourceNode)
}
}
self.interactiveFileNode.dateAndStatusNode.reactionSelected = { [weak self] _, value, sourceView in
guard let strongSelf = self, let item = strongSelf.item else {
return
}
item.controllerInteraction.updateMessageReaction(item.topMessage, .reaction(value), false, sourceView)
}
self.interactiveFileNode.dateAndStatusNode.openReactionPreview = { [weak self] gesture, sourceNode, value in
guard let strongSelf = self, let item = strongSelf.item else {
gesture?.cancel()
return
}
item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceNode, gesture, value)
}
self.interactiveFileNode.updateIsTextSelectionActive = { [weak self] value in
self?.updateIsTextSelectionActive?(value)
}
}
override public func accessibilityActivate() -> Bool {
if let item = self.item {
let _ = item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: .default))
}
return true
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let interactiveFileLayout = self.interactiveFileNode.asyncLayout()
return { item, layoutConstants, preparePosition, selection, constrainedSize, _ in
var selectedFile: TelegramMediaFile?
for media in item.message.media {
if let telegramFile = media as? TelegramMediaFile {
selectedFile = telegramFile
}
}
if let updatingMedia = item.attributes.updatingMedia, case let .update(media) = updatingMedia.media, let file = media.media as? TelegramMediaFile {
selectedFile = file
}
var incoming = item.message.effectivelyIncoming(item.context.account.peerId)
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info {
incoming = false
}
let statusType: ChatMessageDateAndStatusType?
if case .customChatContents = item.associatedData.subject {
statusType = nil
} else if item.message.timestamp == 0 {
statusType = nil
} else {
switch preparePosition {
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if incoming {
statusType = .BubbleIncoming
} else {
if item.message.flags.contains(.Failed) {
statusType = .BubbleOutgoing(.Failed)
} else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil {
statusType = .BubbleOutgoing(.Sending)
} else {
statusType = .BubbleOutgoing(.Sent(read: item.read))
}
}
default:
statusType = nil
}
}
let automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: selectedFile)
let (initialWidth, refineLayout) = interactiveFileLayout(ChatMessageInteractiveFileNode.Arguments(
context: item.context,
presentationData: item.presentationData,
customTintColor: nil,
message: item.message,
topMessage: item.topMessage,
associatedData: item.associatedData,
chatLocation: item.chatLocation,
attributes: item.attributes,
isPinned: item.isItemPinned,
forcedIsEdited: item.isItemEdited,
file: selectedFile!,
automaticDownload: automaticDownload,
incoming: incoming,
isRecentActions: item.associatedData.isRecentActions,
forcedResourceStatus: item.associatedData.forcedResourceStatus,
dateAndStatusType: statusType,
displayReactions: true,
messageSelection: item.message.groupingKey != nil ? selection : nil,
isAttachedContentBlock: false,
layoutConstants: layoutConstants,
constrainedSize: CGSize(width: constrainedSize.width - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right, height: constrainedSize.height),
controllerInteraction: item.controllerInteraction
))
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
return (contentProperties, nil, initialWidth + layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right, { constrainedSize, position in
let (refinedWidth, finishLayout) = refineLayout(CGSize(width: constrainedSize.width - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right, height: constrainedSize.height))
return (refinedWidth + layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right, { boundingWidth in
let (fileSize, fileApply) = finishLayout(boundingWidth - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right)
var bottomInset = layoutConstants.file.bubbleInsets.bottom
if case let .linear(_, bottom) = position {
if case .Neighbour(_, _, .condensed) = bottom {
if selectedFile?.isMusic ?? false {
bottomInset -= 14.0
} else {
bottomInset -= 7.0
}
}
}
return (CGSize(width: fileSize.width + layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right, height: fileSize.height + layoutConstants.file.bubbleInsets.top + bottomInset), { [weak self] animation, synchronousLoads, applyInfo in
if let strongSelf = self {
strongSelf.item = item
strongSelf.interactiveFileNode.frame = CGRect(origin: CGPoint(x: layoutConstants.file.bubbleInsets.left, y: layoutConstants.file.bubbleInsets.top), size: fileSize)
fileApply(synchronousLoads, animation, applyInfo)
}
})
})
})
}
}
override public func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
if self.item?.message.id == messageId {
return self.interactiveFileNode.transitionNode(media: media)
} else {
return nil
}
}
override public func updateHiddenMedia(_ media: [Media]?) -> Bool {
return self.interactiveFileNode.updateHiddenMedia(media)
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double) {
self.interactiveFileNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.interactiveFileNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.interactiveFileNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
override public func willUpdateIsExtractedToContextPreview(_ value: Bool) {
self.interactiveFileNode.willUpdateIsExtractedToContextPreview(value)
}
override public func updateIsExtractedToContextPreview(_ value: Bool) {
self.interactiveFileNode.updateIsExtractedToContextPreview(value)
}
override public 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 ChatMessageBubbleContentTapAction(content: .ignore)
}
if self.interactiveFileNode.hasTapAction(at: self.view.convert(point, to: self.interactiveFileNode.view)) {
return ChatMessageBubbleContentTapAction(content: .ignore)
}
return super.tapActionAtPoint(point, gesture: gesture, isEstimating: isEstimating)
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let result = self.interactiveFileNode.hitTest(self.view.convert(point, to: self.interactiveFileNode.view), with: event) {
return result
}
return super.hitTest(point, with: event)
}
override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? {
if !self.interactiveFileNode.dateAndStatusNode.isHidden {
return self.interactiveFileNode.dateAndStatusNode.reactionView(value: value)
}
return nil
}
override public func messageEffectTargetView() -> UIView? {
if !self.interactiveFileNode.dateAndStatusNode.isHidden {
return self.interactiveFileNode.dateAndStatusNode.messageEffectTargetView()
}
return nil
}
}
@@ -0,0 +1,34 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageForwardInfoNode",
module_name = "ChatMessageForwardInfoNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Postbox",
"//submodules/Display",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/LocalizedPeerData",
"//submodules/PhotoResources",
"//submodules/TelegramStringFormatting",
"//submodules/TextFormat",
"//submodules/InvisibleInkDustNode",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/TelegramUI/Components/AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer",
"//submodules/TelegramUI/Components/TextLoadingEffect",
"//submodules/AvatarNode",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,640 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import TelegramPresentationData
import LocalizedPeerData
import AccountContext
import AvatarNode
import TextLoadingEffect
import SwiftSignalKit
public enum ChatMessageForwardInfoType: Equatable {
case bubble(incoming: Bool)
case standalone
}
private final class InfoButtonNode: HighlightableButtonNode {
private let pressed: () -> Void
let iconNode: ASImageNode
private var theme: ChatPresentationThemeData?
private var type: ChatMessageForwardInfoType?
init(pressed: @escaping () -> Void) {
self.pressed = pressed
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
super.init()
self.addSubnode(self.iconNode)
self.addTarget(self, action: #selector(self.pressedEvent), forControlEvents: .touchUpInside)
}
@objc private func pressedEvent() {
self.pressed()
}
func update(size: CGSize, theme: ChatPresentationThemeData, type: ChatMessageForwardInfoType) {
if self.theme !== theme || self.type != type {
self.theme = theme
self.type = type
let color: UIColor
switch type {
case let .bubble(incoming):
color = incoming ? theme.theme.chat.message.incoming.accentControlColor : theme.theme.chat.message.outgoing.accentControlColor
case .standalone:
let serviceColor = serviceMessageColorComponents(theme: theme.theme, wallpaper: theme.wallpaper)
color = serviceColor.primaryText
}
self.iconNode.image = PresentationResourcesChat.chatPsaInfo(theme.theme, color: color.argb)
}
if let image = self.iconNode.image {
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)
}
}
}
public class ChatMessageForwardInfoNode: ASDisplayNode {
public enum StoryType {
case regular
case expired
case unavailable
}
public struct StoryData: Equatable {
public var storyType: StoryType
public init(storyType: StoryType) {
self.storyType = storyType
}
}
public private(set) var titleNode: TextNode?
public private(set) var nameNode: TextNode?
private var credibilityIconNode: ASImageNode?
private var infoNode: InfoButtonNode?
private var expiredStoryIconView: UIImageView?
private var avatarNode: AvatarNode?
private var theme: PresentationTheme?
private var highlightColor: UIColor?
private var linkHighlightingNode: LinkHighlightingNode?
private var hasLinkProgress: Bool = false
private var linkProgressView: TextLoadingEffectView?
private var linkProgressDisposable: Disposable?
private var previousPeer: Peer?
public var openPsa: ((String, ASDisplayNode) -> Void)?
override public init() {
super.init()
}
deinit {
self.linkProgressDisposable?.dispose()
}
public func hasAction(at point: CGPoint) -> Bool {
if let infoNode = self.infoNode, infoNode.frame.contains(point) {
return true
} else {
return false
}
}
public func updatePsaButtonDisplay(isVisible: Bool, animated: Bool) {
if let infoNode = self.infoNode {
if isVisible != !infoNode.iconNode.alpha.isZero {
let transition: ContainedViewLayoutTransition
if animated {
transition = .animated(duration: 0.25, curve: .easeInOut)
} else {
transition = .immediate
}
transition.updateAlpha(node: infoNode.iconNode, alpha: isVisible ? 1.0 : 0.0)
transition.updateSublayerTransformScale(node: infoNode, scale: isVisible ? 1.0 : 0.1)
}
}
}
public func getBoundingRects() -> [CGRect] {
var initialRects: [CGRect] = []
let addRects: (TextNode, CGPoint, CGFloat) -> Void = { textNode, offset, additionalWidth in
guard let cachedLayout = textNode.cachedLayout else {
return
}
for rect in cachedLayout.linesRects() {
var rect = rect
rect.size.width += rect.origin.x + additionalWidth
rect.origin.x = 0.0
initialRects.append(rect.offsetBy(dx: offset.x, dy: offset.y))
}
}
let offsetY: CGFloat = -12.0
if let titleNode = self.titleNode {
addRects(titleNode, CGPoint(x: titleNode.frame.minX, y: offsetY + titleNode.frame.minY), 0.0)
if let nameNode = self.nameNode {
addRects(nameNode, CGPoint(x: titleNode.frame.minX, y: offsetY + nameNode.frame.minY), nameNode.frame.minX - titleNode.frame.minX)
}
}
return initialRects
}
public func updateTouchesAtPoint(_ point: CGPoint?) {
var isHighlighted = false
if point != nil {
isHighlighted = true
}
var initialRects: [CGRect] = []
let addRects: (TextNode, CGPoint, CGFloat) -> Void = { textNode, offset, additionalWidth in
guard let cachedLayout = textNode.cachedLayout else {
return
}
for rect in cachedLayout.linesRects() {
var rect = rect
rect.size.width += rect.origin.x + additionalWidth
rect.origin.x = 0.0
initialRects.append(rect.offsetBy(dx: offset.x, dy: offset.y))
}
}
let offsetY: CGFloat = -12.0
if let titleNode = self.titleNode {
addRects(titleNode, CGPoint(x: titleNode.frame.minX, y: offsetY + titleNode.frame.minY), 0.0)
if let nameNode = self.nameNode {
addRects(nameNode, CGPoint(x: titleNode.frame.minX, y: offsetY + nameNode.frame.minY), nameNode.frame.minX - titleNode.frame.minX)
}
}
if isHighlighted, !initialRects.isEmpty, let highlightColor = self.highlightColor {
let rects = initialRects
let linkHighlightingNode: LinkHighlightingNode
if let current = self.linkHighlightingNode {
linkHighlightingNode = current
} else {
linkHighlightingNode = LinkHighlightingNode(color: highlightColor)
self.linkHighlightingNode = linkHighlightingNode
self.addSubnode(linkHighlightingNode)
}
linkHighlightingNode.frame = self.bounds
linkHighlightingNode.updateRects(rects)
} else if let linkHighlightingNode = self.linkHighlightingNode {
self.linkHighlightingNode = nil
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
linkHighlightingNode?.removeFromSupernode()
})
}
}
public func makeActivate() -> (() -> Promise<Bool>?)? {
return { [weak self] in
guard let self else {
return nil
}
let promise = Promise<Bool>()
self.linkProgressDisposable?.dispose()
if self.hasLinkProgress {
self.hasLinkProgress = false
self.updateLinkProgressState()
}
self.linkProgressDisposable = (promise.get() |> deliverOnMainQueue).startStrict(next: { [weak self] value in
guard let self else {
return
}
if self.hasLinkProgress != value {
self.hasLinkProgress = value
self.updateLinkProgressState()
}
})
return promise
}
}
private func updateLinkProgressState() {
guard let highlightColor = self.highlightColor else {
return
}
if self.hasLinkProgress, let titleNode = self.titleNode, let nameNode = self.nameNode {
var initialRects: [CGRect] = []
let addRects: (TextNode, CGPoint, CGFloat) -> Void = { textNode, offset, additionalWidth in
guard let cachedLayout = textNode.cachedLayout else {
return
}
for rect in cachedLayout.linesRects() {
var rect = rect
rect.size.width += rect.origin.x + additionalWidth
rect.origin.x = 0.0
initialRects.append(rect.offsetBy(dx: offset.x, dy: offset.y))
}
}
let offsetY: CGFloat = -12.0
if let titleNode = self.titleNode {
addRects(titleNode, CGPoint(x: titleNode.frame.minX, y: offsetY + titleNode.frame.minY), 0.0)
if let nameNode = self.nameNode {
addRects(nameNode, CGPoint(x: titleNode.frame.minX, y: offsetY + nameNode.frame.minY), nameNode.frame.minX - titleNode.frame.minX)
}
}
let linkProgressView: TextLoadingEffectView
if let current = self.linkProgressView {
linkProgressView = current
} else {
linkProgressView = TextLoadingEffectView(frame: CGRect())
self.linkProgressView = linkProgressView
self.view.addSubview(linkProgressView)
}
linkProgressView.frame = titleNode.frame
let progressColor: UIColor = highlightColor
linkProgressView.update(color: progressColor, size: CGRectUnion(titleNode.frame, nameNode.frame).size, rects: initialRects)
} else {
if let linkProgressView = self.linkProgressView {
self.linkProgressView = nil
linkProgressView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak linkProgressView] _ in
linkProgressView?.removeFromSuperview()
})
}
}
}
public static func asyncLayout(_ maybeNode: ChatMessageForwardInfoNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ strings: PresentationStrings, _ type: ChatMessageForwardInfoType, _ peer: Peer?, _ authorName: String?, _ psaType: String?, _ storyData: StoryData?, _ constrainedSize: CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode) {
let titleNodeLayout = TextNode.asyncLayout(maybeNode?.titleNode)
let nameNodeLayout = TextNode.asyncLayout(maybeNode?.nameNode)
let previousPeer = maybeNode?.previousPeer
return { context, presentationData, strings, type, peer, authorName, psaType, storyData, constrainedSize in
let originalPeer = peer
let peer = peer ?? previousPeer
let fontSize = floor(presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)
let prefixFont = Font.regular(fontSize)
let peerFont = Font.medium(fontSize)
let peerString: String
if let peer = peer {
if let authorName = authorName, originalPeer === peer {
peerString = "\(EnginePeer(peer).displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)) (\(authorName))"
} else {
peerString = EnginePeer(peer).displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)
}
} else if let authorName = authorName {
peerString = authorName
} else {
peerString = ""
}
var hasPsaInfo = false
if let _ = psaType {
hasPsaInfo = true
}
let titleColor: UIColor
let titleString: PresentationStrings.FormattedString
var authorString: String?
switch type {
case let .bubble(incoming):
if let psaType = psaType {
titleColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.barPositive : presentationData.theme.theme.chat.message.outgoing.polls.barPositive
var customFormat: String?
let key = "Message.ForwardedPsa.\(psaType)"
if let string = presentationData.strings.primaryComponent.dict[key] {
customFormat = string
} else if let string = presentationData.strings.secondaryComponent?.dict[key] {
customFormat = string
}
if let customFormat = customFormat {
if let range = customFormat.range(of: "%@") {
let leftPart = String(customFormat[customFormat.startIndex ..< range.lowerBound])
let rightPart = String(customFormat[range.upperBound...])
let formattedText = leftPart + peerString + rightPart
titleString = PresentationStrings.FormattedString(string: formattedText, ranges: [PresentationStrings.FormattedString.Range(index: 0, range: NSRange(location: leftPart.count, length: peerString.count))])
} else {
titleString = PresentationStrings.FormattedString(string: customFormat, ranges: [])
}
} else {
titleString = strings.Message_GenericForwardedPsa(peerString)
}
} else {
if incoming {
if let nameColor = peer?.nameColor {
switch nameColor {
case let .preset(nameColor):
titleColor = context.peerNameColors.get(nameColor, dark: presentationData.theme.theme.overallDarkAppearance).main
case let .collectible(collectibleColor):
titleColor = collectibleColor.mainColor(dark: presentationData.theme.theme.overallDarkAppearance)
}
} else {
titleColor = presentationData.theme.theme.chat.message.incoming.accentTextColor
}
} else {
titleColor = presentationData.theme.theme.chat.message.outgoing.accentTextColor
}
if let storyData = storyData {
switch storyData.storyType {
case .regular:
titleString = PresentationStrings.FormattedString(string: presentationData.strings.Chat_MessageForwardInfo_StoryHeader, ranges: [])
authorString = peerString
case .expired:
titleString = PresentationStrings.FormattedString(string: presentationData.strings.Chat_MessageForwardInfo_ExpiredStoryHeader, ranges: [])
authorString = peerString
case .unavailable:
titleString = PresentationStrings.FormattedString(string: presentationData.strings.Chat_MessageForwardInfo_UnavailableStoryHeader, ranges: [])
authorString = peerString
}
} else {
titleString = PresentationStrings.FormattedString(string: presentationData.strings.Chat_MessageForwardInfo_MessageHeader, ranges: [])
authorString = peerString
}
}
case .standalone:
let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)
titleColor = serviceColor.primaryText
if let psaType = psaType {
var customFormat: String?
let key = "Message.ForwardedPsa.\(psaType)"
if let string = presentationData.strings.primaryComponent.dict[key] {
customFormat = string
} else if let string = presentationData.strings.secondaryComponent?.dict[key] {
customFormat = string
}
if let customFormat = customFormat {
if let range = customFormat.range(of: "%@") {
let leftPart = String(customFormat[customFormat.startIndex ..< range.lowerBound])
let rightPart = String(customFormat[range.upperBound...])
let formattedText = leftPart + peerString + rightPart
titleString = PresentationStrings.FormattedString(string: formattedText, ranges: [PresentationStrings.FormattedString.Range(index: 0, range: NSRange(location: leftPart.count, length: peerString.count))])
} else {
titleString = PresentationStrings.FormattedString(string: customFormat, ranges: [])
}
} else {
titleString = strings.Message_GenericForwardedPsa(peerString)
}
} else {
titleString = PresentationStrings.FormattedString(string: presentationData.strings.Chat_MessageForwardInfo_MessageHeader, ranges: [])
authorString = peerString
}
}
var currentCredibilityIconImage: UIImage?
var highlight = true
if let peer = peer {
if let channel = peer as? TelegramChannel, channel.addressName == nil {
if case let .broadcast(info) = channel.info, info.flags.contains(.hasDiscussionGroup) {
} else if case .member = channel.participationStatus {
} else {
highlight = false
}
}
if peer.isFake {
switch type {
case let .bubble(incoming):
currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(presentationData.theme.theme, strings: presentationData.strings, type: incoming ? .regular : .outgoing)
case .standalone:
currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(presentationData.theme.theme, strings: presentationData.strings, type: .service)
}
} else if peer.isScam {
switch type {
case let .bubble(incoming):
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(presentationData.theme.theme, strings: presentationData.strings, type: incoming ? .regular : .outgoing)
case .standalone:
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(presentationData.theme.theme, strings: presentationData.strings, type: .service)
}
} else {
currentCredibilityIconImage = nil
}
} else {
highlight = false
}
let rawTitleString: NSString = titleString.string as NSString
let string = NSMutableAttributedString(string: rawTitleString as String, attributes: [NSAttributedString.Key.foregroundColor: titleColor, NSAttributedString.Key.font: prefixFont])
if highlight, let range = titleString.ranges.first?.range {
string.addAttributes([NSAttributedString.Key.font: peerFont], range: range)
}
var credibilityIconWidth: CGFloat = 0.0
if let icon = currentCredibilityIconImage {
credibilityIconWidth += icon.size.width + 4.0
}
var infoWidth: CGFloat = 0.0
if hasPsaInfo {
infoWidth += 32.0
}
let leftOffset: CGFloat = 0.0
infoWidth += leftOffset
var cutout: TextNodeCutout?
if let storyData {
switch storyData.storyType {
case .regular, .unavailable:
break
case .expired:
cutout = TextNodeCutout(topLeft: CGSize(width: 16.0, height: 10.0))
}
}
let (titleLayout, titleApply) = titleNodeLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: constrainedSize.width - credibilityIconWidth - infoWidth, height: constrainedSize.height), alignment: .natural, cutout: cutout, insets: UIEdgeInsets()))
var authorAvatarInset: CGFloat = 0.0
authorAvatarInset = 20.0
var nameLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if let authorString {
nameLayoutAndApply = nameNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: authorString, font: peer != nil ? peerFont : prefixFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: constrainedSize.width - credibilityIconWidth - infoWidth - authorAvatarInset, height: constrainedSize.height), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
}
let titleAuthorSpacing: CGFloat = 0.0
let resultSize: CGSize
if let nameLayoutAndApply {
resultSize = CGSize(
width: max(
titleLayout.size.width + credibilityIconWidth + infoWidth,
authorAvatarInset + nameLayoutAndApply.0.size.width
),
height: titleLayout.size.height + titleAuthorSpacing + nameLayoutAndApply.0.size.height
)
} else {
resultSize = CGSize(width: titleLayout.size.width + credibilityIconWidth + infoWidth, height: titleLayout.size.height)
}
return (resultSize, { width in
let node: ChatMessageForwardInfoNode
if let maybeNode = maybeNode {
node = maybeNode
} else {
node = ChatMessageForwardInfoNode()
}
node.theme = presentationData.theme.theme
node.highlightColor = titleColor.withMultipliedAlpha(0.1)
node.previousPeer = peer
let titleNode = titleApply()
titleNode.displaysAsynchronously = !presentationData.isPreview
if node.titleNode == nil {
titleNode.isUserInteractionEnabled = false
node.titleNode = titleNode
node.addSubnode(titleNode)
}
titleNode.frame = CGRect(origin: CGPoint(x: leftOffset, y: 0.0), size: titleLayout.size)
var nameFrame = CGRect()
if let (nameLayout, nameApply) = nameLayoutAndApply {
let nameNode = nameApply()
if node.nameNode == nil {
nameNode.isUserInteractionEnabled = false
node.nameNode = nameNode
node.addSubnode(nameNode)
}
nameFrame = CGRect(origin: CGPoint(x: leftOffset + authorAvatarInset, y: titleLayout.size.height + titleAuthorSpacing), size: nameLayout.size)
nameNode.frame = nameFrame
if authorAvatarInset != 0.0 {
let avatarNode: AvatarNode
if let current = node.avatarNode {
avatarNode = current
} else {
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 8.0))
node.avatarNode = avatarNode
node.addSubnode(avatarNode)
}
let avatarSize = CGSize(width: 16.0, height: 16.0)
avatarNode.frame = CGRect(origin: CGPoint(x: leftOffset, y: titleLayout.size.height + titleAuthorSpacing), size: avatarSize)
avatarNode.updateSize(size: avatarSize)
if let peer {
if peer.smallProfileImage != nil {
avatarNode.setPeerV2(context: context, theme: presentationData.theme.theme, peer: EnginePeer(peer), displayDimensions: avatarSize)
} else {
avatarNode.setPeer(context: context, theme: presentationData.theme.theme, peer: EnginePeer(peer), displayDimensions: avatarSize)
}
} else if let authorName, !authorName.isEmpty {
avatarNode.setCustomLetters([String(authorName[authorName.startIndex])])
} else {
avatarNode.setCustomLetters([" "])
}
} else {
if let avatarNode = node.avatarNode {
node.avatarNode = nil
avatarNode.removeFromSupernode()
}
}
} else {
if let nameNode = node.nameNode {
node.nameNode = nil
nameNode.removeFromSupernode()
}
if let avatarNode = node.avatarNode {
node.avatarNode = nil
avatarNode.removeFromSupernode()
}
}
if let storyData, case .expired = storyData.storyType {
let expiredStoryIconView: UIImageView
if let current = node.expiredStoryIconView {
expiredStoryIconView = current
} else {
expiredStoryIconView = UIImageView()
node.expiredStoryIconView = expiredStoryIconView
node.view.addSubview(expiredStoryIconView)
}
let imageType: ChatExpiredStoryIndicatorType
switch type {
case .standalone:
imageType = .free
case let .bubble(incoming):
imageType = incoming ? .incoming : .outgoing
}
expiredStoryIconView.image = PresentationResourcesChat.chatExpiredStoryIndicatorIcon(presentationData.theme.theme, type: imageType)
if let _ = expiredStoryIconView.image {
let imageSize = CGSize(width: 18.0, height: 18.0)
expiredStoryIconView.frame = CGRect(origin: CGPoint(x: -1.0, y: -2.0), size: imageSize)
}
} else if let expiredStoryIconView = node.expiredStoryIconView {
expiredStoryIconView.removeFromSuperview()
}
if let credibilityIconImage = currentCredibilityIconImage {
let credibilityIconNode: ASImageNode
if let node = node.credibilityIconNode {
credibilityIconNode = node
} else {
credibilityIconNode = ASImageNode()
node.credibilityIconNode = credibilityIconNode
node.addSubnode(credibilityIconNode)
}
credibilityIconNode.frame = CGRect(origin: CGPoint(x: nameFrame.maxX + 4.0, y: 17.0), size: credibilityIconImage.size)
credibilityIconNode.image = credibilityIconImage
} else {
node.credibilityIconNode?.removeFromSupernode()
node.credibilityIconNode = nil
}
if hasPsaInfo {
let infoNode: InfoButtonNode
if let current = node.infoNode {
infoNode = current
} else {
infoNode = InfoButtonNode(pressed: { [weak node] in
guard let node = node else {
return
}
if let psaType = psaType, let infoNode = node.infoNode {
node.openPsa?(psaType, infoNode)
}
})
node.infoNode = infoNode
node.addSubnode(infoNode)
}
let infoButtonSize = CGSize(width: 32.0, height: 32.0)
let infoButtonFrame = CGRect(origin: CGPoint(x: width - infoButtonSize.width - 2.0, y: 1.0), size: infoButtonSize)
infoNode.frame = infoButtonFrame
infoNode.update(size: infoButtonFrame.size, theme: presentationData.theme, type: type)
} else if let infoNode = node.infoNode {
node.infoNode = nil
infoNode.removeFromSupernode()
}
return node
})
}
}
}
@@ -0,0 +1,25 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageGameBubbleContentNode",
module_name = "ChatMessageGameBubbleContentNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Postbox",
"//submodules/Display",
"//submodules/AsyncDisplayKit",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramCore",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,159 @@
import Foundation
import UIKit
import Postbox
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import ChatMessageBubbleContentNode
import ChatMessageItemCommon
import ChatMessageAttachedContentNode
public final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode {
private var game: TelegramMediaGame?
private let contentNode: ChatMessageAttachedContentNode
override public var visibility: ListViewItemNodeVisibility {
didSet {
self.contentNode.visibility = visibility
}
}
required public init() {
self.contentNode = ChatMessageAttachedContentNode()
super.init()
self.addSubnode(self.contentNode)
self.contentNode.openMedia = { [weak self] _ in
if let strongSelf = self, let item = strongSelf.item {
item.controllerInteraction.requestMessageActionCallback(item.message, nil, true, false, nil)
}
}
}
override public func accessibilityActivate() -> Bool {
if let item = self.item {
item.controllerInteraction.requestMessageActionCallback(item.message, nil, true, false, nil)
}
return true
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let contentNodeLayout = self.contentNode.asyncLayout()
return { item, layoutConstants, preparePosition, _, constrainedSize, _ in
var game: TelegramMediaGame?
var messageEntities: [MessageTextEntity]?
for media in item.message.media {
if let media = media as? TelegramMediaGame {
game = media
break
}
}
for attribute in item.message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
messageEntities = attribute.entities
break
}
}
var title: String?
var text: String?
var mediaAndFlags: ([Media], ChatMessageAttachedContentNodeMediaFlags)?
if let game = game {
title = game.title
text = game.description
if let file = game.file {
mediaAndFlags = ([file], [.preferMediaBeforeText])
} else if let image = game.image {
mediaAndFlags = ([image], [.preferMediaBeforeText])
}
}
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, .peer(id: item.message.id.peerId), title, nil, nil, item.message.text.isEmpty ? text : item.message.text, item.message.text.isEmpty ? nil : messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize, item.controllerInteraction.presentationContext.animationCache, item.controllerInteraction.presentationContext.animationRenderer)
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
return (contentProperties, nil, initialWidth, { constrainedSize, position in
let (refinedWidth, finalizeLayout) = continueLayout(constrainedSize, position)
return (refinedWidth, { boundingWidth in
let (size, apply) = finalizeLayout(boundingWidth)
return (size, { [weak self] animation, synchronousLoads, applyInfo in
if let strongSelf = self {
strongSelf.item = item
strongSelf.game = game
apply(animation, synchronousLoads, applyInfo)
strongSelf.contentNode.frame = CGRect(origin: CGPoint(), size: size)
}
})
})
})
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
}
override public func animateInsertionIntoBubble(_ duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.bounds.contains(point) {
/*if let webPage = self.webPage, case let .Loaded(content) = webPage.content {
if content.instantPage != nil {
return .instantPage
}
}*/
}
return ChatMessageBubbleContentTapAction(content: .none)
}
override public func updateHiddenMedia(_ media: [Media]?) -> Bool {
return self.contentNode.updateHiddenMedia(media)
}
override public func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
if self.item?.message.id != messageId {
return nil
}
return self.contentNode.transitionNode(media: media)
}
override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? {
if let statusNode = self.contentNode.statusNode, !statusNode.isHidden {
return statusNode.reactionView(value: value)
}
return nil
}
override public func messageEffectTargetView() -> UIView? {
if let statusNode = self.contentNode.statusNode, !statusNode.isHidden {
return statusNode.messageEffectTargetView()
}
return nil
}
}
@@ -0,0 +1,41 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageGiftBubbleContentNode",
module_name = "ChatMessageGiftBubbleContentNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramCore",
"//submodules/AccountContext",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/TextFormat",
"//submodules/LocalizedPeerData",
"//submodules/UrlEscaping",
"//submodules/TelegramStringFormatting",
"//submodules/WallpaperBackgroundNode",
"//submodules/ReactionSelectionNode",
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
"//submodules/ShimmerEffect",
"//submodules/Markdown",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/InvisibleInkDustNode",
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent",
"//submodules/TelegramUI/Components/Gifts/GiftItemComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,36 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageGiftOfferBubbleContentNode",
module_name = "ChatMessageGiftOfferBubbleContentNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramCore",
"//submodules/AccountContext",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/TextFormat",
"//submodules/LocalizedPeerData",
"//submodules/UrlEscaping",
"//submodules/TelegramStringFormatting",
"//submodules/WallpaperBackgroundNode",
"//submodules/ReactionSelectionNode",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
"//submodules/Markdown",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/TelegramUI/Components/Gifts/GiftItemComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,333 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import ComponentFlow
import TelegramCore
import AccountContext
import TelegramPresentationData
import TelegramUIPreferences
import TextFormat
import LocalizedPeerData
import UrlEscaping
import TelegramStringFormatting
import WallpaperBackgroundNode
import ReactionSelectionNode
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import ChatControllerInteraction
import ShimmerEffect
import Markdown
import ChatMessageBubbleContentNode
import ChatMessageItemCommon
import TextNodeWithEntities
import GiftItemComponent
private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id) -> NSAttributedString? {
return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false, forAdditionalServiceMessage: true)
}
public class ChatMessageGiftOfferBubbleContentNode: ChatMessageBubbleContentNode {
private var mediaBackgroundContent: WallpaperBubbleBackgroundNode?
private let titleNode: TextNode
private let subtitleNode: TextNodeWithEntities
private let giftIcon = ComponentView<Empty>()
private var absoluteRect: (CGRect, CGSize)?
private var isPlaying: Bool = false
override public var disablesClipping: Bool {
return true
}
override public var visibility: ListViewItemNodeVisibility {
didSet {
let wasVisible = oldValue != .none
let isVisible = self.visibility != .none
if wasVisible != isVisible {
self.visibilityStatus = isVisible
switch self.visibility {
case .none:
self.subtitleNode.visibilityRect = nil
case let .visible(_, subRect):
var subRect = subRect
subRect.origin.x = 0.0
subRect.size.width = 10000.0
self.subtitleNode.visibilityRect = subRect
}
}
}
}
private var visibilityStatus: Bool? {
didSet {
if self.visibilityStatus != oldValue {
self.updateVisibility()
}
}
}
private var fetchDisposable: Disposable?
private var setupTimestamp: Double?
private var cachedTonImage: (UIImage, UIColor)?
required public init() {
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
self.subtitleNode = TextNodeWithEntities()
self.subtitleNode.textNode.isUserInteractionEnabled = false
self.subtitleNode.textNode.displaysAsynchronously = false
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.subtitleNode.textNode)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.fetchDisposable?.dispose()
}
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeSubtitleLayout = TextNodeWithEntities.asyncLayout(self.subtitleNode)
return { item, layoutConstants, _, _, _, _ in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center)
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
var giftSize = CGSize(width: 260.0, height: 240.0)
var uniqueGift: StarGift.UniqueGift?
let incoming: Bool
if item.message.id.peerId == item.context.account.peerId && item.message.forwardInfo == nil {
incoming = true
} else {
incoming = item.message.effectivelyIncoming(item.context.account.peerId)
}
let textColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
let text: String
let additionalText: String
var hasActionButtons = false
if let action = item.message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftPurchaseOffer(gift, amount, expireDate, isAccepted, isDeclined) = action.action {
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
let priceString: String
switch amount.currency {
case .stars:
priceString = item.presentationData.strings.Notification_StarGiftOffer_Offer_Stars(Int32(clamping: amount.amount.value))
case .ton:
priceString = "\(amount.amount) TON"
}
let peerName = item.message.peers[item.message.id.peerId].flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
let giftTitle: String
if case let .unique(gift) = gift {
giftTitle = "\(gift.title) #\(formatCollectibleNumber(gift.number, dateTimeFormat: item.presentationData.dateTimeFormat))"
uniqueGift = gift
} else {
giftTitle = ""
}
if incoming {
text = item.presentationData.strings.Notification_StarGiftOffer_Offer(peerName, priceString, giftTitle).string
} else {
text = item.presentationData.strings.Notification_StarGiftOffer_OfferYou(peerName, priceString, giftTitle).string
}
if isAccepted {
additionalText = item.presentationData.strings.Notification_StarGiftOffer_Status_Accepted
} else if isDeclined {
additionalText = item.presentationData.strings.Notification_StarGiftOffer_Status_Rejected
} else if expireDate > currentTimestamp {
func textForTimeout(_ value: Int32) -> String {
if value < 3600 {
let minutes = value / 60
return item.presentationData.strings.Notification_StarGiftOffer_Expiration_Minutes(minutes)
} else {
let hours = value / 3600
let minutes = (value % 3600) / 60
return item.presentationData.strings.Notification_StarGiftOffer_Expiration_Hours(hours) + item.presentationData.strings.Notification_StarGiftOffer_Expiration_Delimiter + item.presentationData.strings.Notification_StarGiftOffer_Expiration_Minutes(minutes)
}
}
let delta = expireDate - currentTimestamp
additionalText = item.presentationData.strings.Notification_StarGiftOffer_Status_Expires(textForTimeout(delta)).string
if incoming {
hasActionButtons = true
}
} else {
additionalText = item.presentationData.strings.Notification_StarGiftOffer_Status_Expired
}
} else {
text = ""
additionalText = ""
}
let titleAttributedString = NSMutableAttributedString(attributedString: NSAttributedString(string: additionalText, font: Font.regular(13.0), textColor: textColor, paragraphAlignment: .center))
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: textColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: textColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: textColor),
linkAttribute: { url in
return ("URL", url)
}
), textAlignment: .center)
let textConstrainedSize = CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude)
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
giftSize.height = titleLayout.size.height + subtitleLayout.size.height + 162.0
let backgroundSize = CGSize(width: giftSize.width, height: giftSize.height + 4.0)
return (backgroundSize.width, { boundingWidth in
return (backgroundSize, { [weak self] animation, synchronousLoads, info in
if let strongSelf = self {
strongSelf.item = item
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((boundingWidth - giftSize.width) / 2.0), y: 0.0), size: giftSize)
let mediaBackgroundFrame = imageFrame.insetBy(dx: -2.0, dy: -2.0)
strongSelf.updateVisibility()
let _ = titleApply()
let _ = subtitleApply(TextNodeWithEntities.Arguments(
context: item.context,
cache: item.controllerInteraction.presentationContext.animationCache,
renderer: item.controllerInteraction.presentationContext.animationRenderer,
placeholderColor: item.presentationData.theme.theme.chat.message.freeform.withWallpaper.reactionInactiveBackground,
attemptSynchronous: synchronousLoads
))
let textFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0), y: mediaBackgroundFrame.minY + 126.0), size: subtitleLayout.size)
strongSelf.subtitleNode.textNode.frame = textFrame
let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: textFrame.maxY + 23.0), size: titleLayout.size)
strongSelf.titleNode.frame = titleFrame
if strongSelf.mediaBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
backgroundContent.clipsToBounds = true
backgroundContent.cornerRadius = 24.0
strongSelf.mediaBackgroundContent = backgroundContent
strongSelf.insertSubnode(backgroundContent, at: 0)
}
if let backgroundContent = strongSelf.mediaBackgroundContent {
animation.animator.updateFrame(layer: backgroundContent.layer, frame: mediaBackgroundFrame, completion: nil)
backgroundContent.clipsToBounds = true
if hasActionButtons {
backgroundContent.cornerRadius = 0.0
if backgroundContent.view.mask == nil {
backgroundContent.view.mask = UIImageView(image: generateImage(mediaBackgroundFrame.size, rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setFillColor(UIColor.white.cgColor)
context.addPath(CGPath(roundedRect: CGRect(x: 0, y: 0, width: size.width, height: size.height * 0.5), cornerWidth: 24.0, cornerHeight: 24.0, transform: nil))
context.addPath(CGPath(roundedRect: CGRect(x: 0, y: size.height * 0.5 - 30.0, width: size.width, height: size.height * 0.5 + 30.0), cornerWidth: 8.0, cornerHeight: 8.0, transform: nil))
context.fillPath()
}))
}
} else {
backgroundContent.view.mask = nil
backgroundContent.cornerRadius = 24.0
}
}
if let uniqueGift {
let iconSize = CGSize(width: 94.0, height: 94.0)
let _ = strongSelf.giftIcon.update(
transition: .immediate,
component: AnyComponent(GiftItemComponent(
context: item.context,
theme: item.presentationData.theme.theme,
strings: item.presentationData.strings,
peer: nil,
subject: .uniqueGift(gift: uniqueGift, price: nil),
mode: .thumbnail
)),
environment: {},
containerSize: iconSize
)
if let giftIconView = strongSelf.giftIcon.view {
if giftIconView.superview == nil {
strongSelf.view.addSubview(giftIconView)
}
giftIconView.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - iconSize.width) / 2.0), y: mediaBackgroundFrame.minY + 17.0), size: iconSize)
}
}
if let (rect, size) = strongSelf.absoluteRect {
strongSelf.updateAbsoluteRect(rect, within: size)
}
switch strongSelf.visibility {
case .none:
strongSelf.subtitleNode.visibilityRect = nil
case let .visible(_, subRect):
var subRect = subRect
subRect.origin.x = 0.0
subRect.size.width = 10000.0
strongSelf.subtitleNode.visibilityRect = subRect
}
}
})
})
})
}
}
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
self.absoluteRect = (rect, containerSize)
if let mediaBackgroundContent = self.mediaBackgroundContent {
var backgroundFrame = mediaBackgroundContent.frame
backgroundFrame.origin.x += rect.minX
backgroundFrame.origin.y += rect.minY
mediaBackgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate)
}
}
override public func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) {
}
override public func applyAbsoluteOffsetSpring(value: CGFloat, duration: Double, damping: CGFloat) {
}
override public func unreadMessageRangeUpdated() {
self.updateVisibility()
}
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.mediaBackgroundContent?.frame.contains(point) == true {
return ChatMessageBubbleContentTapAction(content: .openMessage)
} else {
return ChatMessageBubbleContentTapAction(content: .none)
}
}
private func updateVisibility() {
}
}
@@ -0,0 +1,38 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageGiveawayBubbleContentNode",
module_name = "ChatMessageGiveawayBubbleContentNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/AvatarNode",
"//submodules/AccountContext",
"//submodules/PhoneNumberFormat",
"//submodules/TelegramStringFormatting",
"//submodules/Markdown",
"//submodules/ShimmerEffect",
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/TextFormat",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,31 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageInstantVideoBubbleContentNode",
module_name = "ChatMessageInstantVideoBubbleContentNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/TelegramUIPreferences",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/AudioTranscriptionButtonComponent",
"//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,564 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramUIPreferences
import ComponentFlow
import AudioTranscriptionButtonComponent
import ChatMessageDateAndStatusNode
import ChatMessageBubbleContentNode
import ChatMessageItemCommon
import ChatMessageInteractiveInstantVideoNode
import ChatMessageInteractiveFileNode
import ChatControllerInteraction
extension ChatMessageInteractiveInstantVideoNode.AnimateFileNodeDescription {
convenience init(_ node: ChatMessageInteractiveFileNode) {
self.init(
node: node,
textClippingNode: node.textClippingNode,
dateAndStatusNode: node.dateAndStatusNode,
fetchingTextNode: node.fetchingTextNode,
waveformView: node.waveformView,
statusNode: node.statusNode,
audioTranscriptionButton: node.audioTranscriptionButton
)
}
}
public class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentNode {
public let interactiveFileNode: ChatMessageInteractiveFileNode
public let interactiveVideoNode: ChatMessageInteractiveInstantVideoNode
private let maskLayer = SimpleLayer()
private let maskForeground = SimpleLayer()
private let backdropMaskLayer = SimpleLayer()
private let backdropMaskForeground = BubbleMaskLayer()
private var isExpanded = false
private var audioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState = .collapsed
public var hasExpandedAudioTranscription: Bool {
if case .expanded = self.audioTranscriptionState {
return true
} else {
return false
}
}
override public var visibility: ListViewItemNodeVisibility {
didSet {
var wasVisible = false
if case .visible = oldValue {
wasVisible = true
}
let isVisible = self.isContentVisible
if wasVisible != isVisible {
if !isVisible {
Queue.mainQueue().after(0.05) {
if isVisible == self.isContentVisible {
self.interactiveVideoNode.visibility = isVisible
}
}
} else {
self.interactiveVideoNode.visibility = isVisible
}
}
}
}
private var isContentVisible: Bool {
var isVisible = false
if case .visible = self.visibility {
isVisible = true
}
return isVisible
}
required public init() {
self.interactiveFileNode = ChatMessageInteractiveFileNode()
self.interactiveVideoNode = ChatMessageInteractiveInstantVideoNode()
super.init()
self.maskForeground.backgroundColor = UIColor.white.cgColor
self.maskForeground.masksToBounds = true
self.maskLayer.addSublayer(self.maskForeground)
self.addSubnode(self.interactiveVideoNode)
self.interactiveVideoNode.requestUpdateLayout = { [weak self] _ in
if let strongSelf = self, let item = strongSelf.item {
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false)
}
}
self.interactiveVideoNode.updateTranscriptionExpanded = { [weak self] state in
if let strongSelf = self, let item = strongSelf.item {
let previous = strongSelf.audioTranscriptionState
strongSelf.audioTranscriptionState = state
strongSelf.interactiveFileNode.audioTranscriptionState = state
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, state != .inProgress && previous != state)
}
}
self.interactiveVideoNode.updateTranscriptionText = { [weak self] text in
if let strongSelf = self, let item = strongSelf.item {
strongSelf.interactiveFileNode.forcedAudioTranscriptionText = text
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false)
}
}
self.interactiveFileNode.updateTranscriptionExpanded = { [weak self] state in
if let strongSelf = self, let item = strongSelf.item {
let previous = strongSelf.audioTranscriptionState
strongSelf.audioTranscriptionState = state
strongSelf.interactiveVideoNode.audioTranscriptionState = state
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, previous != state)
}
}
self.interactiveFileNode.toggleSelection = { [weak self] value in
if let strongSelf = self, let item = strongSelf.item {
item.controllerInteraction.toggleMessagesSelection([item.message.id], value)
}
}
self.interactiveFileNode.activateLocalContent = { [weak self] in
if let strongSelf = self, let item = strongSelf.item {
let _ = item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: .default))
}
}
self.interactiveFileNode.requestUpdateLayout = { [weak self] _ in
if let strongSelf = self, let item = strongSelf.item {
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false)
}
}
self.interactiveFileNode.displayImportedTooltip = { [weak self] sourceNode in
if let strongSelf = self, let item = strongSelf.item {
let _ = item.controllerInteraction.displayImportedMessageTooltip(sourceNode)
}
}
self.interactiveFileNode.dateAndStatusNode.reactionSelected = { [weak self] _, value, sourceView in
guard let strongSelf = self, let item = strongSelf.item else {
return
}
item.controllerInteraction.updateMessageReaction(item.topMessage, .reaction(value), false, sourceView)
}
self.interactiveFileNode.dateAndStatusNode.openReactionPreview = { [weak self] gesture, sourceNode, value in
guard let strongSelf = self, let item = strongSelf.item else {
gesture?.cancel()
return
}
item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceNode, gesture, value)
}
self.interactiveFileNode.updateIsTextSelectionActive = { [weak self] value in
self?.updateIsTextSelectionActive?(value)
}
}
override public func accessibilityActivate() -> Bool {
if let item = self.item {
let _ = item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: .default))
}
return true
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let interactiveVideoLayout = self.interactiveVideoNode.asyncLayout()
let interactiveFileLayout = self.interactiveFileNode.asyncLayout()
let currentExpanded = self.isExpanded
let audioTranscriptionState = self.audioTranscriptionState
let didSetupFileNode = self.item != nil
return { item, layoutConstants, preparePosition, selection, constrainedSize, avatarInset in
var selectedFile: TelegramMediaFile?
for media in item.message.media {
if let telegramFile = media as? TelegramMediaFile {
selectedFile = telegramFile
}
}
let isViewOnceMessage = item.message.minAutoremoveOrClearTimeout == viewOnceTimeout
let forceIsPlaying = isViewOnceMessage && didSetupFileNode
var incoming = item.message.effectivelyIncoming(item.context.account.peerId)
if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info {
incoming = false
}
let statusType: ChatMessageDateAndStatusType?
if case .customChatContents = item.associatedData.subject {
statusType = nil
} else {
switch preparePosition {
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if incoming {
statusType = .BubbleIncoming
} else {
if item.message.flags.contains(.Failed) {
statusType = .BubbleOutgoing(.Failed)
} else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil {
statusType = .BubbleOutgoing(.Sending)
} else {
statusType = .BubbleOutgoing(.Sent(read: item.read))
}
}
default:
statusType = nil
}
}
let automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: selectedFile!)
let (initialWidth, refineLayout) = interactiveFileLayout(ChatMessageInteractiveFileNode.Arguments(
context: item.context,
presentationData: item.presentationData,
customTintColor: nil,
message: item.message,
topMessage: item.topMessage,
associatedData: item.associatedData,
chatLocation: item.chatLocation,
attributes: item.attributes,
isPinned: item.isItemPinned,
forcedIsEdited: item.isItemEdited,
file: selectedFile!,
automaticDownload: automaticDownload,
incoming: incoming,
isRecentActions: item.associatedData.isRecentActions,
forcedResourceStatus: item.associatedData.forcedResourceStatus,
dateAndStatusType: statusType,
displayReactions: false,
messageSelection: item.message.groupingKey != nil ? selection : nil,
isAttachedContentBlock: false,
layoutConstants: layoutConstants,
constrainedSize: CGSize(width: constrainedSize.width - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right, height: constrainedSize.height),
controllerInteraction: item.controllerInteraction
))
var isReplyThread = false
if case .replyThread = item.chatLocation {
isReplyThread = true
}
var isExpanded = false
if case .expanded = audioTranscriptionState {
isExpanded = true
}
var isPlaying = false
let normalDisplaySize = layoutConstants.instantVideo.dimensions
var displaySize = normalDisplaySize
let maximumDisplaySize = CGSize(width: min(404, constrainedSize.width - 2.0), height: min(404, constrainedSize.width - 2.0))
if (item.associatedData.currentlyPlayingMessageId == item.message.index || forceIsPlaying) && (!isViewOnceMessage || item.associatedData.isStandalone) {
isPlaying = true
if !isExpanded {
displaySize = maximumDisplaySize
}
}
let leftInset: CGFloat = 0.0
let rightInset: CGFloat = 0.0
let (videoLayout, videoApply) = interactiveVideoLayout(ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: item.message, topMessage: item.message, content: item.content, read: item.read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: item.attributes, isItemPinned: item.message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), constrainedSize.width - leftInset - rightInset - avatarInset, displaySize, maximumDisplaySize, isPlaying ? 1.0 : 0.0, .free, automaticDownload, avatarInset)
let videoFrame = CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: videoLayout.contentSize)
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none, shareButtonOffset: isExpanded ? nil : CGPoint(x: avatarInset + displaySize.width + 4.0, y: -25.0), hidesHeaders: !isExpanded, avatarOffset: !isExpanded && isPlaying ? -100.0 : 0.0)
let videoFrameWidth = videoFrame.width + 2.0
return (contentProperties, nil, initialWidth, { constrainedSize, position in
var refinedWidth = videoFrameWidth
var finishLayout: ((CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> Void))?
if isExpanded || !didSetupFileNode {
(refinedWidth, finishLayout) = refineLayout(CGSize(width: constrainedSize.width - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right - 44.0, height: constrainedSize.height))
refinedWidth += layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right
}
if !isExpanded {
refinedWidth = videoFrameWidth
}
return (refinedWidth, { boundingWidth in
var finalSize: CGSize
var finalFileSize: CGSize?
var finalFileApply: ((Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> Void)?
if let finishLayout = finishLayout {
let (fileSize, fileApply) = finishLayout(boundingWidth - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right)
if isExpanded {
finalSize = CGSize(width: fileSize.width + layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right, height: fileSize.height + layoutConstants.file.bubbleInsets.top + layoutConstants.file.bubbleInsets.bottom)
} else {
finalSize = CGSize(width: boundingWidth, height: videoFrame.height + 2.0)
}
finalFileSize = fileSize
finalFileApply = fileApply
} else {
finalSize = CGSize(width: boundingWidth, height: videoFrame.height + 2.0)
}
return (finalSize, { [weak self] animation, synchronousLoads, applyInfo in
if let strongSelf = self {
strongSelf.item = item
strongSelf.isExpanded = isExpanded
strongSelf.bubbleBackgroundNode?.layer.mask = strongSelf.maskLayer
if let bubbleBackdropNode = strongSelf.bubbleBackdropNode, bubbleBackdropNode.hasImage && strongSelf.backdropMaskForeground.superlayer == nil {
strongSelf.bubbleBackdropNode?.overrideMask = true
strongSelf.bubbleBackdropNode?.maskView?.layer.addSublayer(strongSelf.backdropMaskForeground)
}
strongSelf.maskLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 640.0, height: 640.0))
strongSelf.backdropMaskLayer.frame = strongSelf.maskLayer.frame
let bubbleSize = strongSelf.bubbleBackgroundNode?.backgroundFrame.size ?? finalSize
let radius: CGFloat = displaySize.width / 2.0
let maskCornerRadius = isExpanded ? 1.0 : radius
let maskFrame = CGRect(origin: CGPoint(x: isExpanded ? 1.0 : (incoming ? 7.0 : 1.0), y: isExpanded ? 0.0 : 1.0), size: isExpanded ? bubbleSize : CGSize(width: radius * 2.0, height: radius * 2.0))
animation.animator.updateCornerRadius(layer: strongSelf.maskForeground, cornerRadius: maskCornerRadius, completion: nil)
animation.animator.updateFrame(layer: strongSelf.maskForeground, frame: maskFrame, completion: nil)
let backdropMaskFrame = CGRect(origin: CGPoint(x: isExpanded ? (incoming ? 8.0 : 2.0) : (incoming ? 8.0 : 2.0), y: isExpanded ? 2.0 : 2.0), size: isExpanded ? CGSize(width: bubbleSize.width - 8.0, height: bubbleSize.height - 3.0) : CGSize(width: radius * 2.0, height: radius * 2.0))
let topLeftCornerRadius: CGFloat
let topRightCornerRadius: CGFloat
let bottomLeftCornerRadius: CGFloat
let bottomRightCornerRadius: CGFloat
if let bubbleCorners = strongSelf.bubbleBackgroundNode?.currentCorners(bubbleCorners: item.presentationData.chatBubbleCorners) {
topLeftCornerRadius = isExpanded ? bubbleCorners.topLeftRadius : radius
topRightCornerRadius = isExpanded ? bubbleCorners.topRightRadius : radius
bottomLeftCornerRadius = isExpanded ? bubbleCorners.bottomLeftRadius : radius
bottomRightCornerRadius = isExpanded ? bubbleCorners.bottomRightRadius : radius
} else {
let backdropRadius = isExpanded ? item.presentationData.chatBubbleCorners.mainRadius : radius
topLeftCornerRadius = backdropRadius
topRightCornerRadius = backdropRadius
bottomLeftCornerRadius = backdropRadius
bottomRightCornerRadius = backdropRadius
}
strongSelf.backdropMaskForeground.update(
size: backdropMaskFrame.size,
topLeftCornerRadius: topLeftCornerRadius,
topRightCornerRadius: topRightCornerRadius,
bottomLeftCornerRadius: bottomLeftCornerRadius,
bottomRightCornerRadius: bottomRightCornerRadius,
animator: animation.animator
)
animation.animator.updateFrame(layer: strongSelf.backdropMaskForeground, frame: backdropMaskFrame, completion: nil)
let videoLayoutData: ChatMessageInstantVideoItemLayoutData = .constrained(left: 0.0, right: 0.0)
var videoAnimation = animation
var fileAnimation = animation
if currentExpanded != isExpanded {
videoAnimation = .None
fileAnimation = .None
}
animation.animator.updateFrame(layer: strongSelf.interactiveVideoNode.layer, frame: videoFrame, completion: nil)
videoApply(videoLayoutData, videoAnimation)
if let fileSize = finalFileSize {
strongSelf.interactiveFileNode.frame = CGRect(origin: CGPoint(x: layoutConstants.file.bubbleInsets.left, y: layoutConstants.file.bubbleInsets.top), size: fileSize)
finalFileApply?(synchronousLoads, fileAnimation, applyInfo)
}
if currentExpanded != isExpanded {
if isExpanded {
strongSelf.interactiveVideoNode.animateTo(ChatMessageInteractiveInstantVideoNode.AnimateFileNodeDescription(strongSelf.interactiveFileNode), animator: animation.animator)
} else {
strongSelf.interactiveVideoNode.animateFrom(ChatMessageInteractiveInstantVideoNode.AnimateFileNodeDescription(strongSelf.interactiveFileNode), animator: animation.animator)
}
}
}
})
})
})
}
}
override public func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
return nil
}
override public func updateHiddenMedia(_ media: [Media]?) -> Bool {
return false
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double) {
self.interactiveVideoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.interactiveVideoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.interactiveVideoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
override public func willUpdateIsExtractedToContextPreview(_ value: Bool) {
self.interactiveFileNode.willUpdateIsExtractedToContextPreview(value)
}
override public func updateIsExtractedToContextPreview(_ value: Bool) {
self.interactiveFileNode.updateIsExtractedToContextPreview(value)
}
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if !self.interactiveFileNode.isHidden {
if self.interactiveFileNode.dateAndStatusNode.supernode != nil, let _ = self.interactiveFileNode.dateAndStatusNode.hitTest(self.view.convert(point, to: self.interactiveFileNode.dateAndStatusNode.view), with: nil) {
return ChatMessageBubbleContentTapAction(content: .ignore)
}
if self.interactiveFileNode.hasTapAction(at: self.view.convert(point, to: self.interactiveFileNode.view)) {
return ChatMessageBubbleContentTapAction(content: .ignore)
}
}
if !self.interactiveVideoNode.isHidden {
if self.interactiveVideoNode.dateAndStatusNode.supernode != nil, let _ = self.interactiveVideoNode.dateAndStatusNode.hitTest(self.view.convert(point, to: self.interactiveVideoNode.dateAndStatusNode.view), with: nil) {
return ChatMessageBubbleContentTapAction(content: .ignore)
}
if let audioTranscriptionButton = self.interactiveVideoNode.audioTranscriptionButton, let _ = audioTranscriptionButton.hitTest(self.view.convert(point, to: audioTranscriptionButton), with: nil) {
return ChatMessageBubbleContentTapAction(content: .ignore)
}
}
return super.tapActionAtPoint(point, gesture: gesture, isEstimating: isEstimating)
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.isExpanded, let result = self.interactiveFileNode.hitTest(self.view.convert(point, to: self.interactiveFileNode.view), with: event) {
return result
}
if !self.isExpanded, let result = self.interactiveVideoNode.hitTest(self.view.convert(point, to: self.interactiveVideoNode.view), with: event) {
return result
}
return super.hitTest(point, with: event)
}
override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? {
if !self.interactiveVideoNode.dateAndStatusNode.isHidden {
return self.interactiveVideoNode.dateAndStatusNode.reactionView(value: value)
}
return nil
}
override public func messageEffectTargetView() -> UIView? {
if !self.interactiveVideoNode.dateAndStatusNode.isHidden {
return self.interactiveVideoNode.dateAndStatusNode.messageEffectTargetView()
}
return nil
}
override public func targetForStoryTransition(id: StoryId) -> UIView? {
return self.interactiveVideoNode.targetForStoryTransition(id: id)
}
override public var disablesClipping: Bool {
return true
}
}
private class BubbleMaskLayer: SimpleLayer {
private class CornerLayer: SimpleLayer {
private let contentLayer = SimpleLayer()
override init(layer: Any) {
super.init(layer: layer)
}
init(cornerMask: CACornerMask) {
super.init()
self.masksToBounds = true
self.contentLayer.backgroundColor = UIColor.white.cgColor
self.contentLayer.masksToBounds = true
self.contentLayer.maskedCorners = cornerMask
self.addSublayer(self.contentLayer)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(size: CGSize, cornerRadius: CGFloat, animator: ControlledTransitionAnimator) {
animator.updateCornerRadius(layer: self.contentLayer, cornerRadius: cornerRadius, completion: nil)
let mask = self.contentLayer.maskedCorners
var origin = CGPoint()
if mask == .layerMinXMinYCorner {
origin = .zero
} else if mask == .layerMaxXMinYCorner {
origin = CGPoint(x: -size.width / 2.0, y: 0.0)
} else if mask == .layerMinXMaxYCorner {
origin = CGPoint(x: 0.0, y: -size.height / 2.0)
} else if mask == .layerMaxXMaxYCorner {
origin = CGPoint(x: -size.width / 2.0, y: -size.height / 2.0)
}
animator.updateFrame(layer: self.contentLayer, frame: CGRect(origin: origin, size: size), completion: nil)
}
}
private let topLeft = CornerLayer(cornerMask: [.layerMinXMinYCorner])
private let topRight = CornerLayer(cornerMask: [.layerMaxXMinYCorner])
private let bottomLeft = CornerLayer(cornerMask: [.layerMinXMaxYCorner])
private let bottomRight = CornerLayer(cornerMask: [.layerMaxXMaxYCorner])
override init(layer: Any) {
super.init(layer: layer)
}
override init() {
super.init()
self.addSublayer(self.topLeft)
self.addSublayer(self.topRight)
self.addSublayer(self.bottomLeft)
self.addSublayer(self.bottomRight)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(
size: CGSize,
topLeftCornerRadius: CGFloat,
topRightCornerRadius: CGFloat,
bottomLeftCornerRadius: CGFloat,
bottomRightCornerRadius: CGFloat,
animator: ControlledTransitionAnimator
) {
var size = CGSize(width: floor(size.width), height: floor(size.height))
if Int(size.width) % 2 != 0 {
size.width += 1.0
}
if Int(size.height) % 2 != 0 {
size.height += 1.0
}
animator.updateFrame(layer: self.topLeft, frame: CGRect(origin: .zero, size: CGSize(width: size.width / 2.0, height: size.height / 2.0)), completion: nil)
animator.updateFrame(layer: self.topRight, frame: CGRect(origin: CGPoint(x: size.width / 2.0, y: 0.0), size: CGSize(width: size.width / 2.0, height: size.height / 2.0)), completion: nil)
animator.updateFrame(layer: self.bottomLeft, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height / 2.0), size: CGSize(width: size.width / 2.0, height: size.height / 2.0)), completion: nil)
animator.updateFrame(layer: self.bottomRight, frame: CGRect(origin: CGPoint(x: size.width / 2.0, y: size.height / 2.0), size: CGSize(width: size.width / 2.0, height: size.height / 2.0)), completion: nil)
self.topLeft.update(size: size, cornerRadius: topLeftCornerRadius, animator: animator)
self.topRight.update(size: size, cornerRadius: topRightCornerRadius, animator: animator)
self.bottomLeft.update(size: size, cornerRadius: bottomLeftCornerRadius, animator: animator)
self.bottomRight.update(size: size, cornerRadius: bottomRightCornerRadius, animator: animator)
}
}
@@ -0,0 +1,45 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageInstantVideoItemNode",
module_name = "ChatMessageInstantVideoItemNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/TextFormat",
"//submodules/AccountContext",
"//submodules/LocalizedPeerData",
"//submodules/ContextUI",
"//submodules/Markdown",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
"//submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItem",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemView",
"//submodules/TelegramUI/Components/Chat/ChatMessageSwipeToReplyNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageSelectionNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageDeliveryFailedNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageShareButton",
"//submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode",
"//submodules/TelegramUI/Components/Chat/ChatSwipeToReplyRecognizer",
"//submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,52 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageInteractiveFileNode",
module_name = "ChatMessageInteractiveFileNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Postbox",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Display",
"//submodules/TelegramCore",
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/PhotoResources",
"//submodules/TelegramStringFormatting",
"//submodules/SemanticStatusNode",
"//submodules/FileMediaResourceStatus",
"//submodules/CheckNode",
"//submodules/MusicAlbumArtResources",
"//submodules/AudioBlob",
"//submodules/ContextUI",
"//submodules/ChatPresentationInterfaceState",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/AudioTranscriptionButtonComponent",
"//submodules/TelegramUI/Components/AudioWaveformComponent",
"//submodules/ShimmerEffect",
"//submodules/Media/ConvertOpusToAAC",
"//submodules/Media/LocalAudioTranscription",
"//submodules/TextSelectionNode",
"//submodules/TelegramUI/Components/AudioTranscriptionPendingIndicatorComponent",
"//submodules/UndoUI",
"//submodules/TelegramNotices",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
"//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode",
"//submodules/TelegramUI/Components/Chat/ChatHistoryEntry",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/ShimmeringLinkNode",
"//submodules/AnimatedCountLabelNode",
"//submodules/AudioWaveform",
"//submodules/DeviceProximity",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,47 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageInteractiveInstantVideoNode",
module_name = "ChatMessageInteractiveInstantVideoNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
#"-Xfrontend", "-debug-time-function-bodies"
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/RadialStatusNode",
"//submodules/SemanticStatusNode",
"//submodules/PhotoResources",
"//submodules/TelegramUniversalVideoContent",
"//submodules/FileMediaResourceStatus",
"//submodules/Components/HierarchyTrackingLayer",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/AudioTranscriptionButtonComponent",
"//submodules/UndoUI",
"//submodules/TelegramNotices",
"//submodules/Markdown",
"//submodules/TextFormat",
"//submodules/InvisibleInkDustNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode",
"//submodules/TelegramUI/Components/Chat/InstantVideoRadialStatusNode",
"//submodules/TelegramUI/Components/Chat/ChatInstantVideoMessageDurationNode",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,52 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageInteractiveMediaNode",
module_name = "ChatMessageInteractiveMediaNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Postbox",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//submodules/TextFormat",
"//submodules/AccountContext",
"//submodules/RadialStatusNode",
"//submodules/StickerResources",
"//submodules/PhotoResources",
"//submodules/TelegramUniversalVideoContent",
"//submodules/TelegramStringFormatting",
"//submodules/GalleryUI",
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
"//submodules/LocalMediaResources",
"//submodules/WallpaperResources",
"//submodules/ChatMessageInteractiveMediaBadge",
"//submodules/ContextUI",
"//submodules/InvisibleInkDustNode",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
"//submodules/TelegramUI/Components/Stories/StoryContainerScreen",
"//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode",
"//submodules/TelegramUI/Components/Chat/ChatHistoryEntry",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/WallpaperPreviewMedia",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/TelegramUI/Components/Gifts/GiftItemComponent",
"//submodules/Utils/RangeSet",
"//submodules/MediaResources",
"//submodules/UIKitRuntimeUtils",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,27 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMessageInvoiceBubbleContentNode",
module_name = "ChatMessageInvoiceBubbleContentNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Postbox",
"//submodules/Display",
"//submodules/AsyncDisplayKit",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramCore",
"//submodules/TelegramUIPreferences",
"//submodules/TelegramStringFormatting",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode",
],
visibility = [
"//visibility:public",
],
)

Some files were not shown because too many files have changed in this diff Show More