mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
1464 lines
81 KiB
Swift
1464 lines
81 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Postbox
|
|
import SwiftSignalKit
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import TelegramCore
|
|
|
|
public class ChatController: TelegramController {
|
|
private var containerLayout = ContainerViewLayout()
|
|
|
|
private let account: Account
|
|
public let peerId: PeerId
|
|
private let messageId: MessageId?
|
|
private let botStart: ChatControllerInitialBotStart?
|
|
|
|
private let peerDisposable = MetaDisposable()
|
|
private let navigationActionDisposable = MetaDisposable()
|
|
|
|
private let messageIndexDisposable = MetaDisposable()
|
|
|
|
private let _peerReady = Promise<Bool>()
|
|
private var didSetPeerReady = false
|
|
private let peerView = Promise<PeerView>()
|
|
|
|
private var presentationInterfaceState = ChatPresentationInterfaceState()
|
|
|
|
private var chatTitleView: ChatTitleView?
|
|
private var leftNavigationButton: ChatNavigationButton?
|
|
private var rightNavigationButton: ChatNavigationButton?
|
|
private var chatInfoNavigationButton: ChatNavigationButton?
|
|
|
|
private var historyStateDisposable: Disposable?
|
|
|
|
private let galleryHiddenMesageAndMediaDisposable = MetaDisposable()
|
|
private weak var secretMediaPreviewController: SecretMediaPreviewController?
|
|
|
|
private var controllerInteraction: ChatControllerInteraction?
|
|
private var interfaceInteraction: ChatPanelInterfaceInteraction?
|
|
|
|
private let controllerNavigationDisposable = MetaDisposable()
|
|
private let sentMessageEventsDisposable = MetaDisposable()
|
|
private let messageActionCallbackDisposable = MetaDisposable()
|
|
private let editMessageDisposable = MetaDisposable()
|
|
private let enqueueMediaMessageDisposable = MetaDisposable()
|
|
private var resolvePeerByNameDisposable: MetaDisposable?
|
|
|
|
private let editingMessage = ValuePromise<Bool>(false, ignoreRepeated: true)
|
|
private let startingBot = ValuePromise<Bool>(false, ignoreRepeated: true)
|
|
|
|
private let botCallbackAlertMessage = Promise<String?>(nil)
|
|
private var botCallbackAlertMessageDisposable: Disposable?
|
|
|
|
private var resolveUrlDisposable: MetaDisposable?
|
|
|
|
private var contextQueryState: (ChatPresentationInputQuery?, Disposable)?
|
|
|
|
private var audioRecorderValue: ManagedAudioRecorder?
|
|
private var audioRecorderFeedback: HapticFeedback?
|
|
private var audioRecorder = Promise<ManagedAudioRecorder?>()
|
|
private var audioRecorderDisposable: Disposable?
|
|
|
|
private var buttonKeyboardMessageDisposable: Disposable?
|
|
private var chatUnreadCountDisposable: Disposable?
|
|
private var peerInputActivitiesDisposable: Disposable?
|
|
|
|
public init(account: Account, peerId: PeerId, messageId: MessageId? = nil, botStart: ChatControllerInitialBotStart? = nil) {
|
|
self.account = account
|
|
self.peerId = peerId
|
|
self.messageId = messageId
|
|
self.botStart = botStart
|
|
|
|
/*if #available(iOSApplicationExtension 10.0, *) {
|
|
kdebug_signpost(1, 0, 0, 0, 0)
|
|
}*/
|
|
|
|
super.init(account: account)
|
|
|
|
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil)
|
|
|
|
self.ready.set(.never())
|
|
|
|
self.scrollToTop = { [weak self] in
|
|
if let strongSelf = self, strongSelf.isNodeLoaded {
|
|
strongSelf.chatDisplayNode.historyNode.scrollToStartOfHistory()
|
|
}
|
|
}
|
|
|
|
let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] id in
|
|
if let strongSelf = self, strongSelf.isNodeLoaded {
|
|
var galleryMedia: Media?
|
|
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) {
|
|
for media in message.media {
|
|
if let file = media as? TelegramMediaFile {
|
|
galleryMedia = file
|
|
} else if let image = media as? TelegramMediaImage {
|
|
galleryMedia = image
|
|
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
|
|
if let file = content.file {
|
|
galleryMedia = file
|
|
} else if let image = content.image {
|
|
galleryMedia = image
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let galleryMedia = galleryMedia {
|
|
if let file = galleryMedia as? TelegramMediaFile, file.isMusic || file.isVoice {
|
|
if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext {
|
|
let player = ManagedAudioPlaylistPlayer(postbox: strongSelf.account.postbox, playlist: peerMessageHistoryAudioPlaylist(account: strongSelf.account, messageId: id))
|
|
applicationContext.mediaManager.setPlaylistPlayer(player)
|
|
player.control(.navigation(.next))
|
|
}
|
|
} else {
|
|
let gallery = GalleryController(account: strongSelf.account, messageId: id)
|
|
|
|
strongSelf.galleryHiddenMesageAndMediaDisposable.set(gallery.hiddenMedia.start(next: { [weak strongSelf] messageIdAndMedia in
|
|
if let strongSelf = strongSelf {
|
|
if let messageIdAndMedia = messageIdAndMedia {
|
|
strongSelf.controllerInteraction?.hiddenMedia = [messageIdAndMedia.0: [messageIdAndMedia.1]]
|
|
} else {
|
|
strongSelf.controllerInteraction?.hiddenMedia = [:]
|
|
}
|
|
strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in
|
|
if let itemNode = itemNode as? ChatMessageItemView {
|
|
itemNode.updateHiddenMedia()
|
|
}
|
|
}
|
|
}
|
|
}))
|
|
|
|
strongSelf.present(gallery, in: .window, with: GalleryControllerPresentationArguments(transitionArguments: { [weak self] messageId, media in
|
|
if let strongSelf = self {
|
|
var transitionNode: ASDisplayNode?
|
|
strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in
|
|
if let itemNode = itemNode as? ChatMessageItemView {
|
|
if let result = itemNode.transitionNode(id: messageId, media: media) {
|
|
transitionNode = result
|
|
}
|
|
}
|
|
}
|
|
if let transitionNode = transitionNode {
|
|
return GalleryTransitionArguments(transitionNode: transitionNode, transitionContainerNode: strongSelf.chatDisplayNode, transitionBackgroundNode: strongSelf.chatDisplayNode.historyNode)
|
|
}
|
|
}
|
|
return nil
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
}, openSecretMessagePreview: { [weak self] messageId in
|
|
if let strongSelf = self {
|
|
var galleryMedia: Media?
|
|
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
|
|
for media in message.media {
|
|
if let file = media as? TelegramMediaFile, file.isVideo {
|
|
galleryMedia = file
|
|
} else if let image = media as? TelegramMediaImage {
|
|
galleryMedia = image
|
|
}
|
|
}
|
|
}
|
|
if let _ = galleryMedia {
|
|
let gallery = SecretMediaPreviewController(account: strongSelf.account, messageId: messageId)
|
|
strongSelf.secretMediaPreviewController = gallery
|
|
strongSelf.present(gallery, in: .window)
|
|
}
|
|
}
|
|
}, closeSecretMessagePreview: { [weak self] in
|
|
if let strongSelf = self {
|
|
strongSelf.secretMediaPreviewController?.dismiss()
|
|
strongSelf.secretMediaPreviewController = nil
|
|
}
|
|
}, openPeer: { [weak self] id, navigation in
|
|
if let strongSelf = self {
|
|
strongSelf.openPeer(id, navigation)
|
|
}
|
|
}, openPeerMention: { [weak self] name in
|
|
if let strongSelf = self {
|
|
let disposable: MetaDisposable
|
|
if let resolvePeerByNameDisposable = strongSelf.resolvePeerByNameDisposable {
|
|
disposable = resolvePeerByNameDisposable
|
|
} else {
|
|
disposable = MetaDisposable()
|
|
strongSelf.resolvePeerByNameDisposable = disposable
|
|
}
|
|
disposable.set((resolvePeerByName(account: strongSelf.account, name: name, ageLimit: 10) |> take(1) |> deliverOnMainQueue).start(next: { peerId in
|
|
if let strongSelf = self {
|
|
if let peerId = peerId {
|
|
(strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId, messageId: nil))
|
|
}
|
|
}
|
|
}))
|
|
}
|
|
}, openMessageContextMenu: { [weak self] id, node, frame in
|
|
if let strongSelf = self, strongSelf.isNodeLoaded {
|
|
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) {
|
|
if let contextMenuController = contextMenuForChatPresentationIntefaceState(strongSelf.presentationInterfaceState, account: strongSelf.account, message: message, interfaceInteraction: strongSelf.interfaceInteraction) {
|
|
strongSelf.present(contextMenuController, in: .window, with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak strongSelf, weak node] in
|
|
if let node = node {
|
|
return (node, frame)
|
|
} else {
|
|
return nil
|
|
}
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
}, navigateToMessage: { [weak self] fromId, id in
|
|
if let strongSelf = self, strongSelf.isNodeLoaded {
|
|
if id.peerId == strongSelf.peerId {
|
|
var fromIndex: MessageIndex?
|
|
|
|
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(fromId) {
|
|
fromIndex = MessageIndex(message)
|
|
}
|
|
|
|
if let fromIndex = fromIndex {
|
|
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) {
|
|
strongSelf.chatDisplayNode.historyNode.scrollToMessage(from: fromIndex, to: MessageIndex(message))
|
|
} else {
|
|
strongSelf.messageIndexDisposable.set((strongSelf.account.postbox.messageIndexAtId(id) |> deliverOnMainQueue).start(next: { [weak strongSelf] index in
|
|
if let strongSelf = strongSelf, let index = index {
|
|
strongSelf.chatDisplayNode.historyNode.scrollToMessage(from: fromIndex, to: index)
|
|
}
|
|
}))
|
|
}
|
|
}
|
|
} else {
|
|
(strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: id.peerId, messageId: id))
|
|
}
|
|
}
|
|
}, clickThroughMessage: { [weak self] in
|
|
self?.chatDisplayNode.dismissInput()
|
|
}, toggleMessageSelection: { [weak self] id in
|
|
if let strongSelf = self, strongSelf.isNodeLoaded {
|
|
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState { $0.withToggledSelectedMessage(id) } })
|
|
}
|
|
}
|
|
}, sendMessage: { [weak self] text in
|
|
if let strongSelf = self {
|
|
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
|
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
|
|
})
|
|
}
|
|
})
|
|
var attributes: [MessageAttribute] = []
|
|
let entities = generateTextEntities(text)
|
|
if !entities.isEmpty {
|
|
attributes.append(TextEntitiesMessageAttribute(entities: entities))
|
|
}
|
|
enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: text, attributes: attributes, media: nil, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId)]).start()
|
|
}
|
|
}, sendSticker: { [weak self] file in
|
|
if let strongSelf = self {
|
|
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
|
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
|
|
})
|
|
}
|
|
})
|
|
enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "", attributes: [], media: file, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId)]).start()
|
|
}
|
|
}, requestMessageActionCallback: { [weak self] messageId, data, isGame in
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
|
return $0.updatedTitlePanelContext {
|
|
if !$0.contains(where: {
|
|
switch $0 {
|
|
case .requestInProgress:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}) {
|
|
var updatedContexts = $0
|
|
updatedContexts.append(.requestInProgress)
|
|
return updatedContexts
|
|
}
|
|
return $0
|
|
}
|
|
})
|
|
|
|
strongSelf.messageActionCallbackDisposable.set((requestMessageActionCallback(account: strongSelf.account, messageId: messageId, isGame: isGame, data: data) |> afterDisposed {
|
|
Queue.mainQueue().async {
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
|
return $0.updatedTitlePanelContext {
|
|
if let index = $0.index(where: {
|
|
switch $0 {
|
|
case .requestInProgress:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}) {
|
|
var updatedContexts = $0
|
|
updatedContexts.remove(at: index)
|
|
return updatedContexts
|
|
}
|
|
return $0
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}).start(next: { result in
|
|
if let strongSelf = self {
|
|
switch result {
|
|
case .none:
|
|
break
|
|
case let .alert(text):
|
|
let message: Signal<String?, NoError> = .single(text)
|
|
let noMessage: Signal<String?, NoError> = .single(nil)
|
|
let delayedNoMessage: Signal<String?, NoError> = noMessage |> delay(1.0, queue: Queue.mainQueue())
|
|
strongSelf.botCallbackAlertMessage.set(message |> then(delayedNoMessage))
|
|
case let .url(url):
|
|
strongSelf.openUrl(url)
|
|
}
|
|
}
|
|
}))
|
|
}
|
|
}, openUrl: { [weak self] url in
|
|
if let strongSelf = self {
|
|
strongSelf.openUrl(url)
|
|
}
|
|
}, shareCurrentLocation: { [weak self] in
|
|
if let strongSelf = self {
|
|
|
|
}
|
|
}, shareAccountContact: { [weak self] in
|
|
if let strongSelf = self {
|
|
}
|
|
}, sendBotCommand: { [weak self] messageId, command in
|
|
if let strongSelf = self {
|
|
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({})
|
|
var postAsReply = false
|
|
if !command.contains("@") && (strongSelf.peerId.namespace == Namespaces.Peer.CloudChannel || strongSelf.peerId.namespace == Namespaces.Peer.CloudGroup) {
|
|
postAsReply = true
|
|
}
|
|
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
|
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: "")) }
|
|
})
|
|
}
|
|
})
|
|
var attributes: [MessageAttribute] = []
|
|
let entities = generateTextEntities(command)
|
|
if !entities.isEmpty {
|
|
attributes.append(TextEntitiesMessageAttribute(entities: entities))
|
|
}
|
|
enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: command, attributes: attributes, media: nil, replyToMessageId: (postAsReply && messageId != nil) ? messageId! : nil)]).start()
|
|
}
|
|
}, openInstantPage: { [weak self] messageId in
|
|
if let strongSelf = self, strongSelf.isNodeLoaded {
|
|
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
|
|
for media in message.media {
|
|
if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
|
|
if let instantPage = content.instantPage {
|
|
let pageController = InstantPageController(account: strongSelf.account, webPage: webpage)
|
|
(strongSelf.navigationController as? NavigationController)?.pushViewController(pageController)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, openHashtag: { [weak self] peerName, hashtag in
|
|
if let strongSelf = self, !hashtag.isEmpty {
|
|
let searchController = HashtagSearchController(account: strongSelf.account, peerName: peerName, query: hashtag)
|
|
(strongSelf.navigationController as? NavigationController)?.pushViewController(searchController)
|
|
}
|
|
}, updateInputState: { [weak self] f in
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
|
return $0.updatedInterfaceState {
|
|
return $0.withUpdatedEffectiveInputState(f($0.effectiveInputState))
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
self.controllerInteraction = controllerInteraction
|
|
|
|
self.chatTitleView = ChatTitleView(frame: CGRect())
|
|
self.navigationItem.titleView = self.chatTitleView
|
|
self.chatTitleView?.pressed = { [weak self] in
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
|
return $0.updatedTitlePanelContext {
|
|
if let index = $0.index(where: {
|
|
switch $0 {
|
|
case .chatInfo:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}) {
|
|
var updatedContexts = $0
|
|
updatedContexts.remove(at: index)
|
|
return updatedContexts
|
|
} else {
|
|
var updatedContexts = $0
|
|
updatedContexts.insert(.chatInfo, at: 0)
|
|
return updatedContexts
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
let chatInfoButtonItem = UIBarButtonItem(customDisplayNode: ChatAvatarNavigationNode())!
|
|
chatInfoButtonItem.target = self
|
|
chatInfoButtonItem.action = #selector(self.rightNavigationButtonAction)
|
|
self.chatInfoNavigationButton = ChatNavigationButton(action: .openChatInfo, buttonItem: chatInfoButtonItem)
|
|
|
|
self.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in
|
|
if let botStart = botStart, case .interactive = botStart.behavior {
|
|
return state.updatedBotStartPayload(botStart.payload)
|
|
} else {
|
|
return state
|
|
}
|
|
})
|
|
|
|
self.peerView.set(account.viewTracker.peerView(peerId))
|
|
|
|
peerDisposable.set((self.peerView.get()
|
|
|> deliverOnMainQueue).start(next: { [weak self] peerView in
|
|
if let strongSelf = self {
|
|
if let peer = peerViewMainPeer(peerView) {
|
|
strongSelf.chatTitleView?.peerView = peerView
|
|
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(account: strongSelf.account, peer: peer)
|
|
}
|
|
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { return $0.updatedPeer { _ in return peerView.peers[peerId] } })
|
|
if !strongSelf.didSetPeerReady {
|
|
strongSelf.didSetPeerReady = true
|
|
strongSelf._peerReady.set(.single(true))
|
|
}
|
|
}
|
|
}))
|
|
|
|
botCallbackAlertMessageDisposable = (self.botCallbackAlertMessage.get()
|
|
|> deliverOnMainQueue).start(next: { [weak self] message in
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
|
return $0.updatedTitlePanelContext {
|
|
if let message = message {
|
|
if let index = $0.index(where: {
|
|
switch $0 {
|
|
case .toastAlert:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}) {
|
|
if $0[index] != ChatTitlePanelContext.toastAlert(message) {
|
|
var updatedContexts = $0
|
|
updatedContexts[index] = .toastAlert(message)
|
|
return updatedContexts
|
|
} else {
|
|
return $0
|
|
}
|
|
} else {
|
|
var updatedContexts = $0
|
|
updatedContexts.append(.toastAlert(message))
|
|
return updatedContexts
|
|
}
|
|
} else {
|
|
if let index = $0.index(where: {
|
|
switch $0 {
|
|
case .toastAlert:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}) {
|
|
var updatedContexts = $0
|
|
updatedContexts.remove(at: index)
|
|
return updatedContexts
|
|
} else {
|
|
return $0
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
self.audioRecorderDisposable = (self.audioRecorder.get() |> deliverOnMainQueue).start(next: { [weak self] audioRecorder in
|
|
if let strongSelf = self {
|
|
if strongSelf.audioRecorderValue !== audioRecorder {
|
|
strongSelf.audioRecorderValue = audioRecorder
|
|
|
|
if let audioRecorder = audioRecorder {
|
|
/*(audioRecorder.recordingState
|
|
|> filter { state in
|
|
switch state {
|
|
case .recording:
|
|
return true
|
|
case .paused:
|
|
return false
|
|
}
|
|
} |> take(1) |> deliverOnMainQueue).start(completed: {
|
|
self?.audioRecorderFeedback?.tap()
|
|
})*/
|
|
} else {
|
|
strongSelf.audioRecorderFeedback = nil
|
|
}
|
|
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
|
$0.updatedInputTextPanelState { panelState in
|
|
if let audioRecorder = audioRecorder {
|
|
if panelState.audioRecordingState == nil {
|
|
return panelState.withUpdatedAudioRecordingState(ChatTextInputPanelAudioRecordingState(recorder: audioRecorder))
|
|
}
|
|
} else {
|
|
return panelState.withUpdatedAudioRecordingState(nil)
|
|
}
|
|
return panelState
|
|
}
|
|
})
|
|
|
|
if let audioRecorder = audioRecorder {
|
|
audioRecorder.start()
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
if let botStart = botStart, case .automatic = botStart.behavior {
|
|
self.startBot(botStart.payload)
|
|
}
|
|
}
|
|
|
|
required public init(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
self.historyStateDisposable?.dispose()
|
|
self.messageIndexDisposable.dispose()
|
|
self.navigationActionDisposable.dispose()
|
|
self.galleryHiddenMesageAndMediaDisposable.dispose()
|
|
self.peerDisposable.dispose()
|
|
self.controllerNavigationDisposable.dispose()
|
|
self.sentMessageEventsDisposable.dispose()
|
|
self.messageActionCallbackDisposable.dispose()
|
|
self.editMessageDisposable.dispose()
|
|
self.enqueueMediaMessageDisposable.dispose()
|
|
self.resolvePeerByNameDisposable?.dispose()
|
|
self.botCallbackAlertMessageDisposable?.dispose()
|
|
self.contextQueryState?.1.dispose()
|
|
self.audioRecorderDisposable?.dispose()
|
|
self.buttonKeyboardMessageDisposable?.dispose()
|
|
self.resolveUrlDisposable?.dispose()
|
|
self.chatUnreadCountDisposable?.dispose()
|
|
self.peerInputActivitiesDisposable?.dispose()
|
|
}
|
|
|
|
var chatDisplayNode: ChatControllerNode {
|
|
get {
|
|
return super.displayNode as! ChatControllerNode
|
|
}
|
|
}
|
|
|
|
override public func loadDisplayNode() {
|
|
self.displayNode = ChatControllerNode(account: self.account, peerId: self.peerId, messageId: self.messageId, controllerInteraction: self.controllerInteraction!)
|
|
|
|
let initialData = self.chatDisplayNode.historyNode.initialData
|
|
|> take(1)
|
|
|> beforeNext { [weak self] combinedInitialData in
|
|
if let strongSelf = self, let combinedInitialData = combinedInitialData {
|
|
if let interfaceState = combinedInitialData.initialData?.chatInterfaceState as? ChatInterfaceState {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ _ in return interfaceState }).updatedKeyboardButtonsMessage(combinedInitialData.buttonKeyboardMessage) })
|
|
}
|
|
}
|
|
}
|
|
|
|
self.buttonKeyboardMessageDisposable = self.chatDisplayNode.historyNode.buttonKeyboardMessage.start(next: { [weak self] message in
|
|
if let strongSelf = self {
|
|
var buttonKeyboardMessageUpdated = false
|
|
if let currentButtonKeyboardMessage = strongSelf.presentationInterfaceState.keyboardButtonsMessage, let message = message {
|
|
if currentButtonKeyboardMessage.id != message.id || currentButtonKeyboardMessage.stableVersion != message.stableVersion {
|
|
buttonKeyboardMessageUpdated = true
|
|
}
|
|
} else if (strongSelf.presentationInterfaceState.keyboardButtonsMessage != nil) != (message != nil) {
|
|
buttonKeyboardMessageUpdated = true
|
|
}
|
|
if buttonKeyboardMessageUpdated {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedKeyboardButtonsMessage(message) })
|
|
}
|
|
}
|
|
})
|
|
|
|
self.historyStateDisposable = self.chatDisplayNode.historyNode.historyState.get().start(next: { [weak self] state in
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
|
$0.updatedChatHistoryState(state)
|
|
})
|
|
}
|
|
})
|
|
|
|
self.ready.set(combineLatest(self.chatDisplayNode.historyNode.historyState.get(), self._peerReady.get(), initialData) |> map { _, peerReady, _ in
|
|
return peerReady
|
|
})
|
|
|
|
self.chatDisplayNode.historyNode.visibleContentOffsetChanged = { [weak self] offset in
|
|
if let strongSelf = self {
|
|
let offsetAlpha: CGFloat
|
|
switch offset {
|
|
case let .known(offset):
|
|
if offset < 40.0 {
|
|
offsetAlpha = 0.0
|
|
} else {
|
|
offsetAlpha = 1.0
|
|
}
|
|
case .unknown:
|
|
offsetAlpha = 1.0
|
|
case .none:
|
|
offsetAlpha = 0.0
|
|
}
|
|
|
|
if !strongSelf.chatDisplayNode.navigateToLatestButton.alpha.isEqual(to: offsetAlpha) {
|
|
UIView.animate(withDuration: 0.2, delay: 0.0, options: [.beginFromCurrentState], animations: {
|
|
strongSelf.chatDisplayNode.navigateToLatestButton.alpha = offsetAlpha
|
|
}, completion: nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
self.chatDisplayNode.requestLayout = { [weak self] transition in
|
|
self?.requestLayout(transition: transition)
|
|
}
|
|
|
|
self.chatDisplayNode.setupSendActionOnViewUpdate = { [weak self] f in
|
|
self?.chatDisplayNode.historyNode.layoutActionOnViewTransition = { [weak self] transition in
|
|
f()
|
|
if let strongSelf = self {
|
|
var mappedTransition: (ChatHistoryListViewTransition, ListViewUpdateSizeAndInsets?)?
|
|
|
|
strongSelf.chatDisplayNode.containerLayoutUpdated(strongSelf.containerLayout, navigationBarHeight: strongSelf.navigationBar.frame.maxY, transition: .animated(duration: 0.4, curve: .spring), listViewTransaction: { updateSizeAndInsets in
|
|
var options = transition.options
|
|
let _ = options.insert(.Synchronous)
|
|
let _ = options.insert(.LowLatency)
|
|
options.remove(.AnimateInsertion)
|
|
options.insert(.RequestItemInsertionAnimations)
|
|
|
|
let deleteItems = transition.deleteItems.map({ item in
|
|
return ListViewDeleteItem(index: item.index, directionHint: nil)
|
|
})
|
|
|
|
var maxInsertedItem: Int?
|
|
var insertItems: [ListViewInsertItem] = []
|
|
for i in 0 ..< transition.insertItems.count {
|
|
let item = transition.insertItems[i]
|
|
if item.directionHint == .Down && (maxInsertedItem == nil || maxInsertedItem! < item.index) {
|
|
maxInsertedItem = item.index
|
|
}
|
|
insertItems.append(ListViewInsertItem(index: item.index, previousIndex: item.previousIndex, item: item.item, directionHint: item.directionHint == .Down ? .Up : nil))
|
|
}
|
|
|
|
let scrollToItem = ListViewScrollToItem(index: 0, position: .Top, animated: true, curve: .Spring(duration: 0.4), directionHint: .Up)
|
|
|
|
var stationaryItemRange: (Int, Int)?
|
|
if let maxInsertedItem = maxInsertedItem {
|
|
stationaryItemRange = (maxInsertedItem + 1, Int.max)
|
|
}
|
|
|
|
mappedTransition = (ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: deleteItems, insertItems: insertItems, updateItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage), updateSizeAndInsets)
|
|
})
|
|
|
|
if let mappedTransition = mappedTransition {
|
|
return mappedTransition
|
|
}
|
|
}
|
|
return (transition, nil)
|
|
}
|
|
}
|
|
|
|
self.chatDisplayNode.requestUpdateChatInterfaceState = { [weak self] animated, f in
|
|
self?.updateChatPresentationInterfaceState(animated: animated, interactive: true, { $0.updatedInterfaceState(f) })
|
|
}
|
|
|
|
self.chatDisplayNode.displayAttachmentMenu = { [weak self] in
|
|
if let strongSelf = self {
|
|
if true {
|
|
strongSelf.chatDisplayNode.dismissInput()
|
|
|
|
let emptyController = LegacyEmptyController()
|
|
let navigationController = makeLegacyNavigationController(rootController: emptyController)
|
|
navigationController.setNavigationBarHidden(true, animated: false)
|
|
|
|
let legacyController = LegacyController(legacyController: navigationController, presentation: .custom)
|
|
|
|
var presentOverlayController: ((UIViewController) -> (() -> Void))?
|
|
let controller = legacyAttachmentMenu(parentController: legacyController, presentOverlayController: { controller in
|
|
if let presentOverlayController = presentOverlayController {
|
|
return presentOverlayController(controller)
|
|
} else {
|
|
return {
|
|
}
|
|
}
|
|
}, openGallery: {
|
|
self?.presentMediaPicker(fileMode: false)
|
|
}, openCamera: { cameraView, menuController in
|
|
if let strongSelf = self {
|
|
presentedLegacyCamera(cameraView: cameraView, menuController: menuController, parentController: strongSelf, sendMessagesWithSignals: { signals in
|
|
self?.enqueueMediaMessages(signals: signals)
|
|
})
|
|
}
|
|
}, openFileGallery: {
|
|
self?.presentMediaPicker(fileMode: true)
|
|
}, sendMessagesWithSignals: { [weak self] signals in
|
|
self?.enqueueMediaMessages(signals: signals)
|
|
})
|
|
controller.applicationInterface = legacyController.applicationInterface
|
|
controller.didDismiss = { [weak legacyController] _ in
|
|
legacyController?.dismiss()
|
|
}
|
|
|
|
strongSelf.present(legacyController, in: .window)
|
|
controller.present(in: emptyController, sourceView: nil, animated: true)
|
|
|
|
presentOverlayController = { [weak legacyController] controller in
|
|
if let strongSelf = self, let legacyController = legacyController {
|
|
let childController = LegacyController(legacyController: controller, presentation: .custom)
|
|
legacyController.present(childController, in: .window)
|
|
return { [weak childController] in
|
|
childController?.dismiss()
|
|
}
|
|
} else {
|
|
return {
|
|
}
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
let controller = ChatMediaActionSheetController()
|
|
controller.photo = { [weak strongSelf] asset in
|
|
if let strongSelf = strongSelf {
|
|
var randomId: Int64 = 0
|
|
arc4random_buf(&randomId, 8)
|
|
let size = CGSize(width: CGFloat(asset.pixelWidth), height: CGFloat(asset.pixelHeight))
|
|
let scaledSize = size.aspectFitted(CGSize(width: 1280.0, height: 1280.0))
|
|
let resource = PhotoLibraryMediaResource(localIdentifier: asset.localIdentifier)
|
|
|
|
if false {
|
|
let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: scaledSize, resource: resource)])
|
|
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({})
|
|
enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "", attributes: [], media: media, replyToMessageId: nil)]).start()
|
|
} else {
|
|
let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), resource: resource, previewRepresentations: [], mimeType: "image/jpeg", size: 0, attributes: [.FileName(fileName: "image.jpeg"), .ImageSize(size: scaledSize)])
|
|
enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "", attributes: [], media: media, replyToMessageId: nil)]).start()
|
|
}
|
|
}
|
|
}
|
|
controller.files = { [weak strongSelf] in
|
|
if let strongSelf = strongSelf {
|
|
|
|
}
|
|
}
|
|
controller.location = { [weak strongSelf] in
|
|
if let strongSelf = strongSelf {
|
|
let mapInputController = MapInputController()
|
|
strongSelf.present(mapInputController, in: .window)
|
|
}
|
|
}
|
|
controller.contacts = { [weak strongSelf] in
|
|
if let strongSelf = strongSelf {
|
|
}
|
|
}
|
|
strongSelf.present(controller, in: .window)
|
|
}
|
|
}
|
|
|
|
self.chatDisplayNode.navigateToLatestButton.tapped = { [weak self] in
|
|
if let strongSelf = self, strongSelf.isNodeLoaded {
|
|
strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
|
}
|
|
}
|
|
|
|
let interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { [weak self] messageId in
|
|
if let strongSelf = self, strongSelf.isNodeLoaded {
|
|
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(message.id) } })
|
|
strongSelf.chatDisplayNode.ensureInputViewFocused()
|
|
}
|
|
}
|
|
}, setupEditMessage: { [weak self] messageId in
|
|
if let strongSelf = self, strongSelf.isNodeLoaded {
|
|
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withUpdatedEditMessage(ChatEditMessageState(messageId: messageId, inputState: ChatTextInputState(inputText: message.text))) } })
|
|
strongSelf.chatDisplayNode.ensureInputViewFocused()
|
|
}
|
|
}
|
|
}, beginMessageSelection: { [weak self] messageId in
|
|
if let strongSelf = self, strongSelf.isNodeLoaded {
|
|
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true,{ $0.updatedInterfaceState { $0.withUpdatedSelectedMessage(message.id) } })
|
|
}
|
|
}
|
|
}, deleteSelectedMessages: { [weak self] in
|
|
if let strongSelf = self {
|
|
if let messageIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !messageIds.isEmpty {
|
|
deleteMessagesInteractively(postbox: strongSelf.account.postbox, messageIds: Array(messageIds), type: .forLocalPeer).start()
|
|
}
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
|
}
|
|
}, forwardSelectedMessages: { [weak self] in
|
|
if let strongSelf = self {
|
|
//let controller = ShareRecipientsActionSheetController()
|
|
//strongSelf.present(controller, in: .window)
|
|
|
|
if let forwardMessageIdsSet = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds {
|
|
let forwardMessageIds = Array(forwardMessageIdsSet).sorted()
|
|
|
|
let controller = PeerSelectionController(account: strongSelf.account)
|
|
controller.peerSelected = { [weak controller] peerId in
|
|
if let strongSelf = self, let strongController = controller {
|
|
if peerId == strongSelf.peerId {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(forwardMessageIds).withoutSelectionState() }) })
|
|
strongController.dismiss()
|
|
} else {
|
|
(strongSelf.account.postbox.modify({ modifier -> Void in
|
|
modifier.updatePeerChatInterfaceState(peerId, update: { currentState in
|
|
if let currentState = currentState as? ChatInterfaceState {
|
|
return currentState.withUpdatedForwardMessageIds(forwardMessageIds)
|
|
} else {
|
|
return ChatInterfaceState().withUpdatedForwardMessageIds(forwardMessageIds)
|
|
}
|
|
return currentState
|
|
})
|
|
}) |> deliverOnMainQueue).start(completed: {
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) })
|
|
|
|
let ready = ValuePromise<Bool>()
|
|
|
|
strongSelf.controllerNavigationDisposable.set((ready.get() |> take(1) |> deliverOnMainQueue).start(next: { _ in
|
|
if let strongController = controller {
|
|
strongController.dismiss()
|
|
}
|
|
}))
|
|
|
|
(strongSelf.navigationController as? NavigationController)?.replaceTopController(ChatController(account: strongSelf.account, peerId: peerId), animated: false, ready: ready)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
strongSelf.present(controller, in: .window)
|
|
}
|
|
}
|
|
}, updateTextInputState: { [weak self] f in
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withUpdatedEffectiveInputState(f($0.effectiveInputState)) } })
|
|
}
|
|
}, updateInputModeAndDismissedButtonKeyboardMessageId: { [weak self] f in
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
|
let (updatedInputMode, updatedClosedButtonKeyboardMessageId) = f($0)
|
|
return $0.updatedInputMode({ _ in return updatedInputMode }).updatedInterfaceState({ $0.withUpdatedMessageActionsState({ $0.withUpdatedClosedButtonKeyboardMessageId(updatedClosedButtonKeyboardMessageId) }) })
|
|
})
|
|
}
|
|
}, editMessage: { [weak self] messageId, text in
|
|
if let strongSelf = self {
|
|
let editingMessage = strongSelf.editingMessage
|
|
editingMessage.set(true)
|
|
strongSelf.editMessageDisposable.set((requestEditMessage(account: strongSelf.account, messageId: messageId, text: text) |> deliverOnMainQueue |> afterDisposed({
|
|
editingMessage.set(false)
|
|
})).start(completed: {
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedEditMessage(nil) }) })
|
|
}
|
|
}))
|
|
}
|
|
}, beginMessageSearch: { [weak self] in
|
|
if let strongSelf = self {
|
|
|
|
}
|
|
}, openPeerInfo: { [weak self] in
|
|
self?.navigationButtonAction(.openChatInfo)
|
|
}, togglePeerNotifications: {
|
|
|
|
}, sendContextResult: { [weak self] results, result in
|
|
self?.enqueueChatContextResult(results, result)
|
|
}, sendBotCommand: { [weak self] botPeer, command in
|
|
if let strongSelf = self {
|
|
if let peer = strongSelf.presentationInterfaceState.peer, let addressName = botPeer.addressName {
|
|
let messageText: String
|
|
if peer is TelegramUser {
|
|
messageText = command
|
|
} else {
|
|
messageText = command + "@" + addressName
|
|
}
|
|
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
|
|
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
|
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: "")) }
|
|
})
|
|
}
|
|
})
|
|
var attributes: [MessageAttribute] = []
|
|
let entities = generateTextEntities(messageText)
|
|
if !entities.isEmpty {
|
|
attributes.append(TextEntitiesMessageAttribute(entities: entities))
|
|
}
|
|
enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: messageText, attributes: attributes, media: nil, replyToMessageId: replyMessageId)]).start()
|
|
}
|
|
}
|
|
}, sendBotStart: { [weak self] payload in
|
|
if let strongSelf = self {
|
|
strongSelf.startBot(payload)
|
|
}
|
|
}, botSwitchChatWithPayload: { [weak self] peerId, payload in
|
|
if let strongSelf = self {
|
|
strongSelf.openPeer(peerId, .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .automatic(returnToPeerId: strongSelf.peerId))))
|
|
}
|
|
}, beginAudioRecording: { [weak self] in
|
|
self?.requestAudioRecorder()
|
|
}, finishAudioRecording: { [weak self] sendAudio in
|
|
self?.dismissAudioRecorder(sendAudio: sendAudio)
|
|
}, setupMessageAutoremoveTimeout: { [weak self] in
|
|
if let strongSelf = self, strongSelf.peerId.namespace == Namespaces.Peer.SecretChat {
|
|
strongSelf.chatDisplayNode.dismissInput()
|
|
|
|
if let peer = strongSelf.presentationInterfaceState.peer as? TelegramSecretChat {
|
|
let controller = ChatSecretAutoremoveTimerActionSheetController(currentValue: peer.messageAutoremoveTimeout == nil ? 0 : peer.messageAutoremoveTimeout!, applyValue: { value in
|
|
if let strongSelf = self {
|
|
setSecretChatMessageAutoremoveTimeoutInteractively(account: strongSelf.account, peerId: strongSelf.peerId, timeout: value == 0 ? nil : value).start()
|
|
}
|
|
})
|
|
strongSelf.present(controller, in: .window)
|
|
}
|
|
}
|
|
}, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get()))
|
|
|
|
self.chatUnreadCountDisposable = (self.account.postbox.unreadMessageCountsView(items: [.peer(self.peerId)]) |> deliverOnMainQueue).start(next: { [weak self] items in
|
|
if let strongSelf = self {
|
|
var unreadCount: Int32 = 0
|
|
if let count = items.count(for: .peer(strongSelf.peerId)) {
|
|
unreadCount = count
|
|
}
|
|
if unreadCount != 0 {
|
|
strongSelf.chatDisplayNode.navigateToLatestButton.badge = "\(unreadCount)"
|
|
} else {
|
|
strongSelf.chatDisplayNode.navigateToLatestButton.badge = ""
|
|
}
|
|
}
|
|
})
|
|
|
|
let postbox = self.account.postbox
|
|
var previousPeerCache = Atomic<[PeerId: Peer]>(value: [:])
|
|
self.peerInputActivitiesDisposable = (self.account.peerInputActivities(peerId: peerId)
|
|
|> mapToSignal { activities -> Signal<[(Peer, PeerInputActivity)], NoError> in
|
|
var foundAllPeers = true
|
|
var cachedResult: [(Peer, PeerInputActivity)] = []
|
|
previousPeerCache.with { dict -> Void in
|
|
for (peerId, activity) in activities {
|
|
if let peer = dict[peerId] {
|
|
cachedResult.append((peer, activity))
|
|
} else {
|
|
foundAllPeers = false
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if foundAllPeers {
|
|
return .single(cachedResult)
|
|
} else {
|
|
return postbox.modify { modifier -> [(Peer, PeerInputActivity)] in
|
|
var result: [(Peer, PeerInputActivity)] = []
|
|
var peerCache: [PeerId: Peer] = [:]
|
|
for (peerId, activity) in activities {
|
|
if let peer = modifier.getPeer(peerId) {
|
|
result.append((peer, activity))
|
|
peerCache[peerId] = peer
|
|
}
|
|
}
|
|
previousPeerCache.swap(peerCache)
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
|> deliverOnMainQueue).start(next: { [weak self] activities in
|
|
if let strongSelf = self {
|
|
strongSelf.chatTitleView?.inputActivities = (strongSelf.peerId, activities)
|
|
}
|
|
})
|
|
|
|
self.interfaceInteraction = interfaceInteraction
|
|
self.chatDisplayNode.interfaceInteraction = interfaceInteraction
|
|
|
|
self.displayNodeDidLoad()
|
|
|
|
self.sentMessageEventsDisposable.set(self.account.pendingMessageManager.deliveredMessageEvents(peerId: self.peerId).start(next: { _ in
|
|
serviceSoundManager.playMessageDeliveredSound()
|
|
}))
|
|
}
|
|
|
|
override public func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
}
|
|
|
|
override public func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
|
|
self.chatDisplayNode.historyNode.preloadPages = true
|
|
self.chatDisplayNode.historyNode.canReadHistory.set(true)
|
|
|
|
self.chatDisplayNode.loadInputPanels()
|
|
}
|
|
|
|
override public func viewWillDisappear(_ animated: Bool) {
|
|
super.viewWillDisappear(animated)
|
|
|
|
self.chatDisplayNode.historyNode.canReadHistory.set(false)
|
|
let peerId = self.peerId
|
|
let timestamp = Int32(Date().timeIntervalSince1970)
|
|
let interfaceState = self.presentationInterfaceState.interfaceState.withUpdatedTimestamp(timestamp)
|
|
self.account.postbox.modify({ modifier -> Void in
|
|
modifier.updatePeerChatInterfaceState(peerId, update: { _ in
|
|
return interfaceState
|
|
})
|
|
}).start()
|
|
}
|
|
|
|
override public func viewDidDisappear(_ animated: Bool) {
|
|
super.viewDidDisappear(animated)
|
|
|
|
self.updateChatPresentationInterfaceState(animated: false, interactive: false, {
|
|
$0.updatedTitlePanelContext {
|
|
if let index = $0.index(where: {
|
|
switch $0 {
|
|
case .chatInfo:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}) {
|
|
var updatedContexts = $0
|
|
updatedContexts.remove(at: index)
|
|
return updatedContexts
|
|
} else {
|
|
return $0
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
|
super.containerLayoutUpdated(layout, transition: transition)
|
|
|
|
self.containerLayout = layout
|
|
|
|
self.chatDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition, listViewTransaction: { updateSizeAndInsets in
|
|
self.chatDisplayNode.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets)
|
|
})
|
|
}
|
|
|
|
func updateChatPresentationInterfaceState(animated: Bool = true, interactive: Bool, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) {
|
|
var temporaryChatPresentationInterfaceState = f(self.presentationInterfaceState)
|
|
|
|
if self.presentationInterfaceState.keyboardButtonsMessage?.visibleButtonKeyboardMarkup != temporaryChatPresentationInterfaceState.keyboardButtonsMessage?.visibleButtonKeyboardMarkup {
|
|
if let keyboardButtonsMessage = temporaryChatPresentationInterfaceState.keyboardButtonsMessage, let _ = keyboardButtonsMessage.visibleButtonKeyboardMarkup {
|
|
if self.presentationInterfaceState.interfaceState.editMessage == nil && self.presentationInterfaceState.interfaceState.composeInputState.inputText.isEmpty && keyboardButtonsMessage.id != temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.closedButtonKeyboardMessageId && temporaryChatPresentationInterfaceState.botStartPayload == nil {
|
|
temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputMode({ _ in
|
|
return .inputButtons
|
|
})
|
|
}
|
|
|
|
if self.peerId.namespace == Namespaces.Peer.CloudChannel || self.peerId.namespace == Namespaces.Peer.CloudGroup {
|
|
if temporaryChatPresentationInterfaceState.interfaceState.replyMessageId == nil && temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.processedSetupReplyMessageId != keyboardButtonsMessage.id {
|
|
temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInterfaceState({ $0.withUpdatedReplyMessageId(keyboardButtonsMessage.id).withUpdatedMessageActionsState({ $0.withUpdatedProcessedSetupReplyMessageId(keyboardButtonsMessage.id) }) })
|
|
}
|
|
}
|
|
} else {
|
|
temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputMode({ mode in
|
|
if case .inputButtons = mode {
|
|
return .text
|
|
} else {
|
|
return mode
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
if let keyboardButtonsMessage = temporaryChatPresentationInterfaceState.keyboardButtonsMessage, keyboardButtonsMessage.requestsSetupReply {
|
|
if temporaryChatPresentationInterfaceState.interfaceState.replyMessageId == nil && temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.processedSetupReplyMessageId != keyboardButtonsMessage.id {
|
|
temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInterfaceState({ $0.withUpdatedReplyMessageId(keyboardButtonsMessage.id).withUpdatedMessageActionsState({ $0.withUpdatedProcessedSetupReplyMessageId(keyboardButtonsMessage.id) }) })
|
|
}
|
|
}
|
|
|
|
let inputTextPanelState = inputTextPanelStateForChatPresentationInterfaceState(temporaryChatPresentationInterfaceState, account: self.account)
|
|
var updatedChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputTextPanelState({ _ in return inputTextPanelState })
|
|
|
|
if let (updatedContextQueryState, updatedContextQuerySignal) = contextQueryResultStateForChatInterfacePresentationState(updatedChatPresentationInterfaceState, account: self.account, currentQuery: self.contextQueryState?.0) {
|
|
self.contextQueryState?.1.dispose()
|
|
var inScope = true
|
|
var inScopeResult: ((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?)?
|
|
self.contextQueryState = (updatedContextQueryState, (updatedContextQuerySignal |> deliverOnMainQueue).start(next: { [weak self] result in
|
|
if let strongSelf = self {
|
|
if Thread.isMainThread && inScope {
|
|
inScope = false
|
|
inScopeResult = result
|
|
} else {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
|
$0.updatedInputQueryResult { previousResult in
|
|
return result(previousResult)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}))
|
|
inScope = false
|
|
if let inScopeResult = inScopeResult {
|
|
updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedInputQueryResult { previousResult in
|
|
return inScopeResult(previousResult)
|
|
}
|
|
}
|
|
}
|
|
|
|
self.presentationInterfaceState = updatedChatPresentationInterfaceState
|
|
if self.isNodeLoaded {
|
|
self.chatDisplayNode.updateChatPresentationInterfaceState(updatedChatPresentationInterfaceState, animated: animated, interactive: interactive)
|
|
}
|
|
|
|
if let button = leftNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState.interfaceState, currentButton: self.leftNavigationButton, target: self, selector: #selector(self.leftNavigationButtonAction)) {
|
|
self.navigationItem.setLeftBarButton(button.buttonItem, animated: true)
|
|
self.leftNavigationButton = button
|
|
} else if let _ = self.leftNavigationButton {
|
|
self.navigationItem.setLeftBarButton(nil, animated: true)
|
|
self.leftNavigationButton = nil
|
|
}
|
|
|
|
if let button = rightNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState.interfaceState, currentButton: self.rightNavigationButton, target: self, selector: #selector(self.rightNavigationButtonAction), chatInfoNavigationButton: self.chatInfoNavigationButton) {
|
|
self.navigationItem.setRightBarButton(button.buttonItem, animated: true)
|
|
self.rightNavigationButton = button
|
|
} else if let _ = self.rightNavigationButton {
|
|
self.navigationItem.setRightBarButton(nil, animated: true)
|
|
self.rightNavigationButton = nil
|
|
}
|
|
|
|
if let controllerInteraction = self.controllerInteraction {
|
|
if updatedChatPresentationInterfaceState.interfaceState.selectionState != controllerInteraction.selectionState {
|
|
let animated = controllerInteraction.selectionState == nil || updatedChatPresentationInterfaceState.interfaceState.selectionState == nil
|
|
controllerInteraction.selectionState = updatedChatPresentationInterfaceState.interfaceState.selectionState
|
|
self.chatDisplayNode.historyNode.forEachItemNode { itemNode in
|
|
if let itemNode = itemNode as? ChatMessageItemView {
|
|
itemNode.updateSelectionState(animated: animated)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func leftNavigationButtonAction() {
|
|
if let button = self.leftNavigationButton {
|
|
self.navigationButtonAction(button.action)
|
|
}
|
|
}
|
|
|
|
@objc func rightNavigationButtonAction() {
|
|
if let button = self.rightNavigationButton {
|
|
self.navigationButtonAction(button.action)
|
|
}
|
|
}
|
|
|
|
private func navigationButtonAction(_ action: ChatNavigationButtonAction) {
|
|
switch action {
|
|
case .cancelMessageSelection:
|
|
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
|
case .clearHistory:
|
|
let actionSheet = ActionSheetController()
|
|
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
|
ActionSheetButtonItem(title: "Delete All Messages", color: .destructive, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
})
|
|
]), ActionSheetItemGroup(items: [
|
|
ActionSheetButtonItem(title: "Cancel", color: .accent, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
})
|
|
])])
|
|
self.present(actionSheet, in: .window)
|
|
case .openChatInfo:
|
|
self.navigationActionDisposable.set((self.peerView.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { [weak self] peerView in
|
|
if let strongSelf = self, let _ = peerView.peers[peerView.peerId] {
|
|
let chatInfoController = PeerInfoController(account: strongSelf.account, peerId: peerView.peerId)
|
|
(strongSelf.navigationController as? NavigationController)?.pushViewController(chatInfoController)
|
|
}
|
|
}))
|
|
break
|
|
}
|
|
}
|
|
|
|
private func presentMediaPicker(fileMode: Bool) {
|
|
legacyAssetPicker(fileMode: fileMode).start(next: { [weak self] generator in
|
|
if let strongSelf = self {
|
|
var presentOverlayController: ((UIViewController) -> (() -> Void))?
|
|
let controller = generator({ controller in
|
|
return presentOverlayController!(controller)
|
|
})
|
|
let legacyController = LegacyController(legacyController: controller, presentation: .modal)
|
|
|
|
presentOverlayController = { [weak legacyController] controller in
|
|
if let strongSelf = self, let legacyController = legacyController {
|
|
let childController = LegacyController(legacyController: controller, presentation: .custom)
|
|
legacyController.present(childController, in: .window)
|
|
return { [weak childController] in
|
|
childController?.dismiss()
|
|
}
|
|
} else {
|
|
return {
|
|
}
|
|
}
|
|
}
|
|
|
|
configureLegacyAssetPicker(controller)
|
|
controller.descriptionGenerator = legacyAssetPickerItemGenerator()
|
|
controller.completionBlock = { [weak self, weak legacyController] signals in
|
|
if let strongSelf = self, let legacyController = legacyController {
|
|
legacyController.dismiss()
|
|
strongSelf.enqueueMediaMessages(signals: signals)
|
|
}
|
|
}
|
|
controller.dismissalBlock = { [weak legacyController] in
|
|
if let legacyController = legacyController {
|
|
legacyController.dismiss()
|
|
}
|
|
}
|
|
strongSelf.present(legacyController, in: .window)
|
|
}
|
|
})
|
|
}
|
|
|
|
private func enqueueMediaMessages(signals: [Any]?) {
|
|
self.enqueueMediaMessageDisposable.set((legacyAssetPickerEnqueueMessages(account: self.account, peerId: self.peerId, signals: signals!) |> deliverOnMainQueue).start(next: { [weak self] messages in
|
|
if let strongSelf = self {
|
|
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
|
|
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
|
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
|
|
})
|
|
}
|
|
})
|
|
enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: messages.map { $0.withUpdatedReplyToMessageId(replyMessageId) }).start()
|
|
}
|
|
}))
|
|
}
|
|
|
|
private func enqueueChatContextResult(_ results: ChatContextResultCollection, _ result: ChatContextResult) {
|
|
if let message = outgoingMessageWithChatContextResult(results, result) {
|
|
let replyMessageId = self.presentationInterfaceState.interfaceState.replyMessageId
|
|
self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
|
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: "")) }
|
|
})
|
|
}
|
|
})
|
|
enqueueMessages(account: self.account, peerId: self.peerId, messages: [message.withUpdatedReplyToMessageId(replyMessageId)]).start()
|
|
}
|
|
}
|
|
|
|
private func requestAudioRecorder() {
|
|
if self.audioRecorderValue == nil {
|
|
if let applicationContext = self.account.applicationContext as? TelegramApplicationContext {
|
|
if self.audioRecorderFeedback == nil {
|
|
//self.audioRecorderFeedback = HapticFeedback()
|
|
self.audioRecorderFeedback?.prepareTap()
|
|
}
|
|
self.audioRecorder.set(applicationContext.mediaManager.audioRecorder())
|
|
}
|
|
}
|
|
}
|
|
|
|
private func dismissAudioRecorder(sendAudio: Bool) {
|
|
if let audioRecorderValue = self.audioRecorderValue {
|
|
audioRecorderValue.stop()
|
|
if sendAudio {
|
|
(audioRecorderValue.takenRecordedData() |> deliverOnMainQueue).start(next: { [weak self] data in
|
|
if let strongSelf = self, let data = data {
|
|
if data.duration < 0.5 {
|
|
strongSelf.audioRecorderFeedback?.error()
|
|
strongSelf.audioRecorderFeedback = nil
|
|
} else {
|
|
var randomId: Int64 = 0
|
|
arc4random_buf(&randomId, 8)
|
|
|
|
let resource = LocalFileMediaResource(fileId: randomId)
|
|
|
|
strongSelf.account.postbox.mediaBox.storeResourceData(resource.id, data: data.compressedData)
|
|
|
|
var waveformBuffer: MemoryBuffer?
|
|
if let waveform = data.waveform {
|
|
waveformBuffer = MemoryBuffer(data: waveform)
|
|
}
|
|
|
|
enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "", attributes: [], media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), resource: resource, previewRepresentations: [], mimeType: "audio/ogg", size: data.compressedData.count, attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)]), replyToMessageId: nil)]).start()
|
|
|
|
strongSelf.audioRecorderFeedback?.success()
|
|
strongSelf.audioRecorderFeedback = nil
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
self.audioRecorder.set(.single(nil))
|
|
}
|
|
|
|
private func openPeer(_ peerId: PeerId?, _ navigation: ChatControllerInteractionNavigateToPeer) {
|
|
if peerId == self.peerId {
|
|
switch navigation {
|
|
case .info:
|
|
self.navigationButtonAction(.openChatInfo)
|
|
case let .chat(textInputState):
|
|
if let textInputState = textInputState {
|
|
self.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
|
return ($0.updatedInterfaceState {
|
|
return $0.withUpdatedComposeInputState(textInputState)
|
|
}).updatedInputMode({ _ in
|
|
return .text
|
|
})
|
|
})
|
|
}
|
|
case let .withBotStartPayload(botStart):
|
|
self.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
|
$0.updatedBotStartPayload(botStart.payload)
|
|
})
|
|
}
|
|
} else {
|
|
if let peerId = peerId {
|
|
switch navigation {
|
|
case .info:
|
|
break
|
|
case let .chat(textInputState):
|
|
if let textInputState = textInputState {
|
|
(self.account.postbox.modify({ modifier -> Void in
|
|
modifier.updatePeerChatInterfaceState(peerId, update: { currentState in
|
|
if let currentState = currentState as? ChatInterfaceState {
|
|
return currentState.withUpdatedComposeInputState(textInputState)
|
|
} else {
|
|
return ChatInterfaceState().withUpdatedComposeInputState(textInputState)
|
|
}
|
|
return currentState
|
|
})
|
|
})).start(completed: { [weak self] in
|
|
if let strongSelf = self {
|
|
(strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId, messageId: nil))
|
|
}
|
|
})
|
|
} else {
|
|
(self.navigationController as? NavigationController)?.pushViewController(ChatController(account: self.account, peerId: peerId, messageId: nil))
|
|
}
|
|
case let .withBotStartPayload(botStart):
|
|
(self.navigationController as? NavigationController)?.pushViewController(ChatController(account: self.account, peerId: peerId, messageId: nil, botStart: botStart))
|
|
}
|
|
} else {
|
|
switch navigation {
|
|
case .info:
|
|
break
|
|
case let .chat(textInputState):
|
|
if let textInputState = textInputState {
|
|
let controller = PeerSelectionController(account: self.account)
|
|
controller.peerSelected = { [weak self, weak controller] peerId in
|
|
if let strongSelf = self, let strongController = controller {
|
|
if peerId == strongSelf.peerId {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
|
return ($0.updatedInterfaceState {
|
|
return $0.withUpdatedComposeInputState(textInputState)
|
|
}).updatedInputMode({ _ in
|
|
return .text
|
|
})
|
|
})
|
|
strongController.dismiss()
|
|
} else {
|
|
(strongSelf.account.postbox.modify({ modifier -> Void in
|
|
modifier.updatePeerChatInterfaceState(peerId, update: { currentState in
|
|
if let currentState = currentState as? ChatInterfaceState {
|
|
return currentState.withUpdatedComposeInputState(textInputState)
|
|
} else {
|
|
return ChatInterfaceState().withUpdatedComposeInputState(textInputState)
|
|
}
|
|
return currentState
|
|
})
|
|
}) |> deliverOnMainQueue).start(completed: {
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) })
|
|
|
|
let ready = ValuePromise<Bool>()
|
|
|
|
strongSelf.controllerNavigationDisposable.set((ready.get() |> take(1) |> deliverOnMainQueue).start(next: { _ in
|
|
if let strongController = controller {
|
|
strongController.dismiss()
|
|
}
|
|
}))
|
|
|
|
(strongSelf.navigationController as? NavigationController)?.replaceTopController(ChatController(account: strongSelf.account, peerId: peerId), animated: false, ready: ready)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
self.present(controller, in: .window)
|
|
}
|
|
case let .withBotStartPayload(_):
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func startBot(_ payload: String?) {
|
|
let startingBot = self.startingBot
|
|
startingBot.set(true)
|
|
self.editMessageDisposable.set((requestStartBot(account: self.account, botPeerId: self.peerId, payload: payload) |> deliverOnMainQueue |> afterDisposed({
|
|
startingBot.set(false)
|
|
})).start(completed: { [weak self] in
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedBotStartPayload(nil) })
|
|
}
|
|
}))
|
|
}
|
|
|
|
private func openUrl(_ url: String) {
|
|
let disposable: MetaDisposable
|
|
if let current = self.resolveUrlDisposable {
|
|
disposable = current
|
|
} else {
|
|
disposable = MetaDisposable()
|
|
self.resolveUrlDisposable = disposable
|
|
}
|
|
disposable.set((resolveUrl(account: self.account, url: url) |> deliverOnMainQueue).start(next: { [weak self] result in
|
|
if let strongSelf = self {
|
|
switch result {
|
|
case let .externalUrl(url):
|
|
if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext {
|
|
applicationContext.openUrl(url)
|
|
}
|
|
case let .peer(peerId):
|
|
strongSelf.openPeer(peerId, .chat(textInputState: nil))
|
|
case let .botStart(peerId, payload):
|
|
strongSelf.openPeer(peerId, .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .interactive)))
|
|
case let .groupBotStart(peerId, payload):
|
|
break
|
|
case let .channelMessage(peerId, messageId):
|
|
(strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId, messageId: messageId))
|
|
}
|
|
}
|
|
}))
|
|
}
|
|
}
|