mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
514 lines
22 KiB
Swift
514 lines
22 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import ContextUI
|
|
import Postbox
|
|
import TelegramCore
|
|
import SwiftSignalKit
|
|
import ChatMessageItemView
|
|
import AccountContext
|
|
import WallpaperBackgroundNode
|
|
import TelegramPresentationData
|
|
import DustEffect
|
|
import TooltipUI
|
|
import TelegramNotices
|
|
|
|
final class ChatMessageContextLocationContentSource: ContextLocationContentSource {
|
|
private let controller: ViewController
|
|
private let location: CGPoint
|
|
|
|
init(controller: ViewController, location: CGPoint) {
|
|
self.controller = controller
|
|
self.location = location
|
|
}
|
|
|
|
func transitionInfo() -> ContextControllerLocationViewInfo? {
|
|
return ContextControllerLocationViewInfo(location: self.location, contentAreaInScreenSpace: UIScreen.main.bounds)
|
|
}
|
|
}
|
|
|
|
final class ChatMessageContextExtractedContentSource: ContextExtractedContentSource {
|
|
let keepInPlace: Bool = false
|
|
let ignoreContentTouches: Bool = false
|
|
let blurBackground: Bool = true
|
|
let centerVertically: Bool
|
|
|
|
private weak var chatController: ChatControllerImpl?
|
|
private weak var chatNode: ChatControllerNode?
|
|
private let engine: TelegramEngine
|
|
private let message: Message
|
|
private let selectAll: Bool
|
|
|
|
var shouldBeDismissed: Signal<Bool, NoError> {
|
|
if self.message.adAttribute != nil {
|
|
return .single(false)
|
|
}
|
|
if let chatController = self.chatController, case .customChatContents = chatController.subject {
|
|
return .single(false)
|
|
}
|
|
|
|
return self.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.Message(id: self.message.id))
|
|
|> map { message -> Bool in
|
|
if let _ = message {
|
|
return false
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
|> distinctUntilChanged
|
|
}
|
|
|
|
init(chatController: ChatControllerImpl, chatNode: ChatControllerNode, engine: TelegramEngine, message: Message, selectAll: Bool, centerVertically: Bool = false) {
|
|
self.chatController = chatController
|
|
self.chatNode = chatNode
|
|
self.engine = engine
|
|
self.message = message
|
|
self.selectAll = selectAll
|
|
self.centerVertically = centerVertically
|
|
}
|
|
|
|
func takeView() -> ContextControllerTakeViewInfo? {
|
|
guard let chatNode = self.chatNode else {
|
|
return nil
|
|
}
|
|
|
|
var result: ContextControllerTakeViewInfo?
|
|
chatNode.historyNode.forEachItemNode { itemNode in
|
|
guard let itemNode = itemNode as? ChatMessageItemView else {
|
|
return
|
|
}
|
|
guard let item = itemNode.item else {
|
|
return
|
|
}
|
|
if item.content.contains(where: { $0.0.stableId == self.message.stableId }), let contentNode = itemNode.getMessageContextSourceNode(stableId: self.selectAll ? nil : self.message.stableId) {
|
|
result = ContextControllerTakeViewInfo(containingItem: .node(contentNode), contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil))
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func putBack() -> ContextControllerPutBackViewInfo? {
|
|
guard let chatNode = self.chatNode else {
|
|
return nil
|
|
}
|
|
|
|
var result: ContextControllerPutBackViewInfo?
|
|
chatNode.historyNode.forEachItemNode { itemNode in
|
|
guard let itemNode = itemNode as? ChatMessageItemView else {
|
|
return
|
|
}
|
|
guard let item = itemNode.item else {
|
|
return
|
|
}
|
|
if item.content.contains(where: { $0.0.stableId == self.message.stableId }) {
|
|
result = ContextControllerPutBackViewInfo(contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil))
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
|
|
final class ChatViewOnceMessageContextExtractedContentSource: ContextExtractedContentSource {
|
|
let keepInPlace: Bool = false
|
|
let ignoreContentTouches: Bool = false
|
|
let blurBackground: Bool = true
|
|
let centerVertically: Bool = true
|
|
|
|
var initialAppearanceOffset: CGPoint = .zero
|
|
|
|
private let context: AccountContext
|
|
private let presentationData: PresentationData
|
|
private weak var chatNode: ChatControllerNode?
|
|
private weak var backgroundNode: WallpaperBackgroundNode?
|
|
private let engine: TelegramEngine
|
|
private let message: Message
|
|
private let present: (ViewController) -> Void
|
|
|
|
private var messageNodeCopy: ChatMessageItemView?
|
|
private weak var tooltipController: TooltipScreen?
|
|
|
|
private let idleTimerExtensionDisposable = MetaDisposable()
|
|
|
|
var shouldBeDismissed: Signal<Bool, NoError> {
|
|
return self.context.sharedContext.mediaManager.globalMediaPlayerState
|
|
|> filter { playlistStateAndType in
|
|
if let (_, state, _) = playlistStateAndType, case .state = state {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|> take(1)
|
|
|> map { _ in
|
|
return false
|
|
}
|
|
|> then(
|
|
self.context.sharedContext.mediaManager.globalMediaPlayerState
|
|
|> filter { playlistStateAndType in
|
|
return playlistStateAndType == nil
|
|
}
|
|
|> take(1)
|
|
|> map { _ in
|
|
return true
|
|
}
|
|
)
|
|
}
|
|
|
|
init(context: AccountContext, presentationData: PresentationData, chatNode: ChatControllerNode, backgroundNode: WallpaperBackgroundNode, engine: TelegramEngine, message: Message, present: @escaping (ViewController) -> Void) {
|
|
self.context = context
|
|
self.presentationData = presentationData
|
|
self.chatNode = chatNode
|
|
self.backgroundNode = backgroundNode
|
|
self.engine = engine
|
|
self.message = message
|
|
self.present = present
|
|
}
|
|
|
|
deinit {
|
|
self.idleTimerExtensionDisposable.dispose()
|
|
}
|
|
|
|
func takeView() -> ContextControllerTakeViewInfo? {
|
|
guard let chatNode = self.chatNode, let backgroundNode = self.backgroundNode, let validLayout = chatNode.validLayout?.0 else {
|
|
return nil
|
|
}
|
|
|
|
let context = self.context
|
|
self.idleTimerExtensionDisposable.set(self.context.sharedContext.applicationBindings.pushIdleTimerExtension())
|
|
|
|
let isIncoming = self.message.effectivelyIncoming(self.context.account.peerId)
|
|
let isVideo = (self.message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile)?.isInstantVideo ?? false
|
|
|
|
var result: ContextControllerTakeViewInfo?
|
|
var sourceNode: ContextExtractedContentContainingNode?
|
|
var sourceRect: CGRect = .zero
|
|
chatNode.historyNode.forEachItemNode { itemNode in
|
|
guard let itemNode = itemNode as? ChatMessageItemView else {
|
|
return
|
|
}
|
|
guard let item = itemNode.item else {
|
|
return
|
|
}
|
|
if item.content.contains(where: { $0.0.stableId == self.message.stableId }), let contentNode = itemNode.getMessageContextSourceNode(stableId: self.message.stableId) {
|
|
sourceNode = contentNode
|
|
sourceRect = contentNode.view.convert(contentNode.bounds, to: chatNode.view)
|
|
if !isVideo {
|
|
sourceRect.origin.y -= 2.0 + UIScreenPixel
|
|
}
|
|
}
|
|
}
|
|
|
|
var tooltipSourceRect: CGRect = .zero
|
|
|
|
if let sourceNode {
|
|
let videoWidth = min(404.0, chatNode.frame.width - 2.0)
|
|
var bubbleWidth: CGFloat = 0.0
|
|
|
|
if (isIncoming || "".isEmpty) {
|
|
let messageItem = self.context.sharedContext.makeChatMessagePreviewItem(
|
|
context: self.context,
|
|
messages: [self.message],
|
|
theme: self.presentationData.theme,
|
|
strings: self.presentationData.strings,
|
|
wallpaper: self.presentationData.chatWallpaper,
|
|
fontSize: self.presentationData.chatFontSize,
|
|
chatBubbleCorners: self.presentationData.chatBubbleCorners,
|
|
dateTimeFormat: self.presentationData.dateTimeFormat,
|
|
nameOrder: self.presentationData.nameDisplayOrder,
|
|
forcedResourceStatus: nil,
|
|
tapMessage: nil,
|
|
clickThroughMessage: nil,
|
|
backgroundNode: backgroundNode,
|
|
availableReactions: nil,
|
|
accountPeer: nil,
|
|
isCentered: false,
|
|
isPreview: false,
|
|
isStandalone: true
|
|
)
|
|
|
|
let width = chatNode.historyNode.frame.width
|
|
let params = ListViewItemLayoutParams(width: width, leftInset: validLayout.safeInsets.left, rightInset: validLayout.safeInsets.right, availableHeight: chatNode.historyNode.frame.height, isStandalone: false)
|
|
var node: ListViewItemNode?
|
|
|
|
messageItem.nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: nil, nextItem: nil, completion: { messageNode, apply in
|
|
node = messageNode
|
|
apply().1(ListViewItemApply(isOnScreen: true))
|
|
})
|
|
|
|
if let messageNode = node as? ChatMessageItemView, let copyContentNode = messageNode.getMessageContextSourceNode(stableId: self.message.stableId) {
|
|
if isVideo {
|
|
self.initialAppearanceOffset = CGPoint(x: 0.0, y: min(videoWidth, width - 20.0) - copyContentNode.frame.height)
|
|
}
|
|
|
|
messageNode.frame.origin.y = sourceRect.origin.y
|
|
chatNode.addSubnode(messageNode)
|
|
result = ContextControllerTakeViewInfo(containingItem: .node(copyContentNode), contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil))
|
|
|
|
bubbleWidth = copyContentNode.contentNode.subnodes?.first?.frame.width ?? messageNode.frame.width
|
|
|
|
if isVideo {
|
|
messageItem.updateNode(async: { $0() }, node: { return messageNode }, params: params, previousItem: nil, nextItem: nil, animation: .System(duration: 0.4, transition: ControlledTransition(duration: 0.4, curve: .spring, interactive: false)), completion: { (layout, apply) in
|
|
apply(ListViewItemApply(isOnScreen: true))
|
|
})
|
|
}
|
|
}
|
|
|
|
self.messageNodeCopy = node as? ChatMessageItemView
|
|
} else {
|
|
result = ContextControllerTakeViewInfo(containingItem: .node(sourceNode), contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil))
|
|
}
|
|
|
|
let mappedParentRect = chatNode.view.convert(chatNode.bounds, to: nil)
|
|
if isVideo {
|
|
tooltipSourceRect = CGRect(x: mappedParentRect.minX + (isIncoming ? videoWidth / 2.0 : chatNode.frame.width - videoWidth / 2.0), y: floorToScreenPixels((chatNode.frame.height - videoWidth) / 2.0) + 8.0, width: 0.0, height: 0.0)
|
|
} else {
|
|
tooltipSourceRect = CGRect(x: mappedParentRect.minX + (isIncoming ? 22.0 : chatNode.frame.width - bubbleWidth + 10.0), y: floorToScreenPixels((chatNode.frame.height - 75.0) / 2.0) - 43.0, width: 44.0, height: 44.0)
|
|
}
|
|
}
|
|
|
|
let displayTooltip = { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
let absoluteFrame = tooltipSourceRect
|
|
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY), size: CGSize())
|
|
|
|
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
|
var tooltipText: String?
|
|
if isIncoming {
|
|
if isVideo {
|
|
tooltipText = presentationData.strings.Chat_PlayOnceVideoMessageTooltip
|
|
} else {
|
|
tooltipText = presentationData.strings.Chat_PlayOnceVoiceMessageTooltip
|
|
}
|
|
} else if let peer = self.message.peers[self.message.id.peerId] {
|
|
let peerName = EnginePeer(peer).compactDisplayTitle
|
|
if isVideo {
|
|
tooltipText = presentationData.strings.Chat_PlayOnceVideoMessageYourTooltip(peerName).string
|
|
} else {
|
|
tooltipText = presentationData.strings.Chat_PlayOnceVoiceMessageYourTooltip(peerName).string
|
|
}
|
|
}
|
|
|
|
if let tooltipText {
|
|
let tooltipController = TooltipScreen(
|
|
account: self.context.account,
|
|
sharedContext: self.context.sharedContext,
|
|
text: .markdown(text: tooltipText),
|
|
balancedTextLayout: true,
|
|
constrainWidth: 240.0,
|
|
style: .customBlur(UIColor(rgb: 0x18181a), 0.0),
|
|
arrowStyle: .small,
|
|
icon: nil,
|
|
location: .point(location, .bottom),
|
|
displayDuration: .custom(3.0),
|
|
inset: 8.0,
|
|
cornerRadius: 11.0,
|
|
shouldDismissOnTouch: { _, _ in
|
|
return .ignore
|
|
}
|
|
)
|
|
self.tooltipController = tooltipController
|
|
self.present(tooltipController)
|
|
}
|
|
}
|
|
|
|
let tooltipStateSignal: Signal<Int32, NoError>
|
|
let updateTooltipState: () -> Void
|
|
if isVideo {
|
|
if isIncoming {
|
|
tooltipStateSignal = ApplicationSpecificNotice.getIncomingVideoMessagePlayOnceTip(accountManager: context.sharedContext.accountManager)
|
|
updateTooltipState = {
|
|
let _ = ApplicationSpecificNotice.incrementIncomingVideoMessagePlayOnceTip(accountManager: context.sharedContext.accountManager).startStandalone()
|
|
}
|
|
} else {
|
|
tooltipStateSignal = ApplicationSpecificNotice.getOutgoingVideoMessagePlayOnceTip(accountManager: context.sharedContext.accountManager)
|
|
updateTooltipState = {
|
|
let _ = ApplicationSpecificNotice.incrementOutgoingVideoMessagePlayOnceTip(accountManager: context.sharedContext.accountManager).startStandalone()
|
|
}
|
|
}
|
|
} else {
|
|
if isIncoming {
|
|
tooltipStateSignal = ApplicationSpecificNotice.getIncomingVoiceMessagePlayOnceTip(accountManager: context.sharedContext.accountManager)
|
|
updateTooltipState = {
|
|
let _ = ApplicationSpecificNotice.incrementIncomingVoiceMessagePlayOnceTip(accountManager: context.sharedContext.accountManager).startStandalone()
|
|
}
|
|
} else {
|
|
tooltipStateSignal = ApplicationSpecificNotice.getOutgoingVoiceMessagePlayOnceTip(accountManager: context.sharedContext.accountManager)
|
|
updateTooltipState = {
|
|
let _ = ApplicationSpecificNotice.incrementOutgoingVoiceMessagePlayOnceTip(accountManager: context.sharedContext.accountManager).startStandalone()
|
|
}
|
|
}
|
|
}
|
|
|
|
let _ = (tooltipStateSignal
|
|
|> deliverOnMainQueue).startStandalone(next: { counter in
|
|
if counter >= 2 {
|
|
return
|
|
}
|
|
Queue.mainQueue().after(0.3) {
|
|
displayTooltip()
|
|
}
|
|
updateTooltipState()
|
|
})
|
|
|
|
return result
|
|
}
|
|
|
|
private var dustEffectLayer: DustEffectLayer?
|
|
func putBack() -> ContextControllerPutBackViewInfo? {
|
|
guard let chatNode = self.chatNode else {
|
|
return nil
|
|
}
|
|
|
|
self.idleTimerExtensionDisposable.set(nil)
|
|
|
|
if let tooltipController = self.tooltipController {
|
|
tooltipController.dismiss()
|
|
}
|
|
|
|
if let messageNodeCopy = self.messageNodeCopy, let sourceView = messageNodeCopy.supernode?.view, let contentNode = messageNodeCopy.getMessageContextSourceNode(stableId: nil)?.contentNode, let parentNode = contentNode.supernode?.supernode?.supernode {
|
|
let dustEffectLayer = DustEffectLayer()
|
|
dustEffectLayer.position = sourceView.bounds.center.offsetBy(dx: (parentNode.frame.width - messageNodeCopy.frame.width), dy: 0.0)
|
|
dustEffectLayer.bounds = CGRect(origin: CGPoint(), size: sourceView.bounds.size)
|
|
dustEffectLayer.zPosition = 10.0
|
|
parentNode.layer.addSublayer(dustEffectLayer)
|
|
|
|
guard let (image, subFrame) = messageNodeCopy.makeContentSnapshot() else {
|
|
return nil
|
|
}
|
|
var itemFrame = subFrame
|
|
itemFrame.origin.y = floorToScreenPixels((sourceView.frame.height - subFrame.height) / 2.0)
|
|
dustEffectLayer.addItem(frame: itemFrame, image: image)
|
|
messageNodeCopy.removeFromSupernode()
|
|
contentNode.removeFromSupernode()
|
|
return nil
|
|
} else {
|
|
var result: ContextControllerPutBackViewInfo?
|
|
chatNode.historyNode.forEachItemNode { itemNode in
|
|
guard let itemNode = itemNode as? ChatMessageItemView else {
|
|
return
|
|
}
|
|
guard let item = itemNode.item else {
|
|
return
|
|
}
|
|
if item.content.contains(where: { $0.0.stableId == self.message.stableId }) {
|
|
result = ContextControllerPutBackViewInfo(contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil))
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
|
|
final class ChatMessageReactionContextExtractedContentSource: ContextExtractedContentSource {
|
|
let keepInPlace: Bool = false
|
|
let ignoreContentTouches: Bool = true
|
|
let blurBackground: Bool = true
|
|
let actionsHorizontalAlignment: ContextActionsHorizontalAlignment = .center
|
|
|
|
private weak var chatNode: ChatControllerNode?
|
|
private let engine: TelegramEngine
|
|
private let message: Message
|
|
private let contentView: ContextExtractedContentContainingView
|
|
|
|
var shouldBeDismissed: Signal<Bool, NoError> {
|
|
if self.message.adAttribute != nil {
|
|
return .single(false)
|
|
}
|
|
|
|
return self.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.Message(id: self.message.id))
|
|
|> map { message -> Bool in
|
|
if let _ = message {
|
|
return false
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
|> distinctUntilChanged
|
|
}
|
|
|
|
init(chatNode: ChatControllerNode, engine: TelegramEngine, message: Message, contentView: ContextExtractedContentContainingView) {
|
|
self.chatNode = chatNode
|
|
self.engine = engine
|
|
self.message = message
|
|
self.contentView = contentView
|
|
}
|
|
|
|
func takeView() -> ContextControllerTakeViewInfo? {
|
|
guard let chatNode = self.chatNode else {
|
|
return nil
|
|
}
|
|
|
|
var result: ContextControllerTakeViewInfo?
|
|
chatNode.historyNode.forEachItemNode { itemNode in
|
|
guard let itemNode = itemNode as? ChatMessageItemView else {
|
|
return
|
|
}
|
|
guard let item = itemNode.item else {
|
|
return
|
|
}
|
|
if item.content.contains(where: { $0.0.stableId == self.message.stableId }) {
|
|
result = ContextControllerTakeViewInfo(containingItem: .view(self.contentView), contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil))
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func putBack() -> ContextControllerPutBackViewInfo? {
|
|
guard let chatNode = self.chatNode else {
|
|
return nil
|
|
}
|
|
|
|
var result: ContextControllerPutBackViewInfo?
|
|
chatNode.historyNode.forEachItemNode { itemNode in
|
|
guard let itemNode = itemNode as? ChatMessageItemView else {
|
|
return
|
|
}
|
|
guard let item = itemNode.item else {
|
|
return
|
|
}
|
|
if item.content.contains(where: { $0.0.stableId == self.message.stableId }) {
|
|
result = ContextControllerPutBackViewInfo(contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil))
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
|
|
final class ChatMessageNavigationButtonContextExtractedContentSource: ContextExtractedContentSource {
|
|
let keepInPlace: Bool = false
|
|
let ignoreContentTouches: Bool = true
|
|
let blurBackground: Bool = true
|
|
let actionsHorizontalAlignment: ContextActionsHorizontalAlignment = .center
|
|
|
|
private weak var chatNode: ChatControllerNode?
|
|
private let contentNode: ContextExtractedContentContainingNode
|
|
|
|
var shouldBeDismissed: Signal<Bool, NoError> {
|
|
return .single(false)
|
|
}
|
|
|
|
init(chatNode: ChatControllerNode, contentNode: ContextExtractedContentContainingNode) {
|
|
self.chatNode = chatNode
|
|
self.contentNode = contentNode
|
|
}
|
|
|
|
func takeView() -> ContextControllerTakeViewInfo? {
|
|
guard let chatNode = self.chatNode else {
|
|
return nil
|
|
}
|
|
|
|
return ContextControllerTakeViewInfo(containingItem: .node(self.contentNode), contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil))
|
|
}
|
|
|
|
func putBack() -> ContextControllerPutBackViewInfo? {
|
|
guard let chatNode = self.chatNode else {
|
|
return nil
|
|
}
|
|
|
|
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil))
|
|
}
|
|
}
|