mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2026-04-05 21:21:15 +00:00
Fixes
fix localeWithStrings globally (#30)
Fix badge on zoomed devices. closes #9
Hide channel bottom panel closes #27
Another attempt to fix badge on some Zoomed devices
Force System Share sheet tg://sg/debug
fixes for device badge
New Crowdin updates (#34)
* New translations sglocalizable.strings (Chinese Traditional)
* New translations sglocalizable.strings (Chinese Simplified)
* New translations sglocalizable.strings (Chinese Traditional)
Fix input panel hidden on selection (#31)
* added if check for selectionState != nil
* same order of subnodes
Revert "Fix input panel hidden on selection (#31)"
This reverts commit e8a8bb1496.
Fix input panel for channels Closes #37
Quickly share links with system's share menu
force tabbar when editing
increase height for correct animation
New translations sglocalizable.strings (Ukrainian) (#38)
Hide Post Story button
Fix 10.15.1
Fix archive option for long-tap
Enable in-app Safari
Disable some unsupported purchases
disableDeleteChatSwipeOption + refactor restart alert
Hide bot in suggestions list
Fix merge v11.0
Fix exceptions for safari webview controller
New Crowdin updates (#47)
* New translations sglocalizable.strings (Romanian)
* New translations sglocalizable.strings (French)
* New translations sglocalizable.strings (Spanish)
* New translations sglocalizable.strings (Afrikaans)
* New translations sglocalizable.strings (Arabic)
* New translations sglocalizable.strings (Catalan)
* New translations sglocalizable.strings (Czech)
* New translations sglocalizable.strings (Danish)
* New translations sglocalizable.strings (German)
* New translations sglocalizable.strings (Greek)
* New translations sglocalizable.strings (Finnish)
* New translations sglocalizable.strings (Hebrew)
* New translations sglocalizable.strings (Hungarian)
* New translations sglocalizable.strings (Italian)
* New translations sglocalizable.strings (Japanese)
* New translations sglocalizable.strings (Korean)
* New translations sglocalizable.strings (Dutch)
* New translations sglocalizable.strings (Norwegian)
* New translations sglocalizable.strings (Polish)
* New translations sglocalizable.strings (Portuguese)
* New translations sglocalizable.strings (Serbian (Cyrillic))
* New translations sglocalizable.strings (Swedish)
* New translations sglocalizable.strings (Turkish)
* New translations sglocalizable.strings (Vietnamese)
* New translations sglocalizable.strings (Indonesian)
* New translations sglocalizable.strings (Hindi)
* New translations sglocalizable.strings (Uzbek)
New Crowdin updates (#49)
* New translations sglocalizable.strings (Arabic)
* New translations sglocalizable.strings (Arabic)
New translations sglocalizable.strings (Russian) (#51)
Call confirmation
WIP Settings search
Settings Search
Localize placeholder
Update AccountUtils.swift
mark mutual contact
Align back context action to left
New Crowdin updates (#54)
* New translations sglocalizable.strings (Chinese Simplified)
* New translations sglocalizable.strings (Chinese Traditional)
* New translations sglocalizable.strings (Ukrainian)
Independent Playground app for simulator
New translations sglocalizable.strings (Ukrainian) (#55)
Playground UIKit base and controllers
Inject SwiftUI view with overflow to AsyncDisplayKit
Launch Playgound project on simulator
Create .swiftformat
Move Playground to example
Update .swiftformat
Init SwiftUIViewController
wip
New translations sglocalizable.strings (Chinese Traditional) (#57)
Xcode 16 fixes
Fix
New translations sglocalizable.strings (Italian) (#59)
New translations sglocalizable.strings (Chinese Simplified) (#63)
Force disable CallKit integration due to missing NSE Entitlement
Fix merge
Fix whole chat translator
Sweetpad config
Bump version
11.3.1 fixes
Mutual contact placement fix
Disable Video PIP swipe
Update versions.json
Fix PIP crash
979 lines
56 KiB
Swift
979 lines
56 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import Postbox
|
|
import TelegramCore
|
|
import SwiftSignalKit
|
|
import TelegramPresentationData
|
|
import TelegramUIPreferences
|
|
import AccountContext
|
|
import StickerResources
|
|
import PhotoResources
|
|
import TelegramStringFormatting
|
|
import AnimatedCountLabelNode
|
|
import AnimatedNavigationStripeNode
|
|
import ContextUI
|
|
import RadialStatusNode
|
|
import InvisibleInkDustNode
|
|
import TextFormat
|
|
import ChatPresentationInterfaceState
|
|
import TextNodeWithEntities
|
|
import AnimationCache
|
|
import MultiAnimationRenderer
|
|
import TranslateUI
|
|
import ChatControllerInteraction
|
|
|
|
private enum PinnedMessageAnimation {
|
|
case slideToTop
|
|
case slideToBottom
|
|
}
|
|
|
|
private final class ButtonsContainerNode: ASDisplayNode {
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
if let subnodes = self.subnodes {
|
|
for subnode in subnodes {
|
|
if let result = subnode.view.hitTest(self.view.convert(point, to: subnode.view), with: event) {
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
|
|
private let context: AccountContext
|
|
private let tapButton: HighlightTrackingButtonNode
|
|
private let buttonsContainer: ButtonsContainerNode
|
|
private let closeButton: HighlightableButtonNode
|
|
private let listButton: HighlightableButtonNode
|
|
private let activityIndicatorContainer: ASDisplayNode
|
|
private let activityIndicator: RadialStatusNode
|
|
|
|
private let contextContainer: ContextControllerSourceNode
|
|
private let clippingContainer: ASDisplayNode
|
|
private let contentContainer: ASDisplayNode
|
|
private let contentTextContainer: ASDisplayNode
|
|
private let lineNode: AnimatedNavigationStripeNode
|
|
private let titleNode: AnimatedCountLabelNode
|
|
private let textNode: TextNodeWithEntities
|
|
private var spoilerTextNode: TextNodeWithEntities?
|
|
private var dustNode: InvisibleInkDustNode?
|
|
private let actionButton: HighlightableButtonNode
|
|
private let actionButtonTitleNode: ImmediateTextNode
|
|
private let actionButtonBackgroundNode: ASImageNode
|
|
|
|
private let imageNode: TransformImageNode
|
|
private let imageNodeContainer: ASDisplayNode
|
|
|
|
private let separatorNode: ASDisplayNode
|
|
|
|
private var currentLayout: (CGFloat, CGFloat, CGFloat)?
|
|
private var currentMessage: ChatPinnedMessage?
|
|
private var previousMediaReference: AnyMediaReference?
|
|
private var currentTranslateToLanguage: (fromLang: String, toLang: String)?
|
|
private let translationDisposable = MetaDisposable()
|
|
|
|
private var isReplyThread: Bool = false
|
|
|
|
private let fetchDisposable = MetaDisposable()
|
|
|
|
private var statusDisposable: Disposable?
|
|
|
|
private let animationCache: AnimationCache?
|
|
private let animationRenderer: MultiAnimationRenderer?
|
|
|
|
private let queue = Queue()
|
|
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
if let buttonResult = self.buttonsContainer.hitTest(point.offsetBy(dx: -self.buttonsContainer.frame.minX, dy: -self.buttonsContainer.frame.minY), with: event) {
|
|
return buttonResult
|
|
}
|
|
let containerResult = self.contentTextContainer.hitTest(point.offsetBy(dx: -self.contentTextContainer.frame.minX, dy: -self.contentTextContainer.frame.minY), with: event)
|
|
if containerResult?.asyncdisplaykit_node === self.dustNode, self.dustNode?.isRevealed == false {
|
|
return containerResult
|
|
}
|
|
let result = super.hitTest(point, with: event)
|
|
return result
|
|
}
|
|
|
|
init(context: AccountContext, animationCache: AnimationCache?, animationRenderer: MultiAnimationRenderer?) {
|
|
self.context = context
|
|
self.animationCache = animationCache
|
|
self.animationRenderer = animationRenderer
|
|
|
|
self.tapButton = HighlightTrackingButtonNode()
|
|
|
|
self.buttonsContainer = ButtonsContainerNode()
|
|
|
|
self.closeButton = HighlightableButtonNode()
|
|
self.closeButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0)
|
|
self.closeButton.displaysAsynchronously = false
|
|
|
|
self.actionButton = HighlightableButtonNode()
|
|
self.actionButton.isHidden = true
|
|
self.actionButtonTitleNode = ImmediateTextNode()
|
|
self.actionButtonTitleNode.isHidden = true
|
|
self.actionButtonBackgroundNode = ASImageNode()
|
|
self.actionButtonBackgroundNode.isHidden = true
|
|
self.actionButtonBackgroundNode.displaysAsynchronously = false
|
|
|
|
self.listButton = HighlightableButtonNode()
|
|
self.listButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0)
|
|
self.listButton.displaysAsynchronously = false
|
|
|
|
self.activityIndicatorContainer = ASDisplayNode()
|
|
self.activityIndicatorContainer.isUserInteractionEnabled = false
|
|
self.activityIndicator = RadialStatusNode(backgroundNodeColor: .clear)
|
|
self.activityIndicator.isUserInteractionEnabled = false
|
|
self.activityIndicatorContainer.addSubnode(self.activityIndicator)
|
|
self.activityIndicator.alpha = 0.0
|
|
ContainedViewLayoutTransition.immediate.updateSublayerTransformScale(node: self.activityIndicatorContainer, scale: 0.1)
|
|
|
|
self.separatorNode = ASDisplayNode()
|
|
self.separatorNode.isLayerBacked = true
|
|
|
|
self.contextContainer = ContextControllerSourceNode()
|
|
|
|
self.clippingContainer = ASDisplayNode()
|
|
self.clippingContainer.clipsToBounds = true
|
|
|
|
self.contentContainer = ASDisplayNode()
|
|
self.contentTextContainer = ASDisplayNode()
|
|
|
|
self.lineNode = AnimatedNavigationStripeNode()
|
|
|
|
self.titleNode = AnimatedCountLabelNode()
|
|
self.titleNode.isUserInteractionEnabled = false
|
|
self.titleNode.reverseAnimationDirection = true
|
|
|
|
self.textNode = TextNodeWithEntities()
|
|
self.textNode.textNode.displaysAsynchronously = false
|
|
self.textNode.textNode.isUserInteractionEnabled = false
|
|
|
|
self.imageNode = TransformImageNode()
|
|
self.imageNode.contentAnimations = [.subsequentUpdates]
|
|
|
|
self.imageNodeContainer = ASDisplayNode()
|
|
|
|
super.init()
|
|
|
|
self.tapButton.highligthedChanged = { [weak self] highlighted in
|
|
if let strongSelf = self {
|
|
if highlighted {
|
|
strongSelf.titleNode.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.titleNode.alpha = 0.4
|
|
strongSelf.textNode.textNode.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.textNode.textNode.alpha = 0.4
|
|
strongSelf.lineNode.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.lineNode.alpha = 0.4
|
|
} else {
|
|
strongSelf.titleNode.alpha = 1.0
|
|
strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
|
strongSelf.textNode.textNode.alpha = 1.0
|
|
strongSelf.textNode.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
|
strongSelf.lineNode.alpha = 1.0
|
|
strongSelf.lineNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
|
}
|
|
}
|
|
}
|
|
|
|
self.actionButton.highligthedChanged = { [weak self] highlighted in
|
|
if let strongSelf = self {
|
|
if highlighted {
|
|
strongSelf.actionButton.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.actionButton.alpha = 0.4
|
|
} else {
|
|
strongSelf.actionButton.alpha = 1.0
|
|
strongSelf.actionButton.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
|
}
|
|
}
|
|
}
|
|
|
|
self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside])
|
|
self.listButton.addTarget(self, action: #selector(self.listPressed), forControlEvents: [.touchUpInside])
|
|
self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: [.touchUpInside])
|
|
|
|
self.addSubnode(self.contextContainer)
|
|
|
|
self.contextContainer.addSubnode(self.clippingContainer)
|
|
self.clippingContainer.addSubnode(self.contentContainer)
|
|
self.contextContainer.addSubnode(self.lineNode)
|
|
self.contentTextContainer.addSubnode(self.titleNode)
|
|
self.contentTextContainer.addSubnode(self.textNode.textNode)
|
|
self.contentContainer.addSubnode(self.contentTextContainer)
|
|
|
|
self.imageNodeContainer.addSubnode(self.imageNode)
|
|
self.contentContainer.addSubnode(self.imageNodeContainer)
|
|
|
|
self.actionButton.addSubnode(self.actionButtonBackgroundNode)
|
|
self.actionButton.addSubnode(self.actionButtonTitleNode)
|
|
self.buttonsContainer.addSubnode(self.actionButton)
|
|
self.buttonsContainer.addSubnode(self.closeButton)
|
|
self.buttonsContainer.addSubnode(self.listButton)
|
|
self.contextContainer.addSubnode(self.buttonsContainer)
|
|
self.contextContainer.addSubnode(self.activityIndicatorContainer)
|
|
|
|
self.tapButton.addTarget(self, action: #selector(self.tapped), forControlEvents: [.touchUpInside])
|
|
self.contextContainer.addSubnode(self.tapButton)
|
|
|
|
self.addSubnode(self.separatorNode)
|
|
|
|
self.contextContainer.activated = { [weak self] gesture, _ in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if let interfaceInteraction = strongSelf.interfaceInteraction, let _ = strongSelf.currentMessage, !strongSelf.isReplyThread {
|
|
interfaceInteraction.activatePinnedListPreview(strongSelf.contextContainer, gesture)
|
|
}
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
self.fetchDisposable.dispose()
|
|
self.statusDisposable?.dispose()
|
|
self.translationDisposable.dispose()
|
|
}
|
|
|
|
private var theme: PresentationTheme?
|
|
|
|
override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> LayoutResult {
|
|
let panelHeight: CGFloat = 50.0
|
|
var themeUpdated = false
|
|
|
|
self.contextContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))
|
|
|
|
if self.theme !== interfaceState.theme {
|
|
themeUpdated = true
|
|
self.theme = interfaceState.theme
|
|
self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(interfaceState.theme), for: [])
|
|
self.listButton.setImage(PresentationResourcesChat.chatInputPanelPinnedListIconImage(interfaceState.theme), for: [])
|
|
self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor
|
|
|
|
self.actionButtonBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 14.0 * 2.0, color: interfaceState.theme.list.itemCheckColors.fillColor, strokeColor: nil, strokeWidth: nil, backgroundColor: nil)
|
|
}
|
|
|
|
if self.statusDisposable == nil, let interfaceInteraction = self.interfaceInteraction, let statuses = interfaceInteraction.statuses {
|
|
self.statusDisposable = (statuses.loadingMessage
|
|
|> map { status -> Bool in
|
|
return status == .pinnedMessage
|
|
}
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self] isLoading in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut)
|
|
if isLoading {
|
|
if strongSelf.activityIndicator.alpha.isZero {
|
|
transition.updateAlpha(node: strongSelf.activityIndicator, alpha: 1.0)
|
|
transition.updateSublayerTransformScale(node: strongSelf.activityIndicatorContainer, scale: 1.0)
|
|
|
|
transition.updateAlpha(node: strongSelf.buttonsContainer, alpha: 0.0)
|
|
transition.updateSublayerTransformScale(node: strongSelf.buttonsContainer, scale: 0.1)
|
|
|
|
if let theme = strongSelf.theme {
|
|
strongSelf.activityIndicator.transitionToState(.progress(color: theme.chat.inputPanel.panelControlAccentColor, lineWidth: nil, value: nil, cancelEnabled: false, animateRotation: true), animated: false, completion: {
|
|
})
|
|
}
|
|
}
|
|
} else {
|
|
if !strongSelf.activityIndicator.alpha.isZero {
|
|
transition.updateAlpha(node: strongSelf.activityIndicator, alpha: 0.0, completion: { [weak self] completed in
|
|
if completed {
|
|
self?.activityIndicator.transitionToState(.none, animated: false, completion: {
|
|
})
|
|
}
|
|
})
|
|
transition.updateSublayerTransformScale(node: strongSelf.activityIndicatorContainer, scale: 0.1)
|
|
|
|
transition.updateAlpha(node: strongSelf.buttonsContainer, alpha: 1.0)
|
|
transition.updateSublayerTransformScale(node: strongSelf.buttonsContainer, scale: 1.0)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
let isReplyThread: Bool
|
|
if case let .replyThread(message) = interfaceState.chatLocation, !message.isForumPost {
|
|
isReplyThread = true
|
|
} else {
|
|
isReplyThread = false
|
|
}
|
|
self.isReplyThread = isReplyThread
|
|
|
|
self.contextContainer.isGestureEnabled = !isReplyThread
|
|
|
|
var actionTitle: String?
|
|
var messageUpdated = false
|
|
var messageUpdatedAnimation: PinnedMessageAnimation?
|
|
if let currentMessage = self.currentMessage, let pinnedMessage = interfaceState.pinnedMessage {
|
|
if currentMessage != pinnedMessage {
|
|
messageUpdated = true
|
|
}
|
|
if currentMessage.message.id != pinnedMessage.message.id {
|
|
if currentMessage.message.id < pinnedMessage.message.id {
|
|
messageUpdatedAnimation = .slideToTop
|
|
} else {
|
|
messageUpdatedAnimation = .slideToBottom
|
|
}
|
|
}
|
|
} else if (self.currentMessage != nil) != (interfaceState.pinnedMessage != nil) {
|
|
messageUpdated = true
|
|
}
|
|
|
|
var isStarsPayment = false
|
|
if let message = interfaceState.pinnedMessage, !message.message.isRestricted(platform: "ios", contentSettings: self.context.currentContentSettings.with { $0 }) {
|
|
for attribute in message.message.attributes {
|
|
if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), attribute.rows.count == 1, attribute.rows[0].buttons.count == 1 {
|
|
let title = attribute.rows[0].buttons[0].title
|
|
actionTitle = title
|
|
if case .payment = attribute.rows[0].buttons[0].action, title.contains("⭐️") {
|
|
isStarsPayment = true
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
actionTitle = nil
|
|
}
|
|
|
|
var displayCloseButton = false
|
|
var displayListButton = false
|
|
|
|
if isReplyThread || actionTitle != nil {
|
|
displayCloseButton = false
|
|
displayListButton = false
|
|
} else if let message = interfaceState.pinnedMessage {
|
|
if message.totalCount > 1 {
|
|
displayCloseButton = false
|
|
displayListButton = true
|
|
} else {
|
|
displayCloseButton = true
|
|
displayListButton = false
|
|
}
|
|
} else {
|
|
displayCloseButton = false
|
|
displayListButton = true
|
|
}
|
|
|
|
if displayCloseButton != !self.closeButton.isHidden {
|
|
if transition.isAnimated {
|
|
if displayCloseButton {
|
|
self.closeButton.isHidden = false
|
|
self.closeButton.layer.removeAllAnimations()
|
|
|
|
self.closeButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
self.closeButton.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
|
|
} else {
|
|
self.closeButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak self] completed in
|
|
guard let strongSelf = self, completed else {
|
|
return
|
|
}
|
|
strongSelf.closeButton.isHidden = true
|
|
strongSelf.closeButton.layer.removeAllAnimations()
|
|
})
|
|
self.closeButton.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
|
}
|
|
} else {
|
|
self.closeButton.isHidden = !displayCloseButton
|
|
self.closeButton.layer.removeAllAnimations()
|
|
}
|
|
}
|
|
if displayListButton != !self.listButton.isHidden {
|
|
if transition.isAnimated {
|
|
if displayListButton {
|
|
self.listButton.isHidden = false
|
|
self.listButton.layer.removeAllAnimations()
|
|
|
|
self.listButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
self.listButton.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
|
|
} else {
|
|
self.listButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak self] completed in
|
|
guard let strongSelf = self, completed else {
|
|
return
|
|
}
|
|
strongSelf.listButton.isHidden = true
|
|
strongSelf.listButton.layer.removeAllAnimations()
|
|
})
|
|
self.listButton.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
|
}
|
|
} else {
|
|
self.listButton.isHidden = !displayListButton
|
|
self.listButton.layer.removeAllAnimations()
|
|
}
|
|
}
|
|
|
|
let rightInset: CGFloat = 18.0 + rightInset
|
|
|
|
var tapButtonRightInset: CGFloat = rightInset
|
|
|
|
let buttonsContainerSize = CGSize(width: 16.0, height: panelHeight)
|
|
self.buttonsContainer.frame = CGRect(origin: CGPoint(x: width - buttonsContainerSize.width - rightInset, y: 0.0), size: buttonsContainerSize)
|
|
|
|
let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0))
|
|
|
|
if let actionTitle = actionTitle {
|
|
var actionButtonTransition = transition
|
|
var animateButtonIn = false
|
|
if self.actionButton.isHidden {
|
|
actionButtonTransition = .immediate
|
|
animateButtonIn = true
|
|
} else if transition.isAnimated, messageUpdated, actionTitle != self.actionButtonTitleNode.attributedText?.string {
|
|
if let buttonSnapshot = self.actionButton.view.snapshotView(afterScreenUpdates: false) {
|
|
animateButtonIn = true
|
|
buttonSnapshot.frame = self.actionButton.frame
|
|
self.actionButton.view.superview?.insertSubview(buttonSnapshot, belowSubview: self.actionButton.view)
|
|
|
|
buttonSnapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak buttonSnapshot] _ in
|
|
buttonSnapshot?.removeFromSuperview()
|
|
})
|
|
buttonSnapshot.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2)
|
|
}
|
|
}
|
|
|
|
self.actionButton.isHidden = false
|
|
self.actionButtonBackgroundNode.isHidden = false
|
|
self.actionButtonTitleNode.isHidden = false
|
|
|
|
let attributedTitle: NSAttributedString
|
|
if isStarsPayment {
|
|
let updatedTitle = actionTitle.replacingOccurrences(of: "⭐️", with: " # ")
|
|
let buttonAttributedString = NSMutableAttributedString(string: updatedTitle, font: Font.with(size: 15.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: interfaceState.theme.list.itemCheckColors.foregroundColor)
|
|
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: interfaceState.theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string))
|
|
buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string))
|
|
}
|
|
attributedTitle = buttonAttributedString
|
|
} else {
|
|
attributedTitle = NSAttributedString(string: actionTitle, font: Font.with(size: 15.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: interfaceState.theme.list.itemCheckColors.foregroundColor)
|
|
}
|
|
self.actionButtonTitleNode.attributedText = attributedTitle
|
|
|
|
let actionButtonTitleSize = self.actionButtonTitleNode.updateLayout(CGSize(width: 150.0, height: .greatestFiniteMagnitude))
|
|
let actionButtonSize = CGSize(width: max(actionButtonTitleSize.width + 20.0, 40.0), height: 28.0)
|
|
let actionButtonFrame = CGRect(origin: CGPoint(x: buttonsContainerSize.width + 11.0 - actionButtonSize.width, y: floor((panelHeight - actionButtonSize.height) / 2.0)), size: actionButtonSize)
|
|
actionButtonTransition.updateFrame(node: self.actionButton, frame: actionButtonFrame)
|
|
actionButtonTransition.updateFrame(node: self.actionButtonBackgroundNode, frame: CGRect(origin: CGPoint(), size: actionButtonFrame.size))
|
|
actionButtonTransition.updateFrame(node: self.actionButtonTitleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((actionButtonFrame.width - actionButtonTitleSize.width) / 2.0), y: floorToScreenPixels((actionButtonFrame.height - actionButtonTitleSize.height) / 2.0)), size: actionButtonTitleSize))
|
|
|
|
tapButtonRightInset = 18.0 + actionButtonFrame.width
|
|
|
|
if animateButtonIn {
|
|
self.actionButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
self.actionButton.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
|
|
}
|
|
} else if !self.actionButton.isHidden {
|
|
self.actionButton.isHidden = true
|
|
self.actionButtonBackgroundNode.isHidden = true
|
|
self.actionButtonTitleNode.isHidden = true
|
|
|
|
if transition.isAnimated {
|
|
if let buttonSnapshot = self.actionButton.view.snapshotView(afterScreenUpdates: false) {
|
|
buttonSnapshot.frame = self.actionButton.frame
|
|
self.actionButton.view.superview?.insertSubview(buttonSnapshot, belowSubview: self.actionButton.view)
|
|
|
|
buttonSnapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak buttonSnapshot] _ in
|
|
buttonSnapshot?.removeFromSuperview()
|
|
})
|
|
buttonSnapshot.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2)
|
|
}
|
|
}
|
|
}
|
|
|
|
transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: buttonsContainerSize.width - closeButtonSize.width + 1.0, y: 19.0), size: closeButtonSize))
|
|
|
|
let listButtonSize = self.listButton.measure(CGSize(width: 100.0, height: 100.0))
|
|
transition.updateFrame(node: self.listButton, frame: CGRect(origin: CGPoint(x: buttonsContainerSize.width - listButtonSize.width + 4.0, y: 13.0), size: listButtonSize))
|
|
|
|
let indicatorSize = CGSize(width: 22.0, height: 22.0)
|
|
transition.updateFrame(node: self.activityIndicatorContainer, frame: CGRect(origin: CGPoint(x: width - rightInset - indicatorSize.width + 5.0, y: 15.0), size: indicatorSize))
|
|
transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(), size: indicatorSize))
|
|
|
|
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel)))
|
|
self.tapButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: width - tapButtonRightInset, height: panelHeight))
|
|
|
|
self.clippingContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))
|
|
self.contentContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))
|
|
|
|
var translateToLanguage: (fromLang: String, toLang: String)?
|
|
if let translationState = interfaceState.translationState, translationState.isEnabled {
|
|
translateToLanguage = (normalizeTranslationLanguage(translationState.fromLang), normalizeTranslationLanguage(translationState.toLang))
|
|
}
|
|
|
|
var currentTranslateToLanguageUpdated = false
|
|
if self.currentTranslateToLanguage?.fromLang != translateToLanguage?.fromLang || self.currentTranslateToLanguage?.toLang != translateToLanguage?.toLang {
|
|
self.currentTranslateToLanguage = translateToLanguage
|
|
currentTranslateToLanguageUpdated = true
|
|
}
|
|
|
|
if currentTranslateToLanguageUpdated || messageUpdated, let message = interfaceState.pinnedMessage?.message {
|
|
if let translation = message.attributes.first(where: { $0 is TranslationMessageAttribute }) as? TranslationMessageAttribute, translation.toLang == translateToLanguage?.toLang || translation.toLang.hasPrefix("\(translateToLanguage?.toLang ?? "")-") /* MARK: Swiftgram */ {
|
|
} else if let translateToLanguage {
|
|
self.translationDisposable.set(translateMessageIds(context: self.context, messageIds: [message.id], fromLang: translateToLanguage.fromLang, toLang: translateToLanguage.toLang, viaText: !self.context.isPremium).startStrict())
|
|
}
|
|
}
|
|
|
|
if self.currentLayout?.0 != width || self.currentLayout?.1 != leftInset || self.currentLayout?.2 != rightInset || messageUpdated || themeUpdated || currentTranslateToLanguageUpdated {
|
|
self.currentLayout = (width, leftInset, rightInset)
|
|
|
|
let previousMessageWasNil = self.currentMessage == nil
|
|
self.currentMessage = interfaceState.pinnedMessage
|
|
|
|
if let currentMessage = self.currentMessage, let currentLayout = self.currentLayout {
|
|
self.dustNode?.update(revealed: false, animated: false)
|
|
self.enqueueTransition(width: currentLayout.0, panelHeight: panelHeight, leftInset: currentLayout.1, rightInset: currentLayout.2, transition: .immediate, animation: messageUpdatedAnimation, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, dateTimeFormat: interfaceState.dateTimeFormat, accountPeerId: self.context.account.peerId, firstTime: previousMessageWasNil, isReplyThread: isReplyThread, translateToLanguage: translateToLanguage?.toLang)
|
|
}
|
|
}
|
|
|
|
self.currentLayout = (width, leftInset, rightInset)
|
|
|
|
/*if self.currentLayout?.0 != width || self.currentLayout?.1 != leftInset || self.currentLayout?.2 != rightInset || messageUpdated {
|
|
self.currentLayout = (width, leftInset, rightInset)
|
|
|
|
if let currentMessage = self.currentMessage {
|
|
self.enqueueTransition(width: width, panelHeight: panelHeight, leftInset: leftInset, rightInset: rightInset, transition: .immediate, animation: .none, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, dateTimeFormat: interfaceState.dateTimeFormat, accountPeerId: interfaceState.accountPeerId, firstTime: true, isReplyThread: isReplyThread)
|
|
}
|
|
}*/
|
|
|
|
return LayoutResult(backgroundHeight: panelHeight, insetHeight: panelHeight, hitTestSlop: 0.0)
|
|
}
|
|
|
|
private func enqueueTransition(width: CGFloat, panelHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, animation: PinnedMessageAnimation?, pinnedMessage: ChatPinnedMessage, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: PeerId, firstTime: Bool, isReplyThread: Bool, translateToLanguage: String?) {
|
|
let message = pinnedMessage.message
|
|
|
|
var animationTransition: ContainedViewLayoutTransition = .immediate
|
|
|
|
if let animation = animation {
|
|
animationTransition = .animated(duration: 0.2, curve: .easeInOut)
|
|
|
|
if let copyView = self.textNode.textNode.view.snapshotView(afterScreenUpdates: false) {
|
|
let offset: CGFloat
|
|
switch animation {
|
|
case .slideToTop:
|
|
offset = -10.0
|
|
case .slideToBottom:
|
|
offset = 10.0
|
|
}
|
|
|
|
copyView.frame = self.textNode.textNode.frame
|
|
self.textNode.textNode.view.superview?.addSubview(copyView)
|
|
copyView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: offset), duration: 0.2, removeOnCompletion: false, additive: true)
|
|
copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak copyView] _ in
|
|
copyView?.removeFromSuperview()
|
|
})
|
|
self.textNode.textNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -offset), to: CGPoint(), duration: 0.2, additive: true)
|
|
self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
}
|
|
|
|
let makeTitleLayout = self.titleNode.asyncLayout()
|
|
let makeTextLayout = TextNodeWithEntities.asyncLayout(self.textNode)
|
|
let makeSpoilerTextLayout = TextNodeWithEntities.asyncLayout(self.spoilerTextNode)
|
|
let imageNodeLayout = self.imageNode.asyncLayout()
|
|
|
|
let previousMediaReference = self.previousMediaReference
|
|
let context = self.context
|
|
|
|
let targetQueue: Queue
|
|
if firstTime {
|
|
targetQueue = Queue.mainQueue()
|
|
} else {
|
|
targetQueue = self.queue
|
|
}
|
|
|
|
let contentLeftInset: CGFloat = leftInset + 10.0
|
|
var textLineInset: CGFloat = 10.0
|
|
var rightInset: CGFloat = 14.0 + rightInset
|
|
|
|
let textRightInset: CGFloat = 0.0
|
|
|
|
if !self.actionButton.isHidden {
|
|
rightInset += self.actionButton.bounds.width - 14.0
|
|
}
|
|
|
|
targetQueue.async { [weak self] in
|
|
var updatedMediaReference: AnyMediaReference?
|
|
var imageDimensions: CGSize?
|
|
|
|
let giveaway = pinnedMessage.message.media.first(where: { $0 is TelegramMediaGiveaway }) as? TelegramMediaGiveaway
|
|
|
|
var titleStrings: [AnimatedCountLabelNode.Segment] = []
|
|
if let _ = giveaway {
|
|
titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedGiveaway) ", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor)))
|
|
} else {
|
|
if pinnedMessage.totalCount == 2 {
|
|
if pinnedMessage.index == 0 {
|
|
titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedPreviousMessage) ", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor)))
|
|
} else {
|
|
titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedMessage) ", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor)))
|
|
}
|
|
} else if pinnedMessage.totalCount > 1 && pinnedMessage.index != pinnedMessage.totalCount - 1 {
|
|
titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedMessage)", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor)))
|
|
titleStrings.append(.text(1, NSAttributedString(string: " #", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor)))
|
|
titleStrings.append(.number(pinnedMessage.index + 1, NSAttributedString(string: "\(pinnedMessage.index + 1)", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor)))
|
|
} else {
|
|
titleStrings.append(.text(0, NSAttributedString(string: "\(strings.Conversation_PinnedMessage) ", font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor)))
|
|
}
|
|
}
|
|
|
|
if !message.containsSecretMedia {
|
|
for media in message.media {
|
|
if let image = media as? TelegramMediaImage {
|
|
updatedMediaReference = .message(message: MessageReference(message), media: image)
|
|
if let representation = largestRepresentationForPhoto(image) {
|
|
imageDimensions = representation.dimensions.cgSize
|
|
}
|
|
break
|
|
} else if let file = media as? TelegramMediaFile {
|
|
updatedMediaReference = .message(message: MessageReference(message), media: file)
|
|
if !file.isInstantVideo && !file.isSticker, let representation = largestImageRepresentation(file.previewRepresentations) {
|
|
imageDimensions = representation.dimensions.cgSize
|
|
} else if file.isAnimated, let dimensions = file.dimensions {
|
|
imageDimensions = dimensions.cgSize
|
|
}
|
|
break
|
|
} else if let paidContent = media as? TelegramMediaPaidContent, let firstMedia = paidContent.extendedMedia.first {
|
|
switch firstMedia {
|
|
case let .preview(dimensions, immediateThumbnailData, _):
|
|
let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
|
|
if let dimensions {
|
|
imageDimensions = dimensions.cgSize
|
|
}
|
|
updatedMediaReference = .standalone(media: thumbnailMedia)
|
|
case let .full(fullMedia):
|
|
updatedMediaReference = .message(message: MessageReference(message), media: fullMedia)
|
|
if let image = fullMedia as? TelegramMediaImage {
|
|
if let representation = largestRepresentationForPhoto(image) {
|
|
imageDimensions = representation.dimensions.cgSize
|
|
}
|
|
break
|
|
} else if let file = fullMedia as? TelegramMediaFile {
|
|
if let dimensions = file.dimensions {
|
|
imageDimensions = dimensions.cgSize
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if isReplyThread {
|
|
let titleString: String
|
|
if let author = message.effectiveAuthor {
|
|
titleString = EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder)
|
|
} else {
|
|
titleString = ""
|
|
}
|
|
titleStrings = [.text(0, NSAttributedString(string: titleString, font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))]
|
|
} else {
|
|
for media in message.media {
|
|
if let media = media as? TelegramMediaInvoice {
|
|
titleStrings = [.text(0, NSAttributedString(string: media.title, font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor))]
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
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()))
|
|
|
|
textLineInset += 9.0 + 35.0
|
|
}
|
|
|
|
var mediaUpdated = false
|
|
if let updatedMediaReference = updatedMediaReference, let previousMediaReference = previousMediaReference {
|
|
mediaUpdated = !updatedMediaReference.media.isEqual(to: previousMediaReference.media)
|
|
} else if (updatedMediaReference != nil) != (previousMediaReference != nil) {
|
|
mediaUpdated = true
|
|
}
|
|
|
|
let hasSpoiler = message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute })
|
|
|
|
var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
|
|
var updatedFetchMediaSignal: Signal<FetchResourceSourceType, FetchResourceError>?
|
|
if mediaUpdated {
|
|
if let updatedMediaReference = updatedMediaReference, imageDimensions != nil {
|
|
if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) {
|
|
if imageReference.media.representations.isEmpty {
|
|
updateImageSignal = chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, ignoreFullSize: true, synchronousLoad: true)
|
|
} else {
|
|
updateImageSignal = chatMessagePhotoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, blurred: hasSpoiler)
|
|
}
|
|
} else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) {
|
|
if fileReference.media.isAnimatedSticker {
|
|
let dimensions = fileReference.media.dimensions ?? PixelDimensions(width: 512, height: 512)
|
|
updateImageSignal = chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), file: fileReference.media, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)))
|
|
updatedFetchMediaSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(message.id.peerId), userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(fileReference.media.resource))
|
|
} else if fileReference.media.isVideo || fileReference.media.isAnimated {
|
|
updateImageSignal = chatMessageVideoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), fileReference: fileReference, blurred: hasSpoiler)
|
|
} else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) {
|
|
updateImageSignal = chatWebpageSnippetFile(account: context.account, userLocation: .peer(message.id.peerId), mediaReference: fileReference.abstract, representation: iconImageRepresentation)
|
|
}
|
|
}
|
|
} else {
|
|
updateImageSignal = .single({ _ in return nil })
|
|
}
|
|
}
|
|
let (titleLayout, titleApply) = makeTitleLayout(CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), titleStrings)
|
|
|
|
let (textString, _, isText) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId)
|
|
|
|
let messageText: NSAttributedString
|
|
let textFont = Font.regular(15.0)
|
|
if let giveaway {
|
|
let dateString = stringForDateWithoutYear(date: Date(timeIntervalSince1970: TimeInterval(giveaway.untilDate)), timeZone: .current, strings: strings)
|
|
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
|
let isFinished = currentTime >= giveaway.untilDate
|
|
let text: String
|
|
if isFinished {
|
|
let winnersString = strings.Conversation_PinnedGiveaway_Finished_Winners(giveaway.quantity)
|
|
text = strings.Conversation_PinnedGiveaway_Finished(winnersString, dateString).string
|
|
} else {
|
|
let winnersString = strings.Conversation_PinnedGiveaway_Ongoing_Winners(giveaway.quantity)
|
|
text = strings.Conversation_PinnedGiveaway_Ongoing(winnersString, dateString).string
|
|
}
|
|
messageText = NSAttributedString(string: text, font: textFont, textColor: theme.chat.inputPanel.primaryTextColor)
|
|
} else if isText {
|
|
var text = message.text
|
|
var messageEntities = message.textEntitiesAttribute?.entities ?? []
|
|
|
|
if let translateToLanguage = translateToLanguage, !text.isEmpty {
|
|
for attribute in message.attributes {
|
|
if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage {
|
|
text = attribute.text
|
|
messageEntities = attribute.entities
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
let entities = messageEntities.filter { entity in
|
|
switch entity.type {
|
|
case .Spoiler, .CustomEmoji:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
let textColor = theme.chat.inputPanel.primaryTextColor
|
|
if entities.count > 0 {
|
|
messageText = stringWithAppliedEntities(trimToLineCount(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)
|
|
} else {
|
|
messageText = NSAttributedString(string: foldLineBreaks(text), font: textFont, textColor: textColor)
|
|
}
|
|
} else {
|
|
messageText = NSAttributedString(string: foldLineBreaks(textString.string), font: textFont, textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor)
|
|
}
|
|
|
|
let textConstrainedSize = CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude)
|
|
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0)))
|
|
|
|
let spoilerTextLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)?
|
|
if !textLayout.spoilers.isEmpty {
|
|
spoilerTextLayoutAndApply = makeSpoilerTextLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0), displaySpoilers: true, displayEmbeddedItemsUnderSpoilers: true))
|
|
} else {
|
|
spoilerTextLayoutAndApply = nil
|
|
}
|
|
|
|
Queue.mainQueue().async {
|
|
if let strongSelf = self {
|
|
let _ = titleApply(animation != nil)
|
|
|
|
var textArguments: TextNodeWithEntities.Arguments?
|
|
if let cache = strongSelf.animationCache, let renderer = strongSelf.animationRenderer {
|
|
textArguments = TextNodeWithEntities.Arguments(
|
|
context: strongSelf.context,
|
|
cache: cache,
|
|
renderer: renderer,
|
|
placeholderColor: theme.list.mediaPlaceholderColor,
|
|
attemptSynchronous: false
|
|
)
|
|
}
|
|
let _ = textApply(textArguments)
|
|
|
|
strongSelf.previousMediaReference = updatedMediaReference
|
|
|
|
animationTransition.updateFrameAdditive(node: strongSelf.contentTextContainer, frame: CGRect(origin: CGPoint(x: contentLeftInset + textLineInset, y: 0.0), size: CGSize(width: width, height: panelHeight)))
|
|
|
|
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 5.0), size: titleLayout.size)
|
|
|
|
let textFrame = CGRect(origin: CGPoint(x: 0.0, y: 23.0), size: textLayout.size)
|
|
strongSelf.textNode.textNode.frame = textFrame
|
|
|
|
if let (_, spoilerTextApply) = spoilerTextLayoutAndApply {
|
|
let spoilerTextNode = spoilerTextApply(textArguments)
|
|
if strongSelf.spoilerTextNode == nil {
|
|
spoilerTextNode.textNode.alpha = 0.0
|
|
spoilerTextNode.textNode.isUserInteractionEnabled = false
|
|
spoilerTextNode.textNode.contentMode = .topLeft
|
|
spoilerTextNode.textNode.contentsScale = UIScreenScale
|
|
spoilerTextNode.textNode.displaysAsynchronously = false
|
|
strongSelf.contentTextContainer.insertSubnode(spoilerTextNode.textNode, aboveSubnode: strongSelf.textNode.textNode)
|
|
|
|
strongSelf.spoilerTextNode = spoilerTextNode
|
|
}
|
|
|
|
strongSelf.spoilerTextNode?.textNode.frame = textFrame
|
|
|
|
let dustNode: InvisibleInkDustNode
|
|
if let current = strongSelf.dustNode {
|
|
dustNode = current
|
|
} else {
|
|
dustNode = InvisibleInkDustNode(textNode: spoilerTextNode.textNode, enableAnimations: strongSelf.context.sharedContext.energyUsageSettings.fullTranslucency)
|
|
strongSelf.dustNode = dustNode
|
|
strongSelf.contentTextContainer.insertSubnode(dustNode, aboveSubnode: spoilerTextNode.textNode)
|
|
}
|
|
dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0)
|
|
dustNode.update(size: dustNode.frame.size, color: theme.chat.inputPanel.secondaryTextColor, textColor: theme.chat.inputPanel.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) })
|
|
} else if let spoilerTextNode = strongSelf.spoilerTextNode {
|
|
strongSelf.spoilerTextNode = nil
|
|
spoilerTextNode.textNode.removeFromSupernode()
|
|
|
|
if let dustNode = strongSelf.dustNode {
|
|
strongSelf.dustNode = nil
|
|
dustNode.removeFromSupernode()
|
|
}
|
|
}
|
|
|
|
strongSelf.textNode.visibilityRect = CGRect.infinite
|
|
strongSelf.spoilerTextNode?.visibilityRect = CGRect.infinite
|
|
|
|
let lineFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: 0.0), size: CGSize(width: 2.0, height: panelHeight))
|
|
animationTransition.updateFrame(node: strongSelf.lineNode, frame: lineFrame)
|
|
strongSelf.lineNode.update(
|
|
colors: AnimatedNavigationStripeNode.Colors(
|
|
foreground: theme.chat.inputPanel.panelControlAccentColor,
|
|
background: theme.chat.inputPanel.panelControlAccentColor.withAlphaComponent(0.5),
|
|
clearBackground: theme.chat.inputPanel.panelBackgroundColor
|
|
),
|
|
configuration: AnimatedNavigationStripeNode.Configuration(
|
|
height: panelHeight,
|
|
index: pinnedMessage.index,
|
|
count: pinnedMessage.totalCount
|
|
),
|
|
transition: animationTransition
|
|
)
|
|
|
|
strongSelf.imageNodeContainer.frame = CGRect(origin: CGPoint(x: contentLeftInset + 9.0, y: 7.0), size: CGSize(width: 35.0, height: 35.0))
|
|
strongSelf.imageNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 35.0, height: 35.0))
|
|
|
|
if let applyImage = applyImage {
|
|
applyImage()
|
|
|
|
animationTransition.updateSublayerTransformScale(node: strongSelf.imageNodeContainer, scale: 1.0)
|
|
animationTransition.updateAlpha(node: strongSelf.imageNodeContainer, alpha: 1.0, beginWithCurrentState: true)
|
|
} else {
|
|
animationTransition.updateSublayerTransformScale(node: strongSelf.imageNodeContainer, scale: 0.1)
|
|
animationTransition.updateAlpha(node: strongSelf.imageNodeContainer, alpha: 0.0, beginWithCurrentState: true)
|
|
}
|
|
|
|
if let updateImageSignal = updateImageSignal {
|
|
strongSelf.imageNode.setSignal(updateImageSignal)
|
|
}
|
|
if let updatedFetchMediaSignal = updatedFetchMediaSignal {
|
|
strongSelf.fetchDisposable.set(updatedFetchMediaSignal.startStrict())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func tapped() {
|
|
if let interfaceInteraction = self.interfaceInteraction, let message = self.currentMessage {
|
|
if self.isReplyThread {
|
|
interfaceInteraction.scrollToTop()
|
|
} else {
|
|
interfaceInteraction.navigateToMessage(message.message.id, false, true, .pinnedMessage)
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func closePressed() {
|
|
if let interfaceInteraction = self.interfaceInteraction, let message = self.currentMessage {
|
|
interfaceInteraction.unpinMessage(message.message.id, true, nil)
|
|
}
|
|
}
|
|
|
|
@objc func listPressed() {
|
|
if let interfaceInteraction = self.interfaceInteraction, let message = self.currentMessage {
|
|
interfaceInteraction.openPinnedList(message.message.id)
|
|
}
|
|
}
|
|
|
|
@objc private func actionButtonPressed() {
|
|
if let interfaceInteraction = self.interfaceInteraction, let controller = interfaceInteraction.chatController() as? ChatControllerImpl, let controllerInteraction = controller.controllerInteraction, let message = self.currentMessage?.message {
|
|
for attribute in message.attributes {
|
|
if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), attribute.rows.count == 1, attribute.rows[0].buttons.count == 1 {
|
|
let button = attribute.rows[0].buttons[0]
|
|
switch button.action {
|
|
case .text:
|
|
controllerInteraction.sendMessage(button.title)
|
|
case let .url(url):
|
|
var isConcealed = true
|
|
if url.hasPrefix("tg://") {
|
|
isConcealed = false
|
|
}
|
|
controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: isConcealed, progress: Promise()))
|
|
case .requestMap:
|
|
controllerInteraction.shareCurrentLocation()
|
|
case .requestPhone:
|
|
controllerInteraction.shareAccountContact()
|
|
case .openWebApp:
|
|
controllerInteraction.requestMessageActionCallback(message.id, nil, true, false)
|
|
case let .callback(requiresPassword, data):
|
|
controllerInteraction.requestMessageActionCallback(message.id, data, false, requiresPassword)
|
|
case let .switchInline(samePeer, query, peerTypes):
|
|
var botPeer: Peer?
|
|
|
|
var found = false
|
|
for attribute in message.attributes {
|
|
if let attribute = attribute as? InlineBotMessageAttribute {
|
|
if let peerId = attribute.peerId {
|
|
botPeer = message.peers[peerId]
|
|
found = true
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
botPeer = message.author
|
|
}
|
|
|
|
var peerId: PeerId?
|
|
if samePeer {
|
|
peerId = message.id.peerId
|
|
}
|
|
if let botPeer = botPeer, let addressName = botPeer.addressName {
|
|
controllerInteraction.activateSwitchInline(peerId, "@\(addressName) \(query)", peerTypes)
|
|
}
|
|
case .payment:
|
|
controllerInteraction.openCheckoutOrReceipt(message.id, nil)
|
|
case let .urlAuth(url, buttonId):
|
|
controllerInteraction.requestMessageActionUrlAuth(url, .message(id: message.id, buttonId: buttonId))
|
|
case .setupPoll:
|
|
break
|
|
case let .openUserProfile(peerId):
|
|
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|
|
|> deliverOnMainQueue).startStandalone(next: { peer in
|
|
if let peer = peer {
|
|
controllerInteraction.openPeer(peer, .info(nil), nil, .default)
|
|
}
|
|
})
|
|
case let .openWebView(url, simple):
|
|
controllerInteraction.openWebView(button.title, url, simple, .generic)
|
|
case .requestPeer:
|
|
break
|
|
case let .copyText(payload):
|
|
controllerInteraction.copyText(payload)
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|