2024-05-26 20:42:43 +04:00

584 lines
27 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import TelegramPresentationData
import ChatPresentationInterfaceState
import AccountContext
import ChatSendMessageActionUI
import SwiftSignalKit
import ComponentFlow
import Display
import Postbox
import TelegramCore
import WallpaperBackgroundNode
import AudioWaveform
import ChatMessageItemView
import ChatMessageItemCommon
import ChatMessageBubbleContentNode
import ChatMessageMediaBubbleContentNode
import ChatControllerInteraction
import TelegramUIPreferences
import ChatHistoryEntry
public final class ChatSendContactMessageContextPreview: UIView, ChatSendMessageContextScreenMediaPreview {
private let context: AccountContext
private let presentationData: PresentationData
private let wallpaperBackgroundNode: WallpaperBackgroundNode?
private let contactPeers: [ContactListPeer]
private var messageNodes: [ListViewItemNode]?
private let messagesContainer: UIView
public var isReady: Signal<Bool, NoError> {
return .single(true)
}
public var view: UIView {
return self
}
public var globalClippingRect: CGRect? {
return nil
}
public var layoutType: ChatSendMessageContextScreenMediaPreviewLayoutType {
return .message
}
public init(context: AccountContext, presentationData: PresentationData, wallpaperBackgroundNode: WallpaperBackgroundNode?, contactPeers: [ContactListPeer]) {
self.context = context
self.presentationData = presentationData
self.wallpaperBackgroundNode = wallpaperBackgroundNode
self.contactPeers = contactPeers
self.messagesContainer = UIView()
self.messagesContainer.layer.sublayerTransform = CATransform3DMakeScale(-1.0, -1.0, 1.0)
super.init(frame: CGRect())
self.addSubview(self.messagesContainer)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
public func animateIn(transition: Transition) {
transition.animateAlpha(view: self.messagesContainer, from: 0.0, to: 1.0)
transition.animateScale(view: self.messagesContainer, from: 0.001, to: 1.0)
}
public func animateOut(transition: Transition) {
transition.setAlpha(view: self.messagesContainer, alpha: 0.0)
transition.setScale(view: self.messagesContainer, scale: 0.001)
}
public func animateOutOnSend(transition: Transition) {
transition.setAlpha(view: self.messagesContainer, alpha: 0.0)
}
public func update(containerSize: CGSize, transition: Transition) -> CGSize {
var contactsMedia: [TelegramMediaContact] = []
for peer in self.contactPeers {
switch peer {
case let .peer(contact, _, _):
guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else {
continue
}
let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!<Mobile>!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "")
let phone = contactData.basicData.phoneNumbers[0].value
contactsMedia.append(TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: contact.id, vCardData: nil))
case let .deviceContact(_, basicData):
guard !basicData.phoneNumbers.isEmpty else {
continue
}
let contactData = DeviceContactExtendedData(basicData: basicData, middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "")
let phone = contactData.basicData.phoneNumbers[0].value
contactsMedia.append(TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: nil, vCardData: nil))
}
}
var items: [ListViewItem] = []
for contactMedia in contactsMedia {
let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: self.context.account.peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [contactMedia], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
let item = self.context.sharedContext.makeChatMessagePreviewItem(
context: self.context,
messages: [message],
theme: presentationData.theme,
strings: presentationData.strings,
wallpaper: presentationData.chatWallpaper,
fontSize: presentationData.chatFontSize,
chatBubbleCorners: presentationData.chatBubbleCorners,
dateTimeFormat: presentationData.dateTimeFormat,
nameOrder: presentationData.nameDisplayOrder,
forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .fetchStatus(.Local), fetchStatus: .Local),
tapMessage: nil,
clickThroughMessage: nil,
backgroundNode: self.wallpaperBackgroundNode,
availableReactions: nil,
accountPeer: nil,
isCentered: false,
isPreview: true,
isStandalone: true
)
items.append(item)
}
let params = ListViewItemLayoutParams(width: containerSize.width, leftInset: 0.0, rightInset: 0.0, availableHeight: containerSize.height)
if let messageNodes = self.messageNodes {
for i in 0 ..< items.count {
let itemNode = messageNodes[i]
items[i].updateNode(async: { $0() }, node: {
return itemNode
}, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in
let nodeFrame = CGRect(origin: CGPoint(x: itemNode.frame.minX, y: itemNode.frame.minY), size: CGSize(width: containerSize.width, height: layout.size.height))
itemNode.contentSize = layout.contentSize
itemNode.insets = layout.insets
itemNode.frame = nodeFrame
itemNode.isUserInteractionEnabled = false
apply(ListViewItemApply(isOnScreen: true))
})
}
} else {
var messageNodes: [ListViewItemNode] = []
for i in 0 ..< items.count {
var itemNode: ListViewItemNode?
items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in
itemNode = node
apply().1(ListViewItemApply(isOnScreen: true))
})
itemNode!.isUserInteractionEnabled = false
messageNodes.append(itemNode!)
self.messagesContainer.addSubview(itemNode!.view)
}
self.messageNodes = messageNodes
}
var contentSize = CGSize()
for messageNode in self.messageNodes ?? [] {
guard let messageNode = messageNode as? ChatMessageItemView else {
continue
}
if !contentSize.height.isZero {
contentSize.height += 2.0
}
let contentFrame = messageNode.contentFrame()
contentSize.height += contentFrame.height
contentSize.width = max(contentSize.width, contentFrame.width)
}
var contentOffsetY: CGFloat = 0.0
for messageNode in self.messageNodes ?? [] {
guard let messageNode = messageNode as? ChatMessageItemView else {
continue
}
if !contentOffsetY.isZero {
contentOffsetY += 2.0
}
let contentFrame = messageNode.contentFrame()
messageNode.frame = CGRect(origin: CGPoint(x: contentFrame.minX + contentSize.width - contentFrame.width + 6.0, y: 3.0 + contentOffsetY), size: CGSize(width: contentFrame.width, height: contentFrame.height))
contentOffsetY += contentFrame.height
}
self.messagesContainer.frame = CGRect(origin: CGPoint(x: 6.0, y: 3.0), size: CGSize(width: contentSize.width, height: contentSize.height))
return CGSize(width: contentSize.width - 4.0, height: contentSize.height + 2.0)
}
}
public final class ChatSendAudioMessageContextPreview: UIView, ChatSendMessageContextScreenMediaPreview {
private let context: AccountContext
private let presentationData: PresentationData
private let wallpaperBackgroundNode: WallpaperBackgroundNode?
private let waveform: AudioWaveform
private var messageNodes: [ListViewItemNode]?
private let messagesContainer: UIView
public var isReady: Signal<Bool, NoError> {
return .single(true)
}
public var view: UIView {
return self
}
public var globalClippingRect: CGRect? {
return nil
}
public var layoutType: ChatSendMessageContextScreenMediaPreviewLayoutType {
return .message
}
public init(context: AccountContext, presentationData: PresentationData, wallpaperBackgroundNode: WallpaperBackgroundNode?, waveform: AudioWaveform) {
self.context = context
self.presentationData = presentationData
self.wallpaperBackgroundNode = wallpaperBackgroundNode
self.waveform = waveform
self.messagesContainer = UIView()
self.messagesContainer.layer.sublayerTransform = CATransform3DMakeScale(-1.0, -1.0, 1.0)
super.init(frame: CGRect())
self.addSubview(self.messagesContainer)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
public func animateIn(transition: Transition) {
transition.animateAlpha(view: self.messagesContainer, from: 0.0, to: 1.0)
transition.animateScale(view: self.messagesContainer, from: 0.001, to: 1.0)
}
public func animateOut(transition: Transition) {
transition.setAlpha(view: self.messagesContainer, alpha: 0.0)
transition.setScale(view: self.messagesContainer, scale: 0.001)
}
public func animateOutOnSend(transition: Transition) {
transition.setAlpha(view: self.messagesContainer, alpha: 0.0)
}
public func update(containerSize: CGSize, transition: Transition) -> CGSize {
let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: self.waveform.makeBitstream())]
let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes)
let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: self.context.account.peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [voiceMedia], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
let item = self.context.sharedContext.makeChatMessagePreviewItem(
context: self.context,
messages: [message],
theme: presentationData.theme,
strings: presentationData.strings,
wallpaper: presentationData.chatWallpaper,
fontSize: presentationData.chatFontSize,
chatBubbleCorners: presentationData.chatBubbleCorners,
dateTimeFormat: presentationData.dateTimeFormat,
nameOrder: presentationData.nameDisplayOrder,
forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .fetchStatus(.Local), fetchStatus: .Local),
tapMessage: nil,
clickThroughMessage: nil,
backgroundNode: self.wallpaperBackgroundNode,
availableReactions: nil,
accountPeer: nil,
isCentered: false,
isPreview: true,
isStandalone: true
)
let items = [item]
let params = ListViewItemLayoutParams(width: containerSize.width, leftInset: 0.0, rightInset: 0.0, availableHeight: containerSize.height)
if let messageNodes = self.messageNodes {
for i in 0 ..< items.count {
let itemNode = messageNodes[i]
items[i].updateNode(async: { $0() }, node: {
return itemNode
}, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in
let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: containerSize.width, height: layout.size.height))
itemNode.contentSize = layout.contentSize
itemNode.insets = layout.insets
itemNode.frame = nodeFrame
itemNode.isUserInteractionEnabled = false
apply(ListViewItemApply(isOnScreen: true))
})
}
} else {
var messageNodes: [ListViewItemNode] = []
for i in 0 ..< items.count {
var itemNode: ListViewItemNode?
items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in
itemNode = node
apply().1(ListViewItemApply(isOnScreen: true))
})
itemNode!.isUserInteractionEnabled = false
messageNodes.append(itemNode!)
self.messagesContainer.addSubview(itemNode!.view)
}
self.messageNodes = messageNodes
}
guard let messageNode = self.messageNodes?.first as? ChatMessageItemView else {
return CGSize(width: 10.0, height: 10.0)
}
let contentFrame = messageNode.contentFrame()
self.messagesContainer.frame = CGRect(origin: CGPoint(x: 6.0, y: 3.0), size: CGSize(width: contentFrame.width, height: contentFrame.height))
return CGSize(width: contentFrame.width - 4.0, height: contentFrame.height + 2.0)
}
}
public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMessageContextScreenMediaPreview {
private let context: AccountContext
private let presentationData: PresentationData
private let wallpaperBackgroundNode: WallpaperBackgroundNode?
private let messages: [Message]
private var chatPresentationData: ChatPresentationData?
private var messageNode: ChatMessageMediaBubbleContentNode?
private let messagesContainer: UIView
public var isReady: Signal<Bool, NoError> {
return .single(true)
}
public var view: UIView {
return self
}
public var globalClippingRect: CGRect? {
return nil
}
public var layoutType: ChatSendMessageContextScreenMediaPreviewLayoutType {
return .media
}
public init(context: AccountContext, presentationData: PresentationData, wallpaperBackgroundNode: WallpaperBackgroundNode?, messages: [EngineMessage]) {
self.context = context
self.presentationData = presentationData
self.wallpaperBackgroundNode = wallpaperBackgroundNode
self.messages = messages.map { message in
return message._asMessage().withUpdatedTimestamp(0)
}
self.messagesContainer = UIView()
super.init(frame: CGRect())
self.addSubview(self.messagesContainer)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
public func animateIn(transition: Transition) {
transition.animateAlpha(view: self.messagesContainer, from: 0.0, to: 1.0)
transition.animateScale(view: self.messagesContainer, from: 0.001, to: 1.0)
}
public func animateOut(transition: Transition) {
transition.setAlpha(view: self.messagesContainer, alpha: 0.0)
transition.setScale(view: self.messagesContainer, scale: 0.001)
}
public func animateOutOnSend(transition: Transition) {
transition.setAlpha(view: self.messagesContainer, alpha: 0.0)
}
public func update(containerSize: CGSize, transition: Transition) -> CGSize {
let messageNode: ChatMessageMediaBubbleContentNode
if let current = self.messageNode {
messageNode = current
} else {
messageNode = ChatMessageMediaBubbleContentNode()
self.messageNode = messageNode
self.messagesContainer.addSubview(messageNode.view)
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let chatPresentationData: ChatPresentationData
if let current = self.chatPresentationData {
chatPresentationData = current
} else {
chatPresentationData = ChatPresentationData(
theme: ChatPresentationThemeData(
theme: presentationData.theme,
wallpaper: presentationData.chatWallpaper
),
fontSize: presentationData.chatFontSize,
strings: presentationData.strings,
dateTimeFormat: presentationData.dateTimeFormat,
nameDisplayOrder: presentationData.nameDisplayOrder,
disableAnimations: false,
largeEmoji: false,
chatBubbleCorners: presentationData.chatBubbleCorners
)
self.chatPresentationData = chatPresentationData
}
let controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in
return false }, openPeer: { _, _, _, _ in }, openPeerMention: { _, _ in }, openMessageContextMenu: { _, _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in
}, updateMessageReaction: { _, _, _, _ in }, activateMessagePinch: { _ in
}, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in
}, navigateToThreadMessage: { _, _, _ in
}, tapMessage: { _ in
}, clickThroughMessage: {
}, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in
return false
}, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
}, presentController: { _, _ in
}, presentControllerInCurrent: { _, _ in
}, navigationController: {
return nil
}, chatControllerNode: {
return nil
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in }, longTap: { _, _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in
}, canSetupReply: { _ in
return .none
}, canSendMessages: {
return false
}, navigateToFirstDateMessage: { _, _ in
}, requestRedeliveryOfFailedMessages: { _ in
}, addContact: { _ in
}, rateCall: { _, _, _ in
}, requestSelectMessagePollOptions: { _, _ in
}, requestOpenMessagePollResults: { _, _ in
}, openAppStorePage: {
}, displayMessageTooltip: { _, _, _, _, _ in
}, seekToTimecode: { _, _, _ in
}, scheduleCurrentMessage: {
}, sendScheduledMessagesNow: { _ in
}, editScheduledMessagesTime: { _ in
}, performTextSelectionAction: { _, _, _, _ in
}, displayImportedMessageTooltip: { _ in
}, displaySwipeToReplyHint: {
}, dismissReplyMarkupMessage: { _ in
}, openMessagePollResults: { _, _ in
}, openPollCreation: { _ in
}, displayPollSolution: { _, _ in
}, displayPsa: { _, _ in
}, displayDiceTooltip: { _ in
}, animateDiceSuccess: { _, _ in
}, displayPremiumStickerTooltip: { _, _ in
}, displayEmojiPackTooltip: { _, _ in
}, openPeerContextMenu: { _, _, _, _, _ in
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, openMessageStats: { _ in
}, editMessageMedia: { _, _ in
}, copyText: { _ in
}, displayUndo: { _ in
}, isAnimatingMessage: { _ in
return false
}, getMessageTransitionNode: {
return nil
}, updateChoosingSticker: { _ in
}, commitEmojiInteraction: { _, _, _, _ in
}, openLargeEmojiInfo: { _, _, _ in
}, openJoinLink: { _ in
}, openWebView: { _, _, _, _ in
}, activateAdAction: { _, _ in
}, openRequestedPeerSelection: { _, _, _, _ in
}, saveMediaToFiles: { _ in
}, openNoAdsDemo: {
}, openAdsInfo: {
}, displayGiveawayParticipationStatus: { _ in
}, openPremiumStatusInfo: { _, _, _, _ in
}, openRecommendedChannelContextMenu: { _, _, _ in
}, openGroupBoostInfo: { _, _ in
}, openStickerEditor: {
}, openPhoneContextMenu: { _ in
}, openAgeRestrictedMessageMedia: { _, _ in
}, playMessageEffect: { _ in
}, editMessageFactCheck: { _ in
}, requestMessageUpdate: { _, _ in
}, cancelInteractiveKeyboardGestures: {
}, dismissTextInput: {
}, scrollToMessageId: { _ in
}, navigateToStory: { _, _ in
}, attemptedNavigationToPrivateQuote: { _ in
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: self.context, backgroundNode: self.wallpaperBackgroundNode))
let associatedData = ChatMessageItemAssociatedData(
automaticDownloadPeerType: .channel,
automaticDownloadPeerId: nil,
automaticDownloadNetworkType: .cellular,
isRecentActions: false,
availableReactions: nil,
availableMessageEffects: nil,
savedMessageTags: nil,
defaultReaction: nil,
isPremium: false,
accountPeer: nil
)
let entryAttributes = ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil)
let item = ChatMessageBubbleContentItem(
context: self.context,
controllerInteraction: controllerInteraction,
message: self.messages[0],
topMessage: self.messages[0],
read: true,
chatLocation: .peer(id: self.context.account.peerId),
presentationData: chatPresentationData,
associatedData: associatedData,
attributes: entryAttributes,
isItemPinned: false,
isItemEdited: false
)
let makeMessageLayout = messageNode.asyncLayoutContent()
let layoutConstants = chatMessageItemLayoutConstants(
(ChatMessageItemLayoutConstants.compact, ChatMessageItemLayoutConstants.regular),
params: ListViewItemLayoutParams(
width: containerSize.width,
leftInset: 0.0,
rightInset: 0.0,
availableHeight: 10000.0
),
presentationData: chatPresentationData
)
let (_, _, _, continueMessageLayout) = makeMessageLayout(
item,
layoutConstants,
ChatMessageBubblePreparePosition.linear(
top: ChatMessageBubbleRelativePosition.None(.None(.None)),
bottom: ChatMessageBubbleRelativePosition.None(.None(.None))
),
nil,
CGSize(width: containerSize.width, height: 10000.0),
0.0
)
let (finalizedWidth, finalizeMessageLayout) = continueMessageLayout(
CGSize(width: containerSize.width, height: 10000.0),
ChatMessageBubbleContentPosition.linear(
top: ChatMessageBubbleRelativePosition.None(.None(.None)),
bottom: ChatMessageBubbleRelativePosition.None(.None(.None))
)
)
let _ = finalizedWidth
let (finalizedSize, apply) = finalizeMessageLayout(finalizedWidth)
apply(.None, true, nil)
let contentFrameInset = UIEdgeInsets(top: -2.0, left: -2.0, bottom: -2.0, right: -2.0)
let contentFrame = CGRect(origin: CGPoint(x: contentFrameInset.left, y: contentFrameInset.top), size: finalizedSize)
messageNode.frame = contentFrame
let messagesContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentFrame.width + contentFrameInset.left + contentFrameInset.right, height: contentFrame.height + contentFrameInset.top + contentFrameInset.bottom))
self.messagesContainer.frame = messagesContainerFrame
return messagesContainerFrame.size
}
}