mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
Consolidates 137 wave commits + 31 supporting commits (CLAUDE.md bump, typealias additions, AnyObject→EngineMedia restoration) into one squashed commit. Migrates dozens of consumer-side public APIs, struct fields, protocol methods, and enum payloads from Postbox protocols/structs to TelegramEngine engine wrappers and typealiases. Drops `import Postbox` from many files. Adds new TelegramCore typealiases and one TelegramEngineUnauthorized facade. Notable changes by category: **TelegramCore typealias additions** (rule 2 — narrow utility typealiases): - EngineChatListIndex, EngineTempBoxFile, EngineItemCollectionItemIndex, EngineItemCollectionViewEntryIndex, EngineValueBoxEncryptionParameters, EngineMessageAndThreadId, EnginePeerStoryStats, EngineMessageHistoryAnchorIndex, EngineChatListTotalUnreadStateCategory, EngineChatListTotalUnreadStateStats, EnginePeerSummaryCounterTags, EngineChatListTotalUnreadState, EngineItemCacheEntryId, EngineHashFunctions, EngineCachedMediaResourceRepresentationResult, EngineMediaResourceDataFetchResult, EngineMediaResourceDataFetchError, EngineMediaResourceStatus, EngineCachedPeerData **TelegramCore engine extensions/forwarders**: - EngineMessage.engineMedia, EngineMessage.enginePeers, EngineMessage.adAttribute, EngineMessage.effectivelyIncoming - engineFileSize forwarder - TelegramEngine.Resources.clearCachedMediaResources(mediaResourceIds: Set<EngineMediaResource.Id>) - TelegramEngine.Resources.fetchStatus(id:resourceSize:) - TelegramEngineUnauthorized.UnauthorizedResources facade with storeResourceData **Public API/struct migrations to engine types**: - ChatAvailableMessageActions.banAuthor/banAuthors → EnginePeer?/[EnginePeer] - WebSessionsContextState.peers → [EnginePeer.Id: EnginePeer] - CacheUsageStats.peers → [EnginePeer.Id: EnginePeer] - PeerCommand.peer → EnginePeer - PeerInfoControllerMode.calls(messages:) → [EngineMessage] - CallControllerNodeProtocol.updatePeer → EnginePeer params - ChatHistoryListNode.messageInCurrentHistoryView (and 4 variants) → EngineMessage? - ChatHistorySearchContainerNode.messageForGallery → EngineMessage? - PeerInfoPaneNode.findLoadedMessage / ensureMessageIsVisible / transitionNodeForGallery → engine-typed - GalleryHiddenMediaTarget.getTransitionInfo / GalleryHiddenMediaManager.findTarget → engine-typed - ChatPanelInterfaceInteraction.presentReactionDeletionOptions / presentBan*MessageOptions → EnginePeer - DrawingMessageRenderer.messages → [EngineMessage] - ChatVideoGalleryItemScrubberView.setFetchStatusSignal → EngineMediaResource.FetchStatus - ChannelDiscussionGroupActionSheetItem.peer, VoiceChatPeerEntry.peer, VoiceChatFullscreenParticipantItem.peer, MediaStreamComponent.chatPeer, MediaStreamVideoComponent.callPeer, ChatMessageContactBubbleContentNode.contactPeer, ChatMessageForwardInfoNode.peer, ChatMessageCommentFooterContentNode.replyPeers, ChatReportPeerTitlePanelNode.peer, ChatMessageActionUrlAuthController.bot, PeerMediaCollectionInterfaceState.peer, ChatMessageCallBubbleContentNode.peopleAvatars, ChatLoadingNode.renderedPeer (→ EngineRenderedPeer) — all to engine types **Wave-71-shadow stored-field migrations** (Postbox Peer/Message → Engine wrapper): - LegacyCallControllerNode.peer - CallStatusBarNode.currentPeer **Dead-code / dead-field removals**: - CallController.peer, CallControllerNodeV2.account, ContactMultiselectionController PeerNameIndex fields, preparedChatListNodeViewTransition account: Account param, FetchResource.swift entirely (unused function) **Module-level Postbox import drops**: 30+ files including TelegramRootController, EditStories, GiftViewScreen, AnimatedStickerUtils, FetchPhotoLibraryImageResource, PeerInfoGiftsPaneNode, PeerInfoPaneContainerNode, PresentAddMembers, PeerInfoProfileItems, ChatControllerAdminBanUsers, PresentationData typealiases, DefaultDayPresentationTheme, ChatListViewTransition, GalleryHiddenMediaManager, RecentSessionsController, GifContext, AuthorizationSequenceController, PeerInfoHeaderEditingContentNode, PeerInfoHeaderNode, PeerAllowedReactionListController, CallControllerNodeV2, and 6 PeerInfo pane files. **AnyObject restoration**: rule 8 added (never substitute Postbox protocols with Any/AnyObject) — undid previous AnyObject substitutions in waves 141/143 back to EngineMedia. Doc maintenance: CLAUDE.md updated to reflect new typealiases and forwarders. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
419 lines
23 KiB
Swift
419 lines
23 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import TelegramCore
|
|
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: [EnginePeer] = []
|
|
|
|
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(EnginePeer(peer))
|
|
}
|
|
for id in conferenceCall.otherParticipants {
|
|
if let peer = item.message.peers[id] {
|
|
peopleAvatars.append(EnginePeer(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: EngineMessage(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: Array(peopleAvatars.prefix(3)), 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)
|
|
}
|
|
}
|
|
}
|