mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-02 00:17:02 +00:00
Cmd+# shortcuts to navigate between chats Changed day theme service messages background color to match current wallpaper when there is one
698 lines
39 KiB
Swift
698 lines
39 KiB
Swift
import Foundation
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import SwiftSignalKit
|
|
import Postbox
|
|
import TelegramCore
|
|
|
|
private let nameFont = Font.medium(14.0)
|
|
|
|
private let inlineBotPrefixFont = Font.regular(14.0)
|
|
private let inlineBotNameFont = nameFont
|
|
|
|
class ChatMessageInstantVideoItemNode: ChatMessageItemView {
|
|
private let interactiveVideoNode: ChatMessageInteractiveInstantVideoNode
|
|
|
|
private var selectionNode: ChatMessageSelectionNode?
|
|
private var shareButtonNode: HighlightableButtonNode?
|
|
|
|
private var swipeToReplyNode: ChatMessageSwipeToReplyNode?
|
|
private var swipeToReplyFeedback: HapticFeedback?
|
|
|
|
private var appliedItem: ChatMessageItem?
|
|
|
|
private var forwardInfoNode: ChatMessageForwardInfoNode?
|
|
private var forwardBackgroundNode: ASImageNode?
|
|
|
|
private var viaBotNode: TextNode?
|
|
private var replyInfoNode: ChatMessageReplyInfoNode?
|
|
private var replyBackgroundNode: ASImageNode?
|
|
|
|
private var actionButtonsNode: ChatMessageActionButtonsNode?
|
|
|
|
private var currentSwipeToReplyTranslation: CGFloat = 0.0
|
|
|
|
override var visibility: ListViewItemNodeVisibility {
|
|
didSet {
|
|
if self.visibility != oldValue {
|
|
self.interactiveVideoNode.visibility = self.visibility
|
|
}
|
|
}
|
|
}
|
|
|
|
required init() {
|
|
self.interactiveVideoNode = ChatMessageInteractiveInstantVideoNode()
|
|
|
|
super.init(layerBacked: false)
|
|
|
|
self.addSubnode(self.interactiveVideoNode)
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
|
|
recognizer.tapActionAtPoint = { [weak self] point in
|
|
if let strongSelf = self {
|
|
if let shareButtonNode = strongSelf.shareButtonNode, shareButtonNode.frame.contains(point) {
|
|
return .fail
|
|
}
|
|
}
|
|
return .waitForSingleTap
|
|
}
|
|
self.view.addGestureRecognizer(recognizer)
|
|
|
|
let replyRecognizer = ChatSwipeToReplyRecognizer(target: self, action: #selector(self.swipeToReplyGesture(_:)))
|
|
replyRecognizer.shouldBegin = { [weak self] in
|
|
if let strongSelf = self, let item = strongSelf.item {
|
|
if strongSelf.selectionNode != nil {
|
|
return false
|
|
}
|
|
return item.controllerInteraction.canSetupReply(item.message)
|
|
}
|
|
return false
|
|
}
|
|
self.view.addGestureRecognizer(replyRecognizer)
|
|
}
|
|
|
|
override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, Bool) -> Void) {
|
|
let layoutConstants = self.layoutConstants
|
|
|
|
let makeVideoLayout = self.interactiveVideoNode.asyncLayout()
|
|
|
|
let viaBotLayout = TextNode.asyncLayout(self.viaBotNode)
|
|
let makeReplyInfoLayout = ChatMessageReplyInfoNode.asyncLayout(self.replyInfoNode)
|
|
let currentReplyBackgroundNode = self.replyBackgroundNode
|
|
let currentShareButtonNode = self.shareButtonNode
|
|
|
|
let makeForwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode)
|
|
let currentForwardBackgroundNode = self.forwardBackgroundNode
|
|
|
|
let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode)
|
|
|
|
let currentItem = self.appliedItem
|
|
|
|
return { item, params, mergedTop, mergedBottom, dateHeaderAtBottom in
|
|
let baseWidth = params.width - params.leftInset - params.rightInset
|
|
let incoming = item.message.effectivelyIncoming(item.account.peerId)
|
|
|
|
let avatarInset: CGFloat
|
|
var hasAvatar = false
|
|
|
|
switch item.chatLocation {
|
|
case let .peer(peerId):
|
|
if peerId != item.account.peerId {
|
|
if peerId.isGroupOrChannel && item.message.author != nil {
|
|
var isBroadcastChannel = false
|
|
if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info {
|
|
isBroadcastChannel = true
|
|
}
|
|
|
|
if !isBroadcastChannel {
|
|
hasAvatar = true
|
|
}
|
|
}
|
|
} else if incoming {
|
|
hasAvatar = true
|
|
}
|
|
case .group:
|
|
hasAvatar = true
|
|
}
|
|
|
|
if hasAvatar {
|
|
avatarInset = layoutConstants.avatarDiameter
|
|
} else {
|
|
avatarInset = 0.0
|
|
}
|
|
|
|
var needShareButton = false
|
|
if item.message.id.peerId == item.account.peerId {
|
|
for attribute in item.content.firstMessage.attributes {
|
|
if let _ = attribute as? SourceReferenceMessageAttribute {
|
|
needShareButton = true
|
|
break
|
|
}
|
|
}
|
|
} else if item.message.effectivelyIncoming(item.account.peerId) {
|
|
if let peer = item.message.peers[item.message.id.peerId] {
|
|
if let channel = peer as? TelegramChannel {
|
|
if case .broadcast = channel.info {
|
|
needShareButton = true
|
|
}
|
|
}
|
|
}
|
|
if !needShareButton, let author = item.message.author as? TelegramUser, let _ = author.botInfo, !item.message.media.isEmpty {
|
|
needShareButton = true
|
|
}
|
|
if !needShareButton {
|
|
loop: for media in item.message.media {
|
|
if media is TelegramMediaGame || media is TelegramMediaInvoice {
|
|
needShareButton = true
|
|
break loop
|
|
} else if let media = media as? TelegramMediaWebpage, case .Loaded = media.content {
|
|
needShareButton = true
|
|
break loop
|
|
}
|
|
}
|
|
} else {
|
|
loop: for media in item.message.media {
|
|
if media is TelegramMediaAction {
|
|
needShareButton = false
|
|
break loop
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var layoutInsets = layoutConstants.instantVideo.insets
|
|
if dateHeaderAtBottom {
|
|
layoutInsets.top += layoutConstants.timestampHeaderHeight
|
|
}
|
|
|
|
let displaySize = CGSize(width: 212.0, height: 212.0)
|
|
|
|
var automaticDownload = true
|
|
for media in item.message.media {
|
|
if let file = media as? TelegramMediaFile {
|
|
automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, media: file)
|
|
}
|
|
}
|
|
|
|
let (videoLayout, videoApply) = makeVideoLayout(ChatMessageBubbleContentItem(account: item.account, controllerInteraction: item.controllerInteraction, message: item.message, read: item.read, presentationData: item.presentationData, associatedData: item.associatedData), params.width - params.leftInset - params.rightInset - avatarInset, displaySize, .free, automaticDownload)
|
|
|
|
let videoFrame = CGRect(origin: CGPoint(x: (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + avatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - videoLayout.contentSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left)), y: 0.0), size: videoLayout.contentSize)
|
|
|
|
var viaBotApply: (TextNodeLayout, () -> TextNode)?
|
|
var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)?
|
|
var updatedReplyBackgroundNode: ASImageNode?
|
|
var replyBackgroundImage: UIImage?
|
|
var replyMarkup: ReplyMarkupMessageAttribute?
|
|
var inlineBotNameString: String?
|
|
|
|
let availableWidth = max(60.0, params.width - params.leftInset - params.rightInset - videoLayout.contentSize.width - 20.0 - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left)
|
|
|
|
for attribute in item.message.attributes {
|
|
if let attribute = attribute as? InlineBotMessageAttribute {
|
|
var inlineBotNameString: String?
|
|
if let peerId = attribute.peerId, let bot = item.message.peers[peerId] as? TelegramUser {
|
|
inlineBotNameString = bot.username
|
|
} else {
|
|
inlineBotNameString = attribute.title
|
|
}
|
|
|
|
if let inlineBotNameString = inlineBotNameString {
|
|
let inlineBotNameColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
|
|
|
|
let bodyAttributes = MarkdownAttributeSet(font: nameFont, textColor: inlineBotNameColor)
|
|
let boldAttributes = MarkdownAttributeSet(font: inlineBotPrefixFont, textColor: inlineBotNameColor)
|
|
let botString = addAttributesToStringWithRanges(item.presentationData.strings.Conversation_MessageViaUser("@\(inlineBotNameString)"), body: bodyAttributes, argumentAttributes: [0: boldAttributes])
|
|
|
|
viaBotApply = viaBotLayout(TextNodeLayoutArguments(attributedString: botString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0, availableWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
|
}
|
|
}
|
|
if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] {
|
|
replyInfoApply = makeReplyInfoLayout(item.presentationData, item.presentationData.strings, item.account, .standalone, replyMessage, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude))
|
|
} else if let attribute = attribute as? InlineBotMessageAttribute {
|
|
if let peerId = attribute.peerId, let bot = item.message.peers[peerId] as? TelegramUser {
|
|
inlineBotNameString = bot.username
|
|
} else {
|
|
inlineBotNameString = attribute.title
|
|
}
|
|
} else if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty {
|
|
replyMarkup = attribute
|
|
}
|
|
}
|
|
|
|
if replyInfoApply != nil || viaBotApply != nil {
|
|
if let currentReplyBackgroundNode = currentReplyBackgroundNode {
|
|
updatedReplyBackgroundNode = currentReplyBackgroundNode
|
|
} else {
|
|
updatedReplyBackgroundNode = ASImageNode()
|
|
}
|
|
|
|
let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
|
|
replyBackgroundImage = graphics.chatFreeformContentAdditionalInfoBackgroundImage
|
|
}
|
|
|
|
var updatedShareButtonBackground: UIImage?
|
|
|
|
var updatedShareButtonNode: HighlightableButtonNode?
|
|
if needShareButton {
|
|
if currentShareButtonNode != nil {
|
|
updatedShareButtonNode = currentShareButtonNode
|
|
if item.presentationData.theme !== currentItem?.presentationData.theme {
|
|
let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
|
|
if item.message.id.peerId == item.account.peerId {
|
|
updatedShareButtonBackground = graphics.chatBubbleNavigateButtonImage
|
|
} else {
|
|
updatedShareButtonBackground = graphics.chatBubbleShareButtonImage
|
|
}
|
|
}
|
|
} else {
|
|
let buttonNode = HighlightableButtonNode()
|
|
let buttonIcon: UIImage?
|
|
let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
|
|
if item.message.id.peerId == item.account.peerId {
|
|
buttonIcon = graphics.chatBubbleNavigateButtonImage
|
|
} else {
|
|
buttonIcon = graphics.chatBubbleShareButtonImage
|
|
}
|
|
buttonNode.setBackgroundImage(buttonIcon, for: [.normal])
|
|
updatedShareButtonNode = buttonNode
|
|
}
|
|
}
|
|
|
|
let availableContentWidth = params.width - params.leftInset - params.rightInset - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left
|
|
|
|
var forwardInfoSizeApply: (CGSize, () -> ChatMessageForwardInfoNode)?
|
|
var updatedForwardBackgroundNode: ASImageNode?
|
|
var forwardBackgroundImage: UIImage?
|
|
if let forwardInfo = item.message.forwardInfo {
|
|
let forwardSource: Peer
|
|
let forwardAuthorSignature: String?
|
|
|
|
if let source = forwardInfo.source {
|
|
forwardSource = source
|
|
if let authorSignature = forwardInfo.authorSignature {
|
|
forwardAuthorSignature = authorSignature
|
|
} else if forwardInfo.author.id != source.id {
|
|
forwardAuthorSignature = forwardInfo.author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
|
|
} else {
|
|
forwardAuthorSignature = nil
|
|
}
|
|
} else {
|
|
forwardSource = forwardInfo.author
|
|
forwardAuthorSignature = nil
|
|
}
|
|
let availableWidth = max(60.0, availableContentWidth - videoLayout.contentSize.width + 6.0)
|
|
forwardInfoSizeApply = makeForwardInfoLayout(item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude))
|
|
|
|
if let currentForwardBackgroundNode = currentForwardBackgroundNode {
|
|
updatedForwardBackgroundNode = currentForwardBackgroundNode
|
|
} else {
|
|
updatedForwardBackgroundNode = ASImageNode()
|
|
}
|
|
|
|
let graphics = PresentationResourcesChat.additionalGraphics(item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
|
|
forwardBackgroundImage = graphics.chatServiceBubbleFillImage
|
|
}
|
|
|
|
var maxContentWidth = videoLayout.contentSize.width
|
|
var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode))?
|
|
if let replyMarkup = replyMarkup {
|
|
let (minWidth, buttonsLayout) = actionButtonsLayout(item.account, item.presentationData.theme, item.presentationData.strings, replyMarkup, item.message, maxContentWidth)
|
|
maxContentWidth = max(maxContentWidth, minWidth)
|
|
actionButtonsFinalize = buttonsLayout
|
|
}
|
|
|
|
var actionButtonsSizeAndApply: (CGSize, (Bool) -> ChatMessageActionButtonsNode)?
|
|
if let actionButtonsFinalize = actionButtonsFinalize {
|
|
actionButtonsSizeAndApply = actionButtonsFinalize(maxContentWidth)
|
|
}
|
|
|
|
var layoutSize = CGSize(width: params.width, height: videoLayout.contentSize.height)
|
|
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
|
layoutSize.height += actionButtonsSizeAndApply.0.height
|
|
}
|
|
|
|
return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _ in
|
|
if let strongSelf = self {
|
|
strongSelf.appliedItem = item
|
|
|
|
let transition: ContainedViewLayoutTransition
|
|
if animation.isAnimated {
|
|
transition = .animated(duration: 0.2, curve: .spring)
|
|
} else {
|
|
transition = .immediate
|
|
}
|
|
strongSelf.interactiveVideoNode.frame = videoFrame
|
|
let videoLayoutData: ChatMessageInstantVideoItemLayoutData
|
|
if incoming {
|
|
videoLayoutData = .constrained(left: 0.0, right: max(0.0, availableContentWidth - videoFrame.width))
|
|
} else {
|
|
videoLayoutData = .constrained(left: max(0.0, availableContentWidth - videoFrame.width), right: 0.0)
|
|
}
|
|
videoApply(videoLayoutData, transition)
|
|
|
|
if let updatedShareButtonNode = updatedShareButtonNode {
|
|
if updatedShareButtonNode !== strongSelf.shareButtonNode {
|
|
if let shareButtonNode = strongSelf.shareButtonNode {
|
|
shareButtonNode.removeFromSupernode()
|
|
}
|
|
strongSelf.shareButtonNode = updatedShareButtonNode
|
|
strongSelf.addSubnode(updatedShareButtonNode)
|
|
updatedShareButtonNode.addTarget(strongSelf, action: #selector(strongSelf.shareButtonPressed), forControlEvents: .touchUpInside)
|
|
}
|
|
if let updatedShareButtonBackground = updatedShareButtonBackground {
|
|
strongSelf.shareButtonNode?.setBackgroundImage(updatedShareButtonBackground, for: [.normal])
|
|
}
|
|
} else if let shareButtonNode = strongSelf.shareButtonNode {
|
|
shareButtonNode.removeFromSupernode()
|
|
strongSelf.shareButtonNode = nil
|
|
}
|
|
|
|
if let shareButtonNode = strongSelf.shareButtonNode {
|
|
shareButtonNode.frame = CGRect(origin: CGPoint(x: videoFrame.maxX - 7.0, y: videoFrame.maxY - 54.0), size: CGSize(width: 29.0, height: 29.0))
|
|
}
|
|
|
|
if let updatedReplyBackgroundNode = updatedReplyBackgroundNode {
|
|
if strongSelf.replyBackgroundNode == nil {
|
|
strongSelf.replyBackgroundNode = updatedReplyBackgroundNode
|
|
strongSelf.addSubnode(updatedReplyBackgroundNode)
|
|
updatedReplyBackgroundNode.image = replyBackgroundImage
|
|
} else {
|
|
strongSelf.replyBackgroundNode?.image = replyBackgroundImage
|
|
}
|
|
} else if let replyBackgroundNode = strongSelf.replyBackgroundNode {
|
|
replyBackgroundNode.removeFromSupernode()
|
|
strongSelf.replyBackgroundNode = nil
|
|
}
|
|
|
|
if let (viaBotLayout, viaBotApply) = viaBotApply {
|
|
let viaBotNode = viaBotApply()
|
|
if strongSelf.viaBotNode == nil {
|
|
strongSelf.viaBotNode = viaBotNode
|
|
strongSelf.addSubnode(viaBotNode)
|
|
}
|
|
let viaBotFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 10.0) : (params.width - params.rightInset - viaBotLayout.size.width - layoutConstants.bubble.edgeInset - 10.0)), y: 8.0), size: viaBotLayout.size)
|
|
viaBotNode.frame = viaBotFrame
|
|
strongSelf.replyBackgroundNode?.frame = CGRect(origin: CGPoint(x: viaBotFrame.minX - 4.0, y: viaBotFrame.minY - 2.0), size: CGSize(width: viaBotFrame.size.width + 8.0, height: viaBotFrame.size.height + 5.0))
|
|
} else if let viaBotNode = strongSelf.viaBotNode {
|
|
viaBotNode.removeFromSupernode()
|
|
strongSelf.viaBotNode = nil
|
|
}
|
|
|
|
if let (replyInfoSize, replyInfoApply) = replyInfoApply {
|
|
let replyInfoNode = replyInfoApply()
|
|
if strongSelf.replyInfoNode == nil {
|
|
strongSelf.replyInfoNode = replyInfoNode
|
|
strongSelf.addSubnode(replyInfoNode)
|
|
}
|
|
var viaBotSize = CGSize()
|
|
if let viaBotNode = strongSelf.viaBotNode {
|
|
viaBotSize = viaBotNode.frame.size
|
|
}
|
|
let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 10.0) : (params.width - params.rightInset - max(replyInfoSize.width, viaBotSize.width) - layoutConstants.bubble.edgeInset - 10.0)), y: 8.0 + viaBotSize.height), size: replyInfoSize)
|
|
if let viaBotNode = strongSelf.viaBotNode {
|
|
if replyInfoFrame.minX < viaBotNode.frame.minX {
|
|
viaBotNode.frame = viaBotNode.frame.offsetBy(dx: replyInfoFrame.minX - viaBotNode.frame.minX, dy: 0.0)
|
|
}
|
|
}
|
|
replyInfoNode.frame = replyInfoFrame
|
|
strongSelf.replyBackgroundNode?.frame = CGRect(origin: CGPoint(x: replyInfoFrame.minX - 4.0, y: replyInfoFrame.minY - viaBotSize.height - 2.0), size: CGSize(width: max(replyInfoFrame.size.width, viaBotSize.width) + 8.0, height: replyInfoFrame.size.height + viaBotSize.height + 5.0))
|
|
} else if let replyInfoNode = strongSelf.replyInfoNode {
|
|
replyInfoNode.removeFromSupernode()
|
|
strongSelf.replyInfoNode = nil
|
|
}
|
|
|
|
if let updatedForwardBackgroundNode = updatedForwardBackgroundNode {
|
|
if strongSelf.forwardBackgroundNode == nil {
|
|
strongSelf.forwardBackgroundNode = updatedForwardBackgroundNode
|
|
strongSelf.addSubnode(updatedForwardBackgroundNode)
|
|
updatedForwardBackgroundNode.image = forwardBackgroundImage
|
|
}
|
|
} else if let forwardBackgroundNode = strongSelf.forwardBackgroundNode {
|
|
forwardBackgroundNode.removeFromSupernode()
|
|
strongSelf.forwardBackgroundNode = nil
|
|
}
|
|
|
|
if let (forwardInfoSize, forwardInfoApply) = forwardInfoSizeApply {
|
|
let forwardInfoNode = forwardInfoApply()
|
|
if strongSelf.forwardInfoNode == nil {
|
|
strongSelf.forwardInfoNode = forwardInfoNode
|
|
strongSelf.addSubnode(forwardInfoNode)
|
|
}
|
|
let forwardInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 12.0) : (params.width - params.rightInset - forwardInfoSize.width - layoutConstants.bubble.edgeInset - 12.0)), y: 8.0), size: forwardInfoSize)
|
|
forwardInfoNode.frame = forwardInfoFrame
|
|
strongSelf.forwardBackgroundNode?.frame = CGRect(origin: CGPoint(x: forwardInfoFrame.minX - 6.0, y: forwardInfoFrame.minY - 2.0), size: CGSize(width: forwardInfoFrame.size.width + 10.0, height: forwardInfoFrame.size.height + 4.0))
|
|
} else if let forwardInfoNode = strongSelf.forwardInfoNode {
|
|
forwardInfoNode.removeFromSupernode()
|
|
strongSelf.forwardInfoNode = nil
|
|
}
|
|
|
|
if let actionButtonsSizeAndApply = actionButtonsSizeAndApply {
|
|
var animated = false
|
|
if let _ = strongSelf.actionButtonsNode {
|
|
if case .System = animation {
|
|
animated = true
|
|
}
|
|
}
|
|
let actionButtonsNode = actionButtonsSizeAndApply.1(animated)
|
|
let previousFrame = actionButtonsNode.frame
|
|
let actionButtonsFrame = CGRect(origin: CGPoint(x: videoFrame.minX, y: videoFrame.maxY), size: actionButtonsSizeAndApply.0)
|
|
actionButtonsNode.frame = actionButtonsFrame
|
|
if actionButtonsNode !== strongSelf.actionButtonsNode {
|
|
strongSelf.actionButtonsNode = actionButtonsNode
|
|
actionButtonsNode.buttonPressed = { button in
|
|
if let strongSelf = self {
|
|
strongSelf.performMessageButtonAction(button: button)
|
|
}
|
|
}
|
|
actionButtonsNode.buttonLongTapped = { button in
|
|
if let strongSelf = self {
|
|
strongSelf.presentMessageButtonContextMenu(button: button)
|
|
}
|
|
}
|
|
strongSelf.addSubnode(actionButtonsNode)
|
|
} else {
|
|
if case let .System(duration) = animation {
|
|
actionButtonsNode.layer.animateFrame(from: previousFrame, to: actionButtonsFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
|
}
|
|
}
|
|
} else if let actionButtonsNode = strongSelf.actionButtonsNode {
|
|
actionButtonsNode.removeFromSupernode()
|
|
strongSelf.actionButtonsNode = nil
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
@objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
|
switch recognizer.state {
|
|
case .ended:
|
|
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
|
switch gesture {
|
|
case .tap:
|
|
if let avatarNode = self.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(location) {
|
|
if let item = self.item, let author = item.content.firstMessage.author {
|
|
let navigate: ChatControllerInteractionNavigateToPeer
|
|
if item.content.firstMessage.id.peerId == item.account.peerId {
|
|
navigate = .chat(textInputState: nil, messageId: nil)
|
|
} else {
|
|
navigate = .info
|
|
}
|
|
item.controllerInteraction.openPeer(item.effectiveAuthorId ?? author.id, navigate, item.message)
|
|
}
|
|
return
|
|
}
|
|
|
|
if let replyInfoNode = self.replyInfoNode, replyInfoNode.frame.contains(location) {
|
|
if let item = self.item {
|
|
for attribute in item.message.attributes {
|
|
if let attribute = attribute as? ReplyMessageAttribute {
|
|
item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let forwardInfoNode = self.forwardInfoNode, forwardInfoNode.frame.contains(location) {
|
|
if let item = self.item, let forwardInfo = item.message.forwardInfo {
|
|
if let sourceMessageId = forwardInfo.sourceMessageId {
|
|
item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId)
|
|
} else {
|
|
item.controllerInteraction.openPeer(forwardInfo.source?.id ?? forwardInfo.author.id, .chat(textInputState: nil, messageId: nil), nil)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
self.item?.controllerInteraction.clickThroughMessage()
|
|
case .longTap, .doubleTap:
|
|
if let item = self.item, let videoContentNode = self.interactiveVideoNode.videoContentNode(at: self.view.convert(location, to: self.interactiveVideoNode.view)) {
|
|
item.controllerInteraction.openMessageContextMenu(item.message, false, videoContentNode, videoContentNode.bounds)
|
|
}
|
|
case .hold:
|
|
break
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
@objc func shareButtonPressed() {
|
|
if let item = self.item {
|
|
if item.content.firstMessage.id.peerId == item.account.peerId {
|
|
for attribute in item.content.firstMessage.attributes {
|
|
if let attribute = attribute as? SourceReferenceMessageAttribute {
|
|
item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId)
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
item.controllerInteraction.openMessageShareMenu(item.message.id)
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) {
|
|
switch recognizer.state {
|
|
case .began:
|
|
self.currentSwipeToReplyTranslation = 0.0
|
|
if self.swipeToReplyFeedback == nil {
|
|
self.swipeToReplyFeedback = HapticFeedback()
|
|
self.swipeToReplyFeedback?.prepareImpact()
|
|
}
|
|
(self.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures()
|
|
case .changed:
|
|
var translation = recognizer.translation(in: self.view)
|
|
translation.x = max(-80.0, min(0.0, translation.x))
|
|
var animateReplyNodeIn = false
|
|
if (translation.x < -45.0) != (self.currentSwipeToReplyTranslation < -45.0) {
|
|
if translation.x < -45.0, self.swipeToReplyNode == nil, let item = self.item {
|
|
self.swipeToReplyFeedback?.impact()
|
|
|
|
let swipeToReplyNode = ChatMessageSwipeToReplyNode(fillColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.bubble.shareButtonFillColor, wallpaper: item.presentationData.theme.wallpaper), strokeColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.bubble.shareButtonStrokeColor, wallpaper: item.presentationData.theme.wallpaper), foregroundColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.bubble.shareButtonForegroundColor, wallpaper: item.presentationData.theme.wallpaper))
|
|
self.swipeToReplyNode = swipeToReplyNode
|
|
self.addSubnode(swipeToReplyNode)
|
|
animateReplyNodeIn = true
|
|
}
|
|
}
|
|
self.currentSwipeToReplyTranslation = translation.x
|
|
var bounds = self.bounds
|
|
bounds.origin.x = -translation.x
|
|
self.bounds = bounds
|
|
|
|
if let swipeToReplyNode = self.swipeToReplyNode {
|
|
swipeToReplyNode.frame = CGRect(origin: CGPoint(x: bounds.size.width, y: floor((self.contentSize.height - 33.0) / 2.0)), size: CGSize(width: 33.0, height: 33.0))
|
|
if animateReplyNodeIn {
|
|
swipeToReplyNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12)
|
|
swipeToReplyNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4)
|
|
} else {
|
|
swipeToReplyNode.alpha = min(1.0, abs(translation.x / 45.0))
|
|
}
|
|
}
|
|
case .cancelled, .ended:
|
|
self.swipeToReplyFeedback = nil
|
|
|
|
let translation = recognizer.translation(in: self.view)
|
|
if case .ended = recognizer.state, translation.x < -45.0 {
|
|
if let item = self.item {
|
|
item.controllerInteraction.setupReply(item.message.id)
|
|
}
|
|
}
|
|
var bounds = self.bounds
|
|
let previousBounds = bounds
|
|
bounds.origin.x = 0.0
|
|
self.bounds = bounds
|
|
self.layer.animateBounds(from: previousBounds, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
|
if let swipeToReplyNode = self.swipeToReplyNode {
|
|
self.swipeToReplyNode = nil
|
|
swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in
|
|
swipeToReplyNode?.removeFromSupernode()
|
|
})
|
|
swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
if let shareButtonNode = self.shareButtonNode, shareButtonNode.frame.contains(point) {
|
|
return shareButtonNode.view
|
|
}
|
|
if !self.bounds.contains(point) {
|
|
return nil
|
|
}
|
|
return super.hitTest(point, with: event)
|
|
}
|
|
|
|
override func updateSelectionState(animated: Bool) {
|
|
guard let item = self.item else {
|
|
return
|
|
}
|
|
|
|
if let selectionState = item.controllerInteraction.selectionState {
|
|
var selected = false
|
|
var incoming = true
|
|
|
|
selected = selectionState.selectedIds.contains(item.message.id)
|
|
incoming = item.message.effectivelyIncoming(item.account.peerId)
|
|
|
|
let offset: CGFloat = incoming ? 42.0 : 0.0
|
|
|
|
if let selectionNode = self.selectionNode {
|
|
selectionNode.updateSelected(selected, animated: false)
|
|
selectionNode.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentBounds.size.width, height: self.contentBounds.size.height))
|
|
self.subnodeTransform = CATransform3DMakeTranslation(offset, 0.0, 0.0);
|
|
} else {
|
|
let selectionNode = ChatMessageSelectionNode(theme: item.presentationData.theme.theme, toggle: { [weak self] value in
|
|
if let strongSelf = self, let item = strongSelf.item {
|
|
item.controllerInteraction.toggleMessagesSelection([item.message.id], value)
|
|
}
|
|
})
|
|
|
|
selectionNode.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentBounds.size.width, height: self.contentBounds.size.height))
|
|
self.addSubnode(selectionNode)
|
|
self.selectionNode = selectionNode
|
|
selectionNode.updateSelected(selected, animated: false)
|
|
let previousSubnodeTransform = self.subnodeTransform
|
|
self.subnodeTransform = CATransform3DMakeTranslation(offset, 0.0, 0.0);
|
|
if animated {
|
|
selectionNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
|
self.layer.animate(from: NSValue(caTransform3D: previousSubnodeTransform), to: NSValue(caTransform3D: self.subnodeTransform), keyPath: "sublayerTransform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.4)
|
|
|
|
if !incoming {
|
|
let position = selectionNode.layer.position
|
|
selectionNode.layer.animatePosition(from: CGPoint(x: position.x - 42.0, y: position.y), to: position, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if let selectionNode = self.selectionNode {
|
|
self.selectionNode = nil
|
|
let previousSubnodeTransform = self.subnodeTransform
|
|
self.subnodeTransform = CATransform3DIdentity
|
|
if animated {
|
|
self.layer.animate(from: NSValue(caTransform3D: previousSubnodeTransform), to: NSValue(caTransform3D: self.subnodeTransform), keyPath: "sublayerTransform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.4, completion: { [weak selectionNode]_ in
|
|
selectionNode?.removeFromSupernode()
|
|
})
|
|
selectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
|
if CGFloat(0.0).isLessThanOrEqualTo(selectionNode.frame.origin.x) {
|
|
let position = selectionNode.layer.position
|
|
selectionNode.layer.animatePosition(from: position, to: CGPoint(x: position.x - 42.0, y: position.y), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
|
}
|
|
} else {
|
|
selectionNode.removeFromSupernode()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
|
super.animateInsertion(currentTimestamp, duration: duration, short: short)
|
|
|
|
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
|
|
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
|
super.animateRemoved(currentTimestamp, duration: duration)
|
|
|
|
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
|
}
|
|
|
|
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
|
super.animateAdded(currentTimestamp, duration: duration)
|
|
|
|
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
}
|