mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
12034 lines
695 KiB
Swift
12034 lines
695 KiB
Swift
import Foundation
|
||
import UIKit
|
||
import Postbox
|
||
import SwiftSignalKit
|
||
import Display
|
||
import AsyncDisplayKit
|
||
import TelegramCore
|
||
import SyncCore
|
||
import SafariServices
|
||
import MobileCoreServices
|
||
import Intents
|
||
import LegacyComponents
|
||
import TelegramPresentationData
|
||
import TelegramUIPreferences
|
||
import DeviceAccess
|
||
import TextFormat
|
||
import TelegramBaseController
|
||
import AccountContext
|
||
import TelegramStringFormatting
|
||
import OverlayStatusController
|
||
import DeviceLocationManager
|
||
import ShareController
|
||
import UrlEscaping
|
||
import ContextUI
|
||
import ComposePollUI
|
||
import AlertUI
|
||
import PresentationDataUtils
|
||
import UndoUI
|
||
import TelegramCallsUI
|
||
import TelegramNotices
|
||
import GameUI
|
||
import ScreenCaptureDetection
|
||
import GalleryUI
|
||
import OpenInExternalAppUI
|
||
import LegacyUI
|
||
import InstantPageUI
|
||
import LocationUI
|
||
import BotPaymentsUI
|
||
import DeleteChatPeerActionSheetItem
|
||
import HashtagSearchUI
|
||
import LegacyMediaPickerUI
|
||
import WebSearchUI
|
||
import Emoji
|
||
import PeerAvatarGalleryUI
|
||
import PeerInfoUI
|
||
import RaiseToListen
|
||
import UrlHandling
|
||
import ReactionSelectionNode
|
||
import AvatarNode
|
||
import MessageReactionListUI
|
||
import AppBundle
|
||
import LocalizedPeerData
|
||
import PhoneNumberFormat
|
||
import SettingsUI
|
||
import UrlWhitelist
|
||
import TelegramIntents
|
||
import TooltipUI
|
||
import StatisticsUI
|
||
import MediaResources
|
||
import GalleryData
|
||
import ChatInterfaceState
|
||
import InviteLinksUI
|
||
import ChatHistoryImportTasks
|
||
|
||
extension ChatLocation {
|
||
var peerId: PeerId {
|
||
switch self {
|
||
case let .peer(peerId):
|
||
return peerId
|
||
case let .replyThread(replyThreadMessage):
|
||
return replyThreadMessage.messageId.peerId
|
||
}
|
||
}
|
||
}
|
||
|
||
public enum ChatControllerPeekActions {
|
||
case standard
|
||
case remove(() -> Void)
|
||
}
|
||
|
||
public final class ChatControllerOverlayPresentationData {
|
||
public let expandData: (ASDisplayNode?, () -> Void)
|
||
public init(expandData: (ASDisplayNode?, () -> Void)) {
|
||
self.expandData = expandData
|
||
}
|
||
}
|
||
|
||
private enum ChatLocationInfoData {
|
||
case peer(Promise<PeerView>)
|
||
case replyThread(Promise<Message?>)
|
||
//case group(Promise<ChatListTopPeersView>)
|
||
}
|
||
|
||
private enum ChatRecordingActivity {
|
||
case voice
|
||
case instantVideo
|
||
case none
|
||
}
|
||
|
||
public enum NavigateToMessageLocation {
|
||
case id(MessageId)
|
||
case index(MessageIndex)
|
||
case upperBound(PeerId)
|
||
|
||
var messageId: MessageId? {
|
||
switch self {
|
||
case let .id(id):
|
||
return id
|
||
case let .index(index):
|
||
return index.id
|
||
case .upperBound:
|
||
return nil
|
||
}
|
||
}
|
||
|
||
var peerId: PeerId {
|
||
switch self {
|
||
case let .id(id):
|
||
return id.peerId
|
||
case let .index(index):
|
||
return index.id.peerId
|
||
case let .upperBound(peerId):
|
||
return peerId
|
||
}
|
||
}
|
||
}
|
||
|
||
private func isTopmostChatController(_ controller: ChatControllerImpl) -> Bool {
|
||
if let _ = controller.navigationController {
|
||
var hasOther = false
|
||
controller.window?.forEachController({ c in
|
||
if c is ChatControllerImpl && controller !== c {
|
||
hasOther = true
|
||
}
|
||
})
|
||
if hasOther {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
private func calculateSlowmodeActiveUntilTimestamp(account: Account, untilTimestamp: Int32?) -> Int32? {
|
||
guard let untilTimestamp = untilTimestamp else {
|
||
return nil
|
||
}
|
||
let timestamp = Int32(Date().timeIntervalSince1970)
|
||
let remainingTime = max(0, untilTimestamp - timestamp)
|
||
if remainingTime == 0 {
|
||
return nil
|
||
} else {
|
||
return untilTimestamp
|
||
}
|
||
}
|
||
|
||
private struct ScrolledToMessageId: Equatable {
|
||
struct AllowedReplacementDirections: OptionSet {
|
||
var rawValue: Int32
|
||
|
||
static let up = AllowedReplacementDirections(rawValue: 1 << 0)
|
||
static let down = AllowedReplacementDirections(rawValue: 1 << 1)
|
||
}
|
||
|
||
var id: MessageId
|
||
var allowedReplacementDirection: AllowedReplacementDirections
|
||
}
|
||
|
||
enum ChatLoadingMessageSubject {
|
||
case generic
|
||
case pinnedMessage
|
||
}
|
||
|
||
public final class ChatControllerImpl: TelegramBaseController, ChatController, GalleryHiddenMediaTarget, UIDropInteractionDelegate {
|
||
private var validLayout: ContainerViewLayout?
|
||
|
||
public weak var parentController: ViewController?
|
||
|
||
public var peekActions: ChatControllerPeekActions = .standard
|
||
private var didSetup3dTouch: Bool = false
|
||
|
||
private let context: AccountContext
|
||
public let chatLocation: ChatLocation
|
||
public let subject: ChatControllerSubject?
|
||
private let botStart: ChatControllerInitialBotStart?
|
||
|
||
private let peerDisposable = MetaDisposable()
|
||
private let navigationActionDisposable = MetaDisposable()
|
||
private var networkStateDisposable: Disposable?
|
||
|
||
private let messageIndexDisposable = MetaDisposable()
|
||
|
||
private let _chatLocationInfoReady = Promise<Bool>()
|
||
private var didSetChatLocationInfoReady = false
|
||
private let chatLocationInfoData: ChatLocationInfoData
|
||
|
||
private let cachedDataReady = Promise<Bool>()
|
||
private var didSetCachedDataReady = false
|
||
|
||
private var presentationInterfaceState: ChatPresentationInterfaceState
|
||
private var presentationInterfaceStatePromise: ValuePromise<ChatPresentationInterfaceState>
|
||
|
||
private var chatTitleView: ChatTitleView?
|
||
private var leftNavigationButton: ChatNavigationButton?
|
||
private var rightNavigationButton: ChatNavigationButton?
|
||
private var chatInfoNavigationButton: ChatNavigationButton?
|
||
|
||
private var peerView: PeerView?
|
||
|
||
private var historyStateDisposable: Disposable?
|
||
|
||
private let galleryHiddenMesageAndMediaDisposable = MetaDisposable()
|
||
private let temporaryHiddenGalleryMediaDisposable = MetaDisposable()
|
||
|
||
private var controllerInteraction: ChatControllerInteraction?
|
||
private var interfaceInteraction: ChatPanelInterfaceInteraction?
|
||
|
||
private let messageContextDisposable = MetaDisposable()
|
||
private let controllerNavigationDisposable = MetaDisposable()
|
||
private let sentMessageEventsDisposable = MetaDisposable()
|
||
private let failedMessageEventsDisposable = MetaDisposable()
|
||
private weak var currentFailedMessagesAlertController: ViewController?
|
||
private let messageActionCallbackDisposable = MetaDisposable()
|
||
private let messageActionUrlAuthDisposable = MetaDisposable()
|
||
private let editMessageDisposable = MetaDisposable()
|
||
private let editMessageErrorsDisposable = MetaDisposable()
|
||
private let enqueueMediaMessageDisposable = MetaDisposable()
|
||
private var resolvePeerByNameDisposable: MetaDisposable?
|
||
private var shareStatusDisposable: MetaDisposable?
|
||
private var clearCacheDisposable: MetaDisposable?
|
||
private var bankCardDisposable: MetaDisposable?
|
||
private var hasActiveGroupCallDisposable: Disposable?
|
||
|
||
private let editingMessage = ValuePromise<Float?>(nil, ignoreRepeated: true)
|
||
private let startingBot = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||
private let unblockingPeer = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||
private let searching = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||
private let searchResult = Promise<(SearchMessagesResult, SearchMessagesState, SearchMessagesLocation)?>()
|
||
private let loadingMessage = Promise<ChatLoadingMessageSubject?>(nil)
|
||
private let performingInlineSearch = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||
|
||
private var preloadHistoryPeerId: PeerId?
|
||
private let preloadHistoryPeerIdDisposable = MetaDisposable()
|
||
|
||
private let botCallbackAlertMessage = Promise<String?>(nil)
|
||
private var botCallbackAlertMessageDisposable: Disposable?
|
||
|
||
private var selectMessagePollOptionDisposables: DisposableDict<MessageId>?
|
||
private var selectPollOptionFeedback: HapticFeedback?
|
||
|
||
private var resolveUrlDisposable: MetaDisposable?
|
||
|
||
private var contextQueryStates: [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)] = [:]
|
||
private var searchQuerySuggestionState: (ChatPresentationInputQuery?, Disposable)?
|
||
private var urlPreviewQueryState: (String?, Disposable)?
|
||
private var editingUrlPreviewQueryState: (String?, Disposable)?
|
||
private var searchState: ChatSearchState?
|
||
|
||
private var recordingModeFeedback: HapticFeedback?
|
||
private var recorderFeedback: HapticFeedback?
|
||
private var audioRecorderValue: ManagedAudioRecorder?
|
||
private var audioRecorder = Promise<ManagedAudioRecorder?>()
|
||
private var audioRecorderDisposable: Disposable?
|
||
private var audioRecorderStatusDisposable: Disposable?
|
||
|
||
private var videoRecorderValue: InstantVideoController?
|
||
private var tempVideoRecorderValue: InstantVideoController?
|
||
private var videoRecorder = Promise<InstantVideoController?>()
|
||
private var videoRecorderDisposable: Disposable?
|
||
|
||
private var buttonKeyboardMessageDisposable: Disposable?
|
||
private var cachedDataDisposable: Disposable?
|
||
private var chatUnreadCountDisposable: Disposable?
|
||
private var chatUnreadMentionCountDisposable: Disposable?
|
||
private var peerInputActivitiesDisposable: Disposable?
|
||
|
||
private var recentlyUsedInlineBotsValue: [Peer] = []
|
||
private var recentlyUsedInlineBotsDisposable: Disposable?
|
||
|
||
private var unpinMessageDisposable: MetaDisposable?
|
||
|
||
private let typingActivityPromise = Promise<Bool>(false)
|
||
private var inputActivityDisposable: Disposable?
|
||
private var recordingActivityValue: ChatRecordingActivity = .none
|
||
private let recordingActivityPromise = ValuePromise<ChatRecordingActivity>(.none, ignoreRepeated: true)
|
||
private var recordingActivityDisposable: Disposable?
|
||
private var acquiredRecordingActivityDisposable: Disposable?
|
||
|
||
private var searchDisposable: MetaDisposable?
|
||
|
||
private var historyNavigationStack = ChatHistoryNavigationStack()
|
||
|
||
public let canReadHistory = ValuePromise<Bool>(true, ignoreRepeated: true)
|
||
private var reminderActivity: NSUserActivity?
|
||
private var isReminderActivityEnabled: Bool = false
|
||
|
||
private var canReadHistoryValue = false
|
||
private var canReadHistoryDisposable: Disposable?
|
||
|
||
private var presentationData: PresentationData
|
||
private var presentationDataDisposable: Disposable?
|
||
|
||
private var automaticMediaDownloadSettings: MediaAutoDownloadSettings
|
||
private var automaticMediaDownloadSettingsDisposable: Disposable?
|
||
|
||
private var stickerSettings: ChatInterfaceStickerSettings
|
||
private var stickerSettingsDisposable: Disposable?
|
||
|
||
private var applicationInForegroundDisposable: Disposable?
|
||
private var applicationInFocusDisposable: Disposable?
|
||
|
||
private let checksTooltipDisposable = MetaDisposable()
|
||
private var shouldDisplayChecksTooltip = false
|
||
|
||
private var checkedPeerChatServiceActions = false
|
||
|
||
private var willAppear = false
|
||
private var didAppear = false
|
||
private var scheduledActivateInput = false
|
||
|
||
private var raiseToListen: RaiseToListenManager?
|
||
private var voicePlaylistDidEndTimestamp: Double = 0.0
|
||
|
||
private weak var sendingOptionsTooltipController: TooltipController?
|
||
private weak var searchResultsTooltipController: TooltipController?
|
||
private weak var messageTooltipController: TooltipController?
|
||
private weak var videoUnmuteTooltipController: TooltipController?
|
||
private weak var silentPostTooltipController: TooltipController?
|
||
private weak var mediaRecordingModeTooltipController: TooltipController?
|
||
private weak var mediaRestrictedTooltipController: TooltipController?
|
||
private var mediaRestrictedTooltipControllerMode = true
|
||
private weak var checksTooltipController: TooltipController?
|
||
|
||
private var currentMessageTooltipScreens: [(TooltipScreen, ListViewItemNode)] = []
|
||
|
||
private weak var slowmodeTooltipController: ChatSlowmodeHintController?
|
||
|
||
private weak var currentContextController: ContextController?
|
||
|
||
private weak var sendMessageActionsController: ChatSendMessageActionSheetController?
|
||
private var searchResultsController: ChatSearchResultsController?
|
||
|
||
private var screenCaptureManager: ScreenCaptureDetectionManager?
|
||
private let chatAdditionalDataDisposable = MetaDisposable()
|
||
|
||
private var reportIrrelvantGeoNoticePromise = Promise<Bool?>()
|
||
private var reportIrrelvantGeoNotice: Bool?
|
||
private var reportIrrelvantGeoDisposable: Disposable?
|
||
|
||
private var hasScheduledMessages: Bool = false
|
||
|
||
private var volumeButtonsListener: VolumeButtonsListener?
|
||
|
||
private var beginMediaRecordingRequestId: Int = 0
|
||
private var lockMediaRecordingRequestId: Int?
|
||
|
||
private var updateSlowmodeStatusDisposable = MetaDisposable()
|
||
private var updateSlowmodeStatusTimerValue: Int32?
|
||
|
||
private var isDismissed = false
|
||
|
||
private var focusOnSearchAfterAppearance: (ChatSearchDomain, String)?
|
||
|
||
private let keepPeerInfoScreenDataHotDisposable = MetaDisposable()
|
||
private let preloadAvatarDisposable = MetaDisposable()
|
||
|
||
private let peekData: ChatPeekTimeout?
|
||
private let peekTimerDisposable = MetaDisposable()
|
||
|
||
private let createVoiceChatDisposable = MetaDisposable()
|
||
|
||
private let selectAddMemberDisposable = MetaDisposable()
|
||
private let addMemberDisposable = MetaDisposable()
|
||
|
||
private var shouldDisplayDownButton = false
|
||
|
||
private var hasEmbeddedTitleContent = false
|
||
private var isEmbeddedTitleContentHidden = false
|
||
|
||
private let chatLocationContextHolder: Atomic<ChatLocationContextHolder?>
|
||
|
||
private weak var currentImportMessageTooltip: UndoOverlayController?
|
||
|
||
public override var customData: Any? {
|
||
return self.chatLocation
|
||
}
|
||
|
||
public var purposefulAction: (() -> Void)?
|
||
var updatedClosedPinnedMessageId: ((MessageId) -> Void)?
|
||
var requestedUnpinAllMessages: ((Int, MessageId) -> Void)?
|
||
|
||
private let scrolledToMessageId = ValuePromise<ScrolledToMessageId?>(nil, ignoreRepeated: true)
|
||
private var scrolledToMessageIdValue: ScrolledToMessageId? = nil {
|
||
didSet {
|
||
self.scrolledToMessageId.set(self.scrolledToMessageIdValue)
|
||
}
|
||
}
|
||
|
||
private var importStateDisposable: Disposable?
|
||
|
||
public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, greetingData: ChatGreetingData? = nil) {
|
||
let _ = ChatControllerCount.modify { value in
|
||
return value + 1
|
||
}
|
||
|
||
self.context = context
|
||
self.chatLocation = chatLocation
|
||
self.chatLocationContextHolder = chatLocationContextHolder
|
||
self.subject = subject
|
||
self.botStart = botStart
|
||
self.peekData = peekData
|
||
|
||
var locationBroadcastPanelSource: LocationBroadcastPanelSource
|
||
var groupCallPanelSource: GroupCallPanelSource
|
||
|
||
switch chatLocation {
|
||
case let .peer(peerId):
|
||
locationBroadcastPanelSource = .peer(peerId)
|
||
switch subject {
|
||
case .message, .none:
|
||
groupCallPanelSource = .peer(peerId)
|
||
default:
|
||
groupCallPanelSource = .none
|
||
}
|
||
self.chatLocationInfoData = .peer(Promise())
|
||
case let .replyThread(replyThreadMessage):
|
||
locationBroadcastPanelSource = .none
|
||
groupCallPanelSource = .none
|
||
let promise = Promise<Message?>()
|
||
let key = PostboxViewKey.messages([replyThreadMessage.messageId])
|
||
promise.set(context.account.postbox.combinedView(keys: [key])
|
||
|> map { views -> Message? in
|
||
guard let view = views.views[key] as? MessagesView else {
|
||
return nil
|
||
}
|
||
return view.messages[replyThreadMessage.messageId]
|
||
})
|
||
self.chatLocationInfoData = .replyThread(promise)
|
||
}
|
||
|
||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||
self.automaticMediaDownloadSettings = context.sharedContext.currentAutomaticMediaDownloadSettings.with { $0 }
|
||
|
||
self.stickerSettings = ChatInterfaceStickerSettings(loopAnimatedStickers: false)
|
||
|
||
self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: context.account.peerId, mode: mode, chatLocation: chatLocation, subject: subject, peerNearbyData: peerNearbyData, greetingData: greetingData, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil)
|
||
self.presentationInterfaceStatePromise = ValuePromise(self.presentationInterfaceState)
|
||
|
||
var mediaAccessoryPanelVisibility = MediaAccessoryPanelVisibility.none
|
||
if case .standard = mode {
|
||
mediaAccessoryPanelVisibility = .specific(size: .compact)
|
||
} else {
|
||
locationBroadcastPanelSource = .none
|
||
groupCallPanelSource = .none
|
||
}
|
||
let navigationBarPresentationData: NavigationBarPresentationData?
|
||
switch mode {
|
||
case .inline:
|
||
navigationBarPresentationData = nil
|
||
default:
|
||
navigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData, hideBackground: self.context.sharedContext.immediateExperimentalUISettings.playerEmbedding ? true : false, hideBadge: false)
|
||
}
|
||
super.init(context: context, navigationBarPresentationData: navigationBarPresentationData, mediaAccessoryPanelVisibility: mediaAccessoryPanelVisibility, locationBroadcastPanelSource: locationBroadcastPanelSource, groupCallPanelSource: groupCallPanelSource)
|
||
|
||
self.automaticallyControlPresentationContextLayout = false
|
||
self.blocksBackgroundWhenInOverlay = true
|
||
self.acceptsFocusWhenInOverlay = true
|
||
|
||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
|
||
|
||
self.ready.set(.never())
|
||
|
||
self.scrollToTop = { [weak self] in
|
||
guard let strongSelf = self, strongSelf.isNodeLoaded else {
|
||
return
|
||
}
|
||
strongSelf.chatDisplayNode.scrollToTop()
|
||
}
|
||
|
||
self.attemptNavigation = { [weak self] action in
|
||
guard let strongSelf = self else {
|
||
return true
|
||
}
|
||
if strongSelf.presentVoiceMessageDiscardAlert(action: action, performAction: false) {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] message, mode in
|
||
guard let strongSelf = self, strongSelf.isNodeLoaded, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) else {
|
||
return false
|
||
}
|
||
|
||
strongSelf.commitPurposefulAction()
|
||
strongSelf.dismissAllTooltips()
|
||
|
||
var openMessageByAction: Bool = false
|
||
|
||
for media in message.media {
|
||
if let action = media as? TelegramMediaAction {
|
||
switch action.action {
|
||
case .pinnedMessageUpdated:
|
||
for attribute in message.attributes {
|
||
if let attribute = attribute as? ReplyMessageAttribute {
|
||
strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId))
|
||
break
|
||
}
|
||
}
|
||
case let .photoUpdated(image):
|
||
openMessageByAction = image != nil
|
||
case .gameScore:
|
||
for attribute in message.attributes {
|
||
if let attribute = attribute as? ReplyMessageAttribute {
|
||
strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId))
|
||
break
|
||
}
|
||
}
|
||
case .groupPhoneCall, .inviteToGroupPhoneCall:
|
||
if let activeCall = strongSelf.presentationInterfaceState.activeGroupCallInfo?.activeCall {
|
||
strongSelf.context.joinGroupCall(peerId: message.id.peerId, activeCall: CachedChannelData.ActiveCall(id: activeCall.id, accessHash: activeCall.accessHash))
|
||
} else {
|
||
var canManageGroupCalls = false
|
||
if let channel = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramChannel {
|
||
if case .group = channel.info, channel.flags.contains(.isCreator) || channel.hasPermission(.manageCalls) {
|
||
canManageGroupCalls = true
|
||
}
|
||
} else if let group = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramGroup {
|
||
if case .creator = group.role {
|
||
canManageGroupCalls = true
|
||
} else if case let .admin(rights, _) = group.role {
|
||
if rights.flags.contains(.canManageCalls) {
|
||
canManageGroupCalls = true
|
||
}
|
||
}
|
||
}
|
||
|
||
if canManageGroupCalls {
|
||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.VoiceChat_CreateNewVoiceChatText, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.VoiceChat_CreateNewVoiceChatStart, action: {
|
||
if let strongSelf = self {
|
||
var dismissStatus: (() -> Void)?
|
||
let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: {
|
||
dismissStatus?()
|
||
}))
|
||
dismissStatus = { [weak self, weak statusController] in
|
||
self?.createVoiceChatDisposable.set(nil)
|
||
statusController?.dismiss()
|
||
}
|
||
strongSelf.present(statusController, in: .window(.root))
|
||
strongSelf.createVoiceChatDisposable.set((createGroupCall(account: strongSelf.context.account, peerId: message.id.peerId)
|
||
|> deliverOnMainQueue).start(next: { [weak self] info in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.context.joinGroupCall(peerId: message.id.peerId, activeCall: CachedChannelData.ActiveCall(id: info.id, accessHash: info.accessHash))
|
||
}, error: { [weak self] error in
|
||
dismissStatus?()
|
||
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
|
||
let text: String
|
||
switch error {
|
||
case .generic:
|
||
text = strongSelf.presentationData.strings.Login_UnknownError
|
||
case .anonymousNotAllowed:
|
||
text = strongSelf.presentationData.strings.VoiceChat_AnonymousDisabledAlertText
|
||
}
|
||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||
}, completed: {
|
||
dismissStatus?()
|
||
}))
|
||
}
|
||
})]), in: .window(.root))
|
||
}
|
||
}
|
||
return true
|
||
default:
|
||
break
|
||
}
|
||
if !openMessageByAction {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
|
||
var openChatLocation = strongSelf.chatLocation
|
||
if case let .replyThread(replyThreadMessage) = openChatLocation {
|
||
if message.threadId != makeMessageThreadId(replyThreadMessage.messageId) {
|
||
openChatLocation = .peer(message.id.peerId)
|
||
}
|
||
}
|
||
|
||
return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, chatLocation: openChatLocation, chatLocationContextHolder: strongSelf.chatLocationContextHolder, message: message, standalone: false, reverseMessageGalleryOrder: false, mode: mode, navigationController: strongSelf.effectiveNavigationController, dismissInput: {
|
||
self?.chatDisplayNode.dismissInput()
|
||
}, present: { c, a in
|
||
self?.present(c, in: .window(.root), with: a, blockInteraction: true)
|
||
}, transitionNode: { messageId, media in
|
||
var selectedNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
|
||
if let strongSelf = self {
|
||
strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in
|
||
if let itemNode = itemNode as? ChatMessageItemView {
|
||
if let result = itemNode.transitionNode(id: messageId, media: media) {
|
||
selectedNode = result
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return selectedNode
|
||
}, addToTransitionSurface: { view in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.chatDisplayNode.historyNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.chatDisplayNode.historyNode.view)
|
||
}, openUrl: { url in
|
||
self?.openUrl(url, concealed: false, message: nil)
|
||
}, openPeer: { peer, navigation in
|
||
self?.openPeer(peerId: peer.id, navigation: navigation, fromMessage: nil)
|
||
}, callPeer: { peerId, isVideo in
|
||
self?.controllerInteraction?.callPeer(peerId, isVideo)
|
||
}, enqueueMessage: { message in
|
||
self?.sendMessages([message])
|
||
}, sendSticker: canSendMessagesToChat(strongSelf.presentationInterfaceState) ? { fileReference, sourceNode, sourceRect in
|
||
return self?.controllerInteraction?.sendSticker(fileReference, nil, false, sourceNode, sourceRect) ?? false
|
||
} : nil, setupTemporaryHiddenMedia: { signal, centralIndex, galleryMedia in
|
||
if let strongSelf = self {
|
||
strongSelf.temporaryHiddenGalleryMediaDisposable.set((signal |> deliverOnMainQueue).start(next: { entry in
|
||
if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction {
|
||
var messageIdAndMedia: [MessageId: [Media]] = [:]
|
||
|
||
if let entry = entry as? InstantPageGalleryEntry, entry.index == centralIndex {
|
||
messageIdAndMedia[message.id] = [galleryMedia]
|
||
}
|
||
|
||
controllerInteraction.hiddenMedia = messageIdAndMedia
|
||
|
||
strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in
|
||
if let itemNode = itemNode as? ChatMessageItemView {
|
||
itemNode.updateHiddenMedia()
|
||
}
|
||
}
|
||
}
|
||
}))
|
||
}
|
||
}, chatAvatarHiddenMedia: { signal, media in
|
||
if let strongSelf = self {
|
||
strongSelf.temporaryHiddenGalleryMediaDisposable.set((signal |> deliverOnMainQueue).start(next: { messageId in
|
||
if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction {
|
||
var messageIdAndMedia: [MessageId: [Media]] = [:]
|
||
|
||
if let messageId = messageId {
|
||
messageIdAndMedia[messageId] = [media]
|
||
}
|
||
|
||
controllerInteraction.hiddenMedia = messageIdAndMedia
|
||
|
||
strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in
|
||
if let itemNode = itemNode as? ChatMessageItemView {
|
||
itemNode.updateHiddenMedia()
|
||
}
|
||
}
|
||
}
|
||
}))
|
||
}
|
||
}, actionInteraction: GalleryControllerActionInteraction(openUrl: { [weak self] url, concealed in
|
||
if let strongSelf = self {
|
||
strongSelf.openUrl(url, concealed: concealed, message: nil)
|
||
}
|
||
}, openUrlIn: { [weak self] url in
|
||
if let strongSelf = self {
|
||
strongSelf.openUrlIn(url)
|
||
}
|
||
}, openPeerMention: { [weak self] mention in
|
||
if let strongSelf = self {
|
||
strongSelf.controllerInteraction?.openPeerMention(mention)
|
||
}
|
||
}, openPeer: { [weak self] peerId in
|
||
if let strongSelf = self {
|
||
strongSelf.controllerInteraction?.openPeer(peerId, .default, nil)
|
||
}
|
||
}, openHashtag: { [weak self] peerName, hashtag in
|
||
if let strongSelf = self {
|
||
strongSelf.controllerInteraction?.openHashtag(peerName, hashtag)
|
||
}
|
||
}, openBotCommand: { [weak self] command in
|
||
if let strongSelf = self {
|
||
strongSelf.controllerInteraction?.sendBotCommand(nil, command)
|
||
}
|
||
}, addContact: { [weak self] phoneNumber in
|
||
if let strongSelf = self {
|
||
strongSelf.controllerInteraction?.addContact(phoneNumber)
|
||
}
|
||
}, storeMediaPlaybackState: { [weak self] messageId, timestamp in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
var storedState: MediaPlaybackStoredState?
|
||
if let timestamp = timestamp {
|
||
storedState = MediaPlaybackStoredState(timestamp: timestamp, playbackRate: .x1)
|
||
}
|
||
let _ = updateMediaPlaybackStoredStateInteractively(postbox: strongSelf.context.account.postbox, messageId: messageId, state: storedState).start()
|
||
}, editMedia: { [weak self] messageId, snapshots, transitionCompletion in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
|
||
let _ = (strongSelf.context.account.postbox.transaction { transaction -> Message? in
|
||
return transaction.getMessage(messageId)
|
||
} |> deliverOnMainQueue).start(next: { [weak self] message in
|
||
guard let strongSelf = self, let message = message else {
|
||
return
|
||
}
|
||
|
||
var mediaReference: AnyMediaReference?
|
||
for m in message.media {
|
||
if let image = m as? TelegramMediaImage {
|
||
mediaReference = AnyMediaReference.standalone(media: image)
|
||
}
|
||
}
|
||
|
||
if let mediaReference = mediaReference, let peer = message.peers[message.id.peerId] {
|
||
legacyMediaEditor(context: strongSelf.context, peer: peer, media: mediaReference, initialCaption: "", snapshots: snapshots, transitionCompletion: {
|
||
transitionCompletion()
|
||
}, presentStickers: { [weak self] completion in
|
||
if let strongSelf = self {
|
||
let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in
|
||
completion(fileReference.media, fileReference.media.isAnimatedSticker, node.view, rect)
|
||
return true
|
||
})
|
||
strongSelf.present(controller, in: .window(.root))
|
||
return controller
|
||
} else {
|
||
return nil
|
||
}
|
||
}, sendMessagesWithSignals: { [weak self] signals, _, _ in
|
||
if let strongSelf = self {
|
||
strongSelf.enqueueMediaMessages(signals: signals, silentPosting: false)
|
||
}
|
||
}, present: { [weak self] c, a in
|
||
self?.present(c, in: .window(.root), with: a)
|
||
})
|
||
}
|
||
})
|
||
})))
|
||
}, openPeer: { [weak self] id, navigation, fromMessage in
|
||
self?.openPeer(peerId: id, navigation: navigation, fromMessage: fromMessage)
|
||
}, openPeerMention: { [weak self] name in
|
||
self?.openPeerMention(name)
|
||
}, openMessageContextMenu: { [weak self] message, selectAll, node, frame, anyRecognizer in
|
||
guard let strongSelf = self, strongSelf.isNodeLoaded else {
|
||
return
|
||
}
|
||
if strongSelf.presentationInterfaceState.interfaceState.selectionState != nil {
|
||
return
|
||
}
|
||
|
||
strongSelf.dismissAllTooltips()
|
||
|
||
let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer
|
||
let gesture: ContextGesture? = anyRecognizer as? ContextGesture
|
||
if let messages = strongSelf.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) {
|
||
(strongSelf.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures()
|
||
strongSelf.chatDisplayNode.cancelInteractiveKeyboardGestures()
|
||
var updatedMessages = messages
|
||
for i in 0 ..< updatedMessages.count {
|
||
if updatedMessages[i].id == message.id {
|
||
let message = updatedMessages.remove(at: i)
|
||
updatedMessages.insert(message, at: 0)
|
||
break
|
||
}
|
||
}
|
||
let _ = combineLatest(queue: .mainQueue(), contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction), loadedStickerPack(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, reference: .animatedEmoji, forceActualized: false), ApplicationSpecificNotice.getChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager)
|
||
).start(next: { actions, animatedEmojiStickers, chatTextSelectionTips in
|
||
guard let strongSelf = self, !actions.isEmpty else {
|
||
return
|
||
}
|
||
var reactionItems: [ReactionContextItem] = []
|
||
|
||
/*var hasLike = false
|
||
let hearts: [String] = ["❤", "❤️"]
|
||
for attribute in messages[0].attributes {
|
||
if let attribute = attribute as? ReactionsMessageAttribute {
|
||
for reaction in attribute.reactions {
|
||
if hearts.contains(reaction.value) {
|
||
if reaction.isSelected {
|
||
hasLike = true
|
||
}
|
||
}
|
||
}
|
||
} else if let attribute = attribute as? PendingReactionsMessageAttribute {
|
||
if let value = attribute.value, hearts.contains(value) {
|
||
hasLike = true
|
||
}
|
||
}
|
||
}
|
||
|
||
if hasLike {
|
||
reactionItems.append(ReactionContextItem(reaction: .unlike))
|
||
} else {
|
||
reactionItems.append(ReactionContextItem(reaction: .like))
|
||
}*/
|
||
|
||
if Namespaces.Message.allScheduled.contains(message.id.namespace) || message.id.peerId.namespace == Namespaces.Peer.SecretChat {
|
||
reactionItems = []
|
||
}
|
||
|
||
let numberOfComponents = message.text.components(separatedBy: CharacterSet.whitespacesAndNewlines).count
|
||
let displayTextSelectionTip = numberOfComponents >= 3 && !message.text.isEmpty && chatTextSelectionTips < 3
|
||
if displayTextSelectionTip {
|
||
let _ = ApplicationSpecificNotice.incrementChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager).start()
|
||
}
|
||
|
||
let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, message: message, selectAll: selectAll)), items: .single(actions), reactionItems: reactionItems, recognizer: recognizer, gesture: gesture, displayTextSelectionTip: displayTextSelectionTip)
|
||
strongSelf.currentContextController = controller
|
||
controller.reactionSelected = { [weak controller] value in
|
||
guard let strongSelf = self, let message = updatedMessages.first else {
|
||
return
|
||
}
|
||
let hearts: [String] = ["❤", "❤️"]
|
||
strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in
|
||
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item {
|
||
if item.message.id == message.id {
|
||
switch value {
|
||
case .like:
|
||
itemNode.awaitingAppliedReaction = (hearts[0], { [weak itemNode] in
|
||
guard let controller = controller else {
|
||
return
|
||
}
|
||
if let itemNode = itemNode, let (targetEmptyNode, targetFilledNode) = itemNode.targetReactionNode(value: hearts[0]) {
|
||
controller.dismissWithReaction(value: hearts[0], targetEmptyNode: targetEmptyNode, targetFilledNode: targetFilledNode, hideNode: true, completion: {
|
||
})
|
||
} else {
|
||
controller.dismiss()
|
||
}
|
||
})
|
||
case .unlike:
|
||
itemNode.awaitingAppliedReaction = (nil, {
|
||
guard let controller = controller else {
|
||
return
|
||
}
|
||
controller.dismiss()
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
switch value {
|
||
case .like:
|
||
let _ = updateMessageReactionsInteractively(postbox: strongSelf.context.account.postbox, messageId: message.id, reaction: hearts[0]).start()
|
||
case .unlike:
|
||
let _ = updateMessageReactionsInteractively(postbox: strongSelf.context.account.postbox, messageId: message.id, reaction: nil).start()
|
||
}
|
||
}
|
||
strongSelf.forEachController({ controller in
|
||
if let controller = controller as? TooltipScreen {
|
||
controller.dismiss()
|
||
}
|
||
return true
|
||
})
|
||
strongSelf.window?.presentInGlobalOverlay(controller)
|
||
})
|
||
}
|
||
}, openMessageContextActions: { message, node, rect, gesture in
|
||
gesture?.cancel()
|
||
}, navigateToMessage: { [weak self] fromId, id in
|
||
self?.navigateToMessage(from: fromId, to: .id(id), forceInCurrentChat: fromId.peerId == id.peerId)
|
||
}, navigateToMessageStandalone: { [weak self] id in
|
||
self?.navigateToMessage(from: nil, to: .id(id), forceInCurrentChat: false)
|
||
}, tapMessage: nil, clickThroughMessage: { [weak self] in
|
||
self?.chatDisplayNode.dismissInput()
|
||
}, toggleMessagesSelection: { [weak self] ids, value in
|
||
guard let strongSelf = self, strongSelf.isNodeLoaded else {
|
||
return
|
||
}
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withToggledSelectedMessages(ids, value: value) } })
|
||
if let selectionState = strongSelf.presentationInterfaceState.interfaceState.selectionState {
|
||
let count = selectionState.selectedIds.count
|
||
let text: String
|
||
if count == 1 {
|
||
text = "1 message selected"
|
||
} else {
|
||
text = "\(count) messages selected"
|
||
}
|
||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: {
|
||
UIAccessibility.post(notification: UIAccessibility.Notification.announcement, argument: text as NSString)
|
||
})
|
||
}
|
||
}, sendCurrentMessage: { [weak self] silentPosting in
|
||
if let strongSelf = self {
|
||
if let _ = strongSelf.presentationInterfaceState.recordedMediaPreview {
|
||
strongSelf.sendMediaRecording(silentPosting: silentPosting)
|
||
} else {
|
||
strongSelf.chatDisplayNode.sendCurrentMessage(silentPosting: silentPosting)
|
||
}
|
||
}
|
||
}, sendMessage: { [weak self] text in
|
||
guard let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) else {
|
||
return
|
||
}
|
||
|
||
var isScheduledMessages = false
|
||
if case .scheduledMessages = strongSelf.presentationInterfaceState.subject {
|
||
isScheduledMessages = true
|
||
}
|
||
|
||
guard !isScheduledMessages else {
|
||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.ScheduledMessages_BotActionUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||
return
|
||
}
|
||
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, enabledTypes: .all)
|
||
if !entities.isEmpty {
|
||
attributes.append(TextEntitiesMessageAttribute(entities: entities))
|
||
}
|
||
strongSelf.sendMessages([.message(text: text, attributes: attributes, mediaReference: nil, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)])
|
||
}, sendSticker: { [weak self] fileReference, query, clearInput, sourceNode, sourceRect in
|
||
guard let strongSelf = self else {
|
||
return false
|
||
}
|
||
|
||
if let _ = strongSelf.presentationInterfaceState.slowmodeState, strongSelf.presentationInterfaceState.subject != .scheduledMessages {
|
||
strongSelf.interfaceInteraction?.displaySlowmodeTooltip(sourceNode, sourceRect)
|
||
return false
|
||
}
|
||
|
||
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in
|
||
var current = current
|
||
current = current.updatedInterfaceState { interfaceState in
|
||
var interfaceState = interfaceState
|
||
interfaceState = interfaceState.withUpdatedReplyMessageId(nil)
|
||
if clearInput {
|
||
interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString()))
|
||
}
|
||
return interfaceState
|
||
}.updatedInputMode { current in
|
||
if case let .media(mode, maybeExpanded) = current, maybeExpanded != nil {
|
||
return .media(mode: mode, expanded: nil)
|
||
}
|
||
return current
|
||
}
|
||
|
||
return current
|
||
})
|
||
}
|
||
})
|
||
|
||
var attributes: [MessageAttribute] = []
|
||
if let query = query {
|
||
attributes.append(EmojiSearchQueryMessageAttribute(query: query))
|
||
}
|
||
|
||
strongSelf.sendMessages([.message(text: "", attributes: attributes, mediaReference: fileReference.abstract, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)])
|
||
return true
|
||
}, sendGif: { [weak self] fileReference, sourceNode, sourceRect in
|
||
if let strongSelf = self {
|
||
if let _ = strongSelf.presentationInterfaceState.slowmodeState, strongSelf.presentationInterfaceState.subject != .scheduledMessages {
|
||
strongSelf.interfaceInteraction?.displaySlowmodeTooltip(sourceNode, sourceRect)
|
||
return false
|
||
}
|
||
|
||
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }.updatedInputMode { current in
|
||
if case let .media(mode, maybeExpanded) = current, maybeExpanded != nil {
|
||
return .media(mode: mode, expanded: nil)
|
||
}
|
||
return current
|
||
}
|
||
})
|
||
}
|
||
})
|
||
strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: fileReference.abstract, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)])
|
||
}
|
||
return true
|
||
}, sendBotContextResultAsGif: { [weak self] collection, result, sourceNode, sourceRect in
|
||
guard let strongSelf = self else {
|
||
return false
|
||
}
|
||
if case .pinnedMessages = strongSelf.presentationInterfaceState.subject {
|
||
return false
|
||
}
|
||
if let _ = strongSelf.presentationInterfaceState.slowmodeState, strongSelf.presentationInterfaceState.subject != .scheduledMessages {
|
||
strongSelf.interfaceInteraction?.displaySlowmodeTooltip(sourceNode, sourceRect)
|
||
return false
|
||
}
|
||
|
||
strongSelf.enqueueChatContextResult(collection, result, hideVia: true, closeMediaInput: true)
|
||
|
||
return true
|
||
}, requestMessageActionCallback: { [weak self] messageId, data, isGame, requiresPassword in
|
||
if let strongSelf = self {
|
||
guard strongSelf.presentationInterfaceState.subject != .scheduledMessages else {
|
||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.ScheduledMessages_BotActionUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||
return
|
||
}
|
||
|
||
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
|
||
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.sorted()
|
||
}
|
||
return $0
|
||
}
|
||
})
|
||
|
||
let proceedWithResult: (MessageActionCallbackResult) -> Void = { [weak self] result in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
|
||
switch result {
|
||
case .none:
|
||
break
|
||
case let .alert(text):
|
||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||
case let .toast(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):
|
||
if isGame {
|
||
let openBot: () -> Void = {
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
strongSelf.effectiveNavigationController?.pushViewController(GameController(context: strongSelf.context, url: url, message: message))
|
||
}
|
||
|
||
if let botPeer = message.author as? TelegramUser, botPeer.flags.contains(.isVerified) {
|
||
openBot()
|
||
} else {
|
||
let _ = (ApplicationSpecificNotice.getBotGameNotice(accountManager: strongSelf.context.sharedContext.accountManager, peerId: message.id.peerId)
|
||
|> deliverOnMainQueue).start(next: { value in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
if value {
|
||
openBot()
|
||
} else if let botPeer = message.author as? TelegramUser {
|
||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Conversation_BotInteractiveUrlAlert(botPeer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
|
||
if let strongSelf = self {
|
||
let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: strongSelf.context.sharedContext.accountManager, peerId: botPeer.id).start()
|
||
openBot()
|
||
}
|
||
})]), in: .window(.root), with: nil)
|
||
}
|
||
})
|
||
}
|
||
} else {
|
||
strongSelf.openUrl(url, concealed: false)
|
||
}
|
||
}
|
||
}
|
||
|
||
let updateProgress = { [weak self] in
|
||
Queue.mainQueue().async {
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
return $0.updatedTitlePanelContext {
|
||
if let index = $0.firstIndex(where: {
|
||
switch $0 {
|
||
case .requestInProgress:
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}) {
|
||
var updatedContexts = $0
|
||
updatedContexts.remove(at: index)
|
||
return updatedContexts
|
||
}
|
||
return $0
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
let account = strongSelf.context.account
|
||
if requiresPassword {
|
||
strongSelf.messageActionCallbackDisposable.set(((requestMessageActionCallbackPasswordCheck(account: account, messageId: messageId, isGame: isGame, data: data)
|
||
|> afterDisposed {
|
||
updateProgress()
|
||
})
|
||
|> deliverOnMainQueue).start(error: { error in
|
||
let controller = ownershipTransferController(context: context, initialError: error, present: { c, a in
|
||
strongSelf.present(c, in: .window(.root), with: a)
|
||
}, commit: { password in
|
||
return requestMessageActionCallback(account: account, messageId: messageId, isGame: isGame, password: password, data: data)
|
||
|> afterDisposed {
|
||
updateProgress()
|
||
}
|
||
}, completion: { result in
|
||
proceedWithResult(result)
|
||
})
|
||
strongSelf.present(controller, in: .window(.root))
|
||
}))
|
||
} else {
|
||
strongSelf.messageActionCallbackDisposable.set(((requestMessageActionCallback(account: account, messageId: messageId, isGame: isGame, password: nil, data: data)
|
||
|> afterDisposed {
|
||
updateProgress()
|
||
})
|
||
|> deliverOnMainQueue).start(next: { result in
|
||
proceedWithResult(result)
|
||
}))
|
||
}
|
||
}
|
||
}
|
||
}, requestMessageActionUrlAuth: { [weak self] defaultUrl, messageId, buttonId in
|
||
if let strongSelf = self {
|
||
guard strongSelf.presentationInterfaceState.subject != .scheduledMessages else {
|
||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.ScheduledMessages_BotActionUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||
return
|
||
}
|
||
if let _ = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
|
||
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.sorted()
|
||
}
|
||
return $0
|
||
}
|
||
})
|
||
strongSelf.messageActionUrlAuthDisposable.set(((combineLatest(strongSelf.context.account.postbox.loadedPeerWithId(strongSelf.context.account.peerId), requestMessageActionUrlAuth(account: strongSelf.context.account, messageId: messageId, buttonId: buttonId) |> afterDisposed {
|
||
Queue.mainQueue().async {
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
return $0.updatedTitlePanelContext {
|
||
if let index = $0.firstIndex(where: {
|
||
switch $0 {
|
||
case .requestInProgress:
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}) {
|
||
var updatedContexts = $0
|
||
updatedContexts.remove(at: index)
|
||
return updatedContexts
|
||
}
|
||
return $0
|
||
}
|
||
})
|
||
}
|
||
}
|
||
})) |> deliverOnMainQueue).start(next: { peer, result in
|
||
if let strongSelf = self {
|
||
switch result {
|
||
case .default:
|
||
strongSelf.openUrl(defaultUrl, concealed: false)
|
||
case let .request(domain, bot, requestWriteAccess):
|
||
let controller = chatMessageActionUrlAuthController(context: strongSelf.context, defaultUrl: defaultUrl, domain: domain, bot: bot, requestWriteAccess: requestWriteAccess, displayName: peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), open: { [weak self] authorize, allowWriteAccess in
|
||
if let strongSelf = self {
|
||
if authorize {
|
||
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.sorted()
|
||
}
|
||
return $0
|
||
}
|
||
})
|
||
|
||
strongSelf.messageActionUrlAuthDisposable.set(((acceptMessageActionUrlAuth(account: strongSelf.context.account, messageId: messageId, buttonId: buttonId, allowWriteAccess: allowWriteAccess) |> afterDisposed {
|
||
Queue.mainQueue().async {
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
return $0.updatedTitlePanelContext {
|
||
if let index = $0.firstIndex(where: {
|
||
switch $0 {
|
||
case .requestInProgress:
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}) {
|
||
var updatedContexts = $0
|
||
updatedContexts.remove(at: index)
|
||
return updatedContexts
|
||
}
|
||
return $0
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}) |> deliverOnMainQueue).start(next: { [weak self] result in
|
||
if let strongSelf = self {
|
||
switch result {
|
||
case let .accepted(url):
|
||
strongSelf.openUrl(url, concealed: false)
|
||
default:
|
||
strongSelf.openUrl(defaultUrl, concealed: false)
|
||
}
|
||
}
|
||
}))
|
||
} else {
|
||
strongSelf.openUrl(defaultUrl, concealed: false)
|
||
}
|
||
}
|
||
})
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
strongSelf.present(controller, in: .window(.root))
|
||
case let .accepted(url):
|
||
strongSelf.openUrl(url, concealed: false)
|
||
}
|
||
}
|
||
}))
|
||
}
|
||
}
|
||
}, activateSwitchInline: { [weak self] peerId, inputString in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
guard strongSelf.presentationInterfaceState.subject != .scheduledMessages else {
|
||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.ScheduledMessages_BotActionUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||
return
|
||
}
|
||
if let botStart = strongSelf.botStart, case let .automatic(returnToPeerId, scheduled) = botStart.behavior {
|
||
strongSelf.openPeer(peerId: returnToPeerId, navigation: .chat(textInputState: ChatTextInputState(inputText: NSAttributedString(string: inputString)), subject: scheduled ? .scheduledMessages : nil, peekData: nil), fromMessage: nil)
|
||
} else {
|
||
strongSelf.openPeer(peerId: peerId, navigation: .chat(textInputState: ChatTextInputState(inputText: NSAttributedString(string: inputString)), subject: nil, peekData: nil), fromMessage: nil)
|
||
}
|
||
}, openUrl: { [weak self] url, concealed, _, message in
|
||
if let strongSelf = self {
|
||
strongSelf.openUrl(url, concealed: concealed, message: message)
|
||
}
|
||
}, shareCurrentLocation: { [weak self] in
|
||
if let strongSelf = self {
|
||
if case .pinnedMessages = strongSelf.presentationInterfaceState.subject {
|
||
return
|
||
}
|
||
guard strongSelf.presentationInterfaceState.subject != .scheduledMessages else {
|
||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.ScheduledMessages_BotActionUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||
return
|
||
}
|
||
strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.Conversation_ShareBotLocationConfirmationTitle, text: strongSelf.presentationData.strings.Conversation_ShareBotLocationConfirmation, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
|
||
if let strongSelf = self, let locationManager = strongSelf.context.sharedContext.locationManager {
|
||
let _ = (currentLocationManagerCoordinate(manager: locationManager, timeout: 5.0)
|
||
|> deliverOnMainQueue).start(next: { coordinate in
|
||
if let strongSelf = self {
|
||
if let coordinate = coordinate {
|
||
strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)), replyToMessageId: nil, localGroupingKey: nil)])
|
||
} else {
|
||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})]), in: .window(.root))
|
||
}
|
||
}
|
||
})
|
||
}
|
||
})]), in: .window(.root))
|
||
}
|
||
}, shareAccountContact: { [weak self] in
|
||
if let strongSelf = self {
|
||
if case .pinnedMessages = strongSelf.presentationInterfaceState.subject {
|
||
return
|
||
}
|
||
|
||
guard strongSelf.presentationInterfaceState.subject != .scheduledMessages else {
|
||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.ScheduledMessages_BotActionUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||
return
|
||
}
|
||
strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.Conversation_ShareBotContactConfirmationTitle, text: strongSelf.presentationData.strings.Conversation_ShareBotContactConfirmation, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
|
||
if let strongSelf = self {
|
||
let _ = (strongSelf.context.account.postbox.loadedPeerWithId(strongSelf.context.account.peerId)
|
||
|> deliverOnMainQueue).start(next: { peer in
|
||
if let peer = peer as? TelegramUser, let phone = peer.phone, !phone.isEmpty {
|
||
strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil)), replyToMessageId: nil, localGroupingKey: nil)])
|
||
}
|
||
})
|
||
}
|
||
})]), in: .window(.root))
|
||
}
|
||
}, sendBotCommand: { [weak self] messageId, command in
|
||
if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) {
|
||
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({})
|
||
var postAsReply = false
|
||
if !command.contains("@") {
|
||
switch strongSelf.chatLocation {
|
||
case let .peer(peerId):
|
||
if (peerId.namespace == Namespaces.Peer.CloudChannel || peerId.namespace == Namespaces.Peer.CloudGroup) {
|
||
postAsReply = true
|
||
}
|
||
case .replyThread:
|
||
postAsReply = true
|
||
}
|
||
}
|
||
|
||
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) }
|
||
})
|
||
}
|
||
})
|
||
var attributes: [MessageAttribute] = []
|
||
let entities = generateTextEntities(command, enabledTypes: .all)
|
||
if !entities.isEmpty {
|
||
attributes.append(TextEntitiesMessageAttribute(entities: entities))
|
||
}
|
||
strongSelf.sendMessages([.message(text: command, attributes: attributes, mediaReference: nil, replyToMessageId: (postAsReply && messageId != nil) ? messageId! : nil, localGroupingKey: nil)])
|
||
}
|
||
}, openInstantPage: { [weak self] message, associatedData in
|
||
if let strongSelf = self, strongSelf.isNodeLoaded, let navigationController = strongSelf.effectiveNavigationController, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) {
|
||
openChatInstantPage(context: strongSelf.context, message: message, sourcePeerType: associatedData?.automaticDownloadPeerType, navigationController: navigationController)
|
||
|
||
if case .overlay = strongSelf.presentationInterfaceState.mode {
|
||
strongSelf.chatDisplayNode.dismissAsOverlay()
|
||
}
|
||
}
|
||
}, openWallpaper: { [weak self] message in
|
||
if let strongSelf = self, strongSelf.isNodeLoaded, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) {
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
openChatWallpaper(context: strongSelf.context, message: message, present: { [weak self] c, a in
|
||
self?.present(c, in: .window(.root), with: a, blockInteraction: true)
|
||
})
|
||
}
|
||
}, openTheme: { [weak self] message in
|
||
if let strongSelf = self, strongSelf.isNodeLoaded, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) {
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
openChatTheme(context: strongSelf.context, message: message, pushController: { [weak self] c in
|
||
(self?.navigationController as? NavigationController)?.pushViewController(c)
|
||
}, present: { [weak self] c, a in
|
||
self?.present(c, in: .window(.root), with: a, blockInteraction: true)
|
||
})
|
||
}
|
||
}, openHashtag: { [weak self] peerName, hashtag in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
|
||
if strongSelf.resolvePeerByNameDisposable == nil {
|
||
strongSelf.resolvePeerByNameDisposable = MetaDisposable()
|
||
}
|
||
let account = strongSelf.context.account
|
||
var resolveSignal: Signal<Peer?, NoError>
|
||
if let peerName = peerName {
|
||
resolveSignal = resolvePeerByName(account: strongSelf.context.account, name: peerName)
|
||
|> mapToSignal { peerId -> Signal<Peer?, NoError> in
|
||
if let peerId = peerId {
|
||
return account.postbox.loadedPeerWithId(peerId)
|
||
|> map(Optional.init)
|
||
} else {
|
||
return .single(nil)
|
||
}
|
||
}
|
||
} else {
|
||
resolveSignal = context.account.postbox.loadedPeerWithId(strongSelf.chatLocation.peerId)
|
||
|> map(Optional.init)
|
||
}
|
||
var cancelImpl: (() -> Void)?
|
||
let presentationData = strongSelf.presentationData
|
||
let progressSignal = Signal<Never, NoError> { subscriber in
|
||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||
cancelImpl?()
|
||
}))
|
||
self?.present(controller, in: .window(.root))
|
||
return ActionDisposable { [weak controller] in
|
||
Queue.mainQueue().async() {
|
||
controller?.dismiss()
|
||
}
|
||
}
|
||
}
|
||
|> runOn(Queue.mainQueue())
|
||
|> delay(0.15, queue: Queue.mainQueue())
|
||
let progressDisposable = progressSignal.start()
|
||
|
||
resolveSignal = resolveSignal
|
||
|> afterDisposed {
|
||
Queue.mainQueue().async {
|
||
progressDisposable.dispose()
|
||
}
|
||
}
|
||
cancelImpl = {
|
||
self?.resolvePeerByNameDisposable?.set(nil)
|
||
}
|
||
strongSelf.resolvePeerByNameDisposable?.set((resolveSignal
|
||
|> deliverOnMainQueue).start(next: { peer in
|
||
if let strongSelf = self, !hashtag.isEmpty {
|
||
let searchController = HashtagSearchController(context: strongSelf.context, peer: peer, query: hashtag)
|
||
strongSelf.effectiveNavigationController?.pushViewController(searchController)
|
||
}
|
||
}))
|
||
})
|
||
}, updateInputState: { [weak self] f in
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
return $0.updatedInterfaceState {
|
||
let updatedState: ChatTextInputState
|
||
if canSendMessagesToChat(strongSelf.presentationInterfaceState) {
|
||
updatedState = f($0.effectiveInputState)
|
||
} else {
|
||
updatedState = ChatTextInputState()
|
||
}
|
||
return $0.withUpdatedEffectiveInputState(updatedState)
|
||
}
|
||
})
|
||
}
|
||
}, updateInputMode: { [weak self] f in
|
||
self?.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
return $0.updatedInputMode(f)
|
||
})
|
||
}, openMessageShareMenu: { [weak self] id in
|
||
if let strongSelf = self, let messages = strongSelf.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(id), let _ = messages.first {
|
||
let shareController = ShareController(context: strongSelf.context, subject: .messages(messages))
|
||
shareController.dismissed = { [weak self] shared in
|
||
if shared {
|
||
self?.commitPurposefulAction()
|
||
}
|
||
}
|
||
shareController.completed = { [weak self] peerIds in
|
||
if let strongSelf = self {
|
||
let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Peer] in
|
||
var peers: [Peer] = []
|
||
for peerId in peerIds {
|
||
if let peer = transaction.getPeer(peerId) {
|
||
peers.append(peer)
|
||
}
|
||
}
|
||
return peers
|
||
} |> deliverOnMainQueue).start(next: { [weak self] peers in
|
||
if let strongSelf = self {
|
||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||
|
||
let text: String
|
||
var savedMessages = false
|
||
if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId {
|
||
text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many
|
||
savedMessages = true
|
||
} else {
|
||
if peers.count == 1, let peer = peers.first {
|
||
let peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||
text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).0 : presentationData.strings.Conversation_ForwardTooltip_Chat_Many(peerName).0
|
||
} else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last {
|
||
let firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||
let secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||
text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).0 : presentationData.strings.Conversation_ForwardTooltip_TwoChats_Many(firstPeerName, secondPeerName).0
|
||
} else if let peer = peers.first {
|
||
let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||
text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").0 : presentationData.strings.Conversation_ForwardTooltip_ManyChats_Many(peerName, "\(peers.count - 1)").0
|
||
} else {
|
||
text = ""
|
||
}
|
||
}
|
||
|
||
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||
}
|
||
})
|
||
}
|
||
}
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
strongSelf.present(shareController, in: .window(.root), blockInteraction: true)
|
||
}
|
||
}, presentController: { [weak self] controller, arguments in
|
||
self?.present(controller, in: .window(.root), with: arguments)
|
||
}, navigationController: { [weak self] in
|
||
return self?.navigationController as? NavigationController
|
||
}, chatControllerNode: { [weak self] in
|
||
return self?.chatDisplayNode
|
||
}, reactionContainerNode: { [weak self] in
|
||
return self?.chatDisplayNode.reactionContainerNode
|
||
}, presentGlobalOverlayController: { [weak self] controller, arguments in
|
||
self?.presentInGlobalOverlay(controller, with: arguments)
|
||
}, callPeer: { [weak self] peerId, isVideo in
|
||
if let strongSelf = self {
|
||
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
|
||
strongSelf.commitPurposefulAction()
|
||
|
||
let _ = (context.account.viewTracker.peerView(peerId)
|
||
|> take(1)
|
||
|> map { view -> Peer? in
|
||
return peerViewMainPeer(view)
|
||
}
|
||
|> deliverOnMainQueue).start(next: { peer in
|
||
guard let peer = peer else {
|
||
return
|
||
}
|
||
|
||
if let cachedUserData = strongSelf.peerView?.cachedData as? CachedUserData, cachedUserData.callsPrivate {
|
||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||
|
||
strongSelf.present(textAlertController(context: strongSelf.context, title: presentationData.strings.Call_ConnectionErrorTitle, text: presentationData.strings.Call_PrivacyErrorMessage(peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||
return
|
||
}
|
||
|
||
context.requestCall(peerId: peer.id, isVideo: isVideo, completion: {})
|
||
})
|
||
})
|
||
}
|
||
}, longTap: { [weak self] action, message in
|
||
if let strongSelf = self {
|
||
switch action {
|
||
case let .url(url):
|
||
var (cleanUrl, _) = parseUrl(url: url, wasConcealed: false)
|
||
var canAddToReadingList = true
|
||
var canOpenIn = availableOpenInOptions(context: strongSelf.context, item: .url(url: url)).count > 1
|
||
let mailtoString = "mailto:"
|
||
let telString = "tel:"
|
||
var openText = strongSelf.presentationData.strings.Conversation_LinkDialogOpen
|
||
var phoneNumber: String?
|
||
if cleanUrl.hasPrefix(mailtoString) {
|
||
canAddToReadingList = false
|
||
cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: mailtoString.distance(from: mailtoString.startIndex, to: mailtoString.endIndex))...])
|
||
} else if cleanUrl.hasPrefix(telString) {
|
||
canAddToReadingList = false
|
||
phoneNumber = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: telString.distance(from: telString.startIndex, to: telString.endIndex))...])
|
||
cleanUrl = phoneNumber!
|
||
openText = strongSelf.presentationData.strings.UserInfo_PhoneCall
|
||
canOpenIn = false
|
||
} else if canOpenIn {
|
||
openText = strongSelf.presentationData.strings.Conversation_FileOpenIn
|
||
}
|
||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
|
||
|
||
var items: [ActionSheetItem] = []
|
||
items.append(ActionSheetTextItem(title: cleanUrl))
|
||
items.append(ActionSheetButtonItem(title: openText, color: .accent, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
if let strongSelf = self {
|
||
if canOpenIn {
|
||
strongSelf.openUrlIn(url)
|
||
} else {
|
||
strongSelf.openUrl(url, concealed: false)
|
||
}
|
||
}
|
||
}))
|
||
if let phoneNumber = phoneNumber {
|
||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddContact, color: .accent, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
if let strongSelf = self {
|
||
strongSelf.controllerInteraction?.addContact(phoneNumber)
|
||
}
|
||
}))
|
||
}
|
||
items.append(ActionSheetButtonItem(title: canAddToReadingList ? strongSelf.presentationData.strings.ShareMenu_CopyShareLink : strongSelf.presentationData.strings.Conversation_ContextMenuCopy, color: .accent, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
UIPasteboard.general.string = cleanUrl
|
||
}))
|
||
if canAddToReadingList {
|
||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
if let link = URL(string: url) {
|
||
let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil)
|
||
}
|
||
}))
|
||
}
|
||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
})
|
||
])])
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
strongSelf.present(actionSheet, in: .window(.root))
|
||
case let .peerMention(peerId, mention):
|
||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
|
||
var items: [ActionSheetItem] = []
|
||
if !mention.isEmpty {
|
||
items.append(ActionSheetTextItem(title: mention))
|
||
}
|
||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
if let strongSelf = self {
|
||
strongSelf.openPeer(peerId: peerId, navigation: .chat(textInputState: nil, subject: nil, peekData: nil), fromMessage: nil)
|
||
}
|
||
}))
|
||
if !mention.isEmpty {
|
||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
UIPasteboard.general.string = mention
|
||
}))
|
||
}
|
||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
})
|
||
])])
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
strongSelf.present(actionSheet, in: .window(.root))
|
||
case let .mention(mention):
|
||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
|
||
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
||
ActionSheetTextItem(title: mention),
|
||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
if let strongSelf = self {
|
||
strongSelf.openPeerMention(mention)
|
||
}
|
||
}),
|
||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
UIPasteboard.general.string = mention
|
||
})
|
||
]), ActionSheetItemGroup(items: [
|
||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
})
|
||
])])
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
strongSelf.present(actionSheet, in: .window(.root))
|
||
case let .command(command):
|
||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
|
||
var items: [ActionSheetItem] = []
|
||
items.append(ActionSheetTextItem(title: command))
|
||
if canSendMessagesToChat(strongSelf.presentationInterfaceState) {
|
||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ShareMenu_Send, color: .accent, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
if let strongSelf = self {
|
||
strongSelf.sendMessages([.message(text: command, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)])
|
||
}
|
||
}))
|
||
}
|
||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
UIPasteboard.general.string = command
|
||
}))
|
||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
})
|
||
])])
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
strongSelf.present(actionSheet, in: .window(.root))
|
||
case let .hashtag(hashtag):
|
||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
|
||
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
||
ActionSheetTextItem(title: hashtag),
|
||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
if let strongSelf = self {
|
||
let peerSignal: Signal<Peer?, NoError>
|
||
peerSignal = strongSelf.context.account.postbox.loadedPeerWithId(strongSelf.chatLocation.peerId)
|
||
|> map(Optional.init)
|
||
let _ = (peerSignal
|
||
|> deliverOnMainQueue).start(next: { peer in
|
||
if let strongSelf = self {
|
||
let searchController = HashtagSearchController(context: strongSelf.context, peer: peer, query: hashtag)
|
||
strongSelf.effectiveNavigationController?.pushViewController(searchController)
|
||
}
|
||
})
|
||
}
|
||
}),
|
||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
UIPasteboard.general.string = hashtag
|
||
})
|
||
]), ActionSheetItemGroup(items: [
|
||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
})
|
||
])])
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
strongSelf.present(actionSheet, in: .window(.root))
|
||
case let .timecode(timecode, text):
|
||
guard let message = message else {
|
||
return
|
||
}
|
||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
|
||
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
||
ActionSheetTextItem(title: text),
|
||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
if let strongSelf = self {
|
||
strongSelf.controllerInteraction?.seekToTimecode(message, timecode, true)
|
||
}
|
||
}),
|
||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
UIPasteboard.general.string = text
|
||
})
|
||
]), ActionSheetItemGroup(items: [
|
||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
})
|
||
])])
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
strongSelf.present(actionSheet, in: .window(.root))
|
||
case let .bankCard(number):
|
||
guard let message = message else {
|
||
return
|
||
}
|
||
|
||
var signal = getBankCardInfo(account: strongSelf.context.account, cardNumber: number)
|
||
let disposable: MetaDisposable
|
||
if let current = strongSelf.bankCardDisposable {
|
||
disposable = current
|
||
} else {
|
||
disposable = MetaDisposable()
|
||
strongSelf.bankCardDisposable = disposable
|
||
}
|
||
|
||
var cancelImpl: (() -> Void)?
|
||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||
let progressSignal = Signal<Never, NoError> { subscriber in
|
||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||
cancelImpl?()
|
||
}))
|
||
strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||
return ActionDisposable { [weak controller] in
|
||
Queue.mainQueue().async() {
|
||
controller?.dismiss()
|
||
}
|
||
}
|
||
}
|
||
|> runOn(Queue.mainQueue())
|
||
|> delay(0.15, queue: Queue.mainQueue())
|
||
let progressDisposable = progressSignal.start()
|
||
|
||
signal = signal
|
||
|> afterDisposed {
|
||
Queue.mainQueue().async {
|
||
progressDisposable.dispose()
|
||
}
|
||
}
|
||
cancelImpl = {
|
||
disposable.set(nil)
|
||
}
|
||
disposable.set((signal
|
||
|> deliverOnMainQueue).start(next: { [weak self] info in
|
||
if let strongSelf = self, let info = info {
|
||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
|
||
var items: [ActionSheetItem] = []
|
||
items.append(ActionSheetTextItem(title: info.title))
|
||
for url in info.urls {
|
||
items.append(ActionSheetButtonItem(title: url.title, color: .accent, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
if let strongSelf = self {
|
||
strongSelf.controllerInteraction?.openUrl(url.url, false, false, message)
|
||
}
|
||
}))
|
||
}
|
||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
UIPasteboard.general.string = number
|
||
}))
|
||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
})
|
||
])])
|
||
strongSelf.present(actionSheet, in: .window(.root))
|
||
}
|
||
}))
|
||
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
}
|
||
}
|
||
}, openCheckoutOrReceipt: { [weak self] messageId in
|
||
if let strongSelf = self {
|
||
strongSelf.commitPurposefulAction()
|
||
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
|
||
for media in message.media {
|
||
if let invoice = media as? TelegramMediaInvoice {
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
if let receiptMessageId = invoice.receiptMessageId {
|
||
strongSelf.present(BotReceiptController(context: strongSelf.context, invoice: invoice, messageId: receiptMessageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||
} else {
|
||
strongSelf.present(BotCheckoutController(context: strongSelf.context, invoice: invoice, messageId: messageId), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}, openSearch: {
|
||
}, setupReply: { [weak self] messageId in
|
||
self?.interfaceInteraction?.setupReplyMessage(messageId, { _ in })
|
||
}, canSetupReply: { [weak self] message in
|
||
if !message.flags.contains(.Incoming) {
|
||
if !message.flags.intersection([.Failed, .Sending, .Unsent]).isEmpty {
|
||
return .none
|
||
}
|
||
}
|
||
if let strongSelf = self {
|
||
if case let .replyThread(replyThreadMessage) = strongSelf.chatLocation, replyThreadMessage.messageId == message.id {
|
||
return .none
|
||
}
|
||
|
||
if canReplyInChat(strongSelf.presentationInterfaceState) {
|
||
return .reply
|
||
} else if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
|
||
/*var hasLike = false
|
||
let hearts: [String] = ["❤", "❤️"]
|
||
for attribute in message.attributes {
|
||
if let attribute = attribute as? ReactionsMessageAttribute {
|
||
for reaction in attribute.reactions {
|
||
if hearts.contains(reaction.value) {
|
||
if reaction.isSelected {
|
||
hasLike = true
|
||
}
|
||
}
|
||
}
|
||
} else if let attribute = attribute as? PendingReactionsMessageAttribute {
|
||
if let value = attribute.value, hearts.contains(value) {
|
||
hasLike = true
|
||
}
|
||
}
|
||
}
|
||
if hasLike {
|
||
return .unlike
|
||
} else {
|
||
return .like
|
||
}*/
|
||
}
|
||
}
|
||
return .none
|
||
}, navigateToFirstDateMessage: { [weak self] timestamp in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
switch strongSelf.chatLocation {
|
||
case let .peer(peerId):
|
||
strongSelf.navigateToMessage(from: nil, to: .index(MessageIndex(id: MessageId(peerId: peerId, namespace: 0, id: 0), timestamp: timestamp - Int32(NSTimeZone.local.secondsFromGMT()))), scrollPosition: .bottom(0.0), rememberInStack: false, animated: true, completion: nil)
|
||
case let .replyThread(replyThreadMessage):
|
||
let peerId = replyThreadMessage.messageId.peerId
|
||
strongSelf.navigateToMessage(from: nil, to: .index(MessageIndex(id: MessageId(peerId: peerId, namespace: 0, id: 0), timestamp: timestamp - Int32(NSTimeZone.local.secondsFromGMT()))), scrollPosition: .bottom(0.0), rememberInStack: false, animated: true, completion: nil)
|
||
}
|
||
}, requestRedeliveryOfFailedMessages: { [weak self] id in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
if id.namespace == Namespaces.Message.ScheduledCloud {
|
||
let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Message] in
|
||
return transaction.getMessageGroup(id) ?? []
|
||
} |> deliverOnMainQueue).start(next: { messages in
|
||
guard let strongSelf = self, let message = messages.filter({ $0.id == id }).first else {
|
||
return
|
||
}
|
||
|
||
var actions: [ContextMenuItem] = []
|
||
actions.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.ScheduledMessages_SendNow, icon: { theme in
|
||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.actionSheet.primaryTextColor)
|
||
}, action: { [weak self] _, f in
|
||
if let strongSelf = self {
|
||
strongSelf.controllerInteraction?.sendScheduledMessagesNow(messages.map { $0.id })
|
||
}
|
||
f(.dismissWithoutContent)
|
||
})))
|
||
actions.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.ScheduledMessages_EditTime, icon: { theme in
|
||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Schedule"), color: theme.actionSheet.primaryTextColor)
|
||
}, action: { [weak self] _, f in
|
||
if let strongSelf = self {
|
||
strongSelf.controllerInteraction?.editScheduledMessagesTime(messages.map { $0.id })
|
||
}
|
||
f(.dismissWithoutContent)
|
||
})))
|
||
actions.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in
|
||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
|
||
}, action: { [weak self] controller, f in
|
||
if let strongSelf = self {
|
||
strongSelf.interfaceInteraction?.deleteMessages(messages, controller, f)
|
||
}
|
||
})))
|
||
|
||
let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, message: message, selectAll: true)), items: .single(actions), reactionItems: [], recognizer: nil)
|
||
strongSelf.currentContextController = controller
|
||
strongSelf.forEachController({ controller in
|
||
if let controller = controller as? TooltipScreen {
|
||
controller.dismiss()
|
||
}
|
||
return true
|
||
})
|
||
strongSelf.window?.presentInGlobalOverlay(controller)
|
||
})
|
||
} else {
|
||
let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Message] in
|
||
return transaction.getMessageFailedGroup(id) ?? []
|
||
} |> deliverOnMainQueue).start(next: { messages in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
var groups: [UInt32: [Message]] = [:]
|
||
var notGrouped: [Message] = []
|
||
for message in messages {
|
||
if let groupInfo = message.groupInfo {
|
||
if groups[groupInfo.stableId] == nil {
|
||
groups[groupInfo.stableId] = []
|
||
}
|
||
groups[groupInfo.stableId]?.append(message)
|
||
} else {
|
||
notGrouped.append(message)
|
||
}
|
||
}
|
||
|
||
let totalGroupCount = notGrouped.count + groups.count
|
||
|
||
var maybeSelectedGroup: [Message]?
|
||
for (_, group) in groups {
|
||
if group.contains(where: { $0.id == id}) {
|
||
maybeSelectedGroup = group
|
||
break
|
||
}
|
||
}
|
||
for message in notGrouped {
|
||
if message.id == id {
|
||
maybeSelectedGroup = [message]
|
||
}
|
||
}
|
||
|
||
guard let selectedGroup = maybeSelectedGroup, let topMessage = selectedGroup.first else {
|
||
return
|
||
}
|
||
|
||
var actions: [ContextMenuItem] = []
|
||
actions.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_MessageDialogRetry, icon: { theme in
|
||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.actionSheet.primaryTextColor)
|
||
}, action: { [weak self] _, f in
|
||
if let strongSelf = self {
|
||
let _ = resendMessages(account: strongSelf.context.account, messageIds: selectedGroup.map({ $0.id })).start()
|
||
}
|
||
f(.dismissWithoutContent)
|
||
})))
|
||
if totalGroupCount != 1 {
|
||
actions.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_MessageDialogRetryAll(totalGroupCount).0, icon: { theme in
|
||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.actionSheet.primaryTextColor)
|
||
}, action: { [weak self] _, f in
|
||
if let strongSelf = self {
|
||
let _ = resendMessages(account: strongSelf.context.account, messageIds: messages.map({ $0.id })).start()
|
||
}
|
||
f(.dismissWithoutContent)
|
||
})))
|
||
}
|
||
actions.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in
|
||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
|
||
}, action: { [weak self] controller, f in
|
||
if let strongSelf = self {
|
||
let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: [id], type: .forLocalPeer).start()
|
||
}
|
||
f(.dismissWithoutContent)
|
||
})))
|
||
|
||
let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, message: topMessage, selectAll: true)), items: .single(actions), reactionItems: [], recognizer: nil)
|
||
strongSelf.currentContextController = controller
|
||
strongSelf.forEachController({ controller in
|
||
if let controller = controller as? TooltipScreen {
|
||
controller.dismiss()
|
||
}
|
||
return true
|
||
})
|
||
strongSelf.window?.presentInGlobalOverlay(controller)
|
||
})
|
||
}
|
||
}, addContact: { [weak self] phoneNumber in
|
||
if let strongSelf = self {
|
||
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
|
||
strongSelf.context.sharedContext.openAddContact(context: strongSelf.context, firstName: "", lastName: "", phoneNumber: phoneNumber, label: defaultContactLabel, present: { [weak self] controller, arguments in
|
||
self?.present(controller, in: .window(.root), with: arguments)
|
||
}, pushController: { [weak self] controller in
|
||
if let strongSelf = self {
|
||
strongSelf.effectiveNavigationController?.pushViewController(controller)
|
||
}
|
||
}, completed: {})
|
||
})
|
||
}
|
||
}, rateCall: { [weak self] message, callId, isVideo in
|
||
if let strongSelf = self {
|
||
let controller = callRatingController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, callId: callId, userInitiated: true, isVideo: isVideo, present: { [weak self] c, a in
|
||
if let strongSelf = self {
|
||
strongSelf.present(c, in: .window(.root), with: a)
|
||
}
|
||
}, push: { [weak self] c in
|
||
if let strongSelf = self {
|
||
strongSelf.push(c)
|
||
}
|
||
})
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
strongSelf.present(controller, in: .window(.root))
|
||
}
|
||
}, requestSelectMessagePollOptions: { [weak self] id, opaqueIdentifiers in
|
||
guard let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction else {
|
||
return
|
||
}
|
||
|
||
guard strongSelf.presentationInterfaceState.subject != .scheduledMessages else {
|
||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.ScheduledMessages_PollUnavailable, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||
return
|
||
}
|
||
if controllerInteraction.pollActionState.pollMessageIdsInProgress[id] == nil {
|
||
controllerInteraction.pollActionState.pollMessageIdsInProgress[id] = opaqueIdentifiers
|
||
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id)
|
||
let disposables: DisposableDict<MessageId>
|
||
if let current = strongSelf.selectMessagePollOptionDisposables {
|
||
disposables = current
|
||
} else {
|
||
disposables = DisposableDict()
|
||
strongSelf.selectMessagePollOptionDisposables = disposables
|
||
}
|
||
let signal = requestMessageSelectPollOption(account: strongSelf.context.account, messageId: id, opaqueIdentifiers: opaqueIdentifiers)
|
||
disposables.set((signal
|
||
|> deliverOnMainQueue).start(next: { resultPoll in
|
||
guard let strongSelf = self, let resultPoll = resultPoll else {
|
||
return
|
||
}
|
||
guard let _ = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) else {
|
||
return
|
||
}
|
||
|
||
switch resultPoll.kind {
|
||
case .poll:
|
||
if strongSelf.selectPollOptionFeedback == nil {
|
||
strongSelf.selectPollOptionFeedback = HapticFeedback()
|
||
}
|
||
strongSelf.selectPollOptionFeedback?.success()
|
||
case .quiz:
|
||
if let voters = resultPoll.results.voters {
|
||
for voter in voters {
|
||
if voter.selected {
|
||
if voter.isCorrect {
|
||
if strongSelf.selectPollOptionFeedback == nil {
|
||
strongSelf.selectPollOptionFeedback = HapticFeedback()
|
||
}
|
||
strongSelf.selectPollOptionFeedback?.success()
|
||
|
||
strongSelf.chatDisplayNode.animateQuizCorrectOptionSelected()
|
||
} else {
|
||
var found = false
|
||
strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
|
||
if !found, let itemNode = itemNode as? ChatMessageBubbleItemNode, itemNode.item?.message.id == id {
|
||
found = true
|
||
if strongSelf.selectPollOptionFeedback == nil {
|
||
strongSelf.selectPollOptionFeedback = HapticFeedback()
|
||
}
|
||
strongSelf.selectPollOptionFeedback?.error()
|
||
|
||
itemNode.animateQuizInvalidOptionSelected()
|
||
|
||
if let solution = resultPoll.results.solution {
|
||
for contentNode in itemNode.contentNodes {
|
||
if let contentNode = contentNode as? ChatMessagePollBubbleContentNode {
|
||
let sourceNode = contentNode.solutionTipSourceNode
|
||
strongSelf.displayPollSolution(solution: solution, sourceNode: sourceNode, isAutomatic: true)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}, error: { _ in
|
||
guard let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction else {
|
||
return
|
||
}
|
||
if controllerInteraction.pollActionState.pollMessageIdsInProgress.removeValue(forKey: id) != nil {
|
||
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id)
|
||
}
|
||
}, completed: {
|
||
guard let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction else {
|
||
return
|
||
}
|
||
if controllerInteraction.pollActionState.pollMessageIdsInProgress.removeValue(forKey: id) != nil {
|
||
Queue.mainQueue().after(1.0, {
|
||
|
||
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id)
|
||
})
|
||
}
|
||
}), forKey: id)
|
||
}
|
||
}, requestOpenMessagePollResults: { [weak self] messageId, pollId in
|
||
guard let strongSelf = self, pollId.namespace == Namespaces.Media.CloudPoll else {
|
||
return
|
||
}
|
||
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
|
||
let _ = (strongSelf.context.account.postbox.transaction { transaction -> Message? in
|
||
return transaction.getMessage(messageId)
|
||
}
|
||
|> deliverOnMainQueue).start(next: { message in
|
||
guard let message = message else {
|
||
return
|
||
}
|
||
for media in message.media {
|
||
if let poll = media as? TelegramMediaPoll, poll.pollId == pollId {
|
||
strongSelf.push(pollResultsController(context: strongSelf.context, messageId: messageId, poll: poll))
|
||
break
|
||
}
|
||
}
|
||
})
|
||
}, delay: true)
|
||
}, openAppStorePage: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.context.sharedContext.applicationBindings.openAppStorePage()
|
||
}
|
||
}, displayMessageTooltip: { [weak self] messageId, text, node, nodeRect in
|
||
if let strongSelf = self {
|
||
if let node = node {
|
||
strongSelf.messageTooltipController?.dismiss()
|
||
let tooltipController = TooltipController(content: .text(text), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true)
|
||
strongSelf.messageTooltipController = tooltipController
|
||
tooltipController.dismissed = { [weak tooltipController] _ in
|
||
if let strongSelf = self, let tooltipController = tooltipController, strongSelf.messageTooltipController === tooltipController {
|
||
strongSelf.messageTooltipController = nil
|
||
}
|
||
}
|
||
strongSelf.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: {
|
||
if let strongSelf = self {
|
||
var rect = node.view.convert(node.view.bounds, to: strongSelf.chatDisplayNode.view)
|
||
if let nodeRect = nodeRect {
|
||
rect = CGRect(origin: rect.origin.offsetBy(dx: nodeRect.minX, dy: nodeRect.minY - node.bounds.minY), size: nodeRect.size)
|
||
}
|
||
return (strongSelf.chatDisplayNode, rect)
|
||
}
|
||
return nil
|
||
}))
|
||
}
|
||
}
|
||
}, seekToTimecode: { [weak self] message, timestamp, forceOpen in
|
||
if let strongSelf = self {
|
||
var found = false
|
||
if !forceOpen {
|
||
strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
|
||
if !found, let itemNode = itemNode as? ChatMessageItemView, itemNode.item?.message.id == message.id, let (action, _, _, _, _) = itemNode.playMediaWithSound() {
|
||
if case let .visible(fraction) = itemNode.visibility, fraction > 0.7 {
|
||
action(Double(timestamp))
|
||
} else {
|
||
let _ = strongSelf.controllerInteraction?.openMessage(message, .timecode(Double(timestamp)))
|
||
}
|
||
found = true
|
||
}
|
||
}
|
||
}
|
||
if !found {
|
||
let _ = strongSelf.controllerInteraction?.openMessage(message, .timecode(Double(timestamp)))
|
||
}
|
||
}
|
||
}, scheduleCurrentMessage: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.presentScheduleTimePicker(completion: { [weak self] time in
|
||
if let strongSelf = self {
|
||
if let _ = strongSelf.presentationInterfaceState.recordedMediaPreview {
|
||
strongSelf.sendMediaRecording(scheduleTime: time)
|
||
} else {
|
||
strongSelf.chatDisplayNode.sendCurrentMessage(scheduleTime: time, completion: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) }
|
||
})
|
||
|
||
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp {
|
||
strongSelf.openScheduledMessages()
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}, sendScheduledMessagesNow: { [weak self] messageIds in
|
||
if let strongSelf = self {
|
||
if let _ = strongSelf.presentationInterfaceState.slowmodeState {
|
||
if let rect = strongSelf.chatDisplayNode.frameForInputActionButton() {
|
||
strongSelf.interfaceInteraction?.displaySlowmodeTooltip(strongSelf.chatDisplayNode, rect)
|
||
}
|
||
return
|
||
} else {
|
||
let _ = sendScheduledMessageNowInteractively(postbox: strongSelf.context.account.postbox, messageId: messageIds.first!).start()
|
||
}
|
||
}
|
||
}, editScheduledMessagesTime: { [weak self] messageIds in
|
||
if let strongSelf = self, let messageId = messageIds.first {
|
||
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
|
||
let _ = (strongSelf.context.account.postbox.transaction { transaction -> Message? in
|
||
return transaction.getMessage(messageId)
|
||
} |> deliverOnMainQueue).start(next: { [weak self] message in
|
||
guard let strongSelf = self, let message = message else {
|
||
return
|
||
}
|
||
strongSelf.presentScheduleTimePicker(selectedTime: message.timestamp, completion: { [weak self] time in
|
||
if let strongSelf = self {
|
||
var entities: TextEntitiesMessageAttribute?
|
||
for attribute in message.attributes {
|
||
if let attribute = attribute as? TextEntitiesMessageAttribute {
|
||
entities = attribute
|
||
break
|
||
}
|
||
}
|
||
strongSelf.editMessageDisposable.set((requestEditMessage(account: strongSelf.context.account, messageId: messageId, text: message.text, media: .keep, entities: entities, disableUrlPreview: false, scheduleTime: time) |> deliverOnMainQueue).start(next: { result in
|
||
}, error: { error in
|
||
}))
|
||
}
|
||
})
|
||
})
|
||
}, delay: true)
|
||
}
|
||
}, performTextSelectionAction: { [weak self] _, text, action in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
switch action {
|
||
case .copy:
|
||
storeAttributedTextInPasteboard(text)
|
||
case .share:
|
||
let f = {
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
let shareController = ShareController(context: strongSelf.context, subject: .text(text.string), externalShare: true, immediateExternalShare: false)
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
strongSelf.present(shareController, in: .window(.root))
|
||
}
|
||
if let currentContextController = strongSelf.currentContextController {
|
||
currentContextController.dismiss(completion: {
|
||
f()
|
||
})
|
||
} else {
|
||
f()
|
||
}
|
||
case .lookup:
|
||
let controller = UIReferenceLibraryViewController(term: text.string)
|
||
if let window = strongSelf.effectiveNavigationController?.view.window {
|
||
controller.popoverPresentationController?.sourceView = window
|
||
controller.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0))
|
||
window.rootViewController?.present(controller, animated: true)
|
||
}
|
||
case .speak:
|
||
strongSelf.speakText(text.string)
|
||
}
|
||
}, updateMessageLike: { [weak self] messageId, isLiked in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
|
||
let heart = "❤"
|
||
if isLiked {
|
||
let _ = updateMessageReactionsInteractively(postbox: strongSelf.context.account.postbox, messageId: messageId, reaction: heart).start()
|
||
} else {
|
||
let _ = updateMessageReactionsInteractively(postbox: strongSelf.context.account.postbox, messageId: messageId, reaction: nil).start()
|
||
}
|
||
}, openMessageReactions: { [weak self] messageId in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
let _ = (strongSelf.context.account.postbox.transaction { transaction -> Message? in
|
||
return transaction.getMessage(messageId)
|
||
}
|
||
|> deliverOnMainQueue).start(next: { message in
|
||
guard let strongSelf = self, let message = message else {
|
||
return
|
||
}
|
||
var initialReactions: [MessageReaction] = []
|
||
for attribute in message.attributes {
|
||
if let attribute = attribute as? ReactionsMessageAttribute {
|
||
initialReactions = attribute.reactions
|
||
}
|
||
}
|
||
|
||
if !initialReactions.isEmpty {
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
strongSelf.present(MessageReactionListController(context: strongSelf.context, messageId: message.id, initialReactions: initialReactions), in: .window(.root))
|
||
}
|
||
})
|
||
}, displayImportedMessageTooltip: { [weak self] _ in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
if let _ = strongSelf.currentImportMessageTooltip {
|
||
} else {
|
||
let controller = UndoOverlayController(presentationData: strongSelf.presentationData, content: .importedMessage(text: strongSelf.presentationData.strings.Conversation_ImportedMessageHint), elevatedLayout: false, action: { _ in return false })
|
||
strongSelf.currentImportMessageTooltip = controller
|
||
strongSelf.present(controller, in: .current)
|
||
}
|
||
}, displaySwipeToReplyHint: { [weak self] in
|
||
if let strongSelf = self, let validLayout = strongSelf.validLayout, min(validLayout.size.width, validLayout.size.height) > 320.0 {
|
||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .swipeToReply(title: strongSelf.presentationData.strings.Conversation_SwipeToReplyHintTitle, text: strongSelf.presentationData.strings.Conversation_SwipeToReplyHintText), elevatedLayout: false, action: { _ in return false }), in: .current)
|
||
}
|
||
}, dismissReplyMarkupMessage: { [weak self] message in
|
||
guard let strongSelf = self, strongSelf.presentationInterfaceState.keyboardButtonsMessage?.id == message.id else {
|
||
return
|
||
}
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
return $0.updatedInputMode({ _ in .text }).updatedInterfaceState({
|
||
$0.withUpdatedMessageActionsState({ value in
|
||
var value = value
|
||
value.closedButtonKeyboardMessageId = message.id
|
||
return value
|
||
})
|
||
})
|
||
})
|
||
}, openMessagePollResults: { [weak self] messageId, optionOpaqueIdentifier in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
|
||
let _ = (strongSelf.context.account.postbox.transaction { transaction -> Message? in
|
||
return transaction.getMessage(messageId)
|
||
}
|
||
|> deliverOnMainQueue).start(next: { message in
|
||
guard let message = message else {
|
||
return
|
||
}
|
||
for media in message.media {
|
||
if let poll = media as? TelegramMediaPoll, poll.pollId.namespace == Namespaces.Media.CloudPoll {
|
||
strongSelf.push(pollResultsController(context: strongSelf.context, messageId: messageId, poll: poll, focusOnOptionWithOpaqueIdentifier: optionOpaqueIdentifier))
|
||
break
|
||
}
|
||
}
|
||
})
|
||
})
|
||
}, openPollCreation: { [weak self] isQuiz in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
|
||
strongSelf.presentPollCreation(isQuiz: isQuiz)
|
||
})
|
||
}, displayPollSolution: { [weak self] solution, sourceNode in
|
||
self?.displayPollSolution(solution: solution, sourceNode: sourceNode, isAutomatic: false)
|
||
}, displayPsa: { [weak self] type, sourceNode in
|
||
self?.displayPsa(type: type, sourceNode: sourceNode, isAutomatic: false)
|
||
}, displayDiceTooltip: { [weak self] dice in
|
||
self?.displayDiceTooltip(dice: dice)
|
||
}, animateDiceSuccess: { [weak self] onlyHaptic in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
if strongSelf.selectPollOptionFeedback == nil {
|
||
strongSelf.selectPollOptionFeedback = HapticFeedback()
|
||
}
|
||
strongSelf.selectPollOptionFeedback?.success()
|
||
if !onlyHaptic {
|
||
strongSelf.chatDisplayNode.animateQuizCorrectOptionSelected()
|
||
}
|
||
}, greetingStickerNode: { [weak self] in
|
||
return self?.chatDisplayNode.greetingStickerNode
|
||
}, openPeerContextMenu: { [weak self] peer, messageId, node, rect, gesture in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
|
||
if strongSelf.presentationInterfaceState.interfaceState.selectionState != nil {
|
||
return
|
||
}
|
||
|
||
strongSelf.dismissAllTooltips()
|
||
|
||
let context = strongSelf.context
|
||
let _ = (context.account.postbox.transaction { transaction -> (Peer?, Message?) in
|
||
return (transaction.getPeer(peer.id), messageId.flatMap { transaction.getMessage($0) })
|
||
}
|
||
|> deliverOnMainQueue).start(next: { [weak self] peer, message in
|
||
guard let strongSelf = self, let peer = peer, peer.smallProfileImage != nil else {
|
||
return
|
||
}
|
||
|
||
let galleryController = AvatarGalleryController(context: context, peer: peer, remoteEntries: nil, replaceRootController: { controller, ready in
|
||
}, synchronousLoad: true)
|
||
galleryController.setHintWillBePresentedInPreviewingContext(true)
|
||
|
||
let items: Signal<[ContextMenuItem], NoError> = context.account.postbox.transaction { transaction -> [ContextMenuItem] in
|
||
var isChannel = false
|
||
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
|
||
isChannel = true
|
||
}
|
||
var items: [ContextMenuItem] = [
|
||
.action(ContextMenuActionItem(text: isChannel ? strongSelf.presentationData.strings.Conversation_ContextMenuOpenChannelProfile : strongSelf.presentationData.strings.Conversation_ContextMenuOpenProfile, icon: { theme in
|
||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor)
|
||
}, action: { _, f in
|
||
f(.dismissWithoutContent)
|
||
self?.openPeer(peerId: peer.id, navigation: .info, fromMessage: nil)
|
||
}))
|
||
]
|
||
items.append(.action(ContextMenuActionItem(text: isChannel ? strongSelf.presentationData.strings.Conversation_ContextMenuOpenChannel : strongSelf.presentationData.strings.Conversation_ContextMenuSendMessage, icon: { theme in
|
||
return generateTintedImage(image: UIImage(bundleImageName: isChannel ? "Chat/Context Menu/Channels" : "Chat/Context Menu/Message"), color: theme.actionSheet.primaryTextColor)
|
||
}, action: { _, f in
|
||
f(.dismissWithoutContent)
|
||
self?.openPeer(peerId: peer.id, navigation: .chat(textInputState: nil, subject: nil, peekData: nil), fromMessage: nil)
|
||
})))
|
||
if !isChannel && canSendMessagesToChat(strongSelf.presentationInterfaceState) {
|
||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuMention, icon: { theme in
|
||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Mention"), color: theme.actionSheet.primaryTextColor)
|
||
}, action: { _, f in
|
||
f(.dismissWithoutContent)
|
||
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
|
||
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
|
||
strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
||
var inputMode = inputMode
|
||
if inputMode == .none {
|
||
inputMode = .text
|
||
}
|
||
return (chatTextInputAddMentionAttribute(current, peer: peer), inputMode)
|
||
}
|
||
}, delay: true)
|
||
})))
|
||
}
|
||
return items
|
||
}
|
||
|
||
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node, passthroughTouches: false)), items: items, reactionItems: [], gesture: gesture)
|
||
strongSelf.presentInGlobalOverlay(contextController)
|
||
})
|
||
}, openMessageReplies: { [weak self] messageId, isChannelPost, displayModalProgress in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
|
||
strongSelf.openMessageReplies(messageId: messageId, displayProgressInMessage: displayModalProgress ? nil : messageId, isChannelPost: isChannelPost, atMessage: nil, displayModalProgress: displayModalProgress)
|
||
}, openReplyThreadOriginalMessage: { [weak self] message in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
var threadMessageId: MessageId?
|
||
for attribute in message.attributes {
|
||
if let attribute = attribute as? ReplyMessageAttribute {
|
||
threadMessageId = attribute.threadMessageId
|
||
break
|
||
}
|
||
}
|
||
for attribute in message.attributes {
|
||
if let attribute = attribute as? SourceReferenceMessageAttribute {
|
||
if let threadMessageId = threadMessageId {
|
||
if let _ = strongSelf.navigationController as? NavigationController {
|
||
strongSelf.openMessageReplies(messageId: threadMessageId, displayProgressInMessage: message.id, isChannelPost: true, atMessage: attribute.messageId, displayModalProgress: false)
|
||
}
|
||
} else {
|
||
strongSelf.navigateToMessage(from: nil, to: .id(attribute.messageId))
|
||
}
|
||
break
|
||
}
|
||
}
|
||
}, openMessageStats: { [weak self] id in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
|
||
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
|
||
let _ = (context.account.postbox.transaction { transaction -> (MessageId, CachedPeerData?)? in
|
||
if let message = transaction.getMessage(id), let sourceMessageId = message.forwardInfo?.sourceMessageId {
|
||
return (sourceMessageId, transaction.getPeerCachedData(peerId: sourceMessageId.peerId))
|
||
} else {
|
||
return (id, transaction.getPeerCachedData(peerId: id.peerId))
|
||
}
|
||
} |> deliverOnMainQueue).start(next: { [weak self] messageIdAndCachedPeerData in
|
||
guard let strongSelf = self, let (id, cachedPeerDataValue) = messageIdAndCachedPeerData, let cachedPeerData = cachedPeerDataValue else {
|
||
return
|
||
}
|
||
strongSelf.push(messageStatsController(context: context, messageId: id, cachedPeerData: cachedPeerData))
|
||
})
|
||
}, delay: true)
|
||
}, editMessageMedia: { [weak self] messageId, draw in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
|
||
if draw {
|
||
let _ = (strongSelf.context.account.postbox.transaction { transaction -> Message? in
|
||
return transaction.getMessage(messageId)
|
||
} |> deliverOnMainQueue).start(next: { [weak self] message in
|
||
guard let strongSelf = self, let message = message else {
|
||
return
|
||
}
|
||
|
||
var mediaReference: AnyMediaReference?
|
||
for m in message.media {
|
||
if let image = m as? TelegramMediaImage {
|
||
mediaReference = AnyMediaReference.standalone(media: image)
|
||
}
|
||
}
|
||
|
||
if let mediaReference = mediaReference, let peer = message.peers[message.id.peerId] {
|
||
legacyMediaEditor(context: strongSelf.context, peer: peer, media: mediaReference, initialCaption: message.text, snapshots: [], transitionCompletion: nil, presentStickers: { [weak self] completion in
|
||
if let strongSelf = self {
|
||
let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in
|
||
completion(fileReference.media, fileReference.media.isAnimatedSticker, node.view, rect)
|
||
return true
|
||
})
|
||
strongSelf.present(controller, in: .window(.root))
|
||
return controller
|
||
} else {
|
||
return nil
|
||
}
|
||
}, sendMessagesWithSignals: { [weak self] signals, _, _ in
|
||
if let strongSelf = self {
|
||
strongSelf.interfaceInteraction?.setupEditMessage(messageId, { _ in })
|
||
strongSelf.editMessageMediaWithLegacySignals(signals!)
|
||
}
|
||
}, present: { [weak self] c, a in
|
||
self?.present(c, in: .window(.root), with: a)
|
||
})
|
||
}
|
||
})
|
||
} else {
|
||
strongSelf.presentMediaPicker(fileMode: false, editingMedia: true, completion: { signals, _, _ in
|
||
self?.interfaceInteraction?.setupEditMessage(messageId, { _ in })
|
||
self?.editMessageMediaWithLegacySignals(signals)
|
||
})
|
||
}
|
||
}, copyText: { [weak self] text in
|
||
if let strongSelf = self {
|
||
storeMessageTextInPasteboard(text, entities: nil)
|
||
|
||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in
|
||
return true
|
||
}), in: .current)
|
||
}
|
||
}, requestMessageUpdate: { [weak self] id in
|
||
if let strongSelf = self {
|
||
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id)
|
||
}
|
||
}, cancelInteractiveKeyboardGestures: { [weak self] in
|
||
(self?.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures()
|
||
self?.chatDisplayNode.cancelInteractiveKeyboardGestures()
|
||
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: self.stickerSettings)
|
||
|
||
self.controllerInteraction = controllerInteraction
|
||
|
||
if case let .peer(peerId) = chatLocation, peerId != context.account.peerId {
|
||
switch subject {
|
||
case .pinnedMessages, .scheduledMessages:
|
||
break
|
||
default:
|
||
self.navigationBar?.userInfo = PeerInfoNavigationSourceTag(peerId: peerId)
|
||
}
|
||
}
|
||
self.navigationBar?.allowsCustomTransition = { [weak self] in
|
||
guard let strongSelf = self else {
|
||
return false
|
||
}
|
||
return !strongSelf.chatDisplayNode.hasEmbeddedTitleContent
|
||
}
|
||
|
||
self.chatTitleView = ChatTitleView(account: self.context.account, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder)
|
||
self.navigationItem.titleView = self.chatTitleView
|
||
self.chatTitleView?.pressed = { [weak self] in
|
||
if let strongSelf = self {
|
||
if strongSelf.chatLocation == .peer(strongSelf.context.account.peerId) {
|
||
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer, let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: true) {
|
||
strongSelf.effectiveNavigationController?.pushViewController(infoController)
|
||
}
|
||
} else {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
return $0.updatedTitlePanelContext {
|
||
if let index = $0.firstIndex(where: {
|
||
switch $0 {
|
||
case .chatInfo:
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}) {
|
||
var updatedContexts = $0
|
||
updatedContexts.remove(at: index)
|
||
return updatedContexts
|
||
} else {
|
||
var updatedContexts = $0
|
||
updatedContexts.append(.chatInfo)
|
||
return updatedContexts.sorted()
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}
|
||
self.chatTitleView?.longPressed = { [weak self] in
|
||
self?.interfaceInteraction?.beginMessageSearch(.everything, "")
|
||
}
|
||
|
||
let chatInfoButtonItem: UIBarButtonItem
|
||
switch chatLocation {
|
||
case .peer, .replyThread:
|
||
let avatarNode = ChatAvatarNavigationNode()
|
||
avatarNode.contextAction = { [weak self] node, gesture in
|
||
guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer, peer.smallProfileImage != nil else {
|
||
return
|
||
}
|
||
let galleryController = AvatarGalleryController(context: strongSelf.context, peer: peer, remoteEntries: nil, replaceRootController: { controller, ready in
|
||
}, synchronousLoad: true)
|
||
galleryController.setHintWillBePresentedInPreviewingContext(true)
|
||
|
||
let items: Signal<[ContextMenuItem], NoError> = context.account.postbox.transaction { transaction -> [ContextMenuItem] in
|
||
var items: [ContextMenuItem] = [
|
||
.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, icon: { theme in
|
||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor)
|
||
}, action: { _, f in
|
||
f(.dismissWithoutContent)
|
||
self?.navigationButtonAction(.openChatInfo(expandAvatar: true))
|
||
}))
|
||
]
|
||
if let cachedData = transaction.getPeerCachedData(peerId: peer.id) as? CachedChannelData, cachedData.flags.contains(.canViewStats) {
|
||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.ChannelInfo_Stats, icon: { theme in
|
||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Statistics"), color: theme.actionSheet.primaryTextColor)
|
||
}, action: { _, f in
|
||
f(.dismissWithoutContent)
|
||
guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer else {
|
||
return
|
||
}
|
||
strongSelf.view.endEditing(true)
|
||
|
||
let statsController: ViewController
|
||
if let channel = peer as? TelegramChannel, case .group = channel.info {
|
||
statsController = groupStatsController(context: context, peerId: peer.id, cachedPeerData: cachedData)
|
||
} else {
|
||
statsController = channelStatsController(context: context, peerId: peer.id, cachedPeerData: cachedData)
|
||
}
|
||
strongSelf.push(statsController)
|
||
})))
|
||
}
|
||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_Search, icon: { theme in
|
||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Search"), color: theme.actionSheet.primaryTextColor)
|
||
}, action: { _, f in
|
||
f(.dismissWithoutContent)
|
||
self?.interfaceInteraction?.beginMessageSearch(.everything, "")
|
||
})))
|
||
return items
|
||
}
|
||
|
||
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node, passthroughTouches: false)), items: items, reactionItems: [], gesture: gesture)
|
||
strongSelf.presentInGlobalOverlay(contextController)
|
||
}
|
||
chatInfoButtonItem = UIBarButtonItem(customDisplayNode: avatarNode)!
|
||
}
|
||
chatInfoButtonItem.target = self
|
||
chatInfoButtonItem.action = #selector(self.rightNavigationButtonAction)
|
||
chatInfoButtonItem.accessibilityLabel = self.presentationData.strings.Conversation_Info
|
||
self.chatInfoNavigationButton = ChatNavigationButton(action: .openChatInfo(expandAvatar: true), buttonItem: chatInfoButtonItem)
|
||
|
||
self.navigationItem.titleView = self.chatTitleView
|
||
self.chatTitleView?.pressed = { [weak self] in
|
||
self?.navigationButtonAction(.openChatInfo(expandAvatar: false))
|
||
}
|
||
|
||
self.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in
|
||
if let botStart = botStart, case .interactive = botStart.behavior {
|
||
return state.updatedBotStartPayload(botStart.payload)
|
||
} else {
|
||
return state
|
||
}
|
||
})
|
||
|
||
let chatLocationPeerId: PeerId = chatLocation.peerId
|
||
|
||
do {
|
||
let peerId = chatLocationPeerId
|
||
if case let .peer(peerView) = self.chatLocationInfoData {
|
||
peerView.set(context.account.viewTracker.peerView(peerId))
|
||
var onlineMemberCount: Signal<Int32?, NoError> = .single(nil)
|
||
var hasScheduledMessages: Signal<Bool, NoError> = .single(false)
|
||
|
||
if peerId.namespace == Namespaces.Peer.CloudChannel {
|
||
let recentOnlineSignal: Signal<Int32?, NoError> = peerView.get()
|
||
|> map { view -> Bool? in
|
||
if let cachedData = view.cachedData as? CachedChannelData, let peer = peerViewMainPeer(view) as? TelegramChannel {
|
||
if case .broadcast = peer.info {
|
||
return nil
|
||
} else if let memberCount = cachedData.participantsSummary.memberCount, memberCount > 50 {
|
||
return true
|
||
} else {
|
||
return false
|
||
}
|
||
} else {
|
||
return false
|
||
}
|
||
}
|
||
|> distinctUntilChanged
|
||
|> mapToSignal { isLarge -> Signal<Int32?, NoError> in
|
||
if let isLarge = isLarge {
|
||
if isLarge {
|
||
return context.peerChannelMemberCategoriesContextsManager.recentOnline(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId)
|
||
|> map(Optional.init)
|
||
} else {
|
||
return context.peerChannelMemberCategoriesContextsManager.recentOnlineSmall(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId)
|
||
|> map(Optional.init)
|
||
}
|
||
} else {
|
||
return .single(nil)
|
||
}
|
||
}
|
||
onlineMemberCount = recentOnlineSignal
|
||
|
||
self.reportIrrelvantGeoNoticePromise.set(context.account.postbox.transaction { transaction -> Bool? in
|
||
if let _ = transaction.getNoticeEntry(key: ApplicationSpecificNotice.irrelevantPeerGeoReportKey(peerId: peerId)) as? ApplicationSpecificBoolNotice {
|
||
return true
|
||
} else {
|
||
return false
|
||
}
|
||
})
|
||
} else {
|
||
self.reportIrrelvantGeoNoticePromise.set(.single(nil))
|
||
}
|
||
|
||
var isScheduledOrPinnedMessages = false
|
||
switch subject {
|
||
case .scheduledMessages, .pinnedMessages:
|
||
isScheduledOrPinnedMessages = true
|
||
default:
|
||
break
|
||
}
|
||
|
||
if case .peer = chatLocation, !isScheduledOrPinnedMessages, peerId.namespace != Namespaces.Peer.SecretChat {
|
||
let chatLocationContextHolder = self.chatLocationContextHolder
|
||
hasScheduledMessages = peerView.get()
|
||
|> take(1)
|
||
|> mapToSignal { view -> Signal<Bool, NoError> in
|
||
if let peer = peerViewMainPeer(view) as? TelegramChannel, !peer.hasPermission(.sendMessages) {
|
||
return .single(false)
|
||
} else {
|
||
return context.account.viewTracker.scheduledMessagesViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder))
|
||
|> map { view, _, _ in
|
||
return !view.entries.isEmpty
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
var pinnedCountSignal: Signal<Int?, NoError> = .single(nil)
|
||
if case .pinnedMessages = subject {
|
||
pinnedCountSignal = self.topPinnedMessageSignal(latest: true)
|
||
|> map { message -> Int? in
|
||
return message?.totalCount
|
||
}
|
||
|> distinctUntilChanged
|
||
}
|
||
|
||
self.peerDisposable.set((combineLatest(queue: Queue.mainQueue(), peerView.get(), onlineMemberCount, hasScheduledMessages, self.reportIrrelvantGeoNoticePromise.get(), pinnedCountSignal, self.presentationInterfaceStatePromise.get())
|
||
|> deliverOnMainQueue).start(next: { [weak self] peerView, onlineMemberCount, hasScheduledMessages, peerReportNotice, pinnedCount, presentationInterfaceState in
|
||
if let strongSelf = self {
|
||
var isScheduledMessages = false
|
||
if case .scheduledMessages = strongSelf.presentationInterfaceState.subject {
|
||
isScheduledMessages = true
|
||
}
|
||
|
||
if let peer = peerViewMainPeer(peerView) {
|
||
if let selectionState = presentationInterfaceState.interfaceState.selectionState {
|
||
if selectionState.selectedIds.count > 0 {
|
||
strongSelf.chatTitleView?.titleContent = .custom(strongSelf.presentationData.strings.Conversation_SelectedMessages(Int32(selectionState.selectedIds.count)), nil, false)
|
||
} else {
|
||
if let reportReason = presentationInterfaceState.reportReason {
|
||
let title: String
|
||
switch reportReason {
|
||
case .spam:
|
||
title = strongSelf.presentationData.strings.ReportPeer_ReasonSpam
|
||
case .fake:
|
||
title = strongSelf.presentationData.strings.ReportPeer_ReasonFake
|
||
case .violence:
|
||
title = strongSelf.presentationData.strings.ReportPeer_ReasonViolence
|
||
case .porno:
|
||
title = strongSelf.presentationData.strings.ReportPeer_ReasonPornography
|
||
case .childAbuse:
|
||
title = strongSelf.presentationData.strings.ReportPeer_ReasonChildAbuse
|
||
case .copyright:
|
||
title = strongSelf.presentationData.strings.ReportPeer_ReasonCopyright
|
||
case .custom:
|
||
title = strongSelf.presentationData.strings.ReportPeer_ReasonOther
|
||
case .irrelevantLocation:
|
||
title = ""
|
||
}
|
||
strongSelf.chatTitleView?.titleContent = .custom(title, strongSelf.presentationInterfaceState.strings.Conversation_SelectMessages, false)
|
||
} else {
|
||
strongSelf.chatTitleView?.titleContent = .custom(strongSelf.presentationInterfaceState.strings.Conversation_SelectMessages, nil, false)
|
||
}
|
||
}
|
||
} else {
|
||
if case .pinnedMessages = strongSelf.presentationInterfaceState.subject {
|
||
strongSelf.chatTitleView?.titleContent = .custom(strongSelf.presentationData.strings.Chat_TitlePinnedMessages(Int32(pinnedCount ?? 1)), nil, false)
|
||
} else {
|
||
strongSelf.chatTitleView?.titleContent = .peer(peerView: peerView, onlineMemberCount: onlineMemberCount, isScheduledMessages: isScheduledMessages)
|
||
let imageOverride: AvatarNodeImageOverride?
|
||
if strongSelf.context.account.peerId == peer.id {
|
||
imageOverride = .savedMessagesIcon
|
||
} else if peer.id.isReplies {
|
||
imageOverride = .repliesIcon
|
||
} else if peer.isDeleted {
|
||
imageOverride = .deletedIcon
|
||
} else {
|
||
imageOverride = nil
|
||
}
|
||
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(context: strongSelf.context, theme: strongSelf.presentationData.theme, peer: peer, overrideImage: imageOverride)
|
||
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.contextActionIsEnabled = peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil
|
||
}
|
||
}
|
||
}
|
||
|
||
if strongSelf.peerView === peerView && strongSelf.reportIrrelvantGeoNotice == peerReportNotice && strongSelf.hasScheduledMessages == hasScheduledMessages {
|
||
return
|
||
}
|
||
|
||
strongSelf.reportIrrelvantGeoNotice = peerReportNotice
|
||
strongSelf.hasScheduledMessages = hasScheduledMessages
|
||
|
||
var upgradedToPeerId: PeerId?
|
||
if let previous = strongSelf.peerView, let group = previous.peers[previous.peerId] as? TelegramGroup, group.migrationReference == nil, let updatedGroup = peerView.peers[peerView.peerId] as? TelegramGroup, let migrationReference = updatedGroup.migrationReference {
|
||
upgradedToPeerId = migrationReference.peerId
|
||
}
|
||
|
||
var shouldDismiss = false
|
||
if let previous = strongSelf.peerView, let group = previous.peers[previous.peerId] as? TelegramGroup, group.membership != .Removed, let updatedGroup = peerView.peers[peerView.peerId] as? TelegramGroup, updatedGroup.membership == .Removed {
|
||
shouldDismiss = true
|
||
} else if let previous = strongSelf.peerView, let channel = previous.peers[previous.peerId] as? TelegramChannel, channel.participationStatus != .kicked, let updatedChannel = peerView.peers[peerView.peerId] as? TelegramChannel, updatedChannel.participationStatus == .kicked {
|
||
shouldDismiss = true
|
||
} else if let previous = strongSelf.peerView, let secretChat = previous.peers[previous.peerId] as? TelegramSecretChat, case .active = secretChat.embeddedState, let updatedSecretChat = peerView.peers[peerView.peerId] as? TelegramSecretChat, case .terminated = updatedSecretChat.embeddedState {
|
||
shouldDismiss = true
|
||
}
|
||
|
||
var wasGroupChannel: Bool?
|
||
if let previousPeerView = strongSelf.peerView, let info = (previousPeerView.peers[previousPeerView.peerId] as? TelegramChannel)?.info {
|
||
if case .group = info {
|
||
wasGroupChannel = true
|
||
} else {
|
||
wasGroupChannel = false
|
||
}
|
||
}
|
||
var isGroupChannel: Bool?
|
||
if let info = (peerView.peers[peerView.peerId] as? TelegramChannel)?.info {
|
||
if case .group = info {
|
||
isGroupChannel = true
|
||
} else {
|
||
isGroupChannel = false
|
||
}
|
||
}
|
||
let firstTime = strongSelf.peerView == nil
|
||
strongSelf.peerView = peerView
|
||
if wasGroupChannel != isGroupChannel {
|
||
if let isGroupChannel = isGroupChannel, isGroupChannel {
|
||
let (recentDisposable, _) = strongSelf.context.peerChannelMemberCategoriesContextsManager.recent(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, accountPeerId: context.account.peerId, peerId: peerView.peerId, updated: { _ in })
|
||
let (adminsDisposable, _) = strongSelf.context.peerChannelMemberCategoriesContextsManager.admins(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, accountPeerId: context.account.peerId, peerId: peerView.peerId, updated: { _ in })
|
||
let disposable = DisposableSet()
|
||
disposable.add(recentDisposable)
|
||
disposable.add(adminsDisposable)
|
||
strongSelf.chatAdditionalDataDisposable.set(disposable)
|
||
} else {
|
||
strongSelf.chatAdditionalDataDisposable.set(nil)
|
||
}
|
||
}
|
||
if strongSelf.isNodeLoaded {
|
||
strongSelf.chatDisplayNode.peerView = peerView
|
||
}
|
||
var peerIsMuted = false
|
||
if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings {
|
||
if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) {
|
||
peerIsMuted = true
|
||
}
|
||
}
|
||
var peerDiscussionId: PeerId?
|
||
var peerGeoLocation: PeerGeoLocation?
|
||
if let peer = peerView.peers[peerView.peerId] as? TelegramChannel, let cachedData = peerView.cachedData as? CachedChannelData {
|
||
if case .broadcast = peer.info {
|
||
if case let .known(value) = cachedData.linkedDiscussionPeerId {
|
||
peerDiscussionId = value
|
||
}
|
||
} else {
|
||
peerGeoLocation = cachedData.peerGeoLocation
|
||
}
|
||
}
|
||
var renderedPeer: RenderedPeer?
|
||
var contactStatus: ChatContactStatus?
|
||
if let peer = peerView.peers[peerView.peerId] {
|
||
if let cachedData = peerView.cachedData as? CachedUserData {
|
||
contactStatus = ChatContactStatus(canAddContact: !peerView.peerIsContact, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: nil)
|
||
} else if let cachedData = peerView.cachedData as? CachedGroupData {
|
||
var invitedBy: Peer?
|
||
if let invitedByPeerId = cachedData.invitedBy {
|
||
if let peer = peerView.peers[invitedByPeerId] {
|
||
invitedBy = peer
|
||
}
|
||
}
|
||
contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy)
|
||
} else if let cachedData = peerView.cachedData as? CachedChannelData {
|
||
var canReportIrrelevantLocation = true
|
||
if let peer = peerView.peers[peerView.peerId] as? TelegramChannel, peer.participationStatus == .member {
|
||
canReportIrrelevantLocation = false
|
||
}
|
||
if let peerReportNotice = peerReportNotice, peerReportNotice {
|
||
canReportIrrelevantLocation = false
|
||
}
|
||
var invitedBy: Peer?
|
||
if let invitedByPeerId = cachedData.invitedBy {
|
||
if let peer = peerView.peers[invitedByPeerId] {
|
||
invitedBy = peer
|
||
}
|
||
}
|
||
contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: canReportIrrelevantLocation, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy)
|
||
}
|
||
|
||
var peers = SimpleDictionary<PeerId, Peer>()
|
||
peers[peer.id] = peer
|
||
if let associatedPeerId = peer.associatedPeerId, let associatedPeer = peerView.peers[associatedPeerId] {
|
||
peers[associatedPeer.id] = associatedPeer
|
||
}
|
||
renderedPeer = RenderedPeer(peerId: peer.id, peers: peers)
|
||
}
|
||
|
||
var isNotAccessible: Bool = false
|
||
if let cachedChannelData = peerView.cachedData as? CachedChannelData {
|
||
isNotAccessible = cachedChannelData.isNotAccessible
|
||
}
|
||
|
||
if firstTime && isNotAccessible {
|
||
strongSelf.context.account.viewTracker.forceUpdateCachedPeerData(peerId: peerView.peerId)
|
||
}
|
||
|
||
var hasBots: Bool = false
|
||
var autoremoveTimeout: Int32?
|
||
if let peer = peerView.peers[peerView.peerId] {
|
||
if let cachedGroupData = peerView.cachedData as? CachedGroupData {
|
||
if !cachedGroupData.botInfos.isEmpty {
|
||
hasBots = true
|
||
}
|
||
if case let .known(value) = cachedGroupData.autoremoveTimeout {
|
||
autoremoveTimeout = value?.effectiveValue
|
||
}
|
||
} else if let cachedChannelData = peerView.cachedData as? CachedChannelData {
|
||
if let channel = peer as? TelegramChannel, case .group = channel.info {
|
||
if !cachedChannelData.botInfos.isEmpty {
|
||
hasBots = true
|
||
}
|
||
}
|
||
if case let .known(value) = cachedChannelData.autoremoveTimeout {
|
||
autoremoveTimeout = value?.effectiveValue
|
||
}
|
||
} else if let cachedUserData = peerView.cachedData as? CachedUserData {
|
||
if case let .known(value) = cachedUserData.autoremoveTimeout {
|
||
autoremoveTimeout = value?.effectiveValue
|
||
}
|
||
}
|
||
}
|
||
|
||
let isArchived: Bool = peerView.groupId == Namespaces.PeerGroup.archive
|
||
|
||
var explicitelyCanPinMessages: Bool = false
|
||
if let cachedUserData = peerView.cachedData as? CachedUserData {
|
||
explicitelyCanPinMessages = cachedUserData.canPinMessages
|
||
} else if peerView.peerId == context.account.peerId {
|
||
explicitelyCanPinMessages = true
|
||
}
|
||
|
||
var animated = false
|
||
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat, let updated = renderedPeer?.peer as? TelegramSecretChat, peer.embeddedState != updated.embeddedState {
|
||
animated = true
|
||
}
|
||
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, let updated = renderedPeer?.peer as? TelegramChannel {
|
||
if peer.participationStatus != updated.participationStatus {
|
||
animated = true
|
||
}
|
||
}
|
||
|
||
var didDisplayActionsPanel = false
|
||
if let contactStatus = strongSelf.presentationInterfaceState.contactStatus, !contactStatus.isEmpty, let peerStatusSettings = contactStatus.peerStatusSettings {
|
||
if !peerStatusSettings.flags.isEmpty {
|
||
if contactStatus.canAddContact && peerStatusSettings.contains(.canAddContact) {
|
||
didDisplayActionsPanel = true
|
||
} else if peerStatusSettings.contains(.canReport) || peerStatusSettings.contains(.canBlock) {
|
||
didDisplayActionsPanel = true
|
||
} else if peerStatusSettings.contains(.canShareContact) {
|
||
didDisplayActionsPanel = true
|
||
} else if contactStatus.canReportIrrelevantLocation && peerStatusSettings.contains(.canReportIrrelevantGeoLocation) {
|
||
didDisplayActionsPanel = true
|
||
} else if peerStatusSettings.contains(.suggestAddMembers) {
|
||
didDisplayActionsPanel = true
|
||
}
|
||
}
|
||
}
|
||
|
||
var displayActionsPanel = false
|
||
if let contactStatus = contactStatus, !contactStatus.isEmpty, let peerStatusSettings = contactStatus.peerStatusSettings {
|
||
if !peerStatusSettings.flags.isEmpty {
|
||
if contactStatus.canAddContact && peerStatusSettings.contains(.canAddContact) {
|
||
displayActionsPanel = true
|
||
} else if peerStatusSettings.contains(.canReport) || peerStatusSettings.contains(.canBlock) {
|
||
displayActionsPanel = true
|
||
} else if peerStatusSettings.contains(.canShareContact) {
|
||
displayActionsPanel = true
|
||
} else if contactStatus.canReportIrrelevantLocation && peerStatusSettings.contains(.canReportIrrelevantGeoLocation) {
|
||
displayActionsPanel = true
|
||
} else if peerStatusSettings.contains(.suggestAddMembers) {
|
||
displayActionsPanel = true
|
||
}
|
||
}
|
||
}
|
||
|
||
if displayActionsPanel != didDisplayActionsPanel {
|
||
animated = true
|
||
}
|
||
|
||
if strongSelf.preloadHistoryPeerId != peerDiscussionId {
|
||
strongSelf.preloadHistoryPeerId = peerDiscussionId
|
||
if let peerDiscussionId = peerDiscussionId {
|
||
let combinedDisposable = DisposableSet()
|
||
strongSelf.preloadHistoryPeerIdDisposable.set(combinedDisposable)
|
||
combinedDisposable.add(strongSelf.context.account.viewTracker.polledChannel(peerId: peerDiscussionId).start())
|
||
combinedDisposable.add(strongSelf.context.account.addAdditionalPreloadHistoryPeerId(peerId: peerDiscussionId))
|
||
} else {
|
||
strongSelf.preloadHistoryPeerIdDisposable.set(nil)
|
||
}
|
||
}
|
||
|
||
strongSelf.updateChatPresentationInterfaceState(animated: animated, interactive: false, {
|
||
return $0.updatedPeer { _ in
|
||
return renderedPeer
|
||
}.updatedIsNotAccessible(isNotAccessible).updatedContactStatus(contactStatus).updatedHasBots(hasBots).updatedIsArchived(isArchived).updatedPeerIsMuted(peerIsMuted).updatedPeerDiscussionId(peerDiscussionId).updatedPeerGeoLocation(peerGeoLocation).updatedExplicitelyCanPinMessages(explicitelyCanPinMessages).updatedHasScheduledMessages(hasScheduledMessages)
|
||
.updatedAutoremoveTimeout(autoremoveTimeout)
|
||
})
|
||
if !strongSelf.didSetChatLocationInfoReady {
|
||
strongSelf.didSetChatLocationInfoReady = true
|
||
strongSelf._chatLocationInfoReady.set(.single(true))
|
||
}
|
||
strongSelf.updateReminderActivity()
|
||
if let upgradedToPeerId = upgradedToPeerId {
|
||
if let navigationController = strongSelf.effectiveNavigationController {
|
||
var viewControllers = navigationController.viewControllers
|
||
if let index = viewControllers.firstIndex(where: { $0 === strongSelf }) {
|
||
viewControllers[index] = ChatControllerImpl(context: strongSelf.context, chatLocation: .peer(upgradedToPeerId))
|
||
navigationController.setViewControllers(viewControllers, animated: false)
|
||
}
|
||
}
|
||
} else if shouldDismiss {
|
||
strongSelf.dismiss()
|
||
}
|
||
}
|
||
}))
|
||
} else if case let .replyThread(messagePromise) = self.chatLocationInfoData {
|
||
let hasScheduledMessages: Signal<Bool, NoError> = .single(false)
|
||
self.reportIrrelvantGeoNoticePromise.set(.single(nil))
|
||
|
||
let replyThreadType: ChatTitleContent.ReplyThreadType
|
||
switch chatLocation {
|
||
case .peer:
|
||
replyThreadType = .replies
|
||
case let .replyThread(replyThreadMessage):
|
||
if replyThreadMessage.isChannelPost {
|
||
replyThreadType = .comments
|
||
} else {
|
||
replyThreadType = .replies
|
||
}
|
||
}
|
||
|
||
let peerView = context.account.viewTracker.peerView(peerId)
|
||
|
||
self.peerDisposable.set((combineLatest(queue: Queue.mainQueue(),
|
||
peerView,
|
||
messagePromise.get(),
|
||
hasScheduledMessages
|
||
)
|
||
|> deliverOnMainQueue).start(next: { [weak self] peerView, message, onlineMemberCount in
|
||
if let strongSelf = self {
|
||
var count = 0
|
||
if let message = message {
|
||
for attribute in message.attributes {
|
||
if let attribute = attribute as? ReplyThreadMessageAttribute {
|
||
count = Int(attribute.count)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
strongSelf.chatTitleView?.titleContent = .replyThread(type: replyThreadType, count: count)
|
||
|
||
let firstTime = strongSelf.peerView == nil
|
||
strongSelf.peerView = peerView
|
||
|
||
if strongSelf.isNodeLoaded {
|
||
strongSelf.chatDisplayNode.peerView = peerView
|
||
}
|
||
var peerIsMuted = false
|
||
if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings {
|
||
if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) {
|
||
peerIsMuted = true
|
||
}
|
||
}
|
||
var peerDiscussionId: PeerId?
|
||
var peerGeoLocation: PeerGeoLocation?
|
||
if let peer = peerView.peers[peerView.peerId] as? TelegramChannel, let cachedData = peerView.cachedData as? CachedChannelData {
|
||
if case .broadcast = peer.info {
|
||
if case let .known(value) = cachedData.linkedDiscussionPeerId {
|
||
peerDiscussionId = value
|
||
}
|
||
} else {
|
||
peerGeoLocation = cachedData.peerGeoLocation
|
||
}
|
||
}
|
||
var renderedPeer: RenderedPeer?
|
||
var contactStatus: ChatContactStatus?
|
||
if let peer = peerView.peers[peerView.peerId] {
|
||
if let cachedData = peerView.cachedData as? CachedUserData {
|
||
contactStatus = ChatContactStatus(canAddContact: !peerView.peerIsContact, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: nil)
|
||
} else if let cachedData = peerView.cachedData as? CachedGroupData {
|
||
var invitedBy: Peer?
|
||
if let invitedByPeerId = cachedData.invitedBy {
|
||
if let peer = peerView.peers[invitedByPeerId] {
|
||
invitedBy = peer
|
||
}
|
||
}
|
||
contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: false, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy)
|
||
} else if let cachedData = peerView.cachedData as? CachedChannelData {
|
||
var canReportIrrelevantLocation = true
|
||
if let peer = peerView.peers[peerView.peerId] as? TelegramChannel, peer.participationStatus == .member {
|
||
canReportIrrelevantLocation = false
|
||
}
|
||
canReportIrrelevantLocation = false
|
||
var invitedBy: Peer?
|
||
if let invitedByPeerId = cachedData.invitedBy {
|
||
if let peer = peerView.peers[invitedByPeerId] {
|
||
invitedBy = peer
|
||
}
|
||
}
|
||
contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: canReportIrrelevantLocation, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy)
|
||
}
|
||
|
||
var peers = SimpleDictionary<PeerId, Peer>()
|
||
peers[peer.id] = peer
|
||
if let associatedPeerId = peer.associatedPeerId, let associatedPeer = peerView.peers[associatedPeerId] {
|
||
peers[associatedPeer.id] = associatedPeer
|
||
}
|
||
renderedPeer = RenderedPeer(peerId: peer.id, peers: peers)
|
||
}
|
||
|
||
var isNotAccessible: Bool = false
|
||
if let cachedChannelData = peerView.cachedData as? CachedChannelData {
|
||
isNotAccessible = cachedChannelData.isNotAccessible
|
||
}
|
||
|
||
if firstTime && isNotAccessible {
|
||
strongSelf.context.account.viewTracker.forceUpdateCachedPeerData(peerId: peerView.peerId)
|
||
}
|
||
|
||
var hasBots: Bool = false
|
||
if let peer = peerView.peers[peerView.peerId] {
|
||
if let cachedGroupData = peerView.cachedData as? CachedGroupData {
|
||
if !cachedGroupData.botInfos.isEmpty {
|
||
hasBots = true
|
||
}
|
||
} else if let cachedChannelData = peerView.cachedData as? CachedChannelData, let channel = peer as? TelegramChannel, case .group = channel.info {
|
||
if !cachedChannelData.botInfos.isEmpty {
|
||
hasBots = true
|
||
}
|
||
}
|
||
}
|
||
|
||
let isArchived: Bool = peerView.groupId == Namespaces.PeerGroup.archive
|
||
|
||
var explicitelyCanPinMessages: Bool = false
|
||
if let cachedUserData = peerView.cachedData as? CachedUserData {
|
||
explicitelyCanPinMessages = cachedUserData.canPinMessages
|
||
} else if peerView.peerId == context.account.peerId {
|
||
explicitelyCanPinMessages = true
|
||
}
|
||
|
||
var animated = false
|
||
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat, let updated = renderedPeer?.peer as? TelegramSecretChat, peer.embeddedState != updated.embeddedState {
|
||
animated = true
|
||
}
|
||
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, let updated = renderedPeer?.peer as? TelegramChannel {
|
||
if peer.participationStatus != updated.participationStatus {
|
||
animated = true
|
||
}
|
||
}
|
||
|
||
var didDisplayActionsPanel = false
|
||
if let contactStatus = strongSelf.presentationInterfaceState.contactStatus, !contactStatus.isEmpty, let peerStatusSettings = contactStatus.peerStatusSettings {
|
||
if !peerStatusSettings.flags.isEmpty {
|
||
if contactStatus.canAddContact && peerStatusSettings.contains(.canAddContact) {
|
||
didDisplayActionsPanel = true
|
||
} else if peerStatusSettings.contains(.canReport) || peerStatusSettings.contains(.canBlock) {
|
||
didDisplayActionsPanel = true
|
||
} else if peerStatusSettings.contains(.canShareContact) {
|
||
didDisplayActionsPanel = true
|
||
} else if contactStatus.canReportIrrelevantLocation && peerStatusSettings.contains(.canReportIrrelevantGeoLocation) {
|
||
didDisplayActionsPanel = true
|
||
} else if peerStatusSettings.contains(.suggestAddMembers) {
|
||
didDisplayActionsPanel = true
|
||
}
|
||
}
|
||
}
|
||
|
||
var displayActionsPanel = false
|
||
if let contactStatus = contactStatus, !contactStatus.isEmpty, let peerStatusSettings = contactStatus.peerStatusSettings {
|
||
if !peerStatusSettings.flags.isEmpty {
|
||
if contactStatus.canAddContact && peerStatusSettings.contains(.canAddContact) {
|
||
displayActionsPanel = true
|
||
} else if peerStatusSettings.contains(.canReport) || peerStatusSettings.contains(.canBlock) {
|
||
displayActionsPanel = true
|
||
} else if peerStatusSettings.contains(.canShareContact) {
|
||
displayActionsPanel = true
|
||
} else if contactStatus.canReportIrrelevantLocation && peerStatusSettings.contains(.canReportIrrelevantGeoLocation) {
|
||
displayActionsPanel = true
|
||
} else if peerStatusSettings.contains(.suggestAddMembers) {
|
||
displayActionsPanel = true
|
||
}
|
||
}
|
||
}
|
||
|
||
if displayActionsPanel != didDisplayActionsPanel {
|
||
animated = true
|
||
}
|
||
|
||
if strongSelf.preloadHistoryPeerId != peerDiscussionId {
|
||
strongSelf.preloadHistoryPeerId = peerDiscussionId
|
||
if let peerDiscussionId = peerDiscussionId {
|
||
strongSelf.preloadHistoryPeerIdDisposable.set(strongSelf.context.account.addAdditionalPreloadHistoryPeerId(peerId: peerDiscussionId))
|
||
} else {
|
||
strongSelf.preloadHistoryPeerIdDisposable.set(nil)
|
||
}
|
||
}
|
||
|
||
strongSelf.updateChatPresentationInterfaceState(animated: animated, interactive: false, {
|
||
return $0.updatedPeer { _ in
|
||
return renderedPeer
|
||
}.updatedIsNotAccessible(isNotAccessible).updatedContactStatus(contactStatus).updatedHasBots(hasBots).updatedIsArchived(isArchived).updatedPeerIsMuted(peerIsMuted).updatedPeerDiscussionId(peerDiscussionId).updatedPeerGeoLocation(peerGeoLocation).updatedExplicitelyCanPinMessages(explicitelyCanPinMessages).updatedHasScheduledMessages(false)
|
||
})
|
||
if !strongSelf.didSetChatLocationInfoReady {
|
||
strongSelf.didSetChatLocationInfoReady = true
|
||
strongSelf._chatLocationInfoReady.set(.single(true))
|
||
}
|
||
}
|
||
}))
|
||
}
|
||
}
|
||
|
||
self.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.firstIndex(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.sorted()
|
||
}
|
||
} else {
|
||
if let index = $0.firstIndex(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
|
||
strongSelf.lockOrientation = audioRecorder != nil
|
||
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
$0.updatedInputTextPanelState { panelState in
|
||
let isLocked = strongSelf.lockMediaRecordingRequestId == strongSelf.beginMediaRecordingRequestId
|
||
if let audioRecorder = audioRecorder {
|
||
if panelState.mediaRecordingState == nil {
|
||
return panelState.withUpdatedMediaRecordingState(.audio(recorder: audioRecorder, isLocked: isLocked))
|
||
}
|
||
} else {
|
||
if case .waitingForPreview = panelState.mediaRecordingState {
|
||
return panelState
|
||
}
|
||
return panelState.withUpdatedMediaRecordingState(nil)
|
||
}
|
||
return panelState
|
||
}
|
||
})
|
||
strongSelf.audioRecorderStatusDisposable?.dispose()
|
||
|
||
if let audioRecorder = audioRecorder {
|
||
if !audioRecorder.beginWithTone {
|
||
strongSelf.recorderFeedback?.impact(.light)
|
||
}
|
||
audioRecorder.start()
|
||
strongSelf.audioRecorderStatusDisposable = (audioRecorder.recordingState
|
||
|> deliverOnMainQueue).start(next: { value in
|
||
if case .stopped = value {
|
||
self?.stopMediaRecorder()
|
||
}
|
||
})
|
||
} else {
|
||
strongSelf.audioRecorderStatusDisposable = nil
|
||
}
|
||
strongSelf.updateDownButtonVisibility()
|
||
}
|
||
}
|
||
})
|
||
|
||
self.videoRecorderDisposable = (self.videoRecorder.get()
|
||
|> deliverOnMainQueue).start(next: { [weak self] videoRecorder in
|
||
if let strongSelf = self {
|
||
if strongSelf.videoRecorderValue !== videoRecorder {
|
||
let previousVideoRecorderValue = strongSelf.videoRecorderValue
|
||
strongSelf.videoRecorderValue = videoRecorder
|
||
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
$0.updatedInputTextPanelState { panelState in
|
||
if let videoRecorder = videoRecorder {
|
||
if panelState.mediaRecordingState == nil {
|
||
return panelState.withUpdatedMediaRecordingState(.video(status: .recording(videoRecorder.audioStatus), isLocked: strongSelf.lockMediaRecordingRequestId == strongSelf.beginMediaRecordingRequestId))
|
||
}
|
||
} else {
|
||
return panelState.withUpdatedMediaRecordingState(nil)
|
||
}
|
||
return panelState
|
||
}
|
||
})
|
||
|
||
if let videoRecorder = videoRecorder {
|
||
strongSelf.recorderFeedback?.impact(.light)
|
||
|
||
videoRecorder.onDismiss = { [weak self] isCancelled in
|
||
self?.chatDisplayNode.updateRecordedMediaDeleted(isCancelled)
|
||
self?.beginMediaRecordingRequestId += 1
|
||
self?.lockMediaRecordingRequestId = nil
|
||
self?.videoRecorder.set(.single(nil))
|
||
}
|
||
videoRecorder.onStop = {
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
$0.updatedInputTextPanelState { panelState in
|
||
return panelState.withUpdatedMediaRecordingState(.video(status: .editing, isLocked: false))
|
||
}
|
||
})
|
||
}
|
||
}
|
||
strongSelf.present(videoRecorder, in: .window(.root))
|
||
|
||
if strongSelf.lockMediaRecordingRequestId == strongSelf.beginMediaRecordingRequestId {
|
||
videoRecorder.lockVideo()
|
||
}
|
||
}
|
||
strongSelf.updateDownButtonVisibility()
|
||
|
||
if let previousVideoRecorderValue = previousVideoRecorderValue {
|
||
previousVideoRecorderValue.dismissVideo()
|
||
}
|
||
}
|
||
}
|
||
})
|
||
|
||
if let botStart = botStart, case .automatic = botStart.behavior {
|
||
self.startBot(botStart.payload)
|
||
}
|
||
|
||
let activitySpace: PeerActivitySpace
|
||
switch self.chatLocation {
|
||
case let .peer(peerId):
|
||
activitySpace = PeerActivitySpace(peerId: peerId, category: .global)
|
||
case let .replyThread(replyThreadMessage):
|
||
activitySpace = PeerActivitySpace(peerId: replyThreadMessage.messageId.peerId, category: .thread(makeMessageThreadId(replyThreadMessage.messageId)))
|
||
}
|
||
|
||
self.inputActivityDisposable = (self.typingActivityPromise.get()
|
||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||
if let strongSelf = self {
|
||
strongSelf.context.account.updateLocalInputActivity(peerId: activitySpace, activity: .typingText, isPresent: value)
|
||
}
|
||
})
|
||
|
||
self.recordingActivityDisposable = (self.recordingActivityPromise.get()
|
||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||
if let strongSelf = self {
|
||
strongSelf.acquiredRecordingActivityDisposable?.dispose()
|
||
switch value {
|
||
case .voice:
|
||
strongSelf.acquiredRecordingActivityDisposable = strongSelf.context.account.acquireLocalInputActivity(peerId: activitySpace, activity: .recordingVoice)
|
||
case .instantVideo:
|
||
strongSelf.acquiredRecordingActivityDisposable = strongSelf.context.account.acquireLocalInputActivity(peerId: activitySpace, activity: .recordingInstantVideo)
|
||
case .none:
|
||
strongSelf.acquiredRecordingActivityDisposable = nil
|
||
}
|
||
}
|
||
})
|
||
|
||
self.presentationDataDisposable = (context.sharedContext.presentationData
|
||
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
|
||
if let strongSelf = self {
|
||
let previousTheme = strongSelf.presentationData.theme
|
||
let previousStrings = strongSelf.presentationData.strings
|
||
let previousChatWallpaper = strongSelf.presentationData.chatWallpaper
|
||
|
||
strongSelf.presentationData = presentationData
|
||
|
||
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings || presentationData.chatWallpaper != previousChatWallpaper {
|
||
strongSelf.themeAndStringsUpdated()
|
||
}
|
||
}
|
||
})
|
||
|
||
self.automaticMediaDownloadSettingsDisposable = (context.sharedContext.automaticMediaDownloadSettings
|
||
|> deliverOnMainQueue).start(next: { [weak self] downloadSettings in
|
||
if let strongSelf = self, strongSelf.automaticMediaDownloadSettings != downloadSettings {
|
||
strongSelf.automaticMediaDownloadSettings = downloadSettings
|
||
strongSelf.controllerInteraction?.automaticMediaDownloadSettings = downloadSettings
|
||
if strongSelf.isNodeLoaded {
|
||
strongSelf.chatDisplayNode.updateAutomaticMediaDownloadSettings(downloadSettings)
|
||
}
|
||
}
|
||
})
|
||
|
||
self.stickerSettingsDisposable = (context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.stickerSettings])
|
||
|> deliverOnMainQueue).start(next: { [weak self] sharedData in
|
||
var stickerSettings = StickerSettings.defaultSettings
|
||
if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.stickerSettings] as? StickerSettings {
|
||
stickerSettings = value
|
||
}
|
||
|
||
let chatStickerSettings = ChatInterfaceStickerSettings(stickerSettings: stickerSettings)
|
||
|
||
if let strongSelf = self, strongSelf.stickerSettings != chatStickerSettings {
|
||
strongSelf.stickerSettings = chatStickerSettings
|
||
strongSelf.controllerInteraction?.stickerSettings = chatStickerSettings
|
||
if strongSelf.isNodeLoaded {
|
||
strongSelf.chatDisplayNode.updateStickerSettings(chatStickerSettings)
|
||
}
|
||
}
|
||
})
|
||
|
||
var wasInForeground = true
|
||
self.applicationInForegroundDisposable = (context.sharedContext.applicationBindings.applicationInForeground
|
||
|> distinctUntilChanged
|
||
|> deliverOn(Queue.mainQueue())).start(next: { [weak self] value in
|
||
if let strongSelf = self, strongSelf.isNodeLoaded {
|
||
if !value {
|
||
strongSelf.saveInterfaceState()
|
||
strongSelf.raiseToListen?.applicationResignedActive()
|
||
|
||
strongSelf.stopMediaRecorder()
|
||
} else {
|
||
if !wasInForeground {
|
||
strongSelf.chatDisplayNode.recursivelyEnsureDisplaySynchronously(true)
|
||
}
|
||
}
|
||
wasInForeground = value
|
||
}
|
||
})
|
||
|
||
if case let .peer(peerId) = chatLocation, peerId.namespace == Namespaces.Peer.SecretChat {
|
||
self.applicationInFocusDisposable = (context.sharedContext.applicationBindings.applicationIsActive
|
||
|> distinctUntilChanged
|
||
|> deliverOn(Queue.mainQueue())).start(next: { [weak self] value in
|
||
guard let strongSelf = self, strongSelf.isNodeLoaded else {
|
||
return
|
||
}
|
||
strongSelf.chatDisplayNode.updateIsBlurred(!value)
|
||
})
|
||
}
|
||
|
||
self.canReadHistoryDisposable = (combineLatest(context.sharedContext.applicationBindings.applicationInForeground, self.canReadHistory.get()) |> map { a, b in
|
||
return a && b
|
||
} |> deliverOnMainQueue).start(next: { [weak self] value in
|
||
if let strongSelf = self, strongSelf.canReadHistoryValue != value {
|
||
strongSelf.canReadHistoryValue = value
|
||
strongSelf.raiseToListen?.enabled = value
|
||
strongSelf.isReminderActivityEnabled = value
|
||
strongSelf.updateReminderActivity()
|
||
}
|
||
})
|
||
|
||
self.networkStateDisposable = (context.account.networkState |> deliverOnMainQueue).start(next: { [weak self] state in
|
||
if let strongSelf = self, case .standard(previewing: false) = strongSelf.presentationInterfaceState.mode {
|
||
strongSelf.chatTitleView?.networkState = state
|
||
}
|
||
})
|
||
|
||
if case let .peer(peerId) = self.chatLocation {
|
||
self.importStateDisposable = (ChatHistoryImportTasks.importState(peerId: peerId)
|
||
|> distinctUntilChanged
|
||
|> deliverOnMainQueue).start(next: { [weak self] state in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
let mappedState = state.flatMap { state -> ChatPresentationImportState in
|
||
ChatPresentationImportState(progress: state)
|
||
}
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
$0.updatedImportState(mappedState)
|
||
})
|
||
})
|
||
}
|
||
}
|
||
|
||
required public init(coder aDecoder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
deinit {
|
||
let _ = ChatControllerCount.modify { value in
|
||
return value - 1
|
||
}
|
||
self.historyStateDisposable?.dispose()
|
||
self.messageIndexDisposable.dispose()
|
||
self.navigationActionDisposable.dispose()
|
||
self.galleryHiddenMesageAndMediaDisposable.dispose()
|
||
self.temporaryHiddenGalleryMediaDisposable.dispose()
|
||
self.peerDisposable.dispose()
|
||
self.messageContextDisposable.dispose()
|
||
self.controllerNavigationDisposable.dispose()
|
||
self.sentMessageEventsDisposable.dispose()
|
||
self.failedMessageEventsDisposable.dispose()
|
||
self.messageActionCallbackDisposable.dispose()
|
||
self.messageActionUrlAuthDisposable.dispose()
|
||
self.editMessageDisposable.dispose()
|
||
self.editMessageErrorsDisposable.dispose()
|
||
self.enqueueMediaMessageDisposable.dispose()
|
||
self.resolvePeerByNameDisposable?.dispose()
|
||
self.shareStatusDisposable?.dispose()
|
||
self.clearCacheDisposable?.dispose()
|
||
self.bankCardDisposable?.dispose()
|
||
self.botCallbackAlertMessageDisposable?.dispose()
|
||
self.selectMessagePollOptionDisposables?.dispose()
|
||
for (_, info) in self.contextQueryStates {
|
||
info.1.dispose()
|
||
}
|
||
self.urlPreviewQueryState?.1.dispose()
|
||
self.audioRecorderDisposable?.dispose()
|
||
self.audioRecorderStatusDisposable?.dispose()
|
||
self.videoRecorderDisposable?.dispose()
|
||
self.buttonKeyboardMessageDisposable?.dispose()
|
||
self.cachedDataDisposable?.dispose()
|
||
self.resolveUrlDisposable?.dispose()
|
||
self.chatUnreadCountDisposable?.dispose()
|
||
self.chatUnreadMentionCountDisposable?.dispose()
|
||
self.peerInputActivitiesDisposable?.dispose()
|
||
self.recentlyUsedInlineBotsDisposable?.dispose()
|
||
self.unpinMessageDisposable?.dispose()
|
||
self.inputActivityDisposable?.dispose()
|
||
self.recordingActivityDisposable?.dispose()
|
||
self.acquiredRecordingActivityDisposable?.dispose()
|
||
self.presentationDataDisposable?.dispose()
|
||
self.searchDisposable?.dispose()
|
||
self.applicationInForegroundDisposable?.dispose()
|
||
self.applicationInFocusDisposable?.dispose()
|
||
self.canReadHistoryDisposable?.dispose()
|
||
self.networkStateDisposable?.dispose()
|
||
self.chatAdditionalDataDisposable.dispose()
|
||
self.shareStatusDisposable?.dispose()
|
||
self.context.sharedContext.mediaManager.galleryHiddenMediaManager.removeTarget(self)
|
||
self.preloadHistoryPeerIdDisposable.dispose()
|
||
self.reportIrrelvantGeoDisposable?.dispose()
|
||
self.reminderActivity?.invalidate()
|
||
self.updateSlowmodeStatusDisposable.dispose()
|
||
self.keepPeerInfoScreenDataHotDisposable.dispose()
|
||
self.preloadAvatarDisposable.dispose()
|
||
self.peekTimerDisposable.dispose()
|
||
self.hasActiveGroupCallDisposable?.dispose()
|
||
self.createVoiceChatDisposable.dispose()
|
||
self.checksTooltipDisposable.dispose()
|
||
self.selectAddMemberDisposable.dispose()
|
||
self.addMemberDisposable.dispose()
|
||
self.importStateDisposable?.dispose()
|
||
}
|
||
|
||
public func updatePresentationMode(_ mode: ChatControllerPresentationMode) {
|
||
self.updateChatPresentationInterfaceState(animated: false, interactive: false, {
|
||
return $0.updatedMode(mode)
|
||
})
|
||
}
|
||
|
||
var chatDisplayNode: ChatControllerNode {
|
||
get {
|
||
return super.displayNode as! ChatControllerNode
|
||
}
|
||
}
|
||
|
||
private func themeAndStringsUpdated() {
|
||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
|
||
switch self.presentationInterfaceState.mode {
|
||
case .standard:
|
||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
|
||
self.deferScreenEdgeGestures = []
|
||
case .overlay:
|
||
self.statusBar.statusBarStyle = .Hide
|
||
self.deferScreenEdgeGestures = [.top]
|
||
case .inline:
|
||
self.statusBar.statusBarStyle = .Ignore
|
||
}
|
||
self.updateNavigationBarPresentation()
|
||
self.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in
|
||
var state = state
|
||
state = state.updatedTheme(self.presentationData.theme)
|
||
state = state.updatedStrings(self.presentationData.strings)
|
||
state = state.updatedDateTimeFormat(self.presentationData.dateTimeFormat)
|
||
state = state.updatedChatWallpaper(self.presentationData.chatWallpaper)
|
||
state = state.updatedBubbleCorners(self.presentationData.chatBubbleCorners)
|
||
return state
|
||
})
|
||
|
||
self.currentContextController?.updateTheme(presentationData: self.presentationData)
|
||
}
|
||
|
||
private func updateNavigationBarPresentation() {
|
||
let navigationBarTheme: NavigationBarTheme
|
||
|
||
if self.hasEmbeddedTitleContent {
|
||
navigationBarTheme = NavigationBarTheme(rootControllerTheme: defaultDarkPresentationTheme, hideBackground: self.context.sharedContext.immediateExperimentalUISettings.playerEmbedding ? true : false, hideBadge: true)
|
||
} else {
|
||
navigationBarTheme = NavigationBarTheme(rootControllerTheme: self.presentationData.theme, hideBackground: self.context.sharedContext.immediateExperimentalUISettings.playerEmbedding ? true : false, hideBadge: false)
|
||
}
|
||
|
||
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)))
|
||
|
||
self.chatTitleView?.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings, hasEmbeddedTitleContent: self.hasEmbeddedTitleContent)
|
||
}
|
||
|
||
private func topPinnedMessageSignal(latest: Bool) -> Signal<ChatPinnedMessage?, NoError> {
|
||
let topPinnedMessage: Signal<ChatPinnedMessage?, NoError>
|
||
switch self.chatLocation {
|
||
case let .peer(peerId):
|
||
struct ReferenceMessage {
|
||
var id: MessageId
|
||
var isScrolled: Bool
|
||
}
|
||
|
||
let messageRangeEdge: Signal<Bool, NoError> = self.context.sharedContext.accountManager.sharedData(keys: Set([ApplicationSpecificSharedDataKeys.experimentalUISettings]))
|
||
|> map { sharedData -> Bool in
|
||
let experimentalSettings: ExperimentalUISettings = (sharedData.entries[ApplicationSpecificSharedDataKeys.experimentalUISettings] as? ExperimentalUISettings) ?? ExperimentalUISettings.defaultSettings
|
||
return experimentalSettings.snapPinListToTop
|
||
}
|
||
|> distinctUntilChanged
|
||
|
||
let referenceMessage: Signal<ReferenceMessage?, NoError>
|
||
if latest {
|
||
referenceMessage = .single(nil)
|
||
} else {
|
||
referenceMessage = combineLatest(
|
||
queue: Queue.mainQueue(),
|
||
self.scrolledToMessageId.get(),
|
||
self.chatDisplayNode.historyNode.topVisibleMessageRange.get(),
|
||
messageRangeEdge
|
||
)
|
||
|> map { scrolledToMessageId, topVisibleMessageRange, messageRangeEdge -> ReferenceMessage? in
|
||
let topVisibleMessage: MessageId?
|
||
if messageRangeEdge {
|
||
topVisibleMessage = topVisibleMessageRange?.lowerBound
|
||
} else {
|
||
topVisibleMessage = topVisibleMessageRange?.upperBound
|
||
}
|
||
|
||
if let scrolledToMessageId = scrolledToMessageId {
|
||
if let topVisibleMessage = topVisibleMessage {
|
||
if scrolledToMessageId.allowedReplacementDirection.contains(.up) && topVisibleMessage < scrolledToMessageId.id {
|
||
return ReferenceMessage(id: topVisibleMessage, isScrolled: false)
|
||
}
|
||
}
|
||
return ReferenceMessage(id: scrolledToMessageId.id, isScrolled: true)
|
||
} else if let topVisibleMessage = topVisibleMessage {
|
||
return ReferenceMessage(id: topVisibleMessage, isScrolled: false)
|
||
} else {
|
||
return nil
|
||
}
|
||
}
|
||
}
|
||
|
||
let context = self.context
|
||
|
||
func pinnedHistorySignal(anchorMessageId: MessageId?, count: Int) -> Signal<ChatHistoryViewUpdate, NoError> {
|
||
let location: ChatHistoryLocation
|
||
if let anchorMessageId = anchorMessageId {
|
||
location = .InitialSearch(location: .id(anchorMessageId), count: count, highlight: false)
|
||
} else {
|
||
location = .Initial(count: count)
|
||
}
|
||
|
||
return (chatHistoryViewForLocation(ChatHistoryLocationInput(content: location, id: 0), context: context, chatLocation: .peer(peerId), chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil), scheduled: false, fixedCombinedReadStates: nil, tagMask: MessageTags.pinned, appendMessagesFromTheSameGroup: false, additionalData: [], orderStatistics: .combinedLocation)
|
||
|> castError(Bool.self)
|
||
|> mapToSignal { update -> Signal<ChatHistoryViewUpdate, Bool> in
|
||
switch update {
|
||
case let .Loading(_, type):
|
||
if case .Generic(.FillHole) = type {
|
||
return .fail(true)
|
||
}
|
||
case let .HistoryView(_, type, _, _, _, _, _):
|
||
if case .Generic(.FillHole) = type {
|
||
return .fail(true)
|
||
}
|
||
}
|
||
return .single(update)
|
||
})
|
||
|> restartIfError
|
||
}
|
||
|
||
struct TopMessage {
|
||
var message: Message
|
||
var index: Int
|
||
}
|
||
|
||
let topMessage = pinnedHistorySignal(anchorMessageId: nil, count: 3)
|
||
|> map { update -> TopMessage? in
|
||
switch update {
|
||
case .Loading:
|
||
return nil
|
||
case let .HistoryView(viewValue, _, _, _, _, _, _):
|
||
if let entry = viewValue.entries.last {
|
||
let index: Int
|
||
if let location = entry.location {
|
||
index = location.index
|
||
} else {
|
||
index = viewValue.entries.count - 1
|
||
}
|
||
|
||
return TopMessage(
|
||
message: entry.message,
|
||
index: index
|
||
)
|
||
} else {
|
||
return nil
|
||
}
|
||
}
|
||
}
|
||
|
||
let loadCount = 100
|
||
|
||
struct PinnedHistory {
|
||
struct PinnedMessage {
|
||
var message: Message
|
||
var index: Int
|
||
}
|
||
|
||
var messages: [PinnedMessage]
|
||
var totalCount: Int
|
||
}
|
||
|
||
let adjustedReplyHistory: Signal<PinnedHistory, NoError>
|
||
if latest {
|
||
adjustedReplyHistory = pinnedHistorySignal(anchorMessageId: nil, count: loadCount)
|
||
|> map { view -> PinnedHistory in
|
||
switch view {
|
||
case .Loading:
|
||
return PinnedHistory(messages: [], totalCount: 0)
|
||
case let .HistoryView(viewValue, _, _, _, _, _, _):
|
||
var messages: [PinnedHistory.PinnedMessage] = []
|
||
var totalCount = viewValue.entries.count
|
||
for i in 0 ..< viewValue.entries.count {
|
||
let index: Int
|
||
if !viewValue.holeEarlier && viewValue.earlierId == nil {
|
||
index = i
|
||
} else if let location = viewValue.entries[i].location {
|
||
index = location.index
|
||
totalCount = location.count
|
||
} else {
|
||
index = i
|
||
}
|
||
messages.append(PinnedHistory.PinnedMessage(
|
||
message: viewValue.entries[i].message,
|
||
index: index
|
||
))
|
||
}
|
||
return PinnedHistory(messages: messages, totalCount: totalCount)
|
||
}
|
||
}
|
||
} else {
|
||
adjustedReplyHistory = (Signal<PinnedHistory, NoError> { subscriber in
|
||
var referenceMessageValue: ReferenceMessage?
|
||
var view: ChatHistoryViewUpdate?
|
||
|
||
let updateState: () -> Void = {
|
||
guard let view = view else {
|
||
return
|
||
}
|
||
guard case let .HistoryView(viewValue, _, _, _, _, _, _) = view else {
|
||
subscriber.putNext(PinnedHistory(messages: [], totalCount: 0))
|
||
return
|
||
}
|
||
|
||
var messages: [PinnedHistory.PinnedMessage] = []
|
||
for i in 0 ..< viewValue.entries.count {
|
||
messages.append(PinnedHistory.PinnedMessage(
|
||
message: viewValue.entries[i].message,
|
||
index: i
|
||
))
|
||
}
|
||
let result = PinnedHistory(messages: messages, totalCount: messages.count)
|
||
|
||
if let referenceId = referenceMessageValue?.id {
|
||
if viewValue.entries.count < loadCount {
|
||
subscriber.putNext(result)
|
||
} else if referenceId < viewValue.entries[1].message.id {
|
||
if viewValue.earlierId != nil {
|
||
subscriber.putCompletion()
|
||
} else {
|
||
subscriber.putNext(result)
|
||
}
|
||
} else if referenceId > viewValue.entries[viewValue.entries.count - 2].message.id {
|
||
if viewValue.laterId != nil {
|
||
subscriber.putCompletion()
|
||
} else {
|
||
subscriber.putNext(result)
|
||
}
|
||
} else {
|
||
subscriber.putNext(result)
|
||
}
|
||
} else {
|
||
if viewValue.isLoading {
|
||
subscriber.putNext(result)
|
||
} else if viewValue.holeLater || viewValue.laterId != nil {
|
||
subscriber.putCompletion()
|
||
} else {
|
||
subscriber.putNext(result)
|
||
}
|
||
}
|
||
}
|
||
|
||
var initializedView = false
|
||
let viewDisposable = MetaDisposable()
|
||
|
||
let referenceDisposable = (referenceMessage
|
||
|> deliverOnMainQueue).start(next: { referenceMessage in
|
||
referenceMessageValue = referenceMessage
|
||
if !initializedView {
|
||
initializedView = true
|
||
//print("reload at \(String(describing: referenceMessage?.id)) disposable \(unsafeBitCast(viewDisposable, to: UInt64.self))")
|
||
viewDisposable.set((pinnedHistorySignal(anchorMessageId: referenceMessage?.id, count: loadCount)
|
||
|> deliverOnMainQueue).start(next: { next in
|
||
view = next
|
||
updateState()
|
||
}))
|
||
}
|
||
updateState()
|
||
})
|
||
|
||
return ActionDisposable {
|
||
//print("dispose \(unsafeBitCast(viewDisposable, to: UInt64.self))")
|
||
referenceDisposable.dispose()
|
||
viewDisposable.dispose()
|
||
}
|
||
}
|
||
|> runOn(.mainQueue()))
|
||
|> restart
|
||
}
|
||
|
||
topPinnedMessage = combineLatest(queue: .mainQueue(),
|
||
adjustedReplyHistory,
|
||
topMessage,
|
||
referenceMessage
|
||
)
|
||
|> map { pinnedMessages, topMessage, referenceMessage -> ChatPinnedMessage? in
|
||
var message: ChatPinnedMessage?
|
||
|
||
let topMessageId: MessageId
|
||
if pinnedMessages.messages.isEmpty {
|
||
return nil
|
||
}
|
||
topMessageId = topMessage?.message.id ?? pinnedMessages.messages[pinnedMessages.messages.count - 1].message.id
|
||
|
||
if let referenceMessage = referenceMessage, referenceMessage.isScrolled, !pinnedMessages.messages.isEmpty, referenceMessage.id == pinnedMessages.messages[0].message.id, let topMessage = topMessage {
|
||
var index = topMessage.index
|
||
for message in pinnedMessages.messages {
|
||
if message.message.id == topMessage.message.id {
|
||
index = message.index
|
||
break
|
||
}
|
||
}
|
||
|
||
return ChatPinnedMessage(message: topMessage.message, index: index, totalCount: pinnedMessages.totalCount, topMessageId: topMessageId)
|
||
}
|
||
|
||
//print("reference: \(String(describing: referenceMessage?.id.id)) entries: \(view.entries.map(\.index.id.id))")
|
||
for i in 0 ..< pinnedMessages.messages.count {
|
||
let entry = pinnedMessages.messages[i]
|
||
var matches = false
|
||
if message == nil {
|
||
matches = true
|
||
} else if let referenceMessage = referenceMessage {
|
||
if referenceMessage.isScrolled {
|
||
if entry.message.id < referenceMessage.id {
|
||
matches = true
|
||
}
|
||
} else {
|
||
if entry.message.id <= referenceMessage.id {
|
||
matches = true
|
||
}
|
||
}
|
||
} else {
|
||
matches = true
|
||
}
|
||
if matches {
|
||
message = ChatPinnedMessage(message: entry.message, index: entry.index, totalCount: pinnedMessages.totalCount, topMessageId: topMessageId)
|
||
}
|
||
}
|
||
|
||
return message
|
||
}
|
||
|> distinctUntilChanged
|
||
default:
|
||
topPinnedMessage = .single(nil)
|
||
}
|
||
return topPinnedMessage
|
||
}
|
||
|
||
override public func loadDisplayNode() {
|
||
self.displayNode = ChatControllerNode(context: self.context, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, subject: self.subject, controllerInteraction: self.controllerInteraction!, chatPresentationInterfaceState: self.presentationInterfaceState, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, navigationBar: self.navigationBar, controller: self)
|
||
|
||
self.chatDisplayNode.historyNode.didScrollWithOffset = { [weak self] offset, transition, itemNode in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
for (tooltipScreen, tooltipItemNode) in strongSelf.currentMessageTooltipScreens {
|
||
if let itemNode = itemNode {
|
||
if itemNode === tooltipItemNode {
|
||
tooltipScreen.addRelativeScrollingOffset(-offset, transition: transition)
|
||
}
|
||
} else {
|
||
tooltipScreen.addRelativeScrollingOffset(-offset, transition: transition)
|
||
}
|
||
}
|
||
}
|
||
|
||
self.chatDisplayNode.historyNode.didScrollWithOffset = { [weak self] offset, _, _ in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
|
||
if offset > 0.0 {
|
||
if var scrolledToMessageIdValue = strongSelf.scrolledToMessageIdValue {
|
||
scrolledToMessageIdValue.allowedReplacementDirection.insert(.up)
|
||
strongSelf.scrolledToMessageIdValue = scrolledToMessageIdValue
|
||
}
|
||
} else if offset < 0.0 {
|
||
strongSelf.scrolledToMessageIdValue = nil
|
||
}
|
||
}
|
||
|
||
if case .pinnedMessages = self.presentationInterfaceState.subject {
|
||
self.chatDisplayNode.historyNode.setLoadStateUpdated({ [weak self] state, _ in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
if case .empty = state {
|
||
strongSelf.dismiss()
|
||
}
|
||
})
|
||
}
|
||
|
||
self.chatDisplayNode.peerView = self.peerView
|
||
|
||
let initialData = self.chatDisplayNode.historyNode.initialData
|
||
|> take(1)
|
||
|> beforeNext { [weak self] combinedInitialData in
|
||
guard let strongSelf = self, let combinedInitialData = combinedInitialData else {
|
||
return
|
||
}
|
||
if let interfaceState = combinedInitialData.initialData?.chatInterfaceState as? ChatInterfaceState {
|
||
var pinnedMessageId: MessageId?
|
||
var peerIsBlocked: Bool = false
|
||
var callsAvailable: Bool = true
|
||
var callsPrivate: Bool = false
|
||
var activeGroupCallInfo: ChatActiveGroupCallInfo?
|
||
var slowmodeState: ChatSlowmodeState?
|
||
if let cachedData = combinedInitialData.cachedData as? CachedChannelData {
|
||
pinnedMessageId = cachedData.pinnedMessageId
|
||
if let channel = combinedInitialData.initialData?.peer as? TelegramChannel, channel.isRestrictedBySlowmode, let timeout = cachedData.slowModeTimeout {
|
||
if let slowmodeUntilTimestamp = calculateSlowmodeActiveUntilTimestamp(account: strongSelf.context.account, untilTimestamp: cachedData.slowModeValidUntilTimestamp) {
|
||
slowmodeState = ChatSlowmodeState(timeout: timeout, variant: .timestamp(slowmodeUntilTimestamp))
|
||
}
|
||
}
|
||
if let activeCall = cachedData.activeCall {
|
||
activeGroupCallInfo = ChatActiveGroupCallInfo(activeCall: activeCall)
|
||
}
|
||
} else if let cachedData = combinedInitialData.cachedData as? CachedUserData {
|
||
peerIsBlocked = cachedData.isBlocked
|
||
callsAvailable = cachedData.voiceCallsAvailable
|
||
callsPrivate = cachedData.callsPrivate
|
||
pinnedMessageId = cachedData.pinnedMessageId
|
||
} else if let cachedData = combinedInitialData.cachedData as? CachedGroupData {
|
||
pinnedMessageId = cachedData.pinnedMessageId
|
||
if let activeCall = cachedData.activeCall {
|
||
activeGroupCallInfo = ChatActiveGroupCallInfo(activeCall: activeCall)
|
||
}
|
||
} else if let _ = combinedInitialData.cachedData as? CachedSecretChatData {
|
||
}
|
||
|
||
if case let .replyThread(replyThreadMessageId) = strongSelf.chatLocation {
|
||
pinnedMessageId = replyThreadMessageId.effectiveTopId
|
||
}
|
||
|
||
var pinnedMessage: ChatPinnedMessage?
|
||
if let pinnedMessageId = pinnedMessageId {
|
||
if let cachedDataMessages = combinedInitialData.cachedDataMessages {
|
||
if let message = cachedDataMessages[pinnedMessageId] {
|
||
pinnedMessage = ChatPinnedMessage(message: message, index: 0, totalCount: 1, topMessageId: message.id)
|
||
}
|
||
}
|
||
}
|
||
|
||
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { updated in
|
||
var updated = updated
|
||
|
||
updated = updated.updatedInterfaceState({ _ in return interfaceState })
|
||
|
||
updated = updated.updatedKeyboardButtonsMessage(combinedInitialData.buttonKeyboardMessage)
|
||
updated = updated.updatedPinnedMessageId(pinnedMessageId)
|
||
updated = updated.updatedPinnedMessage(pinnedMessage)
|
||
updated = updated.updatedPeerIsBlocked(peerIsBlocked)
|
||
updated = updated.updatedCallsAvailable(callsAvailable)
|
||
updated = updated.updatedCallsPrivate(callsPrivate)
|
||
updated = updated.updatedActiveGroupCallInfo(activeGroupCallInfo)
|
||
updated = updated.updatedTitlePanelContext({ context in
|
||
if pinnedMessageId != nil {
|
||
if !context.contains(where: {
|
||
switch $0 {
|
||
case .pinnedMessage:
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}) {
|
||
var updatedContexts = context
|
||
updatedContexts.append(.pinnedMessage)
|
||
return updatedContexts.sorted()
|
||
} else {
|
||
return context
|
||
}
|
||
} else {
|
||
if let index = context.firstIndex(where: {
|
||
switch $0 {
|
||
case .pinnedMessage:
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}) {
|
||
var updatedContexts = context
|
||
updatedContexts.remove(at: index)
|
||
return updatedContexts
|
||
} else {
|
||
return context
|
||
}
|
||
}
|
||
})
|
||
if let editMessage = interfaceState.editMessage, let message = combinedInitialData.initialData?.associatedMessages[editMessage.messageId] {
|
||
updated = updatedChatEditInterfaceMessageState(state: updated, message: message)
|
||
}
|
||
updated = updated.updatedSlowmodeState(slowmodeState)
|
||
return updated
|
||
})
|
||
}
|
||
if let readStateData = combinedInitialData.readStateData {
|
||
if case let .peer(peerId) = strongSelf.chatLocation, let peerReadStateData = readStateData[peerId], let notificationSettings = peerReadStateData.notificationSettings {
|
||
|
||
let inAppSettings = strongSelf.context.sharedContext.currentInAppNotificationSettings.with { $0 }
|
||
let (count, _) = renderedTotalUnreadCount(inAppSettings: inAppSettings, totalUnreadState: peerReadStateData.totalState ?? ChatListTotalUnreadState(absoluteCounters: [:], filteredCounters: [:]))
|
||
|
||
var globalRemainingUnreadChatCount = count
|
||
if !notificationSettings.isRemovedFromTotalUnreadCount(default: false) && peerReadStateData.unreadCount > 0 {
|
||
if case .messages = inAppSettings.totalUnreadCountDisplayCategory {
|
||
globalRemainingUnreadChatCount -= peerReadStateData.unreadCount
|
||
} else {
|
||
globalRemainingUnreadChatCount -= 1
|
||
}
|
||
}
|
||
if globalRemainingUnreadChatCount > 0 {
|
||
strongSelf.navigationItem.badge = "\(globalRemainingUnreadChatCount)"
|
||
} else {
|
||
strongSelf.navigationItem.badge = ""
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
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) })
|
||
}
|
||
}
|
||
})
|
||
|
||
let hasPendingMessages: Signal<Bool, NoError>
|
||
let chatLocationPeerId = self.chatLocation.peerId
|
||
hasPendingMessages = self.context.account.pendingMessageManager.hasPendingMessages
|
||
|> mapToSignal { peerIds -> Signal<Bool, NoError> in
|
||
let value = peerIds.contains(chatLocationPeerId)
|
||
if value {
|
||
return .single(true)
|
||
} else {
|
||
return .single(false)
|
||
|> delay(0.1, queue: .mainQueue())
|
||
}
|
||
}
|
||
|> distinctUntilChanged
|
||
|
||
let isTopReplyThreadMessageShown: Signal<Bool, NoError> = self.chatDisplayNode.historyNode.isTopReplyThreadMessageShown.get()
|
||
|> distinctUntilChanged
|
||
|
||
let topPinnedMessage: Signal<ChatPinnedMessage?, NoError> = self.topPinnedMessageSignal(latest: false)
|
||
|
||
self.cachedDataDisposable = combineLatest(queue: .mainQueue(), self.chatDisplayNode.historyNode.cachedPeerDataAndMessages, hasPendingMessages, isTopReplyThreadMessageShown, topPinnedMessage).start(next: { [weak self] cachedDataAndMessages, hasPendingMessages, isTopReplyThreadMessageShown, topPinnedMessage in
|
||
if let strongSelf = self {
|
||
let (cachedData, messages) = cachedDataAndMessages
|
||
|
||
var pinnedMessageId: MessageId?
|
||
var peerIsBlocked: Bool = false
|
||
var callsAvailable: Bool = false
|
||
var callsPrivate: Bool = false
|
||
var slowmodeState: ChatSlowmodeState?
|
||
var activeGroupCallInfo: ChatActiveGroupCallInfo?
|
||
if let cachedData = cachedData as? CachedChannelData {
|
||
pinnedMessageId = cachedData.pinnedMessageId
|
||
if let channel = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isRestrictedBySlowmode, let timeout = cachedData.slowModeTimeout {
|
||
if hasPendingMessages {
|
||
slowmodeState = ChatSlowmodeState(timeout: timeout, variant: .pendingMessages)
|
||
} else if let slowmodeUntilTimestamp = calculateSlowmodeActiveUntilTimestamp(account: strongSelf.context.account, untilTimestamp: cachedData.slowModeValidUntilTimestamp) {
|
||
slowmodeState = ChatSlowmodeState(timeout: timeout, variant: .timestamp(slowmodeUntilTimestamp))
|
||
}
|
||
}
|
||
if let activeCall = cachedData.activeCall {
|
||
activeGroupCallInfo = ChatActiveGroupCallInfo(activeCall: activeCall)
|
||
}
|
||
} else if let cachedData = cachedData as? CachedUserData {
|
||
peerIsBlocked = cachedData.isBlocked
|
||
callsAvailable = cachedData.voiceCallsAvailable
|
||
callsPrivate = cachedData.callsPrivate
|
||
pinnedMessageId = cachedData.pinnedMessageId
|
||
} else if let cachedData = cachedData as? CachedGroupData {
|
||
pinnedMessageId = cachedData.pinnedMessageId
|
||
if let activeCall = cachedData.activeCall {
|
||
activeGroupCallInfo = ChatActiveGroupCallInfo(activeCall: activeCall)
|
||
}
|
||
} else if let _ = cachedData as? CachedSecretChatData {
|
||
}
|
||
|
||
var pinnedMessage: ChatPinnedMessage?
|
||
switch strongSelf.chatLocation {
|
||
case let .replyThread(replyThreadMessage):
|
||
if isTopReplyThreadMessageShown {
|
||
pinnedMessageId = nil
|
||
} else {
|
||
pinnedMessageId = replyThreadMessage.effectiveTopId
|
||
}
|
||
if let pinnedMessageId = pinnedMessageId {
|
||
if let message = messages?[pinnedMessageId] {
|
||
pinnedMessage = ChatPinnedMessage(message: message, index: 0, totalCount: 1, topMessageId: message.id)
|
||
}
|
||
}
|
||
case .peer:
|
||
pinnedMessageId = topPinnedMessage?.message.id
|
||
pinnedMessage = topPinnedMessage
|
||
}
|
||
|
||
var pinnedMessageUpdated = false
|
||
if let current = strongSelf.presentationInterfaceState.pinnedMessage, let updated = pinnedMessage {
|
||
if current != updated {
|
||
pinnedMessageUpdated = true
|
||
}
|
||
} else if (strongSelf.presentationInterfaceState.pinnedMessage != nil) != (pinnedMessage != nil) {
|
||
pinnedMessageUpdated = true
|
||
}
|
||
|
||
let callsDataUpdated = strongSelf.presentationInterfaceState.callsAvailable != callsAvailable || strongSelf.presentationInterfaceState.callsPrivate != callsPrivate
|
||
|
||
if strongSelf.presentationInterfaceState.pinnedMessageId != pinnedMessageId || strongSelf.presentationInterfaceState.pinnedMessage != pinnedMessage || strongSelf.presentationInterfaceState.peerIsBlocked != peerIsBlocked || pinnedMessageUpdated || callsDataUpdated || strongSelf.presentationInterfaceState.slowmodeState != slowmodeState || strongSelf.presentationInterfaceState.activeGroupCallInfo != activeGroupCallInfo {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: strongSelf.willAppear, interactive: strongSelf.willAppear, { state in
|
||
return state
|
||
.updatedPinnedMessageId(pinnedMessageId)
|
||
.updatedActiveGroupCallInfo(activeGroupCallInfo)
|
||
.updatedPinnedMessage(pinnedMessage)
|
||
.updatedPeerIsBlocked(peerIsBlocked)
|
||
.updatedCallsAvailable(callsAvailable)
|
||
.updatedCallsPrivate(callsPrivate)
|
||
.updatedTitlePanelContext({ context in
|
||
if pinnedMessageId != nil {
|
||
if !context.contains(where: {
|
||
switch $0 {
|
||
case .pinnedMessage:
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}) {
|
||
var updatedContexts = context
|
||
updatedContexts.append(.pinnedMessage)
|
||
return updatedContexts.sorted()
|
||
} else {
|
||
return context
|
||
}
|
||
} else {
|
||
if let index = context.firstIndex(where: {
|
||
switch $0 {
|
||
case .pinnedMessage:
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}) {
|
||
var updatedContexts = context
|
||
updatedContexts.remove(at: index)
|
||
return updatedContexts
|
||
} else {
|
||
return context
|
||
}
|
||
}
|
||
})
|
||
.updatedSlowmodeState(slowmodeState)
|
||
})
|
||
}
|
||
|
||
if !strongSelf.didSetCachedDataReady {
|
||
strongSelf.didSetCachedDataReady = true
|
||
strongSelf.cachedDataReady.set(.single(true))
|
||
}
|
||
}
|
||
})
|
||
|
||
self.historyStateDisposable = self.chatDisplayNode.historyNode.historyState.get().start(next: { [weak self] state in
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: strongSelf.isViewLoaded && strongSelf.view.window != nil, {
|
||
$0.updatedChatHistoryState(state)
|
||
})
|
||
}
|
||
})
|
||
|
||
let effectiveCachedDataReady: Signal<Bool, NoError>
|
||
if case .replyThread = self.chatLocation {
|
||
effectiveCachedDataReady = self.cachedDataReady.get()
|
||
} else {
|
||
//effectiveCachedDataReady = .single(true)
|
||
effectiveCachedDataReady = self.cachedDataReady.get()
|
||
}
|
||
self.ready.set(combineLatest(self.chatDisplayNode.historyNode.historyState.get(), self._chatLocationInfoReady.get(), effectiveCachedDataReady, initialData) |> map { _, chatLocationInfoReady, cachedDataReady, _ in
|
||
return chatLocationInfoReady && cachedDataReady
|
||
})
|
||
|
||
if self.context.sharedContext.immediateExperimentalUISettings.crashOnLongQueries {
|
||
let _ = (self.ready.get()
|
||
|> filter({ $0 })
|
||
|> take(1)
|
||
|> timeout(0.8, queue: .concurrentDefaultQueue(), alternate: Signal { _ in
|
||
preconditionFailure()
|
||
})).start()
|
||
}
|
||
|
||
self.chatDisplayNode.historyNode.contentPositionChanged = { [weak self] offset in
|
||
guard let strongSelf = self else { return }
|
||
|
||
let offsetAlpha: CGFloat
|
||
let plainInputSeparatorAlpha: CGFloat
|
||
switch offset {
|
||
case let .known(offset):
|
||
if offset < 40.0 {
|
||
offsetAlpha = 0.0
|
||
} else {
|
||
offsetAlpha = 1.0
|
||
}
|
||
if offset < 4.0 {
|
||
plainInputSeparatorAlpha = 0.0
|
||
} else {
|
||
plainInputSeparatorAlpha = 1.0
|
||
}
|
||
case .unknown:
|
||
offsetAlpha = 1.0
|
||
plainInputSeparatorAlpha = 1.0
|
||
case .none:
|
||
offsetAlpha = 0.0
|
||
plainInputSeparatorAlpha = 0.0
|
||
}
|
||
|
||
strongSelf.shouldDisplayDownButton = !offsetAlpha.isZero
|
||
strongSelf.updateDownButtonVisibility()
|
||
strongSelf.chatDisplayNode.updatePlainInputSeparatorAlpha(plainInputSeparatorAlpha, transition: .animated(duration: 0.2, curve: .easeInOut))
|
||
}
|
||
|
||
self.chatDisplayNode.historyNode.scrolledToIndex = { [weak self] toIndex in
|
||
if let strongSelf = self, case let .message(index) = toIndex {
|
||
if let controllerInteraction = strongSelf.controllerInteraction {
|
||
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(index.id) {
|
||
let highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId)
|
||
controllerInteraction.highlightedState = highlightedState
|
||
strongSelf.updateItemNodesHighlightedStates(animated: false)
|
||
strongSelf.scrolledToMessageIdValue = ScrolledToMessageId(id: index.id, allowedReplacementDirection: [])
|
||
|
||
strongSelf.messageContextDisposable.set((Signal<Void, NoError>.complete() |> delay(0.7, queue: Queue.mainQueue())).start(completed: {
|
||
if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction {
|
||
if controllerInteraction.highlightedState == highlightedState {
|
||
controllerInteraction.highlightedState = nil
|
||
strongSelf.updateItemNodesHighlightedStates(animated: true)
|
||
}
|
||
}
|
||
}))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
self.chatDisplayNode.historyNode.scrolledToSomeIndex = { [weak self] in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.scrolledToMessageIdValue = nil
|
||
}
|
||
|
||
self.chatDisplayNode.historyNode.maxVisibleMessageIndexUpdated = { [weak self] index in
|
||
if let strongSelf = self, !strongSelf.historyNavigationStack.isEmpty {
|
||
strongSelf.historyNavigationStack.filterOutIndicesLessThan(index)
|
||
}
|
||
}
|
||
|
||
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, let validLayout = strongSelf.validLayout {
|
||
var mappedTransition: (ChatHistoryListViewTransition, ListViewUpdateSizeAndInsets?)?
|
||
|
||
let isScheduledMessages: Bool
|
||
if case .scheduledMessages = strongSelf.presentationInterfaceState.subject {
|
||
isScheduledMessages = true
|
||
} else {
|
||
isScheduledMessages = false
|
||
}
|
||
strongSelf.chatDisplayNode.containerLayoutUpdated(validLayout, navigationBarHeight: strongSelf.navigationHeight, transition: .animated(duration: 0.2, curve: .easeInOut), listViewTransaction: { updateSizeAndInsets, _, _, _ in
|
||
var options = transition.options
|
||
let _ = options.insert(.Synchronous)
|
||
let _ = options.insert(.LowLatency)
|
||
let _ = options.insert(.PreferSynchronousResourceLoading)
|
||
options.remove(.AnimateInsertion)
|
||
options.insert(.RequestItemInsertionAnimations)
|
||
|
||
let deleteItems = transition.deleteItems.map({ item in
|
||
return ListViewDeleteItem(index: item.index, directionHint: nil)
|
||
})
|
||
|
||
var maxInsertedItem: Int?
|
||
var insertedIndex: 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
|
||
}
|
||
insertedIndex = item.index
|
||
insertItems.append(ListViewInsertItem(index: item.index, previousIndex: item.previousIndex, item: item.item, directionHint: item.directionHint == .Down ? .Up : nil))
|
||
}
|
||
|
||
var scrollToItem: ListViewScrollToItem?
|
||
if isScheduledMessages, let insertedIndex = insertedIndex {
|
||
scrollToItem = ListViewScrollToItem(index: insertedIndex, position: .visible, animated: true, curve: .Default(duration: 0.2), directionHint: .Down)
|
||
} else if transition.historyView.originalView.laterId == nil {
|
||
scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: 0.2), 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, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex, scrolledToSomeIndex: transition.scrolledToSomeIndex, peerType: transition.peerType, networkType: transition.networkType, animateIn: false, reason: transition.reason, flashIndicators: transition.flashIndicators), updateSizeAndInsets)
|
||
})
|
||
|
||
if let mappedTransition = mappedTransition {
|
||
return mappedTransition
|
||
}
|
||
}
|
||
return (transition, nil)
|
||
}
|
||
}
|
||
|
||
self.chatDisplayNode.sendMessages = { [weak self] messages, silentPosting, scheduleTime, isAnyMessageTextPartitioned in
|
||
if let strongSelf = self {
|
||
let peerId = strongSelf.chatLocation.peerId
|
||
strongSelf.commitPurposefulAction()
|
||
|
||
if let channel = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isRestrictedBySlowmode {
|
||
let forwardCount = messages.reduce(0, { count, message -> Int in
|
||
if case .forward = message {
|
||
return count + 1
|
||
} else {
|
||
return count
|
||
}
|
||
})
|
||
|
||
var errorText: String?
|
||
if forwardCount > 1 {
|
||
errorText = strongSelf.presentationData.strings.Chat_AttachmentMultipleForwardDisabled
|
||
} else if isAnyMessageTextPartitioned {
|
||
errorText = strongSelf.presentationData.strings.Chat_MultipleTextMessagesDisabled
|
||
}
|
||
|
||
if let errorText = errorText {
|
||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||
return
|
||
}
|
||
}
|
||
|
||
let transformedMessages: [EnqueueMessage]
|
||
if let silentPosting = silentPosting {
|
||
transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: silentPosting)
|
||
} else if let scheduleTime = scheduleTime {
|
||
transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime)
|
||
} else {
|
||
transformedMessages = strongSelf.transformEnqueueMessages(messages)
|
||
}
|
||
|
||
var forwardedMessages: [[EnqueueMessage]] = []
|
||
var forwardSourcePeerIds = Set<PeerId>()
|
||
for message in transformedMessages {
|
||
if case let .forward(source, _, _) = message {
|
||
forwardSourcePeerIds.insert(source.peerId)
|
||
|
||
var added = false
|
||
if var last = forwardedMessages.last {
|
||
if let currentMessage = last.first, case let .forward(currentSource, _, _) = currentMessage, currentSource.peerId == source.peerId {
|
||
last.append(message)
|
||
added = true
|
||
}
|
||
}
|
||
if !added {
|
||
forwardedMessages.append([message])
|
||
}
|
||
}
|
||
}
|
||
|
||
let signal: Signal<[MessageId?], NoError>
|
||
if forwardSourcePeerIds.count > 1 {
|
||
var signals: [Signal<[MessageId?], NoError>] = []
|
||
for messagesGroup in forwardedMessages {
|
||
signals.append(enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: messagesGroup))
|
||
}
|
||
signal = combineLatest(signals)
|
||
|> map { results in
|
||
var ids: [MessageId?] = []
|
||
for result in results {
|
||
ids.append(contentsOf: result)
|
||
}
|
||
return ids
|
||
}
|
||
} else {
|
||
signal = enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: transformedMessages)
|
||
}
|
||
|
||
let _ = (signal
|
||
|> deliverOnMainQueue).start(next: { messageIds in
|
||
if let strongSelf = self {
|
||
if case .scheduledMessages = strongSelf.presentationInterfaceState.subject {
|
||
} else {
|
||
strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
||
}
|
||
}
|
||
})
|
||
|
||
donateSendMessageIntent(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, intentContext: .chat, peerIds: [peerId])
|
||
}
|
||
}
|
||
|
||
self.chatDisplayNode.requestUpdateChatInterfaceState = { [weak self] animated, saveInterfaceState, f in
|
||
self?.updateChatPresentationInterfaceState(animated: animated, interactive: true, saveInterfaceState: saveInterfaceState, { $0.updatedInterfaceState(f) })
|
||
}
|
||
|
||
self.chatDisplayNode.requestUpdateInterfaceState = { [weak self] transition, interactive, f in
|
||
self?.updateChatPresentationInterfaceState(transition: transition, interactive: interactive, f)
|
||
}
|
||
|
||
self.chatDisplayNode.displayAttachmentMenu = { [weak self] in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
if strongSelf.presentationInterfaceState.interfaceState.editMessage == nil, let _ = strongSelf.presentationInterfaceState.slowmodeState, strongSelf.presentationInterfaceState.subject != .scheduledMessages {
|
||
if let rect = strongSelf.chatDisplayNode.frameForAttachmentButton() {
|
||
strongSelf.interfaceInteraction?.displaySlowmodeTooltip(strongSelf.chatDisplayNode, rect)
|
||
}
|
||
return
|
||
}
|
||
if let messageId = strongSelf.presentationInterfaceState.interfaceState.editMessage?.messageId {
|
||
let _ = (strongSelf.context.account.postbox.transaction { transaction -> Message? in
|
||
return transaction.getMessage(messageId)
|
||
} |> deliverOnMainQueue).start(next: { message in
|
||
guard let strongSelf = self, let editMessageState = strongSelf.presentationInterfaceState.editMessageState, case let .media(options) = editMessageState.content else {
|
||
return
|
||
}
|
||
var originalMediaReference: AnyMediaReference?
|
||
if let message = message {
|
||
for media in message.media {
|
||
if let image = media as? TelegramMediaImage {
|
||
originalMediaReference = .message(message: MessageReference(message), media: image)
|
||
} else if let file = media as? TelegramMediaFile {
|
||
if file.isVideo || file.isAnimated {
|
||
originalMediaReference = .message(message: MessageReference(message), media: file)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
strongSelf.presentAttachmentMenu(editMediaOptions: options, editMediaReference: originalMediaReference)
|
||
})
|
||
} else {
|
||
strongSelf.presentAttachmentMenu(editMediaOptions: nil, editMediaReference: nil)
|
||
}
|
||
}
|
||
self.chatDisplayNode.paste = { [weak self] data in
|
||
switch data {
|
||
case let .images(images):
|
||
self?.displayPasteMenu(images)
|
||
case let .video(data):
|
||
self?.enqueueVideoData(data)
|
||
case let .gif(data):
|
||
self?.enqueueGifData(data)
|
||
case let .sticker(image, isMemoji):
|
||
self?.enqueueStickerImage(image, isMemoji: isMemoji)
|
||
}
|
||
}
|
||
self.chatDisplayNode.updateTypingActivity = { [weak self] value in
|
||
if let strongSelf = self, strongSelf.presentationInterfaceState.interfaceState.editMessage == nil && strongSelf.presentationInterfaceState.subject != .scheduledMessages {
|
||
if value {
|
||
strongSelf.typingActivityPromise.set(Signal<Bool, NoError>.single(true)
|
||
|> then(
|
||
Signal<Bool, NoError>.single(false)
|
||
|> delay(4.0, queue: Queue.mainQueue())
|
||
))
|
||
} else {
|
||
strongSelf.typingActivityPromise.set(.single(false))
|
||
}
|
||
}
|
||
}
|
||
|
||
self.chatDisplayNode.dismissUrlPreview = { [weak self] in
|
||
if let strongSelf = self {
|
||
if let _ = strongSelf.presentationInterfaceState.interfaceState.editMessage {
|
||
if let (link, _) = strongSelf.presentationInterfaceState.editingUrlPreview {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
$0.updatedInterfaceState {
|
||
$0.withUpdatedEditMessage($0.editMessage.flatMap { $0.withUpdatedDisableUrlPreview(link) })
|
||
}
|
||
})
|
||
}
|
||
} else {
|
||
if let (link, _) = strongSelf.presentationInterfaceState.urlPreview {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
$0.updatedInterfaceState {
|
||
$0.withUpdatedComposeDisableUrlPreview(link)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
self.chatDisplayNode.navigateButtons.downPressed = { [weak self] in
|
||
if let strongSelf = self, strongSelf.isNodeLoaded {
|
||
if let messageId = strongSelf.historyNavigationStack.removeLast() {
|
||
strongSelf.navigateToMessage(from: nil, to: .id(messageId.id), rememberInStack: false)
|
||
} else {
|
||
if case .known = strongSelf.chatDisplayNode.historyNode.visibleContentOffset() {
|
||
strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
||
} else if case .peer = strongSelf.chatLocation {
|
||
strongSelf.scrollToEndOfHistory()
|
||
} else if case .replyThread = strongSelf.chatLocation {
|
||
strongSelf.scrollToEndOfHistory()
|
||
} else {
|
||
strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
self.chatDisplayNode.navigateButtons.mentionsPressed = { [weak self] in
|
||
if let strongSelf = self, strongSelf.isNodeLoaded, case let .peer(peerId) = strongSelf.chatLocation {
|
||
let signal = earliestUnseenPersonalMentionMessage(account: strongSelf.context.account, peerId: peerId)
|
||
strongSelf.navigationActionDisposable.set((signal |> deliverOnMainQueue).start(next: { result in
|
||
if let strongSelf = self {
|
||
switch result {
|
||
case let .result(messageId):
|
||
if let messageId = messageId {
|
||
strongSelf.navigateToMessage(from: nil, to: .id(messageId))
|
||
}
|
||
case .loading:
|
||
break
|
||
}
|
||
}
|
||
}))
|
||
}
|
||
}
|
||
|
||
self.chatDisplayNode.navigateButtons.mentionsMenu = { [weak self] in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
|
||
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.WebSearch_RecentSectionClear, color: .accent, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
guard let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation else {
|
||
return
|
||
}
|
||
let _ = clearPeerUnseenPersonalMessagesInteractively(account: strongSelf.context.account, peerId: peerId).start()
|
||
})
|
||
]), ActionSheetItemGroup(items: [
|
||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
})
|
||
])])
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
strongSelf.present(actionSheet, in: .window(.root))
|
||
}
|
||
|
||
let interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { [weak self] messageId, completion in
|
||
if let strongSelf = self, strongSelf.isNodeLoaded, canSendMessagesToChat(strongSelf.presentationInterfaceState) {
|
||
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
|
||
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageId(message.id) }).updatedSearch(nil) }, completion: completion)
|
||
strongSelf.updateItemNodesSearchTextHighlightStates()
|
||
strongSelf.chatDisplayNode.ensureInputViewFocused()
|
||
} else {
|
||
completion(.immediate)
|
||
}
|
||
}, alertAction: {
|
||
completion(.immediate)
|
||
}, delay: true)
|
||
} else {
|
||
completion(.immediate)
|
||
}
|
||
}, setupEditMessage: { [weak self] messageId, completion in
|
||
if let strongSelf = self, strongSelf.isNodeLoaded {
|
||
guard let messageId = messageId else {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
|
||
var state = state
|
||
state = state.updatedInterfaceState {
|
||
$0.withUpdatedEditMessage(nil)
|
||
}
|
||
state = state.updatedEditMessageState(nil)
|
||
return state
|
||
}, completion: completion)
|
||
|
||
return
|
||
}
|
||
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
|
||
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
|
||
var updated = state.updatedInterfaceState {
|
||
var entities: [MessageTextEntity] = []
|
||
for attribute in message.attributes {
|
||
if let attribute = attribute as? TextEntitiesMessageAttribute {
|
||
entities = attribute.entities
|
||
break
|
||
}
|
||
}
|
||
var inputTextMaxLength: Int32 = 4096
|
||
var webpageUrl: String?
|
||
for media in message.media {
|
||
if media is TelegramMediaImage || media is TelegramMediaFile {
|
||
inputTextMaxLength = strongSelf.context.currentLimitsConfiguration.with { $0 }.maxMediaCaptionLength
|
||
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
|
||
webpageUrl = content.url
|
||
}
|
||
}
|
||
let inputText = chatInputStateStringWithAppliedEntities(message.text, entities: entities)
|
||
var disableUrlPreview: String?
|
||
if let detectedWebpageUrl = detectUrl(inputText), webpageUrl == nil {
|
||
disableUrlPreview = detectedWebpageUrl
|
||
}
|
||
return $0.withUpdatedEditMessage(ChatEditMessageState(messageId: messageId, inputState: ChatTextInputState(inputText: inputText), disableUrlPreview: disableUrlPreview, inputTextMaxLength: inputTextMaxLength))
|
||
}
|
||
|
||
updated = updatedChatEditInterfaceMessageState(state: updated, message: message)
|
||
updated = updated.updatedInputMode({ _ in
|
||
return .text
|
||
})
|
||
|
||
return updated
|
||
}, completion: completion)
|
||
}
|
||
}, alertAction: {
|
||
completion(.immediate)
|
||
}, delay: true)
|
||
}
|
||
}, beginMessageSelection: { [weak self] messageIds, completion in
|
||
if let strongSelf = self, strongSelf.isNodeLoaded {
|
||
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withUpdatedSelectedMessages(messageIds) } }, completion: completion)
|
||
|
||
if let selectionState = strongSelf.presentationInterfaceState.interfaceState.selectionState {
|
||
let count = selectionState.selectedIds.count
|
||
let text = strongSelf.presentationData.strings.VoiceOver_Chat_MessagesSelected(Int32(count))
|
||
UIAccessibility.post(notification: UIAccessibility.Notification.announcement, argument: text)
|
||
}
|
||
}, alertAction: {
|
||
completion(.immediate)
|
||
}, delay: true)
|
||
} else {
|
||
completion(.immediate)
|
||
}
|
||
}, deleteSelectedMessages: { [weak self] in
|
||
if let strongSelf = self {
|
||
if let messageIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !messageIds.isEmpty {
|
||
strongSelf.messageContextDisposable.set((strongSelf.context.sharedContext.chatAvailableMessageActions(postbox: strongSelf.context.account.postbox, accountPeerId: strongSelf.context.account.peerId, messageIds: messageIds)
|
||
|> deliverOnMainQueue).start(next: { actions in
|
||
if let strongSelf = self, !actions.options.isEmpty {
|
||
if let banAuthor = actions.banAuthor {
|
||
strongSelf.presentBanMessageOptions(accountPeerId: strongSelf.context.account.peerId, author: banAuthor, messageIds: messageIds, options: actions.options)
|
||
} else {
|
||
if actions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty {
|
||
strongSelf.presentClearCacheSuggestion()
|
||
} else {
|
||
strongSelf.presentDeleteMessageOptions(messageIds: messageIds, options: actions.options, contextController: nil, completion: { _ in })
|
||
}
|
||
}
|
||
}
|
||
}))
|
||
}
|
||
}
|
||
}, reportSelectedMessages: { [weak self] in
|
||
if let strongSelf = self, let messageIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !messageIds.isEmpty {
|
||
if let reportReason = strongSelf.presentationInterfaceState.reportReason {
|
||
let presentationData = strongSelf.presentationData
|
||
let controller = ActionSheetController(presentationData: presentationData, allowInputInset: true)
|
||
let dismissAction: () -> Void = { [weak self, weak controller] in
|
||
self?.view.window?.endEditing(true)
|
||
controller?.dismissAnimated()
|
||
}
|
||
var message = ""
|
||
var items: [ActionSheetItem] = []
|
||
items.append(ReportPeerHeaderActionSheetItem(context: strongSelf.context, text: presentationData.strings.Report_AdditionalDetailsText))
|
||
items.append(ReportPeerDetailsActionSheetItem(context: strongSelf.context, placeholderText: presentationData.strings.Report_AdditionalDetailsPlaceholder, textUpdated: { text in
|
||
message = text
|
||
}))
|
||
items.append(ActionSheetButtonItem(title: presentationData.strings.Report_Report, color: .accent, font: .bold, enabled: true, action: {
|
||
dismissAction()
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }, completion: { _ in
|
||
let _ = (reportPeerMessages(account: strongSelf.context.account, messageIds: Array(messageIds), reason: reportReason, message: message)
|
||
|> deliverOnMainQueue).start(completed: { [weak self] in
|
||
if let strongSelf = self, let path = getAppBundle().path(forResource: "PoliceCar", ofType: "tgs") {
|
||
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .emoji(path: path, text: presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return false }), in: .current)
|
||
}
|
||
})
|
||
})
|
||
}))
|
||
|
||
controller.setItemGroups([
|
||
ActionSheetItemGroup(items: items),
|
||
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
|
||
])
|
||
strongSelf.present(controller, in: .window(.root))
|
||
} else {
|
||
strongSelf.present(peerReportOptionsController(context: strongSelf.context, subject: .messages(Array(messageIds).sorted()), passthrough: false, present: { c, a in
|
||
self?.present(c, in: .window(.root), with: a)
|
||
}, push: { c in
|
||
self?.push(c)
|
||
}, completion: { _, done in
|
||
if done {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
||
}
|
||
}), in: .window(.root))
|
||
}
|
||
}
|
||
}, reportMessages: { [weak self] messages, contextController in
|
||
if let strongSelf = self, !messages.isEmpty {
|
||
presentPeerReportOptions(context: strongSelf.context, parent: strongSelf, contextController: contextController, subject: .messages(messages.map({ $0.id }).sorted()), completion: { _, _ in })
|
||
}
|
||
}, blockMessageAuthor: { [weak self] message, contextController in
|
||
contextController?.dismiss(completion: {
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
|
||
let author = message.forwardInfo?.author
|
||
|
||
guard let peer = author else {
|
||
return
|
||
}
|
||
|
||
let presentationData = strongSelf.presentationData
|
||
let controller = ActionSheetController(presentationData: presentationData)
|
||
let dismissAction: () -> Void = { [weak controller] in
|
||
controller?.dismissAnimated()
|
||
}
|
||
var reportSpam = true
|
||
var items: [ActionSheetItem] = []
|
||
items.append(ActionSheetTextItem(title: presentationData.strings.UserInfo_BlockConfirmationTitle(peer.compactDisplayTitle).0))
|
||
items.append(contentsOf: [
|
||
ActionSheetCheckboxItem(title: presentationData.strings.Conversation_Moderate_Report, label: "", value: reportSpam, action: { [weak controller] checkValue in
|
||
reportSpam = checkValue
|
||
controller?.updateItem(groupIndex: 0, itemIndex: 1, { item in
|
||
if let item = item as? ActionSheetCheckboxItem {
|
||
return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action)
|
||
}
|
||
return item
|
||
})
|
||
}),
|
||
ActionSheetButtonItem(title: presentationData.strings.Replies_BlockAndDeleteRepliesActionTitle, color: .destructive, action: {
|
||
dismissAction()
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
let _ = requestUpdatePeerIsBlocked(account: strongSelf.context.account, peerId: peer.id, isBlocked: true).start()
|
||
let account = strongSelf.context.account
|
||
let _ = (strongSelf.context.account.postbox.transaction { transasction -> Void in
|
||
deleteAllMessagesWithForwardAuthor(transaction: transasction, mediaBox: account.postbox.mediaBox, peerId: message.id.peerId, forwardAuthorId: peer.id, namespace: Namespaces.Message.Cloud)
|
||
}).start()
|
||
let _ = reportRepliesMessage(account: strongSelf.context.account, messageId: message.id, deleteMessage: true, deleteHistory: true, reportSpam: reportSpam).start()
|
||
})
|
||
] as [ActionSheetItem])
|
||
|
||
controller.setItemGroups([
|
||
ActionSheetItemGroup(items: items),
|
||
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
|
||
])
|
||
strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||
})
|
||
}, deleteMessages: { [weak self] messages, contextController, completion in
|
||
if let strongSelf = self, !messages.isEmpty {
|
||
let messageIds = Set(messages.map { $0.id })
|
||
strongSelf.messageContextDisposable.set((strongSelf.context.sharedContext.chatAvailableMessageActions(postbox: strongSelf.context.account.postbox, accountPeerId: strongSelf.context.account.peerId, messageIds: messageIds)
|
||
|> deliverOnMainQueue).start(next: { actions in
|
||
if let strongSelf = self, !actions.options.isEmpty {
|
||
if let banAuthor = actions.banAuthor {
|
||
strongSelf.presentBanMessageOptions(accountPeerId: strongSelf.context.account.peerId, author: banAuthor, messageIds: messageIds, options: actions.options)
|
||
completion(.default)
|
||
} else {
|
||
var isAction = false
|
||
if messages.count == 1 {
|
||
for media in messages[0].media {
|
||
if media is TelegramMediaAction {
|
||
isAction = true
|
||
}
|
||
}
|
||
}
|
||
if isAction && (actions.options == .deleteGlobally || actions.options == .deleteLocally) {
|
||
let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: actions.options == .deleteLocally ? .forLocalPeer : .forEveryone).start()
|
||
completion(.dismissWithoutContent)
|
||
} else if (messages.first?.flags.isSending ?? false) {
|
||
let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forEveryone, deleteAllInGroup: true).start()
|
||
completion(.dismissWithoutContent)
|
||
} else {
|
||
if actions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty {
|
||
strongSelf.presentClearCacheSuggestion()
|
||
completion(.default)
|
||
} else {
|
||
var isScheduled = false
|
||
for id in messageIds {
|
||
if Namespaces.Message.allScheduled.contains(id.namespace) {
|
||
isScheduled = true
|
||
break
|
||
}
|
||
}
|
||
strongSelf.presentDeleteMessageOptions(messageIds: messageIds, options: isScheduled ? [.deleteLocally] : actions.options, contextController: contextController, completion: completion)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}))
|
||
}
|
||
}, forwardSelectedMessages: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.commitPurposefulAction()
|
||
if let forwardMessageIdsSet = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds {
|
||
let forwardMessageIds = Array(forwardMessageIdsSet).sorted()
|
||
strongSelf.forwardMessages(messageIds: forwardMessageIds)
|
||
}
|
||
}
|
||
}, forwardCurrentForwardMessages: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.commitPurposefulAction()
|
||
if let forwardMessageIds = strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds {
|
||
strongSelf.forwardMessages(messageIds: forwardMessageIds, resetCurrent: true)
|
||
}
|
||
}
|
||
}, forwardMessages: { [weak self] messages in
|
||
if let strongSelf = self, !messages.isEmpty {
|
||
strongSelf.commitPurposefulAction()
|
||
let forwardMessageIds = messages.map { $0.id }.sorted()
|
||
strongSelf.forwardMessages(messageIds: forwardMessageIds)
|
||
}
|
||
}, shareSelectedMessages: { [weak self] in
|
||
if let strongSelf = self, let selectedIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !selectedIds.isEmpty {
|
||
strongSelf.commitPurposefulAction()
|
||
let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Message] in
|
||
var messages: [Message] = []
|
||
for id in selectedIds {
|
||
if let message = transaction.getMessage(id) {
|
||
messages.append(message)
|
||
}
|
||
}
|
||
return messages
|
||
} |> deliverOnMainQueue).start(next: { messages in
|
||
if let strongSelf = self, !messages.isEmpty {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) })
|
||
|
||
let shareController = ShareController(context: strongSelf.context, subject: .messages(messages.sorted(by: { lhs, rhs in
|
||
return lhs.index < rhs.index
|
||
})), externalShare: true, immediateExternalShare: true)
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
strongSelf.present(shareController, in: .window(.root))
|
||
}
|
||
})
|
||
}
|
||
}, updateTextInputStateAndMode: { [weak self] f in
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
|
||
let (updatedState, updatedMode) = f(state.interfaceState.effectiveInputState, state.inputMode)
|
||
return state.updatedInterfaceState { interfaceState in
|
||
return interfaceState.withUpdatedEffectiveInputState(updatedState)
|
||
}.updatedInputMode({ _ in updatedMode })
|
||
})
|
||
}
|
||
}, 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({ value in
|
||
var value = value
|
||
value.closedButtonKeyboardMessageId = updatedClosedButtonKeyboardMessageId
|
||
return value
|
||
})
|
||
})
|
||
})
|
||
}
|
||
}, openStickers: { [weak self] in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.chatDisplayNode.openStickers()
|
||
strongSelf.mediaRecordingModeTooltipController?.dismissImmediately()
|
||
}, editMessage: { [weak self] in
|
||
if let strongSelf = self, let editMessage = strongSelf.presentationInterfaceState.interfaceState.editMessage {
|
||
var disableUrlPreview = false
|
||
if let (link, _) = strongSelf.presentationInterfaceState.editingUrlPreview {
|
||
if editMessage.disableUrlPreview == link {
|
||
disableUrlPreview = true
|
||
}
|
||
}
|
||
|
||
let text = trimChatInputText(convertMarkdownToAttributes(editMessage.inputState.inputText))
|
||
let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text))
|
||
var entitiesAttribute: TextEntitiesMessageAttribute?
|
||
if !entities.isEmpty {
|
||
entitiesAttribute = TextEntitiesMessageAttribute(entities: entities)
|
||
}
|
||
|
||
let media: RequestEditMessageMedia
|
||
if let editMediaReference = strongSelf.presentationInterfaceState.editMessageState?.mediaReference {
|
||
media = .update(editMediaReference)
|
||
} else {
|
||
media = .keep
|
||
}
|
||
|
||
strongSelf.context.account.pendingUpdateMessageManager.add(messageId: editMessage.messageId, text: text.string, media: media, entities: entitiesAttribute, disableUrlPreview: disableUrlPreview)
|
||
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
|
||
var state = state
|
||
state = state.updatedInterfaceState({ $0.withUpdatedEditMessage(nil) })
|
||
state = state.updatedEditMessageState(nil)
|
||
return state
|
||
})
|
||
}
|
||
}, beginMessageSearch: { [weak self] domain, query in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
|
||
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
|
||
var interactive = true
|
||
if strongSelf.chatDisplayNode.isInputViewFocused {
|
||
interactive = false
|
||
strongSelf.context.sharedContext.mainWindow?.doNotAnimateLikelyKeyboardAutocorrectionSwitch()
|
||
}
|
||
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: interactive, { current in
|
||
return current.updatedTitlePanelContext {
|
||
if let index = $0.firstIndex(where: {
|
||
switch $0 {
|
||
case .chatInfo:
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}) {
|
||
var updatedContexts = $0
|
||
updatedContexts.remove(at: index)
|
||
return updatedContexts
|
||
} else {
|
||
return $0
|
||
}
|
||
}.updatedSearch(current.search == nil ? ChatSearchData(domain: domain).withUpdatedQuery(query) : current.search?.withUpdatedDomain(domain).withUpdatedQuery(query))
|
||
})
|
||
strongSelf.updateItemNodesSearchTextHighlightStates()
|
||
})
|
||
}, dismissMessageSearch: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in
|
||
return current.updatedSearch(nil)
|
||
})
|
||
strongSelf.updateItemNodesSearchTextHighlightStates()
|
||
strongSelf.searchResultsController = nil
|
||
}
|
||
}, updateMessageSearch: { [weak self] query in
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in
|
||
if let data = current.search {
|
||
return current.updatedSearch(data.withUpdatedQuery(query))
|
||
} else {
|
||
return current
|
||
}
|
||
})
|
||
strongSelf.updateItemNodesSearchTextHighlightStates()
|
||
strongSelf.searchResultsController = nil
|
||
}
|
||
}, openSearchResults: { [weak self] in
|
||
if let strongSelf = self, let searchData = strongSelf.presentationInterfaceState.search, let _ = searchData.resultsState {
|
||
if let controller = strongSelf.searchResultsController {
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
if case let .inline(navigationController) = strongSelf.presentationInterfaceState.mode {
|
||
navigationController?.pushViewController(controller)
|
||
} else {
|
||
strongSelf.push(controller)
|
||
}
|
||
} else {
|
||
let _ = (strongSelf.searchResult.get()
|
||
|> take(1)
|
||
|> deliverOnMainQueue).start(next: { [weak self] searchResult in
|
||
if let strongSelf = self, let (searchResult, searchState, searchLocation) = searchResult {
|
||
|
||
let controller = ChatSearchResultsController(context: strongSelf.context, location: searchLocation, searchQuery: searchData.query, searchResult: searchResult, searchState: searchState, navigateToMessageIndex: { index in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.interfaceInteraction?.navigateMessageSearch(.index(index))
|
||
}, resultsUpdated: { results, state in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
let updatedValue: (SearchMessagesResult, SearchMessagesState, SearchMessagesLocation)? = (results, state, searchLocation)
|
||
strongSelf.searchResult.set(.single(updatedValue))
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in
|
||
if let data = current.search {
|
||
let messageIndices = results.messages.map({ $0.index }).sorted()
|
||
var currentIndex = messageIndices.last
|
||
if let previousResultId = data.resultsState?.currentId {
|
||
for index in messageIndices {
|
||
if index.id >= previousResultId {
|
||
currentIndex = index
|
||
break
|
||
}
|
||
}
|
||
}
|
||
return current.updatedSearch(data.withUpdatedResultsState(ChatSearchResultsState(messageIndices: messageIndices, currentId: currentIndex?.id, state: state, totalCount: results.totalCount, completed: results.completed)))
|
||
} else {
|
||
return current
|
||
}
|
||
})
|
||
})
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
if case let .inline(navigationController) = strongSelf.presentationInterfaceState.mode {
|
||
navigationController?.pushViewController(controller)
|
||
} else {
|
||
strongSelf.push(controller)
|
||
}
|
||
strongSelf.searchResultsController = controller
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}, navigateMessageSearch: { [weak self] action in
|
||
if let strongSelf = self {
|
||
var navigateIndex: MessageIndex?
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in
|
||
if let data = current.search, let resultsState = data.resultsState {
|
||
if let currentId = resultsState.currentId, let index = resultsState.messageIndices.firstIndex(where: { $0.id == currentId }) {
|
||
var updatedIndex: Int?
|
||
switch action {
|
||
case .earlier:
|
||
if index != 0 {
|
||
updatedIndex = index - 1
|
||
}
|
||
case .later:
|
||
if index != resultsState.messageIndices.count - 1 {
|
||
updatedIndex = index + 1
|
||
}
|
||
case let .index(index):
|
||
if index >= 0 && index < resultsState.messageIndices.count {
|
||
updatedIndex = index
|
||
}
|
||
}
|
||
if let updatedIndex = updatedIndex {
|
||
navigateIndex = resultsState.messageIndices[updatedIndex]
|
||
return current.updatedSearch(data.withUpdatedResultsState(ChatSearchResultsState(messageIndices: resultsState.messageIndices, currentId: resultsState.messageIndices[updatedIndex].id, state: resultsState.state, totalCount: resultsState.totalCount, completed: resultsState.completed)))
|
||
}
|
||
}
|
||
}
|
||
return current
|
||
})
|
||
strongSelf.updateItemNodesSearchTextHighlightStates()
|
||
if let navigateIndex = navigateIndex {
|
||
switch strongSelf.chatLocation {
|
||
case .peer, .replyThread:
|
||
strongSelf.navigateToMessage(from: nil, to: .index(navigateIndex), forceInCurrentChat: true)
|
||
}
|
||
}
|
||
}
|
||
}, openCalendarSearch: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
|
||
let controller = ChatDateSelectionSheet(presentationData: strongSelf.presentationData, completion: { timestamp in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.loadingMessage.set(.single(.generic))
|
||
|
||
let peerId: PeerId
|
||
let threadId: Int64?
|
||
switch strongSelf.chatLocation {
|
||
case let .peer(peerIdValue):
|
||
peerId = peerIdValue
|
||
threadId = nil
|
||
case let .replyThread(replyThreadMessage):
|
||
peerId = replyThreadMessage.messageId.peerId
|
||
threadId = makeMessageThreadId(replyThreadMessage.messageId)
|
||
}
|
||
|
||
strongSelf.messageIndexDisposable.set((searchMessageIdByTimestamp(account: strongSelf.context.account, peerId: peerId, threadId: threadId, timestamp: timestamp) |> deliverOnMainQueue).start(next: { messageId in
|
||
if let strongSelf = self {
|
||
strongSelf.loadingMessage.set(.single(nil))
|
||
if let messageId = messageId {
|
||
strongSelf.navigateToMessage(from: nil, to: .id(messageId), forceInCurrentChat: true)
|
||
}
|
||
}
|
||
}))
|
||
})
|
||
strongSelf.present(controller, in: .window(.root))
|
||
}
|
||
}, toggleMembersSearch: { [weak self] value in
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
|
||
if value {
|
||
return state.updatedSearch(ChatSearchData(query: "", domain: .members, domainSuggestionContext: .none, resultsState: nil))
|
||
} else if let search = state.search {
|
||
switch search.domain {
|
||
case .everything:
|
||
return state
|
||
case .members:
|
||
return state.updatedSearch(ChatSearchData(query: "", domain: .everything, domainSuggestionContext: .none, resultsState: nil))
|
||
case .member:
|
||
return state.updatedSearch(ChatSearchData(query: "", domain: .members, domainSuggestionContext: .none, resultsState: nil))
|
||
}
|
||
} else {
|
||
return state
|
||
}
|
||
})
|
||
strongSelf.updateItemNodesSearchTextHighlightStates()
|
||
}
|
||
}, navigateToMessage: { [weak self] messageId, dropStack, forceInCurrentChat, statusSubject in
|
||
self?.navigateToMessage(from: nil, to: .id(messageId), forceInCurrentChat: forceInCurrentChat, dropStack: dropStack, statusSubject: statusSubject)
|
||
}, navigateToChat: { [weak self] peerId in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
if let navigationController = strongSelf.effectiveNavigationController {
|
||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peerId), subject: nil, keepStack: .always))
|
||
}
|
||
}, navigateToProfile: { [weak self] peerId in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.openPeer(peerId: peerId, navigation: .default, fromMessage: nil)
|
||
}, openPeerInfo: { [weak self] in
|
||
self?.navigationButtonAction(.openChatInfo(expandAvatar: false))
|
||
}, togglePeerNotifications: { [weak self] in
|
||
if let strongSelf = self {
|
||
let peerId = strongSelf.chatLocation.peerId
|
||
let _ = togglePeerMuted(account: strongSelf.context.account, peerId: peerId).start()
|
||
}
|
||
}, sendContextResult: { [weak self] results, result, node, rect in
|
||
guard let strongSelf = self else {
|
||
return false
|
||
}
|
||
if let _ = strongSelf.presentationInterfaceState.slowmodeState, strongSelf.presentationInterfaceState.subject != .scheduledMessages {
|
||
strongSelf.interfaceInteraction?.displaySlowmodeTooltip(node, rect)
|
||
return false
|
||
}
|
||
|
||
strongSelf.enqueueChatContextResult(results, result)
|
||
return true
|
||
}, sendBotCommand: { [weak self] botPeer, command in
|
||
if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) {
|
||
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
|
||
let messageText: String
|
||
if let addressName = botPeer.addressName {
|
||
if peer is TelegramUser {
|
||
messageText = command
|
||
} else {
|
||
messageText = command + "@" + addressName
|
||
}
|
||
} else {
|
||
messageText = command
|
||
}
|
||
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: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) }
|
||
})
|
||
}
|
||
})
|
||
var attributes: [MessageAttribute] = []
|
||
let entities = generateTextEntities(messageText, enabledTypes: .all)
|
||
if !entities.isEmpty {
|
||
attributes.append(TextEntitiesMessageAttribute(entities: entities))
|
||
}
|
||
strongSelf.sendMessages([.message(text: messageText, attributes: attributes, mediaReference: nil, replyToMessageId: replyMessageId, localGroupingKey: nil)])
|
||
}
|
||
}
|
||
}, sendBotStart: { [weak self] payload in
|
||
if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) {
|
||
strongSelf.startBot(payload)
|
||
}
|
||
}, botSwitchChatWithPayload: { [weak self] peerId, payload in
|
||
if let strongSelf = self, case let .peer(currentPeerId) = strongSelf.chatLocation {
|
||
var isScheduled = false
|
||
if case .scheduledMessages = strongSelf.presentationInterfaceState.subject {
|
||
isScheduled = true
|
||
}
|
||
strongSelf.openPeer(peerId: peerId, navigation: .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .automatic(returnToPeerId: currentPeerId, scheduled: isScheduled))), fromMessage: nil)
|
||
}
|
||
}, beginMediaRecording: { [weak self] isVideo in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.mediaRecordingModeTooltipController?.dismiss()
|
||
|
||
let requestId = strongSelf.beginMediaRecordingRequestId
|
||
let begin: () -> Void = {
|
||
guard let strongSelf = self, strongSelf.beginMediaRecordingRequestId == requestId else {
|
||
return
|
||
}
|
||
guard checkAvailableDiskSpace(context: strongSelf.context, push: { [weak self] c in
|
||
self?.push(c)
|
||
}) else {
|
||
return
|
||
}
|
||
let hasOngoingCall: Signal<Bool, NoError> = strongSelf.context.sharedContext.hasOngoingCall.get()
|
||
let _ = (hasOngoingCall
|
||
|> deliverOnMainQueue).start(next: { hasOngoingCall in
|
||
guard let strongSelf = self, strongSelf.beginMediaRecordingRequestId == requestId else {
|
||
return
|
||
}
|
||
if hasOngoingCall {
|
||
strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.Call_CallInProgressTitle, text: strongSelf.presentationData.strings.Call_RecordingDisabledMessage, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
|
||
})]), in: .window(.root))
|
||
} else {
|
||
if isVideo {
|
||
strongSelf.requestVideoRecorder()
|
||
} else {
|
||
strongSelf.requestAudioRecorder(beginWithTone: false)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
DeviceAccess.authorizeAccess(to: .microphone(isVideo ? .video : .audio), presentationData: strongSelf.presentationData, present: { c, a in
|
||
self?.present(c, in: .window(.root), with: a)
|
||
}, openSettings: {
|
||
self?.context.sharedContext.applicationBindings.openSettings()
|
||
}, { granted in
|
||
guard let strongSelf = self, granted else {
|
||
return
|
||
}
|
||
if isVideo {
|
||
DeviceAccess.authorizeAccess(to: .camera(.video), presentationData: strongSelf.presentationData, present: { c, a in
|
||
self?.present(c, in: .window(.root), with: a)
|
||
}, openSettings: {
|
||
self?.context.sharedContext.applicationBindings.openSettings()
|
||
}, { granted in
|
||
if granted {
|
||
begin()
|
||
}
|
||
})
|
||
} else {
|
||
begin()
|
||
}
|
||
})
|
||
}, finishMediaRecording: { [weak self] action in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.beginMediaRecordingRequestId += 1
|
||
strongSelf.dismissMediaRecorder(action)
|
||
}, stopMediaRecording: { [weak self] in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.beginMediaRecordingRequestId += 1
|
||
strongSelf.lockMediaRecordingRequestId = nil
|
||
strongSelf.stopMediaRecorder()
|
||
}, lockMediaRecording: { [weak self] in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.lockMediaRecordingRequestId = strongSelf.beginMediaRecordingRequestId
|
||
strongSelf.lockMediaRecorder()
|
||
}, deleteRecordedMedia: { [weak self] in
|
||
self?.deleteMediaRecording()
|
||
}, sendRecordedMedia: { [weak self] silentPosting in
|
||
self?.sendMediaRecording(silentPosting: silentPosting)
|
||
}, displayRestrictedInfo: { [weak self] subject, displayType in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
let subjectFlags: TelegramChatBannedRightsFlags
|
||
switch subject {
|
||
case .stickers:
|
||
subjectFlags = .banSendStickers
|
||
case .mediaRecording:
|
||
subjectFlags = .banSendMedia
|
||
}
|
||
|
||
if case .mediaRecording = subject, let _ = strongSelf.presentationInterfaceState.activeGroupCallInfo {
|
||
let rect = strongSelf.chatDisplayNode.frameForInputActionButton()
|
||
if let rect = rect {
|
||
strongSelf.mediaRestrictedTooltipController?.dismiss()
|
||
let tooltipController = TooltipController(content: .text(strongSelf.presentationInterfaceState.strings.Conversation_VoiceChatMediaRecordingRestricted), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize)
|
||
strongSelf.mediaRestrictedTooltipController = tooltipController
|
||
strongSelf.mediaRestrictedTooltipControllerMode = false
|
||
tooltipController.dismissed = { [weak tooltipController] _ in
|
||
if let strongSelf = self, let tooltipController = tooltipController, strongSelf.mediaRestrictedTooltipController === tooltipController {
|
||
strongSelf.mediaRestrictedTooltipController = nil
|
||
}
|
||
}
|
||
strongSelf.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: {
|
||
if let strongSelf = self {
|
||
return (strongSelf.chatDisplayNode, rect)
|
||
}
|
||
return nil
|
||
}))
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
let bannedPermission: (Int32, Bool)?
|
||
if let channel = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel {
|
||
bannedPermission = channel.hasBannedPermission(subjectFlags)
|
||
} else if let group = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramGroup {
|
||
if group.hasBannedPermission(subjectFlags) {
|
||
bannedPermission = (Int32.max, false)
|
||
} else {
|
||
bannedPermission = nil
|
||
}
|
||
} else {
|
||
bannedPermission = nil
|
||
}
|
||
|
||
if let (untilDate, personal) = bannedPermission {
|
||
let banDescription: String
|
||
switch subject {
|
||
case .stickers:
|
||
if untilDate != 0 && untilDate != Int32.max {
|
||
banDescription = strongSelf.presentationInterfaceState.strings.Conversation_RestrictedStickersTimed(stringForFullDate(timestamp: untilDate, strings: strongSelf.presentationInterfaceState.strings, dateTimeFormat: strongSelf.presentationInterfaceState.dateTimeFormat)).0
|
||
} else if personal {
|
||
banDescription = strongSelf.presentationInterfaceState.strings.Conversation_RestrictedStickers
|
||
} else {
|
||
banDescription = strongSelf.presentationInterfaceState.strings.Conversation_DefaultRestrictedStickers
|
||
}
|
||
case .mediaRecording:
|
||
if untilDate != 0 && untilDate != Int32.max {
|
||
banDescription = strongSelf.presentationInterfaceState.strings.Conversation_RestrictedMediaTimed(stringForFullDate(timestamp: untilDate, strings: strongSelf.presentationInterfaceState.strings, dateTimeFormat: strongSelf.presentationInterfaceState.dateTimeFormat)).0
|
||
} else if personal {
|
||
banDescription = strongSelf.presentationInterfaceState.strings.Conversation_RestrictedMedia
|
||
} else {
|
||
banDescription = strongSelf.presentationInterfaceState.strings.Conversation_DefaultRestrictedMedia
|
||
}
|
||
}
|
||
if strongSelf.recordingModeFeedback == nil {
|
||
strongSelf.recordingModeFeedback = HapticFeedback()
|
||
strongSelf.recordingModeFeedback?.prepareError()
|
||
}
|
||
|
||
strongSelf.recordingModeFeedback?.error()
|
||
|
||
switch displayType {
|
||
case .tooltip:
|
||
var rect: CGRect?
|
||
let isStickers: Bool = subject == .stickers
|
||
switch subject {
|
||
case .stickers:
|
||
rect = strongSelf.chatDisplayNode.frameForStickersButton()
|
||
if var rectValue = rect, let actionRect = strongSelf.chatDisplayNode.frameForInputActionButton() {
|
||
rectValue.origin.y = actionRect.minY
|
||
rect = rectValue
|
||
}
|
||
case .mediaRecording:
|
||
rect = strongSelf.chatDisplayNode.frameForInputActionButton()
|
||
}
|
||
|
||
if let tooltipController = strongSelf.mediaRestrictedTooltipController, strongSelf.mediaRestrictedTooltipControllerMode == isStickers {
|
||
tooltipController.updateContent(.text(banDescription), animated: true, extendTimer: true)
|
||
} else if let rect = rect {
|
||
strongSelf.mediaRestrictedTooltipController?.dismiss()
|
||
let tooltipController = TooltipController(content: .text(banDescription), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize)
|
||
strongSelf.mediaRestrictedTooltipController = tooltipController
|
||
strongSelf.mediaRestrictedTooltipControllerMode = isStickers
|
||
tooltipController.dismissed = { [weak tooltipController] _ in
|
||
if let strongSelf = self, let tooltipController = tooltipController, strongSelf.mediaRestrictedTooltipController === tooltipController {
|
||
strongSelf.mediaRestrictedTooltipController = nil
|
||
}
|
||
}
|
||
strongSelf.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: {
|
||
if let strongSelf = self {
|
||
return (strongSelf.chatDisplayNode, rect)
|
||
}
|
||
return nil
|
||
}))
|
||
}
|
||
case .alert:
|
||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: banDescription, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||
}
|
||
}
|
||
}, displayVideoUnmuteTip: { [weak self] location in
|
||
guard let strongSelf = self, let layout = strongSelf.validLayout, strongSelf.traceVisibility() && isTopmostChatController(strongSelf) else {
|
||
return
|
||
}
|
||
|
||
if let location = location, location.y < strongSelf.navigationHeight {
|
||
return
|
||
}
|
||
|
||
let icon: UIImage?
|
||
if layout.deviceMetrics.hasTopNotch {
|
||
icon = UIImage(bundleImageName: "Chat/Message/VolumeButtonIconX")
|
||
} else {
|
||
icon = UIImage(bundleImageName: "Chat/Message/VolumeButtonIcon")
|
||
}
|
||
if let location = location, let icon = icon {
|
||
strongSelf.videoUnmuteTooltipController?.dismiss()
|
||
let tooltipController = TooltipController(content: .iconAndText(icon, strongSelf.presentationInterfaceState.strings.Conversation_PressVolumeButtonForSound), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize, timeout: 3.5, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true)
|
||
strongSelf.videoUnmuteTooltipController = tooltipController
|
||
tooltipController.dismissed = { [weak tooltipController] _ in
|
||
if let strongSelf = self, let tooltipController = tooltipController, strongSelf.videoUnmuteTooltipController === tooltipController {
|
||
strongSelf.videoUnmuteTooltipController = nil
|
||
ApplicationSpecificNotice.setVolumeButtonToUnmute(accountManager: strongSelf.context.sharedContext.accountManager)
|
||
}
|
||
}
|
||
strongSelf.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: {
|
||
if let strongSelf = self {
|
||
return (strongSelf.chatDisplayNode, CGRect(origin: location, size: CGSize()))
|
||
}
|
||
return nil
|
||
}))
|
||
} else if let tooltipController = strongSelf.videoUnmuteTooltipController {
|
||
tooltipController.dismissImmediately()
|
||
}
|
||
}, switchMediaRecordingMode: { [weak self] in
|
||
if let strongSelf = self {
|
||
if strongSelf.recordingModeFeedback == nil {
|
||
strongSelf.recordingModeFeedback = HapticFeedback()
|
||
strongSelf.recordingModeFeedback?.prepareImpact()
|
||
}
|
||
|
||
strongSelf.recordingModeFeedback?.impact()
|
||
var updatedMode: ChatTextInputMediaRecordingButtonMode?
|
||
|
||
strongSelf.updateChatPresentationInterfaceState(interactive: true, {
|
||
return $0.updatedInterfaceState { current in
|
||
let mode: ChatTextInputMediaRecordingButtonMode
|
||
switch current.mediaRecordingMode {
|
||
case .audio:
|
||
mode = .video
|
||
case .video:
|
||
mode = .audio
|
||
}
|
||
updatedMode = mode
|
||
return current.withUpdatedMediaRecordingMode(mode)
|
||
}
|
||
})
|
||
|
||
if let updatedMode = updatedMode, updatedMode == .video {
|
||
let _ = ApplicationSpecificNotice.incrementChatMediaMediaRecordingTips(accountManager: strongSelf.context.sharedContext.accountManager, count: 3).start()
|
||
}
|
||
|
||
strongSelf.displayMediaRecordingTooltip()
|
||
}
|
||
}, setupMessageAutoremoveTimeout: { [weak self] in
|
||
guard let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation else {
|
||
return
|
||
}
|
||
guard let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
|
||
return
|
||
}
|
||
if peerId.namespace == Namespaces.Peer.SecretChat {
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
|
||
if let peer = peer as? TelegramSecretChat {
|
||
let controller = ChatSecretAutoremoveTimerActionSheetController(context: strongSelf.context, currentValue: peer.messageAutoremoveTimeout == nil ? 0 : peer.messageAutoremoveTimeout!, applyValue: { value in
|
||
if let strongSelf = self {
|
||
let _ = setSecretChatMessageAutoremoveTimeoutInteractively(account: strongSelf.context.account, peerId: peer.id, timeout: value == 0 ? nil : value).start()
|
||
}
|
||
})
|
||
strongSelf.present(controller, in: .window(.root))
|
||
}
|
||
} else {
|
||
var currentAutoremoveTimeout: Int32? = strongSelf.presentationInterfaceState.autoremoveTimeout
|
||
var canSetupAutoremoveTimeout = false
|
||
|
||
if let secretChat = peer as? TelegramSecretChat {
|
||
currentAutoremoveTimeout = secretChat.messageAutoremoveTimeout
|
||
canSetupAutoremoveTimeout = true
|
||
} else if let group = peer as? TelegramGroup {
|
||
if case .creator = group.role {
|
||
canSetupAutoremoveTimeout = true
|
||
} else if case let .admin(rights, _) = group.role {
|
||
if rights.flags.contains(.canChangeInfo) {
|
||
canSetupAutoremoveTimeout = true
|
||
}
|
||
} else if let defaultBannedRights = group.defaultBannedRights {
|
||
if !defaultBannedRights.flags.contains(.banChangeInfo) {
|
||
canSetupAutoremoveTimeout = true
|
||
}
|
||
}
|
||
} else if let _ = peer as? TelegramUser {
|
||
canSetupAutoremoveTimeout = true
|
||
} else if let channel = peer as? TelegramChannel {
|
||
if channel.hasPermission(.changeInfo) {
|
||
canSetupAutoremoveTimeout = true
|
||
}
|
||
}
|
||
|
||
if canSetupAutoremoveTimeout {
|
||
strongSelf.presentAutoremoveSetup()
|
||
} else if let currentAutoremoveTimeout = currentAutoremoveTimeout, let rect = strongSelf.chatDisplayNode.frameForInputPanelAccessoryButton(.messageAutoremoveTimeout(currentAutoremoveTimeout)) {
|
||
|
||
//TODO:localize
|
||
let intervalText = timeIntervalString(strings: strongSelf.presentationData.strings, value: currentAutoremoveTimeout)
|
||
let text: String = "Messages in this chat are automatically\ndeleted \(intervalText) after they have been sent."
|
||
|
||
if let tooltipController = strongSelf.silentPostTooltipController {
|
||
tooltipController.updateContent(.text(text), animated: true, extendTimer: true)
|
||
} else {
|
||
let tooltipController = TooltipController(content: .text(text), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize)
|
||
strongSelf.silentPostTooltipController = tooltipController
|
||
tooltipController.dismissed = { [weak tooltipController] _ in
|
||
if let strongSelf = self, let tooltipController = tooltipController, strongSelf.silentPostTooltipController === tooltipController {
|
||
strongSelf.silentPostTooltipController = nil
|
||
}
|
||
}
|
||
strongSelf.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: {
|
||
if let strongSelf = self {
|
||
return (strongSelf.chatDisplayNode, rect)
|
||
}
|
||
return nil
|
||
}))
|
||
}
|
||
}
|
||
}
|
||
}, sendSticker: { [weak self] file, sourceNode, sourceRect in
|
||
if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) {
|
||
return strongSelf.controllerInteraction?.sendSticker(file, nil, true, sourceNode, sourceRect) ?? false
|
||
} else {
|
||
return false
|
||
}
|
||
}, unblockPeer: { [weak self] in
|
||
self?.unblockPeer()
|
||
}, pinMessage: { [weak self] messageId, contextController in
|
||
if let strongSelf = self, case let .peer(currentPeerId) = strongSelf.chatLocation {
|
||
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
|
||
if strongSelf.canManagePin() {
|
||
let pinAction: (Bool, Bool) -> Void = { notify, forThisPeerOnlyIfPossible in
|
||
if let strongSelf = self {
|
||
let disposable: MetaDisposable
|
||
if let current = strongSelf.unpinMessageDisposable {
|
||
disposable = current
|
||
} else {
|
||
disposable = MetaDisposable()
|
||
strongSelf.unpinMessageDisposable = disposable
|
||
}
|
||
disposable.set(requestUpdatePinnedMessage(account: strongSelf.context.account, peerId: currentPeerId, update: .pin(id: messageId, silent: !notify, forThisPeerOnlyIfPossible: forThisPeerOnlyIfPossible)).start(completed: {
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.scrolledToMessageIdValue = nil
|
||
}))
|
||
}
|
||
}
|
||
|
||
if let peer = peer as? TelegramChannel, case .broadcast = peer.info, let contextController = contextController {
|
||
contextController.dismiss(completion: {
|
||
pinAction(true, false)
|
||
})
|
||
} else if let peer = peer as? TelegramUser, let contextController = contextController {
|
||
if peer.id == strongSelf.context.account.peerId {
|
||
contextController.dismiss(completion: {
|
||
pinAction(true, true)
|
||
})
|
||
} else {
|
||
var contextItems: [ContextMenuItem] = []
|
||
contextItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_PinMessagesFor(peer.compactDisplayTitle).0, textColor: .primary, icon: { _ in nil }, action: { c, _ in
|
||
c.dismiss(completion: {
|
||
pinAction(true, false)
|
||
})
|
||
})))
|
||
|
||
contextItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_PinMessagesForMe, textColor: .primary, icon: { _ in nil }, action: { c, _ in
|
||
c.dismiss(completion: {
|
||
pinAction(true, true)
|
||
})
|
||
})))
|
||
|
||
contextController.setItems(.single(contextItems))
|
||
}
|
||
return
|
||
} else {
|
||
if let contextController = contextController {
|
||
var contextItems: [ContextMenuItem] = []
|
||
|
||
contextItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_PinMessageAlert_PinAndNotifyMembers, textColor: .primary, icon: { _ in nil }, action: { c, _ in
|
||
c.dismiss(completion: {
|
||
pinAction(true, false)
|
||
})
|
||
})))
|
||
|
||
contextItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_PinMessageAlert_OnlyPin, textColor: .primary, icon: { _ in nil }, action: { c, _ in
|
||
c.dismiss(completion: {
|
||
pinAction(false, false)
|
||
})
|
||
})))
|
||
|
||
contextController.setItems(.single(contextItems))
|
||
|
||
return
|
||
} else {
|
||
let continueAction: () -> Void = {
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
|
||
var pinImmediately = false
|
||
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
|
||
pinImmediately = true
|
||
} else if let _ = peer as? TelegramUser {
|
||
pinImmediately = true
|
||
}
|
||
|
||
if pinImmediately {
|
||
pinAction(true, false)
|
||
} else {
|
||
let topPinnedMessage: Signal<ChatPinnedMessage?, NoError> = strongSelf.topPinnedMessageSignal(latest: true)
|
||
|> take(1)
|
||
|
||
let _ = (topPinnedMessage
|
||
|> deliverOnMainQueue).start(next: { value in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
|
||
let title: String?
|
||
let text: String
|
||
let actionLayout: TextAlertContentActionLayout
|
||
let actions: [TextAlertAction]
|
||
if let value = value, value.message.id > messageId {
|
||
title = strongSelf.presentationData.strings.Conversation_PinOlderMessageAlertTitle
|
||
text = strongSelf.presentationData.strings.Conversation_PinOlderMessageAlertText
|
||
actionLayout = .vertical
|
||
actions = [
|
||
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Conversation_PinMessageAlertPin, action: {
|
||
pinAction(false, false)
|
||
}),
|
||
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
|
||
})
|
||
]
|
||
} else {
|
||
title = nil
|
||
text = strongSelf.presentationData.strings.Conversation_PinMessageAlertGroup
|
||
actionLayout = .horizontal
|
||
actions = [
|
||
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Conversation_PinMessageAlert_OnlyPin, action: {
|
||
pinAction(false, false)
|
||
}),
|
||
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Yes, action: {
|
||
pinAction(true, false)
|
||
})
|
||
]
|
||
}
|
||
|
||
strongSelf.present(textAlertController(context: strongSelf.context, title: title, text: text, actions: actions, actionLayout: actionLayout), in: .window(.root))
|
||
})
|
||
}
|
||
}
|
||
|
||
continueAction()
|
||
}
|
||
}
|
||
} else {
|
||
if let topPinnedMessageId = strongSelf.presentationInterfaceState.pinnedMessage?.topMessageId {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
return $0.updatedInterfaceState({ $0.withUpdatedMessageActionsState({ value in
|
||
var value = value
|
||
value.closedPinnedMessageId = topPinnedMessageId
|
||
return value
|
||
})
|
||
})
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}, unpinMessage: { [weak self] id, askForConfirmation, contextController in
|
||
let impl: () -> Void = {
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
guard let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
|
||
return
|
||
}
|
||
|
||
if strongSelf.canManagePin() {
|
||
let action: () -> Void = {
|
||
if let strongSelf = self {
|
||
let disposable: MetaDisposable
|
||
if let current = strongSelf.unpinMessageDisposable {
|
||
disposable = current
|
||
} else {
|
||
disposable = MetaDisposable()
|
||
strongSelf.unpinMessageDisposable = disposable
|
||
}
|
||
|
||
if askForConfirmation {
|
||
strongSelf.chatDisplayNode.historyNode.pendingUnpinnedAllMessages = true
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
return $0.updatedPendingUnpinnedAllMessages(true)
|
||
})
|
||
|
||
strongSelf.present(
|
||
UndoOverlayController(
|
||
presentationData: strongSelf.presentationData,
|
||
content: .messagesUnpinned(
|
||
title: strongSelf.presentationData.strings.Chat_MessagesUnpinned(1),
|
||
text: "",
|
||
undo: askForConfirmation,
|
||
isHidden: false
|
||
),
|
||
elevatedLayout: false,
|
||
action: { action in
|
||
switch action {
|
||
case .commit:
|
||
disposable.set((requestUpdatePinnedMessage(account: strongSelf.context.account, peerId: peer.id, update: .clear(id: id))
|
||
|> deliverOnMainQueue).start(error: { _ in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.chatDisplayNode.historyNode.pendingUnpinnedAllMessages = false
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
return $0.updatedPendingUnpinnedAllMessages(false)
|
||
})
|
||
}, completed: {
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.chatDisplayNode.historyNode.pendingUnpinnedAllMessages = false
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
return $0.updatedPendingUnpinnedAllMessages(false)
|
||
})
|
||
}))
|
||
case .undo:
|
||
strongSelf.chatDisplayNode.historyNode.pendingUnpinnedAllMessages = false
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
return $0.updatedPendingUnpinnedAllMessages(false)
|
||
})
|
||
default:
|
||
break
|
||
}
|
||
return true
|
||
}
|
||
),
|
||
in: .current
|
||
)
|
||
} else {
|
||
if case .pinnedMessages = strongSelf.presentationInterfaceState.subject {
|
||
strongSelf.chatDisplayNode.historyNode.pendingRemovedMessages.insert(id)
|
||
strongSelf.present(
|
||
UndoOverlayController(
|
||
presentationData: strongSelf.presentationData,
|
||
content: .messagesUnpinned(
|
||
title: strongSelf.presentationData.strings.Chat_MessagesUnpinned(1),
|
||
text: "",
|
||
undo: true,
|
||
isHidden: false
|
||
),
|
||
elevatedLayout: false,
|
||
action: { action in
|
||
guard let strongSelf = self else {
|
||
return true
|
||
}
|
||
switch action {
|
||
case .commit:
|
||
let _ = (requestUpdatePinnedMessage(account: strongSelf.context.account, peerId: peer.id, update: .clear(id: id))
|
||
|> deliverOnMainQueue).start(completed: {
|
||
Queue.mainQueue().after(1.0, {
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.chatDisplayNode.historyNode.pendingRemovedMessages.remove(id)
|
||
})
|
||
})
|
||
case .undo:
|
||
strongSelf.chatDisplayNode.historyNode.pendingRemovedMessages.remove(id)
|
||
default:
|
||
break
|
||
}
|
||
return true
|
||
}
|
||
),
|
||
in: .current
|
||
)
|
||
} else {
|
||
disposable.set((requestUpdatePinnedMessage(account: strongSelf.context.account, peerId: peer.id, update: .clear(id: id))
|
||
|> deliverOnMainQueue).start())
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if askForConfirmation {
|
||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Conversation_UnpinMessageAlert, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Conversation_Unpin, action: {
|
||
action()
|
||
}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})], actionLayout: .vertical), in: .window(.root))
|
||
} else {
|
||
action()
|
||
}
|
||
} else {
|
||
if let pinnedMessage = strongSelf.presentationInterfaceState.pinnedMessage {
|
||
let previousClosedPinnedMessageId = strongSelf.presentationInterfaceState.interfaceState.messageActionsState.closedPinnedMessageId
|
||
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
return $0.updatedInterfaceState({ $0.withUpdatedMessageActionsState({ value in
|
||
var value = value
|
||
value.closedPinnedMessageId = pinnedMessage.topMessageId
|
||
return value
|
||
}) })
|
||
})
|
||
strongSelf.present(
|
||
UndoOverlayController(
|
||
presentationData: strongSelf.presentationData,
|
||
content: .messagesUnpinned(
|
||
title: strongSelf.presentationData.strings.Chat_PinnedMessagesHiddenTitle,
|
||
text: strongSelf.presentationData.strings.Chat_PinnedMessagesHiddenText,
|
||
undo: true,
|
||
isHidden: false
|
||
),
|
||
elevatedLayout: false,
|
||
action: { action in
|
||
guard let strongSelf = self else {
|
||
return true
|
||
}
|
||
switch action {
|
||
case .commit:
|
||
break
|
||
case .undo:
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
return $0.updatedInterfaceState({ $0.withUpdatedMessageActionsState({ value in
|
||
var value = value
|
||
value.closedPinnedMessageId = previousClosedPinnedMessageId
|
||
return value
|
||
}) })
|
||
})
|
||
default:
|
||
break
|
||
}
|
||
return true
|
||
}
|
||
),
|
||
in: .current
|
||
)
|
||
strongSelf.updatedClosedPinnedMessageId?(pinnedMessage.topMessageId)
|
||
}
|
||
}
|
||
}
|
||
|
||
if let contextController = contextController {
|
||
contextController.dismiss(completion: {
|
||
impl()
|
||
})
|
||
} else {
|
||
impl()
|
||
}
|
||
}, unpinAllMessages: { [weak self] in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
|
||
let topPinnedMessage: Signal<ChatPinnedMessage?, NoError> = strongSelf.topPinnedMessageSignal(latest: true)
|
||
|> take(1)
|
||
|
||
let _ = (topPinnedMessage
|
||
|> deliverOnMainQueue).start(next: { topPinnedMessage in
|
||
guard let strongSelf = self, let topPinnedMessage = topPinnedMessage else {
|
||
return
|
||
}
|
||
|
||
if strongSelf.canManagePin() {
|
||
let count = strongSelf.presentationInterfaceState.pinnedMessage?.totalCount ?? 1
|
||
|
||
strongSelf.requestedUnpinAllMessages?(count, topPinnedMessage.topMessageId)
|
||
strongSelf.dismiss()
|
||
} else {
|
||
strongSelf.updatedClosedPinnedMessageId?(topPinnedMessage.topMessageId)
|
||
strongSelf.dismiss()
|
||
}
|
||
})
|
||
}, openPinnedList: { [weak self] messageId in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.openPinnedMessages(at: messageId)
|
||
}, shareAccountContact: { [weak self] in
|
||
self?.shareAccountContact()
|
||
}, reportPeer: { [weak self] in
|
||
self?.reportPeer()
|
||
}, presentPeerContact: { [weak self] in
|
||
self?.addPeerContact()
|
||
}, dismissReportPeer: { [weak self] in
|
||
self?.dismissPeerContactOptions()
|
||
}, deleteChat: { [weak self] in
|
||
self?.deleteChat(reportChatSpam: false)
|
||
}, beginCall: { [weak self] isVideo in
|
||
if let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation {
|
||
strongSelf.controllerInteraction?.callPeer(peerId, isVideo)
|
||
}
|
||
}, toggleMessageStickerStarred: { [weak self] messageId in
|
||
if let strongSelf = self, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
|
||
var stickerFile: TelegramMediaFile?
|
||
for media in message.media {
|
||
if let file = media as? TelegramMediaFile, file.isSticker {
|
||
stickerFile = file
|
||
}
|
||
}
|
||
if let stickerFile = stickerFile {
|
||
let postbox = strongSelf.context.account.postbox
|
||
let network = strongSelf.context.account.network
|
||
let _ = (strongSelf.context.account.postbox.transaction { transaction -> Signal<Void, NoError> in
|
||
if getIsStickerSaved(transaction: transaction, fileId: stickerFile.fileId) {
|
||
removeSavedSticker(transaction: transaction, mediaId: stickerFile.fileId)
|
||
return .complete()
|
||
} else {
|
||
return addSavedSticker(postbox: postbox, network: network, file: stickerFile)
|
||
|> `catch` { _ -> Signal<Void, NoError> in
|
||
return .complete()
|
||
}
|
||
}
|
||
} |> switchToLatest).start()
|
||
}
|
||
}
|
||
}, presentController: { [weak self] controller, arguments in
|
||
self?.present(controller, in: .window(.root), with: arguments)
|
||
}, getNavigationController: { [weak self] in
|
||
return self?.navigationController as? NavigationController
|
||
}, presentGlobalOverlayController: { [weak self] controller, arguments in
|
||
self?.presentInGlobalOverlay(controller, with: arguments)
|
||
}, navigateFeed: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.chatDisplayNode.historyNode.scrollToNextMessage()
|
||
}
|
||
}, openGrouping: {
|
||
}, toggleSilentPost: { [weak self] in
|
||
if let strongSelf = self {
|
||
var value: Bool = false
|
||
strongSelf.updateChatPresentationInterfaceState(interactive: true, {
|
||
$0.updatedInterfaceState {
|
||
value = !$0.silentPosting
|
||
return $0.withUpdatedSilentPosting(value)
|
||
}
|
||
})
|
||
strongSelf.saveInterfaceState()
|
||
|
||
if let navigationController = strongSelf.navigationController as? NavigationController {
|
||
for controller in navigationController.globalOverlayControllers {
|
||
if controller is VoiceChatOverlayController {
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
var rect: CGRect? = strongSelf.chatDisplayNode.frameForInputPanelAccessoryButton(.silentPost(true))
|
||
if rect == nil {
|
||
rect = strongSelf.chatDisplayNode.frameForInputPanelAccessoryButton(.silentPost(false))
|
||
}
|
||
|
||
let text: String
|
||
if !value {
|
||
text = strongSelf.presentationData.strings.Conversation_SilentBroadcastTooltipOn
|
||
} else {
|
||
text = strongSelf.presentationData.strings.Conversation_SilentBroadcastTooltipOff
|
||
}
|
||
|
||
if let tooltipController = strongSelf.silentPostTooltipController {
|
||
tooltipController.updateContent(.text(text), animated: true, extendTimer: true)
|
||
} else if let rect = rect {
|
||
let tooltipController = TooltipController(content: .text(text), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize)
|
||
strongSelf.silentPostTooltipController = tooltipController
|
||
tooltipController.dismissed = { [weak tooltipController] _ in
|
||
if let strongSelf = self, let tooltipController = tooltipController, strongSelf.silentPostTooltipController === tooltipController {
|
||
strongSelf.silentPostTooltipController = nil
|
||
}
|
||
}
|
||
strongSelf.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: {
|
||
if let strongSelf = self {
|
||
return (strongSelf.chatDisplayNode, rect)
|
||
}
|
||
return nil
|
||
}))
|
||
}
|
||
}
|
||
}, requestUnvoteInMessage: { [weak self] id in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
let disposables: DisposableDict<MessageId>
|
||
if let current = strongSelf.selectMessagePollOptionDisposables {
|
||
disposables = current
|
||
} else {
|
||
disposables = DisposableDict()
|
||
strongSelf.selectMessagePollOptionDisposables = disposables
|
||
}
|
||
let controller = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: nil))
|
||
strongSelf.present(controller, in: .window(.root))
|
||
let signal = requestMessageSelectPollOption(account: strongSelf.context.account, messageId: id, opaqueIdentifiers: [])
|
||
|> afterDisposed { [weak controller] in
|
||
Queue.mainQueue().async {
|
||
controller?.dismiss()
|
||
}
|
||
}
|
||
disposables.set((signal
|
||
|> deliverOnMainQueue).start(error: { _ in
|
||
guard let _ = self else {
|
||
return
|
||
}
|
||
}, completed: {
|
||
if strongSelf.selectPollOptionFeedback == nil {
|
||
strongSelf.selectPollOptionFeedback = HapticFeedback()
|
||
}
|
||
strongSelf.selectPollOptionFeedback?.success()
|
||
}), forKey: id)
|
||
}, requestStopPollInMessage: { [weak self] id in
|
||
guard let strongSelf = self, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) else {
|
||
return
|
||
}
|
||
|
||
var maybePoll: TelegramMediaPoll?
|
||
for media in message.media {
|
||
if let poll = media as? TelegramMediaPoll {
|
||
maybePoll = poll
|
||
break
|
||
}
|
||
}
|
||
|
||
guard let poll = maybePoll else {
|
||
return
|
||
}
|
||
|
||
let actionTitle: String
|
||
let actionButtonText: String
|
||
switch poll.kind {
|
||
case .poll:
|
||
actionTitle = strongSelf.presentationData.strings.Conversation_StopPollConfirmationTitle
|
||
actionButtonText = strongSelf.presentationData.strings.Conversation_StopPollConfirmation
|
||
case .quiz:
|
||
actionTitle = strongSelf.presentationData.strings.Conversation_StopQuizConfirmationTitle
|
||
actionButtonText = strongSelf.presentationData.strings.Conversation_StopQuizConfirmation
|
||
}
|
||
|
||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
|
||
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
||
ActionSheetTextItem(title: actionTitle),
|
||
ActionSheetButtonItem(title: actionButtonText, color: .destructive, action: { [weak self, weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
let disposables: DisposableDict<MessageId>
|
||
if let current = strongSelf.selectMessagePollOptionDisposables {
|
||
disposables = current
|
||
} else {
|
||
disposables = DisposableDict()
|
||
strongSelf.selectMessagePollOptionDisposables = disposables
|
||
}
|
||
let controller = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: nil))
|
||
strongSelf.present(controller, in: .window(.root))
|
||
let signal = requestClosePoll(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, stateManager: strongSelf.context.account.stateManager, messageId: id)
|
||
|> afterDisposed { [weak controller] in
|
||
Queue.mainQueue().async {
|
||
controller?.dismiss()
|
||
}
|
||
}
|
||
disposables.set((signal
|
||
|> deliverOnMainQueue).start(error: { _ in
|
||
}, completed: {
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
if strongSelf.selectPollOptionFeedback == nil {
|
||
strongSelf.selectPollOptionFeedback = HapticFeedback()
|
||
}
|
||
strongSelf.selectPollOptionFeedback?.success()
|
||
}), forKey: id)
|
||
})
|
||
]), ActionSheetItemGroup(items: [
|
||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
})
|
||
])])
|
||
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
strongSelf.present(actionSheet, in: .window(.root))
|
||
}, updateInputLanguage: { [weak self] f in
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
return $0.updatedInterfaceState({ $0.withUpdatedInputLanguage(f($0.inputLanguage)) })
|
||
})
|
||
}
|
||
}, unarchiveChat: { [weak self] in
|
||
guard let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation else {
|
||
return
|
||
}
|
||
strongSelf.updateChatPresentationInterfaceState(interactive: true, { state in
|
||
return state.updatedTitlePanelContext({
|
||
$0.filter({ item in
|
||
if case .chatInfo = item {
|
||
return false
|
||
} else {
|
||
return true
|
||
}
|
||
})
|
||
})
|
||
})
|
||
let _ = (strongSelf.context.account.postbox.transaction { transaction -> Void in
|
||
updatePeerGroupIdInteractively(transaction: transaction, peerId: peerId, groupId: .root)
|
||
}
|
||
|> deliverOnMainQueue).start()
|
||
}, openLinkEditing: { [weak self] in
|
||
if let strongSelf = self {
|
||
var selectionRange: Range<Int>?
|
||
var text: String?
|
||
var inputMode: ChatInputMode?
|
||
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in
|
||
selectionRange = state.interfaceState.effectiveInputState.selectionRange
|
||
if let selectionRange = selectionRange {
|
||
text = state.interfaceState.effectiveInputState.inputText.attributedSubstring(from: NSRange(location: selectionRange.startIndex, length: selectionRange.count)).string
|
||
}
|
||
inputMode = state.inputMode
|
||
return state
|
||
})
|
||
|
||
let controller = chatTextLinkEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, text: text ?? "", link: nil, apply: { [weak self] link in
|
||
if let strongSelf = self, let inputMode = inputMode, let selectionRange = selectionRange {
|
||
if let link = link {
|
||
strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
||
return (chatTextInputAddLinkAttribute(current, url: link), inputMode)
|
||
}
|
||
} else {
|
||
|
||
}
|
||
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, {
|
||
return $0.updatedInputMode({ _ in return inputMode }).updatedInterfaceState({
|
||
$0.withUpdatedEffectiveInputState(ChatTextInputState(inputText: $0.effectiveInputState.inputText, selectionRange: selectionRange.endIndex ..< selectionRange.endIndex))
|
||
})
|
||
})
|
||
}
|
||
})
|
||
strongSelf.present(controller, in: .window(.root))
|
||
|
||
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { $0.updatedInputMode({ _ in return .none }) })
|
||
}
|
||
}, reportPeerIrrelevantGeoLocation: { [weak self] in
|
||
guard let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation else {
|
||
return
|
||
}
|
||
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
|
||
let actions = [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
|
||
}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.ReportGroupLocation_Report, action: { [weak self] in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.reportIrrelvantGeoDisposable = (TelegramCore.reportPeer(account: strongSelf.context.account, peerId: peerId, reason: .irrelevantLocation, message: "")
|
||
|> deliverOnMainQueue).start(completed: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.reportIrrelvantGeoNoticePromise.set(.single(true))
|
||
let _ = ApplicationSpecificNotice.setIrrelevantPeerGeoReport(postbox: strongSelf.context.account.postbox, peerId: peerId).start()
|
||
|
||
if let path = getAppBundle().path(forResource: "PoliceCar", ofType: "tgs") {
|
||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .emoji(path: path, text: strongSelf.presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return false }), in: .current)
|
||
}
|
||
}
|
||
})
|
||
})]
|
||
strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.ReportGroupLocation_Title, text: strongSelf.presentationData.strings.ReportGroupLocation_Text, actions: actions), in: .window(.root))
|
||
}, displaySlowmodeTooltip: { [weak self] node, nodeRect in
|
||
guard let strongSelf = self, let slowmodeState = strongSelf.presentationInterfaceState.slowmodeState else {
|
||
return
|
||
}
|
||
let rect = node.view.convert(nodeRect, to: strongSelf.view)
|
||
if let slowmodeTooltipController = strongSelf.slowmodeTooltipController {
|
||
if let arguments = slowmodeTooltipController.presentationArguments as? TooltipControllerPresentationArguments, case let .node(f) = arguments.sourceAndRect, let (previousNode, previousRect) = f() {
|
||
if previousNode === strongSelf.chatDisplayNode && previousRect == rect {
|
||
return
|
||
}
|
||
}
|
||
|
||
strongSelf.slowmodeTooltipController = nil
|
||
slowmodeTooltipController.dismiss()
|
||
}
|
||
let slowmodeTooltipController = ChatSlowmodeHintController(presentationData: strongSelf.presentationData, slowmodeState:
|
||
slowmodeState)
|
||
slowmodeTooltipController.presentationArguments = TooltipControllerPresentationArguments(sourceNodeAndRect: {
|
||
if let strongSelf = self {
|
||
return (strongSelf.chatDisplayNode, rect)
|
||
}
|
||
return nil
|
||
})
|
||
strongSelf.slowmodeTooltipController = slowmodeTooltipController
|
||
|
||
strongSelf.window?.presentInGlobalOverlay(slowmodeTooltipController)
|
||
}, displaySendMessageOptions: { [weak self] node, gesture in
|
||
if let strongSelf = self, let textInputNode = strongSelf.chatDisplayNode.textInputNode(), let layout = strongSelf.validLayout {
|
||
let previousSupportedOrientations = strongSelf.supportedOrientations
|
||
if layout.size.width > layout.size.height {
|
||
strongSelf.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .landscape)
|
||
} else {
|
||
strongSelf.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
||
}
|
||
|
||
let _ = ApplicationSpecificNotice.incrementChatMessageOptionsTip(accountManager: strongSelf.context.sharedContext.accountManager, count: 4).start()
|
||
|
||
let controller = ChatSendMessageActionSheetController(context: strongSelf.context, controllerInteraction: strongSelf.controllerInteraction, interfaceState: strongSelf.presentationInterfaceState, gesture: gesture, sendButtonFrame: node.view.convert(node.bounds, to: nil), textInputNode: textInputNode, completion: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.supportedOrientations = previousSupportedOrientations
|
||
}
|
||
})
|
||
strongSelf.sendMessageActionsController = controller
|
||
if layout.isNonExclusive {
|
||
strongSelf.present(controller, in: .window(.root))
|
||
} else {
|
||
strongSelf.presentInGlobalOverlay(controller, with: nil)
|
||
}
|
||
}
|
||
}, openScheduledMessages: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.openScheduledMessages()
|
||
}
|
||
}, openPeersNearby: { [weak self] in
|
||
if let strongSelf = self {
|
||
let controller = strongSelf.context.sharedContext.makePeersNearbyController(context: strongSelf.context)
|
||
controller.navigationPresentation = .master
|
||
strongSelf.effectiveNavigationController?.pushViewController(controller, animated: true, completion: { })
|
||
}
|
||
}, displaySearchResultsTooltip: { [weak self] node, nodeRect in
|
||
if let strongSelf = self {
|
||
strongSelf.searchResultsTooltipController?.dismiss()
|
||
let tooltipController = TooltipController(content: .text(strongSelf.presentationData.strings.ChatSearch_ResultsTooltip), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true)
|
||
strongSelf.searchResultsTooltipController = tooltipController
|
||
tooltipController.dismissed = { [weak tooltipController] _ in
|
||
if let strongSelf = self, let tooltipController = tooltipController, strongSelf.searchResultsTooltipController === tooltipController {
|
||
strongSelf.searchResultsTooltipController = nil
|
||
}
|
||
}
|
||
strongSelf.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: {
|
||
if let strongSelf = self {
|
||
var rect = node.view.convert(node.view.bounds, to: strongSelf.chatDisplayNode.view)
|
||
rect = CGRect(origin: rect.origin.offsetBy(dx: nodeRect.minX, dy: nodeRect.minY - node.bounds.minY), size: nodeRect.size)
|
||
return (strongSelf.chatDisplayNode, rect)
|
||
}
|
||
return nil
|
||
}))
|
||
}
|
||
}, unarchivePeer: { [weak self] in
|
||
guard let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation else {
|
||
return
|
||
}
|
||
unarchiveAutomaticallyArchivedPeer(account: strongSelf.context.account, peerId: peerId)
|
||
|
||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .succeed(text: strongSelf.presentationData.strings.Conversation_UnarchiveDone), elevatedLayout: false, action: { _ in return false }), in: .current)
|
||
}, scrollToTop: { [weak self] in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
|
||
strongSelf.chatDisplayNode.historyNode.scrollToStartOfHistory()
|
||
}, viewReplies: { [weak self] sourceMessageId, replyThreadResult in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
|
||
if let navigationController = strongSelf.effectiveNavigationController {
|
||
let subject: ChatControllerSubject? = sourceMessageId.flatMap { ChatControllerSubject.message(id: $0, highlight: true) }
|
||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .replyThread(replyThreadResult), subject: subject, keepStack: .always))
|
||
}
|
||
}, activatePinnedListPreview: { [weak self] node, gesture in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
guard case let .peer(peerId) = strongSelf.chatLocation else {
|
||
return
|
||
}
|
||
guard let pinnedMessage = strongSelf.presentationInterfaceState.pinnedMessage else {
|
||
return
|
||
}
|
||
let count = pinnedMessage.totalCount
|
||
let topMessageId = pinnedMessage.topMessageId
|
||
|
||
var items: [ContextMenuItem] = []
|
||
|
||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Chat_PinnedListPreview_ShowAllMessages, icon: { theme in
|
||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/PinnedList"), color: theme.contextMenu.primaryColor)
|
||
}, action: { [weak self] _, f in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.openPinnedMessages(at: nil)
|
||
f(.dismissWithoutContent)
|
||
})))
|
||
|
||
if strongSelf.canManagePin() {
|
||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Chat_PinnedListPreview_UnpinAllMessages, icon: { theme in
|
||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unpin"), color: theme.contextMenu.primaryColor)
|
||
}, action: { [weak self] _, f in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.performRequestedUnpinAllMessages(count: count, pinnedMessageId: topMessageId)
|
||
f(.dismissWithoutContent)
|
||
})))
|
||
} else {
|
||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Chat_PinnedListPreview_HidePinnedMessages, icon: { theme in
|
||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unpin"), color: theme.contextMenu.primaryColor)
|
||
}, action: { [weak self] _, f in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
|
||
strongSelf.performUpdatedClosedPinnedMessageId(pinnedMessageId: topMessageId)
|
||
f(.dismissWithoutContent)
|
||
})))
|
||
}
|
||
|
||
let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peerId), subject: .pinnedMessages(id: pinnedMessage.message.id), botStart: nil, mode: .standard(previewing: true))
|
||
chatController.canReadHistory.set(false)
|
||
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, passthroughTouches: true)), items: .single(items), reactionItems: [], gesture: gesture)
|
||
strongSelf.presentInGlobalOverlay(contextController)
|
||
}, joinGroupCall: { [weak self] activeCall in
|
||
guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
|
||
return
|
||
}
|
||
strongSelf.context.joinGroupCall(peerId: peer.id, activeCall: activeCall)
|
||
}, presentInviteMembers: { [weak self] in
|
||
guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
|
||
return
|
||
}
|
||
if !(peer is TelegramGroup || peer is TelegramChannel) {
|
||
return
|
||
}
|
||
presentAddMembers(context: strongSelf.context, parentController: strongSelf, groupPeer: peer, selectAddMemberDisposable: strongSelf.selectAddMemberDisposable, addMemberDisposable: strongSelf.addMemberDisposable)
|
||
}, editMessageMedia: { [weak self] messageId, draw in
|
||
if let strongSelf = self {
|
||
strongSelf.controllerInteraction?.editMessageMedia(messageId, draw)
|
||
}
|
||
}, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get(), searching: self.searching.get(), loadingMessage: self.loadingMessage.get(), inlineSearch: self.performingInlineSearch.get()))
|
||
|
||
do {
|
||
let peerId = self.chatLocation.peerId
|
||
if let subject = self.subject, case .scheduledMessages = subject {
|
||
} else {
|
||
if case .peer = self.chatLocation {
|
||
let unreadCountsKey: PostboxViewKey = .unreadCounts(items: [.peer(peerId), .total(nil)])
|
||
let notificationSettingsKey: PostboxViewKey = .peerNotificationSettings(peerIds: Set([peerId]))
|
||
self.chatUnreadCountDisposable = (self.context.account.postbox.combinedView(keys: [unreadCountsKey, notificationSettingsKey])
|
||
|> deliverOnMainQueue).start(next: { [weak self] views in
|
||
if let strongSelf = self {
|
||
var unreadCount: Int32 = 0
|
||
var totalChatCount: Int32 = 0
|
||
|
||
let inAppSettings = strongSelf.context.sharedContext.currentInAppNotificationSettings.with { $0 }
|
||
if let view = views.views[unreadCountsKey] as? UnreadMessageCountsView {
|
||
if let count = view.count(for: .peer(peerId)) {
|
||
unreadCount = count
|
||
}
|
||
if let (_, state) = view.total() {
|
||
let (count, _) = renderedTotalUnreadCount(inAppSettings: inAppSettings, totalUnreadState: state)
|
||
totalChatCount = count
|
||
}
|
||
}
|
||
|
||
strongSelf.chatDisplayNode.navigateButtons.unreadCount = unreadCount
|
||
|
||
if let view = views.views[notificationSettingsKey] as? PeerNotificationSettingsView, let notificationSettings = view.notificationSettings[peerId] {
|
||
var globalRemainingUnreadChatCount = totalChatCount
|
||
if !notificationSettings.isRemovedFromTotalUnreadCount(default: false) && unreadCount > 0 {
|
||
if case .messages = inAppSettings.totalUnreadCountDisplayCategory {
|
||
globalRemainingUnreadChatCount -= unreadCount
|
||
} else {
|
||
globalRemainingUnreadChatCount -= 1
|
||
}
|
||
}
|
||
|
||
if globalRemainingUnreadChatCount > 0 {
|
||
strongSelf.navigationItem.badge = "\(globalRemainingUnreadChatCount)"
|
||
} else {
|
||
strongSelf.navigationItem.badge = ""
|
||
}
|
||
}
|
||
}
|
||
})
|
||
|
||
self.chatUnreadMentionCountDisposable = (self.context.account.viewTracker.unseenPersonalMessagesCount(peerId: peerId) |> deliverOnMainQueue).start(next: { [weak self] count in
|
||
if let strongSelf = self {
|
||
if case let .standard(previewing) = strongSelf.presentationInterfaceState.mode, previewing {
|
||
strongSelf.chatDisplayNode.navigateButtons.mentionCount = 0
|
||
} else {
|
||
strongSelf.chatDisplayNode.navigateButtons.mentionCount = count
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
let postbox = self.context.account.postbox
|
||
let previousPeerCache = Atomic<[PeerId: Peer]>(value: [:])
|
||
var activityCategory: PeerActivitySpace.Category = .global
|
||
if case let .replyThread(replyThreadMessage) = self.chatLocation {
|
||
activityCategory = .thread(makeMessageThreadId(replyThreadMessage.messageId))
|
||
}
|
||
self.peerInputActivitiesDisposable = (self.context.account.peerInputActivities(peerId: PeerActivitySpace(peerId: peerId, category: activityCategory))
|
||
|> 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.transaction { transaction -> [(Peer, PeerInputActivity)] in
|
||
var result: [(Peer, PeerInputActivity)] = []
|
||
var peerCache: [PeerId: Peer] = [:]
|
||
for (peerId, activity) in activities {
|
||
if let peer = transaction.getPeer(peerId) {
|
||
result.append((peer, activity))
|
||
peerCache[peerId] = peer
|
||
}
|
||
}
|
||
let _ = previousPeerCache.swap(peerCache)
|
||
return result
|
||
}
|
||
}
|
||
}
|
||
|> deliverOnMainQueue).start(next: { [weak self] activities in
|
||
if let strongSelf = self {
|
||
strongSelf.chatTitleView?.inputActivities = (peerId, activities)
|
||
}
|
||
})
|
||
}
|
||
|
||
self.sentMessageEventsDisposable.set((self.context.account.pendingMessageManager.deliveredMessageEvents(peerId: peerId)
|
||
|> deliverOnMainQueue).start(next: { [weak self] namespace in
|
||
if let strongSelf = self {
|
||
let inAppNotificationSettings = strongSelf.context.sharedContext.currentInAppNotificationSettings.with { $0 }
|
||
if inAppNotificationSettings.playSounds {
|
||
serviceSoundManager.playMessageDeliveredSound()
|
||
}
|
||
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && namespace == Namespaces.Message.ScheduledCloud {
|
||
strongSelf.openScheduledMessages()
|
||
}
|
||
|
||
if strongSelf.shouldDisplayChecksTooltip {
|
||
Queue.mainQueue().after(1.0) {
|
||
strongSelf.displayChecksTooltip()
|
||
}
|
||
strongSelf.shouldDisplayChecksTooltip = false
|
||
strongSelf.checksTooltipDisposable.set(dismissServerProvidedSuggestion(account: strongSelf.context.account, suggestion: .newcomerTicks).start())
|
||
}
|
||
}
|
||
}))
|
||
|
||
self.failedMessageEventsDisposable.set((self.context.account.pendingMessageManager.failedMessageEvents(peerId: peerId)
|
||
|> deliverOnMainQueue).start(next: { [weak self] reason in
|
||
if let strongSelf = self, strongSelf.currentFailedMessagesAlertController == nil {
|
||
let text: String
|
||
let moreInfo: Bool
|
||
switch reason {
|
||
case .flood:
|
||
text = strongSelf.presentationData.strings.Conversation_SendMessageErrorFlood
|
||
moreInfo = true
|
||
case .publicBan:
|
||
text = strongSelf.presentationData.strings.Conversation_SendMessageErrorGroupRestricted
|
||
moreInfo = true
|
||
case .mediaRestricted:
|
||
strongSelf.interfaceInteraction?.displayRestrictedInfo(.mediaRecording, .alert)
|
||
return
|
||
case .slowmodeActive:
|
||
text = strongSelf.presentationData.strings.Chat_SlowmodeSendError
|
||
moreInfo = false
|
||
case .tooMuchScheduled:
|
||
text = strongSelf.presentationData.strings.Conversation_SendMessageErrorTooMuchScheduled
|
||
moreInfo = false
|
||
}
|
||
let actions: [TextAlertAction]
|
||
if moreInfo {
|
||
actions = [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Generic_ErrorMoreInfo, action: {
|
||
self?.openPeerMention("spambot", navigation: .chat(textInputState: nil, subject: nil, peekData: nil))
|
||
}), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]
|
||
} else {
|
||
actions = [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]
|
||
}
|
||
let controller = textAlertController(context: strongSelf.context, title: nil, text: text, actions: actions)
|
||
strongSelf.currentFailedMessagesAlertController = controller
|
||
strongSelf.present(controller, in: .window(.root))
|
||
}
|
||
}))
|
||
}
|
||
|
||
self.chatDisplayNode.updateHasEmbeddedTitleContent = { [weak self] in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
|
||
let hasEmbeddedTitleContent = strongSelf.chatDisplayNode.hasEmbeddedTitleContent
|
||
let isEmbeddedTitleContentHidden = strongSelf.chatDisplayNode.isEmbeddedTitleContentHidden
|
||
|
||
if strongSelf.hasEmbeddedTitleContent != hasEmbeddedTitleContent {
|
||
strongSelf.hasEmbeddedTitleContent = hasEmbeddedTitleContent
|
||
|
||
if strongSelf.hasEmbeddedTitleContent {
|
||
strongSelf.statusBar.statusBarStyle = .White
|
||
} else {
|
||
strongSelf.statusBar.statusBarStyle = strongSelf.presentationData.theme.rootController.statusBarStyle.style
|
||
}
|
||
|
||
if let navigationBar = strongSelf.navigationBar {
|
||
if let navigationBarCopy = navigationBar.view.snapshotContentTree() {
|
||
navigationBar.view.superview?.insertSubview(navigationBarCopy, aboveSubview: navigationBar.view)
|
||
navigationBarCopy.alpha = 0.0
|
||
navigationBarCopy.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak navigationBarCopy] _ in
|
||
navigationBarCopy?.removeFromSuperview()
|
||
})
|
||
}
|
||
}
|
||
strongSelf.updateNavigationBarPresentation()
|
||
}
|
||
|
||
if strongSelf.isEmbeddedTitleContentHidden != isEmbeddedTitleContentHidden {
|
||
strongSelf.isEmbeddedTitleContentHidden = isEmbeddedTitleContentHidden
|
||
|
||
if let navigationBar = strongSelf.navigationBar {
|
||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||
transition.updateAlpha(node: navigationBar, alpha: isEmbeddedTitleContentHidden ? 0.0 : 1.0)
|
||
}
|
||
}
|
||
}
|
||
|
||
self.interfaceInteraction = interfaceInteraction
|
||
|
||
if let search = self.focusOnSearchAfterAppearance {
|
||
self.focusOnSearchAfterAppearance = nil
|
||
self.interfaceInteraction?.beginMessageSearch(search.0, search.1)
|
||
}
|
||
|
||
self.chatDisplayNode.interfaceInteraction = interfaceInteraction
|
||
|
||
self.context.sharedContext.mediaManager.galleryHiddenMediaManager.addTarget(self)
|
||
self.galleryHiddenMesageAndMediaDisposable.set(self.context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().start(next: { [weak self] ids in
|
||
if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction {
|
||
var messageIdAndMedia: [MessageId: [Media]] = [:]
|
||
|
||
for id in ids {
|
||
if case let .chat(accountId, messageId, media) = id, accountId == strongSelf.context.account.id {
|
||
messageIdAndMedia[messageId] = [media]
|
||
}
|
||
}
|
||
|
||
controllerInteraction.hiddenMedia = messageIdAndMedia
|
||
|
||
strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in
|
||
if let itemNode = itemNode as? ChatMessageItemView {
|
||
itemNode.updateHiddenMedia()
|
||
}
|
||
}
|
||
}
|
||
}))
|
||
|
||
self.chatDisplayNode.dismissAsOverlay = { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.statusBar.statusBarStyle = .Ignore
|
||
strongSelf.chatDisplayNode.animateDismissAsOverlay(completion: {
|
||
self?.dismiss()
|
||
})
|
||
}
|
||
}
|
||
|
||
let hasActiveCalls: Signal<Bool, NoError>
|
||
if let callManager = self.context.sharedContext.callManager as? PresentationCallManagerImpl {
|
||
hasActiveCalls = callManager.hasActiveCalls
|
||
|
||
self.hasActiveGroupCallDisposable = ((callManager.currentGroupCallSignal
|
||
|> map { call -> Bool in
|
||
return call != nil
|
||
}) |> deliverOnMainQueue).start(next: { [weak self] hasActiveGroupCall in
|
||
self?.updateChatPresentationInterfaceState(animated: true, interactive: false, { state in
|
||
return state.updatedHasActiveGroupCall(hasActiveGroupCall)
|
||
})
|
||
})
|
||
} else {
|
||
hasActiveCalls = .single(false)
|
||
}
|
||
|
||
let shouldBeActive = combineLatest(self.context.sharedContext.mediaManager.audioSession.isPlaybackActive() |> deliverOnMainQueue, self.chatDisplayNode.historyNode.hasVisiblePlayableItemNodes, hasActiveCalls)
|
||
|> mapToSignal { [weak self] isPlaybackActive, hasVisiblePlayableItemNodes, hasActiveCalls -> Signal<Bool, NoError> in
|
||
if hasVisiblePlayableItemNodes && !isPlaybackActive && !hasActiveCalls {
|
||
return Signal<Bool, NoError> { [weak self] subscriber in
|
||
guard let strongSelf = self else {
|
||
subscriber.putCompletion()
|
||
return EmptyDisposable
|
||
}
|
||
|
||
subscriber.putNext(strongSelf.traceVisibility() && isTopmostChatController(strongSelf) && !strongSelf.context.sharedContext.mediaManager.audioSession.isOtherAudioPlaying())
|
||
subscriber.putCompletion()
|
||
return EmptyDisposable
|
||
} |> then(.complete() |> delay(1.0, queue: Queue.mainQueue())) |> restart
|
||
} else {
|
||
return .single(false)
|
||
}
|
||
}
|
||
|
||
self.volumeButtonsListener = VolumeButtonsListener(shouldBeActive: shouldBeActive, valueChanged: { [weak self] in
|
||
guard let strongSelf = self, strongSelf.traceVisibility() && isTopmostChatController(strongSelf) else {
|
||
return
|
||
}
|
||
strongSelf.videoUnmuteTooltipController?.dismiss()
|
||
|
||
var actions: [(Bool, (Double?) -> Void)] = []
|
||
var hasUnconsumed = false
|
||
strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
|
||
if let itemNode = itemNode as? ChatMessageItemView, let (action, _, _, isUnconsumed, _) = itemNode.playMediaWithSound() {
|
||
if case let .visible(fraction) = itemNode.visibility, fraction > 0.7 {
|
||
actions.insert((isUnconsumed, action), at: 0)
|
||
if !hasUnconsumed && isUnconsumed {
|
||
hasUnconsumed = true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
for (isUnconsumed, action) in actions {
|
||
if (!hasUnconsumed || isUnconsumed) {
|
||
action(nil)
|
||
break
|
||
}
|
||
}
|
||
})
|
||
|
||
self.displayNodeDidLoad()
|
||
}
|
||
|
||
override public func viewWillAppear(_ animated: Bool) {
|
||
super.viewWillAppear(animated)
|
||
|
||
if self.willAppear {
|
||
self.chatDisplayNode.historyNode.refreshPollActionsForVisibleMessages()
|
||
} else {
|
||
self.willAppear = true
|
||
|
||
// Limit this to reply threads just to be safe now
|
||
if case .replyThread = self.chatLocation {
|
||
self.chatDisplayNode.historyNode.refocusOnUnreadMessagesIfNeeded()
|
||
}
|
||
}
|
||
|
||
if self.scheduledActivateInput {
|
||
self.scheduledActivateInput = false
|
||
|
||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
|
||
return state.updatedInputMode({ _ in .text })
|
||
})
|
||
}
|
||
}
|
||
|
||
override public func viewDidAppear(_ animated: Bool) {
|
||
super.viewDidAppear(animated)
|
||
|
||
self.didAppear = true
|
||
|
||
self.chatDisplayNode.historyNode.preloadPages = true
|
||
self.chatDisplayNode.historyNode.experimentalSnapScrollToItem = false
|
||
self.chatDisplayNode.historyNode.canReadHistory.set(combineLatest(context.sharedContext.applicationBindings.applicationInForeground, self.canReadHistory.get()) |> map { a, b in
|
||
return a && b
|
||
})
|
||
|
||
self.chatDisplayNode.loadInputPanels(theme: self.presentationInterfaceState.theme, strings: self.presentationInterfaceState.strings, fontSize: self.presentationInterfaceState.fontSize)
|
||
|
||
self.recentlyUsedInlineBotsDisposable = (recentlyUsedInlineBots(postbox: self.context.account.postbox) |> deliverOnMainQueue).start(next: { [weak self] peers in
|
||
self?.recentlyUsedInlineBotsValue = peers.filter({ $0.1 >= 0.14 }).map({ $0.0 })
|
||
})
|
||
|
||
if case .standard(false) = self.presentationInterfaceState.mode, self.raiseToListen == nil {
|
||
self.raiseToListen = RaiseToListenManager(shouldActivate: { [weak self] in
|
||
if let strongSelf = self, strongSelf.isNodeLoaded && strongSelf.canReadHistoryValue, strongSelf.presentationInterfaceState.interfaceState.editMessage == nil, strongSelf.playlistStateAndType == nil {
|
||
if strongSelf.presentationInterfaceState.inputTextPanelState.mediaRecordingState != nil {
|
||
return false
|
||
}
|
||
|
||
if !strongSelf.traceVisibility() {
|
||
return false
|
||
}
|
||
|
||
if !isTopmostChatController(strongSelf) {
|
||
return false
|
||
}
|
||
|
||
if strongSelf.firstLoadedMessageToListen() != nil || strongSelf.chatDisplayNode.isTextInputPanelActive {
|
||
if strongSelf.context.sharedContext.immediateHasOngoingCall {
|
||
return false
|
||
}
|
||
|
||
if case let .media(_, expanded) = strongSelf.presentationInterfaceState.inputMode, expanded != nil {
|
||
return false
|
||
}
|
||
|
||
if !strongSelf.context.sharedContext.currentMediaInputSettings.with({ $0.enableRaiseToSpeak }) {
|
||
return false
|
||
}
|
||
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}, activate: { [weak self] in
|
||
self?.activateRaiseGesture()
|
||
}, deactivate: { [weak self] in
|
||
self?.deactivateRaiseGesture()
|
||
})
|
||
self.raiseToListen?.enabled = self.canReadHistoryValue
|
||
self.tempVoicePlaylistEnded = { [weak self] in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
if !canSendMessagesToChat(strongSelf.presentationInterfaceState) {
|
||
return
|
||
}
|
||
|
||
if let raiseToListen = strongSelf.raiseToListen {
|
||
strongSelf.voicePlaylistDidEndTimestamp = CACurrentMediaTime()
|
||
raiseToListen.activateBasedOnProximity(delay: 0.0)
|
||
}
|
||
}
|
||
self.tempVoicePlaylistItemChanged = { [weak self] previousItem, currentItem in
|
||
guard let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation else {
|
||
return
|
||
}
|
||
if let currentItem = currentItem?.id as? PeerMessagesMediaPlaylistItemId, let previousItem = previousItem?.id as? PeerMessagesMediaPlaylistItemId, previousItem.messageId.peerId == peerId, currentItem.messageId.peerId == peerId, currentItem.messageId != previousItem.messageId {
|
||
if strongSelf.chatDisplayNode.historyNode.isMessageVisibleOnScreen(currentItem.messageId) {
|
||
strongSelf.navigateToMessage(from: nil, to: .id(currentItem.messageId), scrollPosition: .center(.bottom), rememberInStack: false, animated: true, completion: nil)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if let arguments = self.presentationArguments as? ChatControllerOverlayPresentationData {
|
||
//TODO clear arguments
|
||
self.chatDisplayNode.animateInAsOverlay(from: arguments.expandData.0, completion: {
|
||
arguments.expandData.1()
|
||
})
|
||
}
|
||
|
||
if !self.didSetup3dTouch {
|
||
self.didSetup3dTouch = true
|
||
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||
let dropInteraction = UIDropInteraction(delegate: self)
|
||
self.chatDisplayNode.view.addInteraction(dropInteraction)
|
||
}
|
||
}
|
||
|
||
if !self.checkedPeerChatServiceActions {
|
||
self.checkedPeerChatServiceActions = true
|
||
|
||
if case let .peer(peerId) = self.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat, self.screenCaptureManager == nil {
|
||
self.screenCaptureManager = ScreenCaptureDetectionManager(check: { [weak self] in
|
||
if let strongSelf = self, strongSelf.canReadHistoryValue, strongSelf.traceVisibility() {
|
||
let _ = addSecretChatMessageScreenshot(account: strongSelf.context.account, peerId: peerId).start()
|
||
return true
|
||
} else {
|
||
return false
|
||
}
|
||
})
|
||
}
|
||
|
||
if case let .peer(peerId) = self.chatLocation {
|
||
let _ = checkPeerChatServiceActions(postbox: self.context.account.postbox, peerId: peerId).start()
|
||
}
|
||
|
||
if self.chatDisplayNode.frameForInputActionButton() != nil {
|
||
let inputText = self.presentationInterfaceState.interfaceState.effectiveInputState.inputText.string
|
||
if !inputText.isEmpty {
|
||
if inputText.count > 4 {
|
||
let _ = (ApplicationSpecificNotice.getChatMessageOptionsTip(accountManager: self.context.sharedContext.accountManager)
|
||
|> deliverOnMainQueue).start(next: { [weak self] counter in
|
||
if let strongSelf = self, counter < 3 {
|
||
let _ = ApplicationSpecificNotice.incrementChatMessageOptionsTip(accountManager: strongSelf.context.sharedContext.accountManager).start()
|
||
strongSelf.displaySendingOptionsTooltip()
|
||
}
|
||
})
|
||
}
|
||
} else if self.presentationInterfaceState.interfaceState.mediaRecordingMode == .audio {
|
||
var canSendMedia = false
|
||
if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel {
|
||
if channel.hasBannedPermission(.banSendMedia) == nil {
|
||
canSendMedia = true
|
||
}
|
||
} else if let group = self.presentationInterfaceState.renderedPeer?.peer as? TelegramGroup {
|
||
if !group.hasBannedPermission(.banSendMedia) {
|
||
canSendMedia = true
|
||
}
|
||
} else {
|
||
canSendMedia = true
|
||
}
|
||
if canSendMedia {
|
||
let _ = (ApplicationSpecificNotice.getChatMediaMediaRecordingTips(accountManager: self.context.sharedContext.accountManager)
|
||
|> deliverOnMainQueue).start(next: { [weak self] counter in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
var displayTip = false
|
||
if counter == 0 {
|
||
displayTip = true
|
||
} else if counter < 3 && arc4random_uniform(4) == 1 {
|
||
displayTip = true
|
||
}
|
||
if displayTip {
|
||
let _ = ApplicationSpecificNotice.incrementChatMediaMediaRecordingTips(accountManager: strongSelf.context.sharedContext.accountManager).start()
|
||
strongSelf.displayMediaRecordingTooltip()
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
self.editMessageErrorsDisposable.set((self.context.account.pendingUpdateMessageManager.errors
|
||
|> deliverOnMainQueue).start(next: { [weak self] (_, error) in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
|
||
let text: String
|
||
switch error {
|
||
case .generic, .textTooLong, .invalidGrouping:
|
||
text = strongSelf.presentationData.strings.Channel_EditMessageErrorGeneric
|
||
case .restricted:
|
||
text = strongSelf.presentationData.strings.Group_ErrorSendRestrictedMedia
|
||
}
|
||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
|
||
})]), in: .window(.root))
|
||
}))
|
||
|
||
if case let .peer(peerId) = self.chatLocation {
|
||
let context = self.context
|
||
self.keepPeerInfoScreenDataHotDisposable.set(keepPeerInfoScreenDataHot(context: context, peerId: peerId).start())
|
||
|
||
if peerId.namespace == Namespaces.Peer.CloudUser {
|
||
self.preloadAvatarDisposable.set((peerInfoProfilePhotosWithCache(context: context, peerId: peerId)
|
||
|> mapToSignal { (complete, result) -> Signal<Never, NoError> in
|
||
var signals: [Signal<Never, NoError>] = [.complete()]
|
||
for i in 0 ..< min(1, result.count) {
|
||
if let video = result[i].videoRepresentations.first {
|
||
let duration: Double = (video.representation.startTimestamp ?? 0.0) + (i == 0 ? 4.0 : 2.0)
|
||
signals.append(preloadVideoResource(postbox: context.account.postbox, resourceReference: video.reference, duration: duration))
|
||
}
|
||
}
|
||
return combineLatest(signals) |> mapToSignal { _ in
|
||
return .never()
|
||
}
|
||
}).start())
|
||
}
|
||
}
|
||
}
|
||
|
||
if let _ = self.focusOnSearchAfterAppearance {
|
||
self.focusOnSearchAfterAppearance = nil
|
||
if let searchNode = self.navigationBar?.contentNode as? ChatSearchNavigationContentNode {
|
||
searchNode.activate()
|
||
}
|
||
}
|
||
|
||
if let peekData = self.peekData, case let .peer(peerId) = self.chatLocation {
|
||
let timestamp = Int32(Date().timeIntervalSince1970)
|
||
let remainingTime = max(1, peekData.deadline - timestamp)
|
||
self.peekTimerDisposable.set((
|
||
combineLatest(
|
||
self.context.account.postbox.peerView(id: peerId),
|
||
Signal<Bool, NoError>.single(true)
|
||
|> suspendAwareDelay(Double(remainingTime), granularity: 2.0, queue: .mainQueue())
|
||
)
|
||
|> deliverOnMainQueue
|
||
).start(next: { [weak self] peerView, _ in
|
||
guard let strongSelf = self, let peer = peerViewMainPeer(peerView) else {
|
||
return
|
||
}
|
||
if let peer = peer as? TelegramChannel {
|
||
switch peer.participationStatus {
|
||
case .member:
|
||
return
|
||
default:
|
||
break
|
||
}
|
||
}
|
||
strongSelf.present(textAlertController(
|
||
context: strongSelf.context,
|
||
title: strongSelf.presentationData.strings.Conversation_PrivateChannelTimeLimitedAlertTitle,
|
||
text: strongSelf.presentationData.strings.Conversation_PrivateChannelTimeLimitedAlertText,
|
||
actions: [
|
||
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Conversation_PrivateChannelTimeLimitedAlertJoin, action: {
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.peekTimerDisposable.set(
|
||
(joinChatInteractively(with: peekData.linkData, account: strongSelf.context.account)
|
||
|> deliverOnMainQueue).start(next: { peerId in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
if peerId == nil {
|
||
strongSelf.dismiss()
|
||
}
|
||
}, error: { _ in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.dismiss()
|
||
})
|
||
)
|
||
}),
|
||
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.dismiss()
|
||
})
|
||
],
|
||
actionLayout: .vertical,
|
||
dismissOnOutsideTap: false
|
||
), in: .window(.root))
|
||
}))
|
||
}
|
||
|
||
self.checksTooltipDisposable.set((getServerProvidedSuggestions(postbox: self.context.account.postbox)
|
||
|> deliverOnMainQueue).start(next: { [weak self] values in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
if !values.contains(.newcomerTicks) {
|
||
return
|
||
}
|
||
strongSelf.shouldDisplayChecksTooltip = true
|
||
}))
|
||
|
||
if self.scheduledActivateInput {
|
||
self.scheduledActivateInput = false
|
||
|
||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
|
||
return state.updatedInputMode({ _ in .text })
|
||
})
|
||
}
|
||
}
|
||
|
||
override public func viewWillDisappear(_ animated: Bool) {
|
||
super.viewWillDisappear(animated)
|
||
|
||
self.chatDisplayNode.historyNode.canReadHistory.set(.single(false))
|
||
self.saveInterfaceState()
|
||
|
||
self.dismissAllTooltips()
|
||
|
||
self.sendMessageActionsController?.dismiss()
|
||
|
||
if let _ = self.peekData {
|
||
self.peekTimerDisposable.set(nil)
|
||
}
|
||
}
|
||
|
||
private func saveInterfaceState(includeScrollState: Bool = true) {
|
||
var peerId: PeerId
|
||
var threadId: Int64?
|
||
switch self.chatLocation {
|
||
case let .peer(peerIdValue):
|
||
peerId = peerIdValue
|
||
case let .replyThread(replyThreadMessage):
|
||
peerId = replyThreadMessage.messageId.peerId
|
||
threadId = makeMessageThreadId(replyThreadMessage.messageId)
|
||
}
|
||
|
||
let timestamp = Int32(Date().timeIntervalSince1970)
|
||
var interfaceState = self.presentationInterfaceState.interfaceState.withUpdatedTimestamp(timestamp)
|
||
if includeScrollState && threadId == nil {
|
||
let scrollState = self.chatDisplayNode.historyNode.immediateScrollState()
|
||
interfaceState = interfaceState.withUpdatedHistoryScrollState(scrollState)
|
||
}
|
||
interfaceState = interfaceState.withUpdatedInputLanguage(self.chatDisplayNode.currentTextInputLanguage)
|
||
let _ = updatePeerChatInterfaceState(account: self.context.account, peerId: peerId, threadId: threadId, state: interfaceState).start()
|
||
}
|
||
|
||
override public func viewDidDisappear(_ animated: Bool) {
|
||
super.viewDidDisappear(animated)
|
||
|
||
self.updateChatPresentationInterfaceState(animated: false, interactive: false, {
|
||
$0.updatedTitlePanelContext {
|
||
if let index = $0.firstIndex(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 viewWillLeaveNavigation() {
|
||
self.chatDisplayNode.willNavigateAway()
|
||
}
|
||
|
||
override public func inFocusUpdated(isInFocus: Bool) {
|
||
self.chatDisplayNode.inFocusUpdated(isInFocus: isInFocus)
|
||
}
|
||
|
||
private func canManagePin() -> Bool {
|
||
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
|
||
return false
|
||
}
|
||
|
||
var canManagePin = false
|
||
if let channel = peer as? TelegramChannel {
|
||
canManagePin = channel.hasPermission(.pinMessages)
|
||
} else if let group = peer as? TelegramGroup {
|
||
switch group.role {
|
||
case .creator, .admin:
|
||
canManagePin = true
|
||
default:
|
||
if let defaultBannedRights = group.defaultBannedRights {
|
||
canManagePin = !defaultBannedRights.flags.contains(.banPinMessages)
|
||
} else {
|
||
canManagePin = true
|
||
}
|
||
}
|
||
} else if let _ = peer as? TelegramUser, self.presentationInterfaceState.explicitelyCanPinMessages {
|
||
canManagePin = true
|
||
}
|
||
|
||
return canManagePin
|
||
}
|
||
|
||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||
super.containerLayoutUpdated(layout, transition: transition)
|
||
|
||
self.validLayout = layout
|
||
self.chatTitleView?.layout = layout
|
||
|
||
switch self.presentationInterfaceState.mode {
|
||
case .standard, .inline:
|
||
break
|
||
case .overlay:
|
||
if case .Ignore = self.statusBar.statusBarStyle {
|
||
} else if layout.safeInsets.top.isZero {
|
||
self.statusBar.statusBarStyle = .Hide
|
||
} else {
|
||
self.statusBar.statusBarStyle = .Ignore
|
||
}
|
||
}
|
||
|
||
self.chatDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition, listViewTransaction: { updateSizeAndInsets, additionalScrollDistance, scrollToTop, completion in
|
||
self.chatDisplayNode.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, additionalScrollDistance: additionalScrollDistance, scrollToTop: scrollToTop, completion: completion)
|
||
})
|
||
}
|
||
|
||
override public func updateToInterfaceOrientation(_ orientation: UIInterfaceOrientation) {
|
||
guard let layout = self.validLayout, case .compact = layout.metrics.widthClass else {
|
||
return
|
||
}
|
||
let hasOverlayNodes = self.context.sharedContext.mediaManager.overlayMediaManager.controller?.hasNodes ?? false
|
||
if self.validLayout != nil && orientation.isLandscape && !hasOverlayNodes && self.traceVisibility() && isTopmostChatController(self) {
|
||
var completed = false
|
||
self.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
|
||
if !completed, let itemNode = itemNode as? ChatMessageItemView, let message = itemNode.item?.message, let (_, soundEnabled, _, _, _) = itemNode.playMediaWithSound(), soundEnabled {
|
||
let _ = self.controllerInteraction?.openMessage(message, .landscape)
|
||
completed = true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func updateChatPresentationInterfaceState(animated: Bool = true, interactive: Bool, saveInterfaceState: Bool = false, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) {
|
||
self.updateChatPresentationInterfaceState(transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate, interactive: interactive, saveInterfaceState: saveInterfaceState, f, completion: completion)
|
||
}
|
||
|
||
func updateChatPresentationInterfaceState(transition: ContainedViewLayoutTransition, interactive: Bool, saveInterfaceState: Bool = false, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion externalCompletion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) {
|
||
var completion = externalCompletion
|
||
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.length == 0 && keyboardButtonsMessage.id != temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.closedButtonKeyboardMessageId && temporaryChatPresentationInterfaceState.botStartPayload == nil {
|
||
temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputMode({ _ in
|
||
return .inputButtons
|
||
})
|
||
}
|
||
|
||
if case let .peer(peerId) = self.chatLocation, peerId.namespace == Namespaces.Peer.CloudChannel || peerId.namespace == Namespaces.Peer.CloudGroup {
|
||
if temporaryChatPresentationInterfaceState.interfaceState.replyMessageId == nil && temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.processedSetupReplyMessageId != keyboardButtonsMessage.id {
|
||
temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInterfaceState({ $0.withUpdatedReplyMessageId(keyboardButtonsMessage.id).withUpdatedMessageActionsState({ value in
|
||
var value = value
|
||
value.processedSetupReplyMessageId = keyboardButtonsMessage.id
|
||
return value
|
||
}) })
|
||
}
|
||
}
|
||
} 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({ value in
|
||
var value = value
|
||
value.processedSetupReplyMessageId = keyboardButtonsMessage.id
|
||
return value
|
||
}) })
|
||
}
|
||
}
|
||
|
||
let inputTextPanelState = inputTextPanelStateForChatPresentationInterfaceState(temporaryChatPresentationInterfaceState, context: self.context)
|
||
var updatedChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputTextPanelState({ _ in return inputTextPanelState })
|
||
|
||
let contextQueryUpdates = contextQueryResultStateForChatInterfacePresentationState(updatedChatPresentationInterfaceState, context: self.context, currentQueryStates: &self.contextQueryStates, requestBotLocationStatus: { [weak self] peerId in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
let _ = (ApplicationSpecificNotice.updateInlineBotLocationRequestState(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId, timestamp: Int32(Date().timeIntervalSince1970 + 10 * 60))
|
||
|> deliverOnMainQueue).start(next: { value in
|
||
guard let strongSelf = self, value else {
|
||
return
|
||
}
|
||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Conversation_ShareInlineBotLocationConfirmation, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
|
||
}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
|
||
let _ = ApplicationSpecificNotice.setInlineBotLocationRequest(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId, value: 0).start()
|
||
})]), in: .window(.root))
|
||
})
|
||
})
|
||
|
||
for (kind, update) in contextQueryUpdates {
|
||
switch update {
|
||
case .remove:
|
||
if let (_, disposable) = self.contextQueryStates[kind] {
|
||
disposable.dispose()
|
||
self.contextQueryStates.removeValue(forKey: kind)
|
||
|
||
updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedInputQueryResult(queryKind: kind, { _ in
|
||
return nil
|
||
})
|
||
}
|
||
if case .contextRequest = kind {
|
||
self.performingInlineSearch.set(false)
|
||
}
|
||
case let .update(query, signal):
|
||
let currentQueryAndDisposable = self.contextQueryStates[kind]
|
||
currentQueryAndDisposable?.1.dispose()
|
||
|
||
var inScope = true
|
||
var inScopeResult: ((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?)?
|
||
self.contextQueryStates[kind] = (query, (signal
|
||
|> 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(queryKind: kind, { previousResult in
|
||
return result(previousResult)
|
||
})
|
||
})
|
||
}
|
||
}
|
||
}, error: { [weak self] error in
|
||
if let strongSelf = self {
|
||
if case .contextRequest = kind {
|
||
strongSelf.performingInlineSearch.set(false)
|
||
}
|
||
|
||
switch error {
|
||
case .generic:
|
||
break
|
||
case let .inlineBotLocationRequest(peerId):
|
||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Conversation_ShareInlineBotLocationConfirmation, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
|
||
let _ = ApplicationSpecificNotice.setInlineBotLocationRequest(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId, value: Int32(Date().timeIntervalSince1970 + 10 * 60)).start()
|
||
}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
|
||
let _ = ApplicationSpecificNotice.setInlineBotLocationRequest(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId, value: 0).start()
|
||
})]), in: .window(.root))
|
||
}
|
||
}
|
||
}, completed: { [weak self] in
|
||
if let strongSelf = self {
|
||
if case .contextRequest = kind {
|
||
strongSelf.performingInlineSearch.set(false)
|
||
}
|
||
}
|
||
}))
|
||
inScope = false
|
||
if let inScopeResult = inScopeResult {
|
||
updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedInputQueryResult(queryKind: kind, { previousResult in
|
||
return inScopeResult(previousResult)
|
||
})
|
||
} else {
|
||
if case .contextRequest = kind {
|
||
self.performingInlineSearch.set(true)
|
||
}
|
||
}
|
||
|
||
if case let .peer(peerId) = self.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat {
|
||
if case .contextRequest = query {
|
||
let _ = (ApplicationSpecificNotice.getSecretChatInlineBotUsage(accountManager: self.context.sharedContext.accountManager)
|
||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||
if let strongSelf = self, !value {
|
||
let _ = ApplicationSpecificNotice.setSecretChatInlineBotUsage(accountManager: strongSelf.context.sharedContext.accountManager).start()
|
||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Conversation_SecretChatContextBotAlert, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if let (updatedSearchQuerySuggestionState, updatedSearchQuerySuggestionSignal) = searchQuerySuggestionResultStateForChatInterfacePresentationState(updatedChatPresentationInterfaceState, context: context, currentQuery: self.searchQuerySuggestionState?.0) {
|
||
self.searchQuerySuggestionState?.1.dispose()
|
||
var inScope = true
|
||
var inScopeResult: ((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?)?
|
||
self.searchQuerySuggestionState = (updatedSearchQuerySuggestionState, (updatedSearchQuerySuggestionSignal |> 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.updatedSearchQuerySuggestionResult { previousResult in
|
||
return result(previousResult)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}))
|
||
inScope = false
|
||
if let inScopeResult = inScopeResult {
|
||
updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedSearchQuerySuggestionResult { previousResult in
|
||
return inScopeResult(previousResult)
|
||
}
|
||
}
|
||
}
|
||
|
||
if let (updatedUrlPreviewUrl, updatedUrlPreviewSignal) = urlPreviewStateForInputText(updatedChatPresentationInterfaceState.interfaceState.composeInputState.inputText, context: self.context, currentQuery: self.urlPreviewQueryState?.0) {
|
||
self.urlPreviewQueryState?.1.dispose()
|
||
var inScope = true
|
||
var inScopeResult: ((TelegramMediaWebpage?) -> TelegramMediaWebpage?)?
|
||
let linkPreviews: Signal<Bool, NoError>
|
||
if case let .peer(peerId) = self.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat {
|
||
linkPreviews = interactiveChatLinkPreviewsEnabled(accountManager: self.context.sharedContext.accountManager, displayAlert: { [weak self] f in
|
||
if let strongSelf = self {
|
||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Conversation_SecretLinkPreviewAlert, actions: [
|
||
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Yes, action: {
|
||
f.f(true)
|
||
}), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_No, action: {
|
||
f.f(false)
|
||
})]), in: .window(.root))
|
||
}
|
||
})
|
||
} else {
|
||
var bannedEmbedLinks = false
|
||
if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.hasBannedPermission(.banEmbedLinks) != nil {
|
||
bannedEmbedLinks = true
|
||
} else if let group = self.presentationInterfaceState.renderedPeer?.peer as? TelegramGroup, group.hasBannedPermission(.banEmbedLinks) {
|
||
bannedEmbedLinks = true
|
||
}
|
||
if bannedEmbedLinks {
|
||
linkPreviews = .single(false)
|
||
} else {
|
||
linkPreviews = .single(true)
|
||
}
|
||
}
|
||
let filteredPreviewSignal = linkPreviews
|
||
|> take(1)
|
||
|> mapToSignal { value -> Signal<(TelegramMediaWebpage?) -> TelegramMediaWebpage?, NoError> in
|
||
if value {
|
||
return updatedUrlPreviewSignal
|
||
} else {
|
||
return .single({ _ in return nil })
|
||
}
|
||
}
|
||
|
||
self.urlPreviewQueryState = (updatedUrlPreviewUrl, (filteredPreviewSignal |> 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, {
|
||
if let updatedUrlPreviewUrl = updatedUrlPreviewUrl, let webpage = result($0.urlPreview?.1) {
|
||
return $0.updatedUrlPreview((updatedUrlPreviewUrl, webpage))
|
||
} else {
|
||
return $0.updatedUrlPreview(nil)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}))
|
||
inScope = false
|
||
if let inScopeResult = inScopeResult {
|
||
if let updatedUrlPreviewUrl = updatedUrlPreviewUrl, let webpage = inScopeResult(updatedChatPresentationInterfaceState.urlPreview?.1) {
|
||
updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedUrlPreview((updatedUrlPreviewUrl, webpage))
|
||
} else {
|
||
updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedUrlPreview(nil)
|
||
}
|
||
}
|
||
}
|
||
|
||
let isEditingMedia: Bool = updatedChatPresentationInterfaceState.editMessageState?.content != .plaintext
|
||
let editingUrlPreviewText: NSAttributedString? = isEditingMedia ? nil : updatedChatPresentationInterfaceState.interfaceState.editMessage?.inputState.inputText
|
||
if let (updatedEditingUrlPreviewUrl, updatedEditingUrlPreviewSignal) = urlPreviewStateForInputText(editingUrlPreviewText, context: self.context, currentQuery: self.editingUrlPreviewQueryState?.0) {
|
||
self.editingUrlPreviewQueryState?.1.dispose()
|
||
var inScope = true
|
||
var inScopeResult: ((TelegramMediaWebpage?) -> TelegramMediaWebpage?)?
|
||
self.editingUrlPreviewQueryState = (updatedEditingUrlPreviewUrl, (updatedEditingUrlPreviewSignal |> 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, {
|
||
if let updatedEditingUrlPreviewUrl = updatedEditingUrlPreviewUrl, let webpage = result($0.editingUrlPreview?.1) {
|
||
return $0.updatedEditingUrlPreview((updatedEditingUrlPreviewUrl, webpage))
|
||
} else {
|
||
return $0.updatedEditingUrlPreview(nil)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}))
|
||
inScope = false
|
||
if let inScopeResult = inScopeResult {
|
||
if let updatedEditingUrlPreviewUrl = updatedEditingUrlPreviewUrl, let webpage = inScopeResult(updatedChatPresentationInterfaceState.editingUrlPreview?.1) {
|
||
updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedEditingUrlPreview((updatedEditingUrlPreviewUrl, webpage))
|
||
} else {
|
||
updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedEditingUrlPreview(nil)
|
||
}
|
||
}
|
||
}
|
||
|
||
if let updated = self.updateSearch(updatedChatPresentationInterfaceState) {
|
||
updatedChatPresentationInterfaceState = updated
|
||
}
|
||
|
||
let recordingActivityValue: ChatRecordingActivity
|
||
if let mediaRecordingState = updatedChatPresentationInterfaceState.inputTextPanelState.mediaRecordingState {
|
||
switch mediaRecordingState {
|
||
case .audio:
|
||
recordingActivityValue = .voice
|
||
case .video(ChatVideoRecordingStatus.recording, _):
|
||
recordingActivityValue = .instantVideo
|
||
default:
|
||
recordingActivityValue = .none
|
||
}
|
||
} else {
|
||
recordingActivityValue = .none
|
||
}
|
||
if recordingActivityValue != self.recordingActivityValue {
|
||
self.recordingActivityValue = recordingActivityValue
|
||
self.recordingActivityPromise.set(recordingActivityValue)
|
||
}
|
||
|
||
self.presentationInterfaceState = updatedChatPresentationInterfaceState
|
||
|
||
self.updateSlowmodeStatus()
|
||
|
||
if self.isNodeLoaded {
|
||
self.chatDisplayNode.updateChatPresentationInterfaceState(updatedChatPresentationInterfaceState, transition: transition, interactive: interactive, completion: completion)
|
||
} else {
|
||
completion(.immediate)
|
||
}
|
||
|
||
if let button = leftNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState, subject: self.subject, strings: updatedChatPresentationInterfaceState.strings, currentButton: self.leftNavigationButton, target: self, selector: #selector(self.leftNavigationButtonAction)) {
|
||
if self.leftNavigationButton != button {
|
||
var animated = transition.isAnimated
|
||
if let currentButton = self.leftNavigationButton?.action, currentButton == button.action {
|
||
animated = false
|
||
}
|
||
self.navigationItem.setLeftBarButton(button.buttonItem, animated: animated)
|
||
self.leftNavigationButton = button
|
||
}
|
||
} else if let _ = self.leftNavigationButton {
|
||
self.navigationItem.setLeftBarButton(nil, animated: transition.isAnimated)
|
||
self.leftNavigationButton = nil
|
||
}
|
||
|
||
if let button = rightNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState, strings: updatedChatPresentationInterfaceState.strings, currentButton: self.rightNavigationButton, target: self, selector: #selector(self.rightNavigationButtonAction), chatInfoNavigationButton: self.chatInfoNavigationButton) {
|
||
if self.rightNavigationButton != button {
|
||
var animated = transition.isAnimated
|
||
if let currentButton = self.rightNavigationButton?.action, currentButton == button.action {
|
||
animated = false
|
||
}
|
||
if case .replyThread = self.chatLocation {
|
||
animated = false
|
||
}
|
||
self.navigationItem.setRightBarButton(button.buttonItem, animated: animated)
|
||
self.rightNavigationButton = button
|
||
}
|
||
} else if let _ = self.rightNavigationButton {
|
||
self.navigationItem.setRightBarButton(nil, animated: transition.isAnimated)
|
||
self.rightNavigationButton = nil
|
||
}
|
||
|
||
if let controllerInteraction = self.controllerInteraction {
|
||
if updatedChatPresentationInterfaceState.interfaceState.selectionState != controllerInteraction.selectionState {
|
||
controllerInteraction.selectionState = updatedChatPresentationInterfaceState.interfaceState.selectionState
|
||
let isBlackout = controllerInteraction.selectionState != nil
|
||
completion = { [weak self] transition in
|
||
completion(transition)
|
||
(self?.navigationController as? NavigationController)?.updateMasterDetailsBlackout(isBlackout ? .master : nil, transition: transition)
|
||
}
|
||
self.updateItemNodesSelectionStates(animated: transition.isAnimated)
|
||
}
|
||
}
|
||
|
||
switch updatedChatPresentationInterfaceState.mode {
|
||
case .standard:
|
||
if self.hasEmbeddedTitleContent {
|
||
self.statusBar.statusBarStyle = .White
|
||
} else {
|
||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
|
||
}
|
||
self.deferScreenEdgeGestures = []
|
||
case .overlay:
|
||
self.deferScreenEdgeGestures = [.top]
|
||
case .inline:
|
||
self.statusBar.statusBarStyle = .Ignore
|
||
}
|
||
|
||
if saveInterfaceState {
|
||
self.saveInterfaceState(includeScrollState: false)
|
||
}
|
||
|
||
if let navigationController = self.navigationController as? NavigationController, isTopmostChatController(self) {
|
||
var voiceChatOverlayController: VoiceChatOverlayController?
|
||
for controller in navigationController.globalOverlayControllers {
|
||
if let controller = controller as? VoiceChatOverlayController {
|
||
voiceChatOverlayController = controller
|
||
break
|
||
}
|
||
}
|
||
|
||
if let controller = voiceChatOverlayController {
|
||
controller.updateVisibility()
|
||
}
|
||
}
|
||
|
||
self.presentationInterfaceStatePromise.set(self.presentationInterfaceState)
|
||
}
|
||
|
||
private func updateItemNodesSelectionStates(animated: Bool) {
|
||
self.chatDisplayNode.historyNode.forEachItemNode { itemNode in
|
||
if let itemNode = itemNode as? ChatMessageItemView {
|
||
itemNode.updateSelectionState(animated: animated)
|
||
}
|
||
}
|
||
}
|
||
|
||
private func updatePollTooltipMessageState(animated: Bool) {
|
||
self.chatDisplayNode.historyNode.forEachItemNode { itemNode in
|
||
if let itemNode = itemNode as? ChatMessageBubbleItemNode {
|
||
for contentNode in itemNode.contentNodes {
|
||
if let contentNode = contentNode as? ChatMessagePollBubbleContentNode {
|
||
contentNode.updatePollTooltipMessageState(animated: animated)
|
||
}
|
||
}
|
||
itemNode.updatePsaTooltipMessageState(animated: animated)
|
||
}
|
||
}
|
||
}
|
||
|
||
private func updateItemNodesSearchTextHighlightStates() {
|
||
var searchString: String?
|
||
var resultsMessageIndices: [MessageIndex]?
|
||
if let search = self.presentationInterfaceState.search, let resultsState = search.resultsState, !resultsState.messageIndices.isEmpty {
|
||
searchString = search.query
|
||
resultsMessageIndices = resultsState.messageIndices
|
||
}
|
||
if searchString != self.controllerInteraction?.searchTextHighightState?.0 || resultsMessageIndices?.count != self.controllerInteraction?.searchTextHighightState?.1.count {
|
||
var searchTextHighightState: (String, [MessageIndex])?
|
||
if let searchString = searchString, let resultsMessageIndices = resultsMessageIndices {
|
||
searchTextHighightState = (searchString, resultsMessageIndices)
|
||
}
|
||
self.controllerInteraction?.searchTextHighightState = searchTextHighightState
|
||
self.chatDisplayNode.historyNode.forEachItemNode { itemNode in
|
||
if let itemNode = itemNode as? ChatMessageItemView {
|
||
itemNode.updateSearchTextHighlightState()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private func updateItemNodesHighlightedStates(animated: Bool) {
|
||
self.chatDisplayNode.historyNode.forEachItemNode { itemNode in
|
||
if let itemNode = itemNode as? ChatMessageItemView {
|
||
itemNode.updateHighlightedState(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 .spacer:
|
||
break
|
||
case .cancelMessageSelection:
|
||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
||
case .clearHistory:
|
||
if case let .peer(peerId) = self.chatLocation {
|
||
guard let peer = self.presentationInterfaceState.renderedPeer, let chatPeer = peer.peers[peer.peerId], let mainPeer = peer.chatMainPeer else {
|
||
return
|
||
}
|
||
|
||
var isClearCache = false
|
||
let text: String
|
||
if peerId == self.context.account.peerId {
|
||
text = self.presentationData.strings.Conversation_ClearSelfHistory
|
||
} else if peerId.namespace == Namespaces.Peer.SecretChat {
|
||
text = self.presentationData.strings.Conversation_ClearSecretHistory
|
||
} else if peerId.namespace == Namespaces.Peer.CloudGroup || peerId.namespace == Namespaces.Peer.CloudChannel {
|
||
if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info {
|
||
isClearCache = true
|
||
text = self.presentationData.strings.Conversation_ClearCache
|
||
} else {
|
||
text = self.presentationData.strings.Conversation_ClearGroupHistory
|
||
}
|
||
} else {
|
||
text = self.presentationData.strings.Conversation_ClearPrivateHistory
|
||
}
|
||
|
||
var canRemoveGlobally = false
|
||
let limitsConfiguration = self.context.currentLimitsConfiguration.with { $0 }
|
||
if peerId.namespace == Namespaces.Peer.CloudUser && peerId != self.context.account.peerId {
|
||
if limitsConfiguration.maxMessageRevokeIntervalInPrivateChats == LimitsConfiguration.timeIntervalForever {
|
||
canRemoveGlobally = true
|
||
}
|
||
}
|
||
if let user = chatPeer as? TelegramUser, user.botInfo != nil {
|
||
canRemoveGlobally = false
|
||
}
|
||
|
||
let account = self.context.account
|
||
|
||
let beginClear: (InteractiveHistoryClearingType) -> Void = { [weak self] type in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
||
strongSelf.chatDisplayNode.historyNode.historyAppearsCleared = true
|
||
|
||
let statusText: String
|
||
if case .scheduledMessages = strongSelf.presentationInterfaceState.subject {
|
||
statusText = strongSelf.presentationData.strings.Undo_ScheduledMessagesCleared
|
||
} else if case .forEveryone = type {
|
||
statusText = strongSelf.presentationData.strings.Undo_ChatClearedForBothSides
|
||
} else {
|
||
statusText = strongSelf.presentationData.strings.Undo_ChatCleared
|
||
}
|
||
|
||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(text: statusText), elevatedLayout: false, action: { value in
|
||
if value == .commit {
|
||
let _ = clearHistoryInteractively(postbox: account.postbox, peerId: peerId, type: type).start(completed: {
|
||
self?.chatDisplayNode.historyNode.historyAppearsCleared = false
|
||
})
|
||
return true
|
||
} else if value == .undo {
|
||
self?.chatDisplayNode.historyNode.historyAppearsCleared = false
|
||
return true
|
||
}
|
||
return false
|
||
}), in: .current)
|
||
}
|
||
|
||
let actionSheet = ActionSheetController(presentationData: self.presentationData)
|
||
var items: [ActionSheetItem] = []
|
||
|
||
if case .scheduledMessages = self.presentationInterfaceState.subject {
|
||
items.append(ActionSheetButtonItem(title: self.presentationData.strings.ScheduledMessages_ClearAllConfirmation, color: .destructive, action: { [weak self, weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
|
||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [
|
||
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
|
||
}),
|
||
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationAction, action: {
|
||
beginClear(.scheduledMessages)
|
||
})
|
||
], parseMarkdown: true), in: .window(.root))
|
||
}))
|
||
} else if canRemoveGlobally {
|
||
items.append(DeleteChatPeerActionSheetItem(context: self.context, peer: mainPeer, chatPeer: chatPeer, action: .clearHistory, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder))
|
||
items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
beginClear(.forLocalPeer)
|
||
}))
|
||
items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).0, color: .destructive, action: { [weak self, weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
|
||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationText, actions: [
|
||
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
|
||
}),
|
||
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationAction, action: {
|
||
beginClear(.forEveryone)
|
||
})
|
||
], parseMarkdown: true), in: .window(.root))
|
||
}))
|
||
} else {
|
||
items.append(ActionSheetTextItem(title: text))
|
||
items.append(ActionSheetButtonItem(title: isClearCache ? self.presentationData.strings.Conversation_ClearCache : self.presentationData.strings.Conversation_ClearAll, color: isClearCache ? .accent : .destructive, action: { [weak self, weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
if isClearCache {
|
||
strongSelf.navigationButtonAction(.clearCache)
|
||
} else {
|
||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [
|
||
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
|
||
}),
|
||
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationAction, action: {
|
||
beginClear(.forLocalPeer)
|
||
})
|
||
], parseMarkdown: true), in: .window(.root))
|
||
}
|
||
}))
|
||
}
|
||
|
||
if let peer = self.presentationInterfaceState.renderedPeer?.peer {
|
||
var currentAutoremoveTimeout: Int32? = self.presentationInterfaceState.autoremoveTimeout
|
||
var canSetupAutoremoveTimeout = false
|
||
|
||
if let secretChat = peer as? TelegramSecretChat {
|
||
currentAutoremoveTimeout = secretChat.messageAutoremoveTimeout
|
||
canSetupAutoremoveTimeout = true
|
||
} else if let group = peer as? TelegramGroup {
|
||
if case .creator = group.role {
|
||
canSetupAutoremoveTimeout = true
|
||
} else if case let .admin(rights, _) = group.role {
|
||
if rights.flags.contains(.canChangeInfo) {
|
||
canSetupAutoremoveTimeout = true
|
||
}
|
||
} else if let defaultBannedRights = group.defaultBannedRights {
|
||
if !defaultBannedRights.flags.contains(.banChangeInfo) {
|
||
canSetupAutoremoveTimeout = true
|
||
}
|
||
}
|
||
} else if let _ = self.presentationInterfaceState.renderedPeer?.peer as? TelegramUser {
|
||
canSetupAutoremoveTimeout = true
|
||
} else if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel {
|
||
if channel.hasPermission(.changeInfo) {
|
||
canSetupAutoremoveTimeout = true
|
||
}
|
||
}
|
||
|
||
if canSetupAutoremoveTimeout {
|
||
//TODO:localize
|
||
items.append(ActionSheetButtonItem(title: currentAutoremoveTimeout == nil ? "Enable Auto-Delete" : "Edit Auto-Delete Settings", color: .accent, action: { [weak self, weak actionSheet] in
|
||
guard let actionSheet = actionSheet else {
|
||
return
|
||
}
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
|
||
Queue.mainQueue().after(0.8, {
|
||
self?.updateChatPresentationInterfaceState(animated: false, interactive: false, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) })
|
||
})
|
||
|
||
actionSheet.dismissAnimated()
|
||
|
||
strongSelf.presentAutoremoveSetup()
|
||
}))
|
||
}
|
||
}
|
||
|
||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
})
|
||
])])
|
||
|
||
self.chatDisplayNode.dismissInput()
|
||
self.present(actionSheet, in: .window(.root))
|
||
}
|
||
case let .openChatInfo(expandAvatar):
|
||
let _ = self.presentVoiceMessageDiscardAlert(action: {
|
||
switch self.chatLocationInfoData {
|
||
case let .peer(peerView):
|
||
self.navigationActionDisposable.set((peerView.get()
|
||
|> take(1)
|
||
|> deliverOnMainQueue).start(next: { [weak self] peerView in
|
||
if let strongSelf = self, let peer = peerView.peers[peerView.peerId], peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil && !strongSelf.presentationInterfaceState.isNotAccessible {
|
||
if peer.id == strongSelf.context.account.peerId {
|
||
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer, let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: true) {
|
||
strongSelf.effectiveNavigationController?.pushViewController(infoController)
|
||
}
|
||
} else {
|
||
var expandAvatar = expandAvatar
|
||
if peer.smallProfileImage == nil {
|
||
expandAvatar = false
|
||
}
|
||
if let validLayout = strongSelf.validLayout, validLayout.deviceMetrics.type == .tablet {
|
||
expandAvatar = false
|
||
}
|
||
if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: expandAvatar, fromChat: true) {
|
||
strongSelf.effectiveNavigationController?.pushViewController(infoController)
|
||
}
|
||
}
|
||
}
|
||
}))
|
||
case .replyThread:
|
||
break
|
||
}
|
||
})
|
||
case .search:
|
||
self.interfaceInteraction?.beginMessageSearch(.everything, "")
|
||
case .dismiss:
|
||
self.dismiss()
|
||
case .clearCache:
|
||
let controller = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: nil))
|
||
self.present(controller, in: .window(.root))
|
||
|
||
let disposable: MetaDisposable
|
||
if let currentDisposable = self.clearCacheDisposable {
|
||
disposable = currentDisposable
|
||
} else {
|
||
disposable = MetaDisposable()
|
||
self.clearCacheDisposable = disposable
|
||
}
|
||
|
||
switch self.chatLocationInfoData {
|
||
case let .peer(peerView):
|
||
self.navigationActionDisposable.set((peerView.get()
|
||
|> take(1)
|
||
|> deliverOnMainQueue).start(next: { [weak self] peerView in
|
||
guard let strongSelf = self, let peer = peerView.peers[peerView.peerId] else {
|
||
return
|
||
}
|
||
let peerId = peer.id
|
||
|
||
let _ = (collectCacheUsageStats(account: strongSelf.context.account, peerId: peer.id)
|
||
|> deliverOnMainQueue).start(next: { [weak self, weak controller] result in
|
||
controller?.dismiss()
|
||
|
||
guard let strongSelf = self, case let .result(stats) = result, let categories = stats.media[peer.id] else {
|
||
return
|
||
}
|
||
let presentationData = strongSelf.presentationData
|
||
let controller = ActionSheetController(presentationData: presentationData)
|
||
let dismissAction: () -> Void = { [weak controller] in
|
||
controller?.dismissAnimated()
|
||
}
|
||
|
||
var sizeIndex: [PeerCacheUsageCategory: (Bool, Int64)] = [:]
|
||
|
||
var itemIndex = 1
|
||
|
||
var selectedSize: Int64 = 0
|
||
let updateTotalSize: () -> Void = { [weak controller] in
|
||
controller?.updateItem(groupIndex: 0, itemIndex: itemIndex, { item in
|
||
let title: String
|
||
let filteredSize = sizeIndex.values.reduce(0, { $0 + ($1.0 ? $1.1 : 0) })
|
||
selectedSize = filteredSize
|
||
|
||
if filteredSize == 0 {
|
||
title = presentationData.strings.Cache_ClearNone
|
||
} else {
|
||
title = presentationData.strings.Cache_Clear("\(dataSizeString(filteredSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))").0
|
||
}
|
||
|
||
if let item = item as? ActionSheetButtonItem {
|
||
return ActionSheetButtonItem(title: title, color: filteredSize != 0 ? .accent : .disabled, enabled: filteredSize != 0, action: item.action)
|
||
}
|
||
return item
|
||
})
|
||
}
|
||
|
||
let toggleCheck: (PeerCacheUsageCategory, Int) -> Void = { [weak controller] category, itemIndex in
|
||
if let (value, size) = sizeIndex[category] {
|
||
sizeIndex[category] = (!value, size)
|
||
}
|
||
controller?.updateItem(groupIndex: 0, itemIndex: itemIndex, { item in
|
||
if let item = item as? ActionSheetCheckboxItem {
|
||
return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action)
|
||
}
|
||
return item
|
||
})
|
||
updateTotalSize()
|
||
}
|
||
var items: [ActionSheetItem] = []
|
||
|
||
items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: peer, chatPeer: peer, action: .clearCache, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder))
|
||
|
||
let validCategories: [PeerCacheUsageCategory] = [.image, .video, .audio, .file]
|
||
|
||
var totalSize: Int64 = 0
|
||
|
||
func stringForCategory(strings: PresentationStrings, category: PeerCacheUsageCategory) -> String {
|
||
switch category {
|
||
case .image:
|
||
return strings.Cache_Photos
|
||
case .video:
|
||
return strings.Cache_Videos
|
||
case .audio:
|
||
return strings.Cache_Music
|
||
case .file:
|
||
return strings.Cache_Files
|
||
}
|
||
}
|
||
|
||
for categoryId in validCategories {
|
||
if let media = categories[categoryId] {
|
||
var categorySize: Int64 = 0
|
||
for (_, size) in media {
|
||
categorySize += size
|
||
}
|
||
sizeIndex[categoryId] = (true, categorySize)
|
||
totalSize += categorySize
|
||
if categorySize > 1024 {
|
||
let index = itemIndex
|
||
items.append(ActionSheetCheckboxItem(title: stringForCategory(strings: presentationData.strings, category: categoryId), label: dataSizeString(categorySize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator), value: true, action: { value in
|
||
toggleCheck(categoryId, index)
|
||
}))
|
||
itemIndex += 1
|
||
}
|
||
}
|
||
}
|
||
selectedSize = totalSize
|
||
|
||
if items.isEmpty {
|
||
strongSelf.presentClearCacheSuggestion()
|
||
} else {
|
||
items.append(ActionSheetButtonItem(title: presentationData.strings.Cache_Clear("\(dataSizeString(totalSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))").0, action: {
|
||
let clearCategories = sizeIndex.keys.filter({ sizeIndex[$0]!.0 })
|
||
var clearMediaIds = Set<MediaId>()
|
||
|
||
var media = stats.media
|
||
if var categories = media[peerId] {
|
||
for category in clearCategories {
|
||
if let contents = categories[category] {
|
||
for (mediaId, _) in contents {
|
||
clearMediaIds.insert(mediaId)
|
||
}
|
||
}
|
||
categories.removeValue(forKey: category)
|
||
}
|
||
|
||
media[peerId] = categories
|
||
}
|
||
|
||
var clearResourceIds = Set<WrappedMediaResourceId>()
|
||
for id in clearMediaIds {
|
||
if let ids = stats.mediaResourceIds[id] {
|
||
for resourceId in ids {
|
||
clearResourceIds.insert(WrappedMediaResourceId(resourceId))
|
||
}
|
||
}
|
||
}
|
||
|
||
var signal = clearCachedMediaResources(account: strongSelf.context.account, mediaResourceIds: clearResourceIds)
|
||
|
||
var cancelImpl: (() -> Void)?
|
||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||
let progressSignal = Signal<Never, NoError> { subscriber in
|
||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||
cancelImpl?()
|
||
}))
|
||
strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||
return ActionDisposable { [weak controller] in
|
||
Queue.mainQueue().async() {
|
||
controller?.dismiss()
|
||
}
|
||
}
|
||
}
|
||
|> runOn(Queue.mainQueue())
|
||
|> delay(0.15, queue: Queue.mainQueue())
|
||
let progressDisposable = progressSignal.start()
|
||
|
||
signal = signal
|
||
|> afterDisposed {
|
||
Queue.mainQueue().async {
|
||
progressDisposable.dispose()
|
||
}
|
||
}
|
||
cancelImpl = {
|
||
disposable.set(nil)
|
||
}
|
||
disposable.set((signal
|
||
|> deliverOnMainQueue).start(completed: { [weak self] in
|
||
if let strongSelf = self, let _ = strongSelf.validLayout {
|
||
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))", stringForDeviceType()).0), elevatedLayout: false, action: { _ in return false }), in: .current)
|
||
}
|
||
}))
|
||
|
||
dismissAction()
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) })
|
||
}))
|
||
|
||
items.append(ActionSheetButtonItem(title: presentationData.strings.ClearCache_StorageUsage, action: { [weak self] in
|
||
dismissAction()
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) })
|
||
|
||
if let strongSelf = self {
|
||
let controller = storageUsageController(context: strongSelf.context, isModal: true)
|
||
strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||
}
|
||
}))
|
||
|
||
controller.setItemGroups([
|
||
ActionSheetItemGroup(items: items),
|
||
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
|
||
])
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
strongSelf.present(controller, in: .window(.root))
|
||
}
|
||
})
|
||
}))
|
||
case .replyThread:
|
||
break
|
||
}
|
||
case .toggleInfoPanel:
|
||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
return $0.updatedTitlePanelContext {
|
||
if let index = $0.firstIndex(where: {
|
||
switch $0 {
|
||
case .chatInfo:
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}) {
|
||
var updatedContexts = $0
|
||
updatedContexts.remove(at: index)
|
||
return updatedContexts
|
||
} else {
|
||
var updatedContexts = $0
|
||
updatedContexts.append(.chatInfo)
|
||
return updatedContexts.sorted()
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
private func editMessageMediaWithMessages(_ messages: [EnqueueMessage]) {
|
||
if let message = messages.first, case let .message(text, _, maybeMediaReference, _, _) = message, let mediaReference = maybeMediaReference {
|
||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
|
||
var state = state
|
||
if let editMessageState = state.editMessageState, case let .media(options) = editMessageState.content, !options.isEmpty {
|
||
state = state.updatedEditMessageState(ChatEditInterfaceMessageState(content: editMessageState.content, mediaReference: mediaReference))
|
||
}
|
||
if !text.isEmpty {
|
||
state = state.updatedInterfaceState { state in
|
||
if let editMessage = state.editMessage {
|
||
return state.withUpdatedEditMessage(editMessage.withUpdatedInputState(ChatTextInputState(inputText: NSAttributedString(string: text))))
|
||
}
|
||
return state
|
||
}
|
||
}
|
||
return state
|
||
})
|
||
self.interfaceInteraction?.editMessage()
|
||
}
|
||
}
|
||
|
||
private func editMessageMediaWithLegacySignals(_ signals: [Any]) {
|
||
let _ = (legacyAssetPickerEnqueueMessages(account: self.context.account, signals: signals)
|
||
|> deliverOnMainQueue).start(next: { [weak self] messages in
|
||
self?.editMessageMediaWithMessages(messages)
|
||
})
|
||
}
|
||
|
||
private func presentAttachmentMenu(editMediaOptions: MessageMediaEditingOptions?, editMediaReference: AnyMediaReference?) {
|
||
let _ = (self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in
|
||
let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings) as? GeneratedMediaStoreSettings
|
||
return entry ?? GeneratedMediaStoreSettings.defaultSettings
|
||
}
|
||
|> deliverOnMainQueue).start(next: { [weak self] settings in
|
||
guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
|
||
return
|
||
}
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
|
||
var bannedSendMedia: (Int32, Bool)?
|
||
var canSendPolls = true
|
||
if let channel = peer as? TelegramChannel {
|
||
if let value = channel.hasBannedPermission(.banSendMedia) {
|
||
bannedSendMedia = value
|
||
}
|
||
if channel.hasBannedPermission(.banSendPolls) != nil {
|
||
canSendPolls = false
|
||
}
|
||
} else if let group = peer as? TelegramGroup {
|
||
if group.hasBannedPermission(.banSendMedia) {
|
||
bannedSendMedia = (Int32.max, false)
|
||
}
|
||
if group.hasBannedPermission(.banSendPolls) {
|
||
canSendPolls = false
|
||
}
|
||
}
|
||
|
||
if editMediaOptions == nil, let (untilDate, personal) = bannedSendMedia {
|
||
let banDescription: String
|
||
if untilDate != 0 && untilDate != Int32.max {
|
||
banDescription = strongSelf.presentationInterfaceState.strings.Conversation_RestrictedMediaTimed(stringForFullDate(timestamp: untilDate, strings: strongSelf.presentationInterfaceState.strings, dateTimeFormat: strongSelf.presentationInterfaceState.dateTimeFormat)).0
|
||
} else if personal {
|
||
banDescription = strongSelf.presentationInterfaceState.strings.Conversation_RestrictedMedia
|
||
} else {
|
||
banDescription = strongSelf.presentationInterfaceState.strings.Conversation_DefaultRestrictedMedia
|
||
}
|
||
|
||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
|
||
var items: [ActionSheetItem] = []
|
||
items.append(ActionSheetTextItem(title: banDescription))
|
||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_Location, color: .accent, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
self?.presentLocationPicker()
|
||
}))
|
||
if canSendPolls {
|
||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.AttachmentMenu_Poll, color: .accent, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
self?.presentPollCreation()
|
||
}))
|
||
}
|
||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_Contact, color: .accent, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
self?.presentContactPicker()
|
||
}))
|
||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
})
|
||
])])
|
||
strongSelf.present(actionSheet, in: .window(.root))
|
||
|
||
return
|
||
}
|
||
|
||
let legacyController = LegacyController(presentation: .custom, theme: strongSelf.presentationData.theme, initialLayout: strongSelf.validLayout)
|
||
legacyController.blocksBackgroundWhenInOverlay = true
|
||
legacyController.acceptsFocusWhenInOverlay = true
|
||
legacyController.statusBar.statusBarStyle = .Ignore
|
||
legacyController.controllerLoaded = { [weak legacyController] in
|
||
legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true
|
||
}
|
||
|
||
let emptyController = LegacyEmptyController(context: legacyController.context)!
|
||
let navigationController = makeLegacyNavigationController(rootController: emptyController)
|
||
navigationController.setNavigationBarHidden(true, animated: false)
|
||
legacyController.bind(controller: navigationController)
|
||
|
||
legacyController.enableSizeClassSignal = true
|
||
|
||
let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText
|
||
let menuEditMediaOptions = editMediaOptions.flatMap { options -> LegacyAttachmentMenuMediaEditing in
|
||
var result: LegacyAttachmentMenuMediaEditing = .none
|
||
if options.contains(.imageOrVideo) {
|
||
result = .imageOrVideo(editMediaReference)
|
||
}
|
||
return result
|
||
}
|
||
|
||
var slowModeEnabled = false
|
||
if let channel = peer as? TelegramChannel, channel.isRestrictedBySlowmode {
|
||
slowModeEnabled = true
|
||
}
|
||
|
||
let controller = legacyAttachmentMenu(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, editMediaOptions: menuEditMediaOptions, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, canSendPolls: canSendPolls, presentationData: strongSelf.presentationData, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, initialCaption: inputText.string, openGallery: {
|
||
self?.presentMediaPicker(fileMode: false, editingMedia: editMediaOptions != nil, completion: { signals, silentPosting, scheduleTime in
|
||
if !inputText.string.isEmpty {
|
||
//strongSelf.clearInputText()
|
||
}
|
||
if editMediaOptions != nil {
|
||
self?.editMessageMediaWithLegacySignals(signals)
|
||
} else {
|
||
self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil)
|
||
}
|
||
})
|
||
}, openCamera: { [weak self] cameraView, menuController in
|
||
if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
|
||
var photoOnly = false
|
||
if let callManager = strongSelf.context.sharedContext.callManager as? PresentationCallManagerImpl, callManager.hasActiveCall {
|
||
photoOnly = true
|
||
}
|
||
|
||
presentedLegacyCamera(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: menuController, parentController: strongSelf, editingMedia: editMediaOptions != nil, saveCapturedPhotos: settings.storeEditedPhotos, mediaGrouping: true, initialCaption: inputText.string, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, photoOnly: photoOnly, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in
|
||
if let strongSelf = self {
|
||
if editMediaOptions != nil {
|
||
strongSelf.editMessageMediaWithLegacySignals(signals!)
|
||
} else {
|
||
strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil)
|
||
}
|
||
if !inputText.string.isEmpty {
|
||
//strongSelf.clearInputText()
|
||
}
|
||
}
|
||
}, recognizedQRCode: { [weak self] code in
|
||
if let strongSelf = self {
|
||
if let (host, port, username, password, secret) = parseProxyUrl(code) {
|
||
strongSelf.openResolved(ResolvedUrl.proxy(host: host, port: port, username: username, password: password, secret: secret))
|
||
}/* else if let url = URL(string: code), let parsedWalletUrl = parseWalletUrl(url) {
|
||
//strongSelf.openResolved(ResolvedUrl.wallet(address: parsedWalletUrl.address, amount: parsedWalletUrl.amount, comment: parsedWalletUrl.comment))
|
||
}*/
|
||
}
|
||
}, presentSchedulePicker: { [weak self] done in
|
||
if let strongSelf = self {
|
||
strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in
|
||
if let strongSelf = self {
|
||
done(time)
|
||
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp {
|
||
strongSelf.openScheduledMessages()
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}, presentTimerPicker: { [weak self] done in
|
||
if let strongSelf = self {
|
||
strongSelf.presentTimerPicker(style: .media, completion: { time in
|
||
done(time)
|
||
})
|
||
}
|
||
}, presentStickers: { [weak self] completion in
|
||
if let strongSelf = self {
|
||
let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in
|
||
completion(fileReference.media, fileReference.media.isAnimatedSticker, node.view, rect)
|
||
return true
|
||
})
|
||
strongSelf.present(controller, in: .window(.root))
|
||
return controller
|
||
} else {
|
||
return nil
|
||
}
|
||
})
|
||
}
|
||
}, openFileGallery: {
|
||
self?.presentFileMediaPickerOptions(editingMessage: editMediaOptions != nil)
|
||
}, openWebSearch: {
|
||
self?.presentWebSearch(editingMessage : editMediaOptions != nil)
|
||
}, openMap: {
|
||
self?.presentLocationPicker()
|
||
}, openContacts: {
|
||
self?.presentContactPicker()
|
||
}, openPoll: {
|
||
self?.presentPollCreation()
|
||
}, presentSelectionLimitExceeded: {
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
let text: String
|
||
if slowModeEnabled {
|
||
text = strongSelf.presentationData.strings.Chat_SlowmodeAttachmentLimitReached
|
||
} else {
|
||
text = strongSelf.presentationData.strings.Chat_AttachmentLimitReached
|
||
}
|
||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||
}, presentCantSendMultipleFiles: {
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Chat_AttachmentMultipleFilesDisabled, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||
}, presentSchedulePicker: { [weak self] done in
|
||
if let strongSelf = self {
|
||
strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in
|
||
if let strongSelf = self {
|
||
done(time)
|
||
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp {
|
||
strongSelf.openScheduledMessages()
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}, presentTimerPicker: { [weak self] done in
|
||
if let strongSelf = self {
|
||
strongSelf.presentTimerPicker(style: .media, completion: { time in
|
||
done(time)
|
||
})
|
||
}
|
||
}, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in
|
||
if !inputText.string.isEmpty {
|
||
//strongSelf.clearInputText()
|
||
}
|
||
if editMediaOptions != nil {
|
||
self?.editMessageMediaWithLegacySignals(signals!)
|
||
} else {
|
||
self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil)
|
||
}
|
||
}, selectRecentlyUsedInlineBot: { [weak self] peer in
|
||
if let strongSelf = self, let addressName = peer.addressName {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||
$0.updatedInterfaceState({ $0.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: "@" + addressName + " "))) }).updatedInputMode({ _ in
|
||
return .text
|
||
})
|
||
})
|
||
}
|
||
}, presentStickers: { [weak self] completion in
|
||
if let strongSelf = self {
|
||
let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in
|
||
completion(fileReference.media, fileReference.media.isAnimatedSticker, node.view, rect)
|
||
return true
|
||
})
|
||
strongSelf.present(controller, in: .window(.root))
|
||
return controller
|
||
} else {
|
||
return nil
|
||
}
|
||
}, present: { [weak self] c, a in
|
||
self?.present(c, in: .window(.root), with: a)
|
||
})
|
||
controller.didDismiss = { [weak legacyController] _ in
|
||
legacyController?.dismiss()
|
||
}
|
||
controller.customRemoveFromParentViewController = { [weak legacyController] in
|
||
legacyController?.dismiss()
|
||
}
|
||
|
||
strongSelf.present(legacyController, in: .window(.root))
|
||
controller.present(in: emptyController, sourceView: nil, animated: true)
|
||
|
||
let presentationDisposable = strongSelf.context.sharedContext.presentationData.start(next: { [weak controller] presentationData in
|
||
if let controller = controller {
|
||
controller.pallete = legacyMenuPaletteFromTheme(presentationData.theme)
|
||
}
|
||
})
|
||
legacyController.disposables.add(presentationDisposable)
|
||
})
|
||
}
|
||
|
||
private func presentFileMediaPickerOptions(editingMessage: Bool) {
|
||
let actionSheet = ActionSheetController(presentationData: self.presentationData)
|
||
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
||
ActionSheetButtonItem(title: self.presentationData.strings.Conversation_FilePhotoOrVideo, action: { [weak self, weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
if let strongSelf = self {
|
||
strongSelf.presentMediaPicker(fileMode: true, editingMedia: editingMessage, completion: { signals, silentPosting, scheduleTime in
|
||
if editingMessage {
|
||
self?.editMessageMediaWithLegacySignals(signals)
|
||
} else {
|
||
self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil)
|
||
}
|
||
})
|
||
}
|
||
}),
|
||
ActionSheetButtonItem(title: self.presentationData.strings.Conversation_FileICloudDrive, action: { [weak self, weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
if let strongSelf = self {
|
||
strongSelf.present(legacyICloudFilePicker(theme: strongSelf.presentationData.theme, completion: { urls in
|
||
if let strongSelf = self, !urls.isEmpty {
|
||
var signals: [Signal<ICloudFileDescription?, NoError>] = []
|
||
for url in urls {
|
||
signals.append(iCloudFileDescription(url))
|
||
}
|
||
strongSelf.enqueueMediaMessageDisposable.set((combineLatest(signals)
|
||
|> deliverOnMainQueue).start(next: { results in
|
||
if let strongSelf = self {
|
||
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
|
||
|
||
var groupingKey: Int64?
|
||
var fileTypes: (music: Bool, other: Bool) = (false, false)
|
||
if results.count > 1 {
|
||
for item in results {
|
||
if let item = item {
|
||
let pathExtension = (item.fileName as NSString).pathExtension.lowercased()
|
||
if ["mp3", "m4a"].contains(pathExtension) {
|
||
fileTypes.music = true
|
||
} else {
|
||
fileTypes.other = true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if fileTypes.music != fileTypes.other {
|
||
groupingKey = arc4random64()
|
||
}
|
||
|
||
var messages: [EnqueueMessage] = []
|
||
for item in results {
|
||
if let item = item {
|
||
let fileId = arc4random64()
|
||
let mimeType = guessMimeTypeByFileExtension((item.fileName as NSString).pathExtension)
|
||
var previewRepresentations: [TelegramMediaImageRepresentation] = []
|
||
if mimeType.hasPrefix("image/") || mimeType == "application/pdf" {
|
||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 320, height: 320), resource: ICloudFileResource(urlData: item.urlData, thumbnail: true), progressiveSizes: []))
|
||
}
|
||
var attributes: [TelegramMediaFileAttribute] = []
|
||
attributes.append(.FileName(fileName: item.fileName))
|
||
if let audioMetadata = item.audioMetadata {
|
||
attributes.append(.Audio(isVoice: false, duration: audioMetadata.duration, title: audioMetadata.title, performer: audioMetadata.performer, waveform: nil))
|
||
}
|
||
|
||
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: item.fileSize, attributes: attributes)
|
||
let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: replyMessageId, localGroupingKey: groupingKey)
|
||
messages.append(message)
|
||
}
|
||
if let _ = groupingKey, messages.count % 10 == 0 {
|
||
groupingKey = arc4random64()
|
||
}
|
||
}
|
||
|
||
if !messages.isEmpty {
|
||
if editingMessage {
|
||
strongSelf.editMessageMediaWithMessages(messages)
|
||
} else {
|
||
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
|
||
})
|
||
}
|
||
})
|
||
strongSelf.sendMessages(messages)
|
||
}
|
||
}
|
||
}
|
||
}))
|
||
}
|
||
}), in: .window(.root))
|
||
}
|
||
})
|
||
]), ActionSheetItemGroup(items: [
|
||
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
})
|
||
])])
|
||
self.chatDisplayNode.dismissInput()
|
||
self.present(actionSheet, in: .window(.root))
|
||
}
|
||
|
||
private func presentMediaPicker(fileMode: Bool, editingMedia: Bool, completion: @escaping ([Any], Bool, Int32) -> Void) {
|
||
let postbox = self.context.account.postbox
|
||
let _ = (self.context.sharedContext.accountManager.transaction { transaction -> Signal<(GeneratedMediaStoreSettings, SearchBotsConfiguration), NoError> in
|
||
let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings) as? GeneratedMediaStoreSettings
|
||
return postbox.transaction { transaction -> (GeneratedMediaStoreSettings, SearchBotsConfiguration) in
|
||
let configuration = currentSearchBotsConfiguration(transaction: transaction)
|
||
return (entry ?? GeneratedMediaStoreSettings.defaultSettings, configuration)
|
||
}
|
||
}
|
||
|> switchToLatest
|
||
|> deliverOnMainQueue).start(next: { [weak self] settings, searchBotsConfiguration in
|
||
guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
|
||
return
|
||
}
|
||
let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText
|
||
var selectionLimit: Int = 100
|
||
var slowModeEnabled = false
|
||
if let channel = peer as? TelegramChannel, channel.isRestrictedBySlowmode {
|
||
selectionLimit = 10
|
||
slowModeEnabled = true
|
||
}
|
||
|
||
let _ = legacyAssetPicker(context: strongSelf.context, presentationData: strongSelf.presentationData, editingMedia: editingMedia, fileMode: fileMode, peer: peer, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, selectionLimit: selectionLimit).start(next: { generator in
|
||
if let strongSelf = self {
|
||
let legacyController = LegacyController(presentation: .navigation, theme: strongSelf.presentationData.theme, initialLayout: strongSelf.validLayout)
|
||
legacyController.navigationPresentation = .modal
|
||
legacyController.statusBar.statusBarStyle = strongSelf.presentationData.theme.rootController.statusBarStyle.style
|
||
legacyController.controllerLoaded = { [weak legacyController] in
|
||
legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true
|
||
legacyController?.view.disablesInteractiveModalDismiss = true
|
||
}
|
||
let controller = generator(legacyController.context)
|
||
legacyController.bind(controller: controller)
|
||
legacyController.deferScreenEdgeGestures = [.top]
|
||
|
||
configureLegacyAssetPicker(controller, context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, initialCaption: inputText.string, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, presentWebSearch: editingMedia ? nil : { [weak self, weak legacyController] in
|
||
if let strongSelf = self {
|
||
let controller = WebSearchController(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, configuration: searchBotsConfiguration, mode: .media(completion: { results, selectionState, editingState, silentPosting in
|
||
if let legacyController = legacyController {
|
||
legacyController.dismiss()
|
||
}
|
||
legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { result in
|
||
if let strongSelf = self {
|
||
strongSelf.enqueueChatContextResult(results, result, hideVia: true)
|
||
}
|
||
}, enqueueMediaMessages: { signals in
|
||
if let strongSelf = self {
|
||
if editingMedia {
|
||
strongSelf.editMessageMediaWithLegacySignals(signals)
|
||
} else {
|
||
strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting)
|
||
}
|
||
}
|
||
})
|
||
}))
|
||
controller.presentStickers = { [weak self] completion in
|
||
if let strongSelf = self {
|
||
let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in
|
||
completion(fileReference.media, fileReference.media.isAnimatedSticker, node.view, rect)
|
||
return true
|
||
})
|
||
strongSelf.present(controller, in: .window(.root))
|
||
return controller
|
||
} else {
|
||
return nil
|
||
}
|
||
}
|
||
strongSelf.effectiveNavigationController?.pushViewController(controller)
|
||
}
|
||
}, presentSelectionLimitExceeded: {
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
|
||
let text: String
|
||
if slowModeEnabled {
|
||
text = strongSelf.presentationData.strings.Chat_SlowmodeAttachmentLimitReached
|
||
} else {
|
||
text = strongSelf.presentationData.strings.Chat_AttachmentLimitReached
|
||
}
|
||
|
||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||
}, presentSchedulePicker: { [weak self] done in
|
||
if let strongSelf = self {
|
||
strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in
|
||
if let strongSelf = self {
|
||
done(time)
|
||
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp {
|
||
strongSelf.openScheduledMessages()
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}, presentTimerPicker: { [weak self] done in
|
||
if let strongSelf = self {
|
||
strongSelf.presentTimerPicker(style: .media, completion: { time in
|
||
done(time)
|
||
})
|
||
}
|
||
}, presentStickers: { [weak self] completion in
|
||
if let strongSelf = self {
|
||
let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in
|
||
completion(fileReference.media, fileReference.media.isAnimatedSticker, node.view, rect)
|
||
return true
|
||
})
|
||
strongSelf.present(controller, in: .window(.root))
|
||
return controller
|
||
} else {
|
||
return nil
|
||
}
|
||
})
|
||
controller.descriptionGenerator = legacyAssetPickerItemGenerator()
|
||
controller.completionBlock = { [weak legacyController] signals, silentPosting, scheduleTime in
|
||
if let legacyController = legacyController {
|
||
legacyController.dismiss()
|
||
completion(signals!, silentPosting, scheduleTime)
|
||
}
|
||
}
|
||
controller.dismissalBlock = { [weak legacyController] in
|
||
if let legacyController = legacyController {
|
||
legacyController.dismiss()
|
||
}
|
||
}
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
strongSelf.effectiveNavigationController?.pushViewController(legacyController)
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
private func presentWebSearch(editingMessage: Bool) {
|
||
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
|
||
return
|
||
}
|
||
|
||
let _ = (self.context.account.postbox.transaction { transaction -> SearchBotsConfiguration in
|
||
if let entry = transaction.getPreferencesEntry(key: PreferencesKeys.searchBotsConfiguration) as? SearchBotsConfiguration {
|
||
return entry
|
||
} else {
|
||
return SearchBotsConfiguration.defaultValue
|
||
}
|
||
}
|
||
|> deliverOnMainQueue).start(next: { [weak self] configuration in
|
||
if let strongSelf = self {
|
||
let controller = WebSearchController(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, configuration: configuration, mode: .media(completion: { [weak self] results, selectionState, editingState, silentPosting in
|
||
legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak self] result in
|
||
if let strongSelf = self {
|
||
strongSelf.enqueueChatContextResult(results, result, hideVia: true)
|
||
}
|
||
}, enqueueMediaMessages: { [weak self] signals in
|
||
if let strongSelf = self, !signals.isEmpty {
|
||
if editingMessage {
|
||
strongSelf.editMessageMediaWithLegacySignals(signals)
|
||
} else {
|
||
strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting)
|
||
}
|
||
}
|
||
})
|
||
}))
|
||
controller.presentStickers = { [weak self] completion in
|
||
if let strongSelf = self {
|
||
let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in
|
||
completion(fileReference.media, fileReference.media.isAnimatedSticker, node.view, rect)
|
||
return true
|
||
})
|
||
strongSelf.present(controller, in: .window(.root))
|
||
return controller
|
||
} else {
|
||
return nil
|
||
}
|
||
}
|
||
strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||
}
|
||
})
|
||
}
|
||
|
||
private func presentLocationPicker() {
|
||
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
|
||
return
|
||
}
|
||
let selfPeerId: PeerId
|
||
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
|
||
selfPeerId = peer.id
|
||
} else if let peer = peer as? TelegramChannel, case .group = peer.info, peer.hasPermission(.canBeAnonymous) {
|
||
selfPeerId = peer.id
|
||
} else {
|
||
selfPeerId = self.context.account.peerId
|
||
}
|
||
let _ = (self.context.account.postbox.transaction { transaction -> Peer? in
|
||
return transaction.getPeer(selfPeerId)
|
||
}
|
||
|> deliverOnMainQueue).start(next: { [weak self] selfPeer in
|
||
guard let strongSelf = self, let selfPeer = selfPeer else {
|
||
return
|
||
}
|
||
let hasLiveLocation = peer.id.namespace != Namespaces.Peer.SecretChat && peer.id != strongSelf.context.account.peerId && strongSelf.presentationInterfaceState.subject != .scheduledMessages
|
||
let controller = LocationPickerController(context: strongSelf.context, mode: .share(peer: peer, selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { [weak self] location, _ in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
|
||
let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: location), replyToMessageId: replyMessageId, localGroupingKey: nil)
|
||
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
|
||
})
|
||
}
|
||
})
|
||
strongSelf.sendMessages([message])
|
||
})
|
||
strongSelf.effectiveNavigationController?.pushViewController(controller)
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
})
|
||
}
|
||
|
||
private func presentContactPicker() {
|
||
let contactsController = ContactSelectionControllerImpl(ContactSelectionControllerParams(context: self.context, title: { $0.Contacts_Title }, displayDeviceContacts: true))
|
||
contactsController.navigationPresentation = .modal
|
||
self.chatDisplayNode.dismissInput()
|
||
self.effectiveNavigationController?.pushViewController(contactsController)
|
||
self.controllerNavigationDisposable.set((contactsController.result
|
||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||
if let strongSelf = self, let (peer, _) = peer {
|
||
let dataSignal: Signal<(Peer?, DeviceContactExtendedData?), NoError>
|
||
switch peer {
|
||
case let .peer(contact, _, _):
|
||
guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else {
|
||
return
|
||
}
|
||
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 context = strongSelf.context
|
||
dataSignal = (strongSelf.context.sharedContext.contactDataManager?.basicData() ?? .single([:]))
|
||
|> take(1)
|
||
|> mapToSignal { basicData -> Signal<(Peer?, DeviceContactExtendedData?), NoError> in
|
||
var stableId: String?
|
||
let queryPhoneNumber = formatPhoneNumber(phoneNumber)
|
||
outer: for (id, data) in basicData {
|
||
for phoneNumber in data.phoneNumbers {
|
||
if formatPhoneNumber(phoneNumber.value) == queryPhoneNumber {
|
||
stableId = id
|
||
break outer
|
||
}
|
||
}
|
||
}
|
||
|
||
if let stableId = stableId {
|
||
return (context.sharedContext.contactDataManager?.extendedData(stableId: stableId) ?? .single(nil))
|
||
|> take(1)
|
||
|> map { extendedData -> (Peer?, DeviceContactExtendedData?) in
|
||
return (contact, extendedData)
|
||
}
|
||
} else {
|
||
return .single((contact, contactData))
|
||
}
|
||
}
|
||
case let .deviceContact(id, _):
|
||
dataSignal = (strongSelf.context.sharedContext.contactDataManager?.extendedData(stableId: id) ?? .single(nil))
|
||
|> take(1)
|
||
|> map { extendedData -> (Peer?, DeviceContactExtendedData?) in
|
||
return (nil, extendedData)
|
||
}
|
||
}
|
||
strongSelf.controllerNavigationDisposable.set((dataSignal
|
||
|> deliverOnMainQueue).start(next: { peerAndContactData in
|
||
if let strongSelf = self, let contactData = peerAndContactData.1, contactData.basicData.phoneNumbers.count != 0 {
|
||
if contactData.isPrimitive {
|
||
let phone = contactData.basicData.phoneNumbers[0].value
|
||
let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peerAndContactData.0?.id, vCardData: nil)
|
||
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
|
||
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
|
||
})
|
||
}
|
||
})
|
||
let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil)
|
||
strongSelf.sendMessages([message])
|
||
} else {
|
||
let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: strongSelf.context, subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in
|
||
guard let strongSelf = self, !contactData.basicData.phoneNumbers.isEmpty else {
|
||
return
|
||
}
|
||
let phone = contactData.basicData.phoneNumbers[0].value
|
||
if let vCardData = contactData.serializedVCard() {
|
||
let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peer?.id, vCardData: vCardData)
|
||
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
|
||
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
|
||
})
|
||
}
|
||
})
|
||
let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil)
|
||
strongSelf.sendMessages([message])
|
||
}
|
||
}), completed: nil, cancelled: nil)
|
||
strongSelf.effectiveNavigationController?.pushViewController(contactController)
|
||
}
|
||
}
|
||
}))
|
||
}
|
||
}))
|
||
}
|
||
|
||
private func displayPollSolution(solution: TelegramMediaPollResults.Solution, sourceNode: ASDisplayNode, isAutomatic: Bool) {
|
||
var maybeFoundItemNode: ChatMessageItemView?
|
||
self.chatDisplayNode.historyNode.forEachItemNode { itemNode in
|
||
if let itemNode = itemNode as? ChatMessageItemView {
|
||
if sourceNode.view.isDescendant(of: itemNode.view) {
|
||
maybeFoundItemNode = itemNode
|
||
}
|
||
}
|
||
}
|
||
guard let foundItemNode = maybeFoundItemNode, let item = foundItemNode.item else {
|
||
return
|
||
}
|
||
|
||
var found = false
|
||
self.forEachController({ controller in
|
||
if let controller = controller as? TooltipScreen {
|
||
if controller.text == solution.text && controller.textEntities == solution.entities {
|
||
found = true
|
||
controller.dismiss()
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
})
|
||
if found {
|
||
return
|
||
}
|
||
|
||
let tooltipScreen = TooltipScreen(text: solution.text, textEntities: solution.entities, icon: .info, location: .top, shouldDismissOnTouch: { point in
|
||
return .ignore
|
||
}, openActiveTextItem: { [weak self] item, action in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
switch item {
|
||
case let .url(url, concealed):
|
||
switch action {
|
||
case .tap:
|
||
strongSelf.openUrl(url, concealed: concealed)
|
||
case .longTap:
|
||
strongSelf.controllerInteraction?.longTap(.url(url), nil)
|
||
}
|
||
case let .mention(peerId, mention):
|
||
switch action {
|
||
case .tap:
|
||
strongSelf.controllerInteraction?.openPeer(peerId, .default, nil)
|
||
case .longTap:
|
||
strongSelf.controllerInteraction?.longTap(.peerMention(peerId, mention), nil)
|
||
}
|
||
case let .textMention(mention):
|
||
switch action {
|
||
case .tap:
|
||
strongSelf.controllerInteraction?.openPeerMention(mention)
|
||
case .longTap:
|
||
strongSelf.controllerInteraction?.longTap(.mention(mention), nil)
|
||
}
|
||
case let .botCommand(command):
|
||
switch action {
|
||
case .tap:
|
||
strongSelf.controllerInteraction?.sendBotCommand(nil, command)
|
||
case .longTap:
|
||
strongSelf.controllerInteraction?.longTap(.command(command), nil)
|
||
}
|
||
case let .hashtag(hashtag):
|
||
switch action {
|
||
case .tap:
|
||
strongSelf.controllerInteraction?.openHashtag(nil, hashtag)
|
||
case .longTap:
|
||
strongSelf.controllerInteraction?.longTap(.hashtag(hashtag), nil)
|
||
}
|
||
}
|
||
})
|
||
|
||
let messageId = item.message.id
|
||
self.controllerInteraction?.currentPollMessageWithTooltip = messageId
|
||
self.updatePollTooltipMessageState(animated: !isAutomatic)
|
||
|
||
tooltipScreen.willBecomeDismissed = { [weak self] tooltipScreen in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
if strongSelf.controllerInteraction?.currentPollMessageWithTooltip == messageId {
|
||
strongSelf.controllerInteraction?.currentPollMessageWithTooltip = nil
|
||
strongSelf.updatePollTooltipMessageState(animated: true)
|
||
}
|
||
}
|
||
|
||
self.forEachController({ controller in
|
||
if let controller = controller as? TooltipScreen {
|
||
controller.dismiss()
|
||
}
|
||
return true
|
||
})
|
||
|
||
self.present(tooltipScreen, in: .current)
|
||
}
|
||
|
||
public func displayPromoAnnouncement(text: String) {
|
||
let psaText: String = text
|
||
let psaEntities: [MessageTextEntity] = generateTextEntities(psaText, enabledTypes: .allUrl)
|
||
|
||
var found = false
|
||
self.forEachController({ controller in
|
||
if let controller = controller as? TooltipScreen {
|
||
if controller.text == psaText {
|
||
found = true
|
||
controller.dismiss()
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
})
|
||
if found {
|
||
return
|
||
}
|
||
|
||
let tooltipScreen = TooltipScreen(text: psaText, textEntities: psaEntities, icon: .info, location: .top, displayDuration: .custom(10.0), shouldDismissOnTouch: { point in
|
||
return .ignore
|
||
}, openActiveTextItem: { [weak self] item, action in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
switch item {
|
||
case let .url(url, concealed):
|
||
switch action {
|
||
case .tap:
|
||
strongSelf.openUrl(url, concealed: concealed)
|
||
case .longTap:
|
||
strongSelf.controllerInteraction?.longTap(.url(url), nil)
|
||
}
|
||
case let .mention(peerId, mention):
|
||
switch action {
|
||
case .tap:
|
||
strongSelf.controllerInteraction?.openPeer(peerId, .default, nil)
|
||
case .longTap:
|
||
strongSelf.controllerInteraction?.longTap(.peerMention(peerId, mention), nil)
|
||
}
|
||
case let .textMention(mention):
|
||
switch action {
|
||
case .tap:
|
||
strongSelf.controllerInteraction?.openPeerMention(mention)
|
||
case .longTap:
|
||
strongSelf.controllerInteraction?.longTap(.mention(mention), nil)
|
||
}
|
||
case let .botCommand(command):
|
||
switch action {
|
||
case .tap:
|
||
strongSelf.controllerInteraction?.sendBotCommand(nil, command)
|
||
case .longTap:
|
||
strongSelf.controllerInteraction?.longTap(.command(command), nil)
|
||
}
|
||
case let .hashtag(hashtag):
|
||
switch action {
|
||
case .tap:
|
||
strongSelf.controllerInteraction?.openHashtag(nil, hashtag)
|
||
case .longTap:
|
||
strongSelf.controllerInteraction?.longTap(.hashtag(hashtag), nil)
|
||
}
|
||
}
|
||
})
|
||
|
||
self.forEachController({ controller in
|
||
if let controller = controller as? TooltipScreen {
|
||
controller.dismiss()
|
||
}
|
||
return true
|
||
})
|
||
|
||
self.present(tooltipScreen, in: .current)
|
||
}
|
||
|
||
private func displayPsa(type: String, sourceNode: ASDisplayNode, isAutomatic: Bool) {
|
||
var maybeFoundItemNode: ChatMessageItemView?
|
||
self.chatDisplayNode.historyNode.forEachItemNode { itemNode in
|
||
if let itemNode = itemNode as? ChatMessageItemView {
|
||
if sourceNode.view.isDescendant(of: itemNode.view) {
|
||
maybeFoundItemNode = itemNode
|
||
}
|
||
}
|
||
}
|
||
guard let foundItemNode = maybeFoundItemNode, let item = foundItemNode.item else {
|
||
return
|
||
}
|
||
|
||
var psaText = self.presentationData.strings.Chat_GenericPsaTooltip
|
||
let key = "Chat.PsaTooltip.\(type)"
|
||
if let string = self.presentationData.strings.primaryComponent.dict[key] {
|
||
psaText = string
|
||
} else if let string = self.presentationData.strings.secondaryComponent?.dict[key] {
|
||
psaText = string
|
||
}
|
||
|
||
let psaEntities: [MessageTextEntity] = generateTextEntities(psaText, enabledTypes: .allUrl)
|
||
|
||
let messageId = item.message.id
|
||
|
||
var found = false
|
||
self.forEachController({ controller in
|
||
if let controller = controller as? TooltipScreen {
|
||
if controller.text == psaText {
|
||
found = true
|
||
controller.resetDismissTimeout()
|
||
|
||
controller.willBecomeDismissed = { [weak self] tooltipScreen in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
if strongSelf.controllerInteraction?.currentPsaMessageWithTooltip == messageId {
|
||
strongSelf.controllerInteraction?.currentPsaMessageWithTooltip = nil
|
||
strongSelf.updatePollTooltipMessageState(animated: true)
|
||
}
|
||
}
|
||
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
})
|
||
if found {
|
||
self.controllerInteraction?.currentPsaMessageWithTooltip = messageId
|
||
self.updatePollTooltipMessageState(animated: !isAutomatic)
|
||
|
||
return
|
||
}
|
||
|
||
let tooltipScreen = TooltipScreen(text: psaText, textEntities: psaEntities, icon: .info, location: .top, displayDuration: .custom(10.0), shouldDismissOnTouch: { point in
|
||
return .ignore
|
||
}, openActiveTextItem: { [weak self] item, action in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
switch item {
|
||
case let .url(url, concealed):
|
||
switch action {
|
||
case .tap:
|
||
strongSelf.openUrl(url, concealed: concealed)
|
||
case .longTap:
|
||
strongSelf.controllerInteraction?.longTap(.url(url), nil)
|
||
}
|
||
case let .mention(peerId, mention):
|
||
switch action {
|
||
case .tap:
|
||
strongSelf.controllerInteraction?.openPeer(peerId, .default, nil)
|
||
case .longTap:
|
||
strongSelf.controllerInteraction?.longTap(.peerMention(peerId, mention), nil)
|
||
}
|
||
case let .textMention(mention):
|
||
switch action {
|
||
case .tap:
|
||
strongSelf.controllerInteraction?.openPeerMention(mention)
|
||
case .longTap:
|
||
strongSelf.controllerInteraction?.longTap(.mention(mention), nil)
|
||
}
|
||
case let .botCommand(command):
|
||
switch action {
|
||
case .tap:
|
||
strongSelf.controllerInteraction?.sendBotCommand(nil, command)
|
||
case .longTap:
|
||
strongSelf.controllerInteraction?.longTap(.command(command), nil)
|
||
}
|
||
case let .hashtag(hashtag):
|
||
switch action {
|
||
case .tap:
|
||
strongSelf.controllerInteraction?.openHashtag(nil, hashtag)
|
||
case .longTap:
|
||
strongSelf.controllerInteraction?.longTap(.hashtag(hashtag), nil)
|
||
}
|
||
}
|
||
})
|
||
|
||
self.controllerInteraction?.currentPsaMessageWithTooltip = messageId
|
||
self.updatePollTooltipMessageState(animated: !isAutomatic)
|
||
|
||
tooltipScreen.willBecomeDismissed = { [weak self] tooltipScreen in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
if strongSelf.controllerInteraction?.currentPsaMessageWithTooltip == messageId {
|
||
strongSelf.controllerInteraction?.currentPsaMessageWithTooltip = nil
|
||
strongSelf.updatePollTooltipMessageState(animated: true)
|
||
}
|
||
}
|
||
|
||
self.forEachController({ controller in
|
||
if let controller = controller as? TooltipScreen {
|
||
controller.dismiss()
|
||
}
|
||
return true
|
||
})
|
||
|
||
self.present(tooltipScreen, in: .current)
|
||
}
|
||
|
||
private func presentPollCreation(isQuiz: Bool? = nil) {
|
||
if let peer = self.presentationInterfaceState.renderedPeer?.peer {
|
||
self.effectiveNavigationController?.pushViewController(createPollController(context: self.context, peer: peer, isQuiz: isQuiz, completion: { [weak self] message in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
|
||
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
|
||
})
|
||
}
|
||
})
|
||
strongSelf.sendMessages([message.withUpdatedReplyToMessageId(replyMessageId)])
|
||
}))
|
||
}
|
||
}
|
||
|
||
func transformEnqueueMessages(_ messages: [EnqueueMessage]) -> [EnqueueMessage] {
|
||
let silentPosting = self.presentationInterfaceState.interfaceState.silentPosting
|
||
return transformEnqueueMessages(messages, silentPosting: silentPosting)
|
||
}
|
||
|
||
private func displayDiceTooltip(dice: TelegramMediaDice) {
|
||
guard let _ = dice.value else {
|
||
return
|
||
}
|
||
self.window?.forEachController({ controller in
|
||
if let controller = controller as? UndoOverlayController {
|
||
controller.dismissWithCommitAction()
|
||
}
|
||
})
|
||
self.forEachController({ controller in
|
||
if let controller = controller as? UndoOverlayController {
|
||
controller.dismissWithCommitAction()
|
||
}
|
||
return true
|
||
})
|
||
|
||
let value: String?
|
||
let emoji = dice.emoji.strippedEmoji
|
||
switch emoji {
|
||
case "🎲":
|
||
value = self.presentationData.strings.Conversation_Dice_u1F3B2
|
||
case "🎯":
|
||
value = self.presentationData.strings.Conversation_Dice_u1F3AF
|
||
case "🏀":
|
||
value = self.presentationData.strings.Conversation_Dice_u1F3C0
|
||
case "⚽":
|
||
value = self.presentationData.strings.Conversation_Dice_u26BD
|
||
case "🎰":
|
||
value = self.presentationData.strings.Conversation_Dice_u1F3B0
|
||
case "🎳":
|
||
value = self.presentationData.strings.Conversation_Dice_u1F3B3
|
||
default:
|
||
let emojiHex = emoji.unicodeScalars.map({ String(format:"%02x", $0.value) }).joined().uppercased()
|
||
let key = "Conversation.Dice.u\(emojiHex)"
|
||
if let string = self.presentationData.strings.primaryComponent.dict[key] {
|
||
value = string
|
||
} else if let string = self.presentationData.strings.secondaryComponent?.dict[key] {
|
||
value = string
|
||
} else {
|
||
value = nil
|
||
}
|
||
}
|
||
if let value = value {
|
||
self.present(UndoOverlayController(presentationData: self.presentationData, content: .dice(dice: dice, account: self.context.account, text: value, action: canSendMessagesToChat(self.presentationInterfaceState) ? self.presentationData.strings.Conversation_SendDice : nil), elevatedLayout: false, action: { [weak self] action in
|
||
if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState), action == .undo {
|
||
strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: dice.emoji)), replyToMessageId: nil, localGroupingKey: nil)])
|
||
}
|
||
return false
|
||
}), in: .current)
|
||
}
|
||
}
|
||
|
||
private func transformEnqueueMessages(_ messages: [EnqueueMessage], silentPosting: Bool, scheduleTime: Int32? = nil) -> [EnqueueMessage] {
|
||
var defaultReplyMessageId: MessageId?
|
||
switch self.chatLocation {
|
||
case .peer:
|
||
break
|
||
case let .replyThread(replyThreadMessage):
|
||
defaultReplyMessageId = replyThreadMessage.messageId
|
||
}
|
||
|
||
return messages.map { message in
|
||
var message = message
|
||
|
||
if let defaultReplyMessageId = defaultReplyMessageId {
|
||
switch message {
|
||
case let .message(text, attributes, mediaReference, replyToMessageId, localGroupingKey):
|
||
if replyToMessageId == nil {
|
||
message = .message(text: text, attributes: attributes, mediaReference: mediaReference, replyToMessageId: defaultReplyMessageId, localGroupingKey: localGroupingKey)
|
||
}
|
||
case .forward:
|
||
break
|
||
}
|
||
}
|
||
|
||
if silentPosting || scheduleTime != nil {
|
||
return message.withUpdatedAttributes { attributes in
|
||
var attributes = attributes
|
||
for i in (0 ..< attributes.count).reversed() {
|
||
if attributes[i] is NotificationInfoMessageAttribute {
|
||
attributes.remove(at: i)
|
||
} else if let _ = scheduleTime, attributes[i] is OutgoingScheduleInfoMessageAttribute {
|
||
attributes.remove(at: i)
|
||
}
|
||
}
|
||
if silentPosting {
|
||
attributes.append(NotificationInfoMessageAttribute(flags: .muted))
|
||
}
|
||
if let scheduleTime = scheduleTime {
|
||
attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: scheduleTime))
|
||
}
|
||
return attributes
|
||
}
|
||
} else {
|
||
return message
|
||
}
|
||
}
|
||
}
|
||
|
||
private func sendMessages(_ messages: [EnqueueMessage], commit: Bool = false) {
|
||
let peerId = self.chatLocation.peerId
|
||
|
||
var isScheduledMessages = false
|
||
if case .scheduledMessages = self.presentationInterfaceState.subject {
|
||
isScheduledMessages = true
|
||
}
|
||
|
||
if commit || !isScheduledMessages {
|
||
self.commitPurposefulAction()
|
||
|
||
let _ = (enqueueMessages(account: self.context.account, peerId: peerId, messages: self.transformEnqueueMessages(messages))
|
||
|> deliverOnMainQueue).start(next: { [weak self] _ in
|
||
if let strongSelf = self, strongSelf.presentationInterfaceState.subject != .scheduledMessages {
|
||
strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
||
}
|
||
})
|
||
|
||
donateSendMessageIntent(account: self.context.account, sharedContext: self.context.sharedContext, intentContext: .chat, peerIds: [peerId])
|
||
} else {
|
||
self.presentScheduleTimePicker(dismissByTapOutside: false, completion: { [weak self] time in
|
||
if let strongSelf = self {
|
||
strongSelf.sendMessages(strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: time), commit: true)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
private func enqueueMediaMessages(signals: [Any]?, silentPosting: Bool, scheduleTime: Int32? = nil) {
|
||
self.enqueueMediaMessageDisposable.set((legacyAssetPickerEnqueueMessages(account: self.context.account, signals: signals!)
|
||
|> deliverOnMainQueue).start(next: { [weak self] messages in
|
||
if let strongSelf = self {
|
||
let messages = strongSelf.transformEnqueueMessages(messages, silentPosting: silentPosting, scheduleTime: scheduleTime)
|
||
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
|
||
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
|
||
})
|
||
}
|
||
})
|
||
strongSelf.sendMessages(messages.map { $0.withUpdatedReplyToMessageId(replyMessageId) })
|
||
}
|
||
}))
|
||
}
|
||
|
||
private func displayPasteMenu(_ images: [UIImage]) {
|
||
let _ = (self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in
|
||
let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings) as? GeneratedMediaStoreSettings
|
||
return entry ?? GeneratedMediaStoreSettings.defaultSettings
|
||
}
|
||
|> deliverOnMainQueue).start(next: { [weak self] settings in
|
||
if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
let _ = presentLegacyPasteMenu(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, presentationData: strongSelf.presentationData, images: images, presentSchedulePicker: { [weak self] done in
|
||
if let strongSelf = self {
|
||
strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in
|
||
if let strongSelf = self {
|
||
done(time)
|
||
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp {
|
||
strongSelf.openScheduledMessages()
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}, presentTimerPicker: { [weak self] done in
|
||
if let strongSelf = self {
|
||
strongSelf.presentTimerPicker(style: .media, completion: { time in
|
||
done(time)
|
||
})
|
||
}
|
||
}, sendMessagesWithSignals: { signals, silentPosting, scheduleTime in
|
||
self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil)
|
||
}, presentStickers: { [weak self] completion in
|
||
if let strongSelf = self {
|
||
let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in
|
||
completion(fileReference.media, fileReference.media.isAnimatedSticker, node.view, rect)
|
||
return true
|
||
})
|
||
strongSelf.present(controller, in: .window(.root))
|
||
return controller
|
||
} else {
|
||
return nil
|
||
}
|
||
}, present: { [weak self] controller, arguments in
|
||
if let strongSelf = self {
|
||
strongSelf.present(controller, in: .window(.root), with: arguments)
|
||
}
|
||
}, initialLayout: strongSelf.validLayout)
|
||
}
|
||
})
|
||
}
|
||
|
||
private func enqueueGifData(_ data: Data) {
|
||
self.enqueueMediaMessageDisposable.set((legacyEnqueueGifMessage(account: self.context.account, data: data) |> deliverOnMainQueue).start(next: { [weak self] message 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) }
|
||
})
|
||
}
|
||
})
|
||
strongSelf.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageId) })
|
||
}
|
||
}))
|
||
}
|
||
|
||
private func enqueueVideoData(_ data: Data) {
|
||
self.enqueueMediaMessageDisposable.set((legacyEnqueueGifMessage(account: self.context.account, data: data) |> deliverOnMainQueue).start(next: { [weak self] message 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) }
|
||
})
|
||
}
|
||
})
|
||
strongSelf.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageId) })
|
||
}
|
||
}))
|
||
}
|
||
|
||
private func enqueueStickerImage(_ image: UIImage, isMemoji: Bool) {
|
||
let size = image.size.aspectFitted(CGSize(width: 512.0, height: 512.0))
|
||
self.enqueueMediaMessageDisposable.set((convertToWebP(image: image, targetSize: size, targetBoundingSize: size, quality: 0.9) |> deliverOnMainQueue).start(next: { [weak self] data in
|
||
if let strongSelf = self, !data.isEmpty {
|
||
let resource = LocalFileMediaResource(fileId: arc4random64())
|
||
strongSelf.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
|
||
|
||
var fileAttributes: [TelegramMediaFileAttribute] = []
|
||
fileAttributes.append(.FileName(fileName: "sticker.webp"))
|
||
fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil))
|
||
fileAttributes.append(.ImageSize(size: PixelDimensions(size)))
|
||
|
||
let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: arc4random64()), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: data.count, attributes: fileAttributes)
|
||
let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: nil)
|
||
|
||
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
|
||
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
|
||
})
|
||
}
|
||
})
|
||
strongSelf.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageId) })
|
||
}
|
||
}))
|
||
}
|
||
|
||
private func enqueueChatContextResult(_ results: ChatContextResultCollection, _ result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false) {
|
||
let peerId = self.chatLocation.peerId
|
||
|
||
if let message = outgoingMessageWithChatContextResult(to: peerId, results: results, result: result, hideVia: hideVia), canSendMessagesToChat(self.presentationInterfaceState) {
|
||
let replyMessageId = self.presentationInterfaceState.interfaceState.replyMessageId
|
||
self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
|
||
var state = state
|
||
state = state.updatedInterfaceState { interfaceState in
|
||
var interfaceState = interfaceState
|
||
interfaceState = interfaceState.withUpdatedReplyMessageId(nil)
|
||
interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: "")))
|
||
interfaceState = interfaceState.withUpdatedComposeDisableUrlPreview(nil)
|
||
return interfaceState
|
||
}
|
||
state = state.updatedInputMode { current in
|
||
if case let .media(mode, maybeExpanded) = current, maybeExpanded != nil {
|
||
return .media(mode: mode, expanded: nil)
|
||
}
|
||
return current
|
||
}
|
||
return state
|
||
})
|
||
}
|
||
})
|
||
self.sendMessages([message.withUpdatedReplyToMessageId(replyMessageId)])
|
||
}
|
||
}
|
||
|
||
private func firstLoadedMessageToListen() -> Message? {
|
||
var messageToListen: Message?
|
||
self.chatDisplayNode.historyNode.forEachMessageInCurrentHistoryView { message in
|
||
if message.flags.contains(.Incoming) && message.tags.contains(.voiceOrInstantVideo) {
|
||
for attribute in message.attributes {
|
||
if let attribute = attribute as? ConsumableContentMessageAttribute, !attribute.consumed {
|
||
messageToListen = message
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
return messageToListen
|
||
}
|
||
|
||
private var raiseToListenActivateRecordingTimer: SwiftSignalKit.Timer?
|
||
|
||
private func activateRaiseGesture() {
|
||
self.raiseToListenActivateRecordingTimer?.invalidate()
|
||
self.raiseToListenActivateRecordingTimer = nil
|
||
if let messageToListen = self.firstLoadedMessageToListen() {
|
||
let _ = self.controllerInteraction?.openMessage(messageToListen, .default)
|
||
} else {
|
||
let timeout = (self.voicePlaylistDidEndTimestamp + 1.0) - CACurrentMediaTime()
|
||
self.raiseToListenActivateRecordingTimer = SwiftSignalKit.Timer(timeout: max(0.0, timeout), repeat: false, completion: { [weak self] in
|
||
self?.requestAudioRecorder(beginWithTone: true)
|
||
}, queue: .mainQueue())
|
||
self.raiseToListenActivateRecordingTimer?.start()
|
||
}
|
||
}
|
||
|
||
private func deactivateRaiseGesture() {
|
||
self.raiseToListenActivateRecordingTimer?.invalidate()
|
||
self.raiseToListenActivateRecordingTimer = nil
|
||
self.dismissMediaRecorder(.preview)
|
||
}
|
||
|
||
private func requestAudioRecorder(beginWithTone: Bool) {
|
||
if self.audioRecorderValue == nil {
|
||
if self.recorderFeedback == nil {
|
||
self.recorderFeedback = HapticFeedback()
|
||
self.recorderFeedback?.prepareImpact(.light)
|
||
}
|
||
|
||
self.audioRecorder.set(self.context.sharedContext.mediaManager.audioRecorder(beginWithTone: beginWithTone, applicationBindings: self.context.sharedContext.applicationBindings, beganWithTone: { _ in
|
||
}))
|
||
}
|
||
}
|
||
|
||
private func requestVideoRecorder() {
|
||
let peerId = self.chatLocation.peerId
|
||
|
||
if self.videoRecorderValue == nil {
|
||
if let currentInputPanelFrame = self.chatDisplayNode.currentInputPanelFrame() {
|
||
if self.recorderFeedback == nil {
|
||
self.recorderFeedback = HapticFeedback()
|
||
self.recorderFeedback?.prepareImpact(.light)
|
||
}
|
||
|
||
var isScheduledMessages = false
|
||
if case .scheduledMessages = self.presentationInterfaceState.subject {
|
||
isScheduledMessages = true
|
||
}
|
||
|
||
self.videoRecorder.set(.single(legacyInstantVideoController(theme: self.presentationData.theme, panelFrame: self.view.convert(currentInputPanelFrame, to: nil), context: self.context, peerId: peerId, slowmodeState: !isScheduledMessages ? self.presentationInterfaceState.slowmodeState : nil, hasSchedule: !isScheduledMessages && peerId.namespace != Namespaces.Peer.SecretChat, send: { [weak self] message 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) }
|
||
})
|
||
}
|
||
})
|
||
let updatedMessage = message.withUpdatedReplyToMessageId(replyMessageId)
|
||
strongSelf.sendMessages([updatedMessage])
|
||
}
|
||
}, displaySlowmodeTooltip: { [weak self] node, rect in
|
||
self?.interfaceInteraction?.displaySlowmodeTooltip(node, rect)
|
||
}, presentSchedulePicker: { [weak self] done in
|
||
if let strongSelf = self {
|
||
strongSelf.presentScheduleTimePicker(completion: { [weak self] time in
|
||
if let strongSelf = self {
|
||
done(time)
|
||
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp {
|
||
strongSelf.openScheduledMessages()
|
||
}
|
||
}
|
||
})
|
||
}
|
||
})))
|
||
}
|
||
}
|
||
}
|
||
|
||
private func dismissMediaRecorder(_ action: ChatFinishMediaRecordingAction) {
|
||
var updatedAction = action
|
||
var isScheduledMessages = false
|
||
if case .scheduledMessages = self.presentationInterfaceState.subject {
|
||
isScheduledMessages = true
|
||
}
|
||
|
||
if let _ = self.presentationInterfaceState.slowmodeState, !isScheduledMessages {
|
||
updatedAction = .preview
|
||
}
|
||
|
||
if let audioRecorderValue = self.audioRecorderValue {
|
||
audioRecorderValue.stop()
|
||
|
||
switch updatedAction {
|
||
case .dismiss:
|
||
self.chatDisplayNode.updateRecordedMediaDeleted(true)
|
||
break
|
||
case .preview:
|
||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
$0.updatedInputTextPanelState { panelState in
|
||
return panelState.withUpdatedMediaRecordingState(.waitingForPreview)
|
||
}
|
||
})
|
||
let _ = (audioRecorderValue.takenRecordedData() |> deliverOnMainQueue).start(next: { [weak self] data in
|
||
if let strongSelf = self, let data = data {
|
||
if data.duration < 0.5 {
|
||
strongSelf.recorderFeedback?.error()
|
||
strongSelf.recorderFeedback = nil
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
$0.updatedInputTextPanelState { panelState in
|
||
return panelState.withUpdatedMediaRecordingState(nil)
|
||
}
|
||
})
|
||
} else if let waveform = data.waveform {
|
||
let resource = LocalFileMediaResource(fileId: arc4random64(), size: data.compressedData.count)
|
||
|
||
strongSelf.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data.compressedData)
|
||
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
$0.updatedRecordedMediaPreview(ChatRecordedMediaPreview(resource: resource, duration: Int32(data.duration), fileSize: Int32(data.compressedData.count), waveform: AudioWaveform(bitstream: waveform, bitsPerSample: 5))).updatedInputTextPanelState { panelState in
|
||
return panelState.withUpdatedMediaRecordingState(nil)
|
||
}
|
||
})
|
||
strongSelf.recorderFeedback = nil
|
||
}
|
||
}
|
||
})
|
||
case .send:
|
||
self.chatDisplayNode.updateRecordedMediaDeleted(false)
|
||
let _ = (audioRecorderValue.takenRecordedData()
|
||
|> deliverOnMainQueue).start(next: { [weak self] data in
|
||
if let strongSelf = self, let data = data {
|
||
if data.duration < 0.5 {
|
||
strongSelf.recorderFeedback?.error()
|
||
strongSelf.recorderFeedback = nil
|
||
} else {
|
||
let randomId = arc4random64()
|
||
|
||
let resource = LocalFileMediaResource(fileId: randomId)
|
||
strongSelf.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data.compressedData)
|
||
|
||
var waveformBuffer: MemoryBuffer?
|
||
if let waveform = data.waveform {
|
||
waveformBuffer = MemoryBuffer(data: waveform)
|
||
}
|
||
|
||
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
|
||
})
|
||
}
|
||
})
|
||
|
||
strongSelf.sendMessages([.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: data.compressedData.count, attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)])), replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)])
|
||
|
||
strongSelf.recorderFeedback?.tap()
|
||
strongSelf.recorderFeedback = nil
|
||
}
|
||
}
|
||
})
|
||
}
|
||
self.audioRecorder.set(.single(nil))
|
||
} else if let videoRecorderValue = self.videoRecorderValue {
|
||
if case .send = updatedAction {
|
||
self.chatDisplayNode.updateRecordedMediaDeleted(false)
|
||
videoRecorderValue.completeVideo()
|
||
self.videoRecorder.set(.single(nil))
|
||
} else {
|
||
if case .dismiss = updatedAction {
|
||
self.chatDisplayNode.updateRecordedMediaDeleted(true)
|
||
}
|
||
if case .preview = updatedAction, videoRecorderValue.stopVideo() {
|
||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
$0.updatedInputTextPanelState { panelState in
|
||
return panelState.withUpdatedMediaRecordingState(.video(status: .editing, isLocked: false))
|
||
}
|
||
})
|
||
} else {
|
||
self.videoRecorder.set(.single(nil))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private func stopMediaRecorder() {
|
||
if let audioRecorderValue = self.audioRecorderValue {
|
||
if let _ = self.presentationInterfaceState.inputTextPanelState.mediaRecordingState {
|
||
self.dismissMediaRecorder(.preview)
|
||
} else {
|
||
audioRecorderValue.stop()
|
||
self.audioRecorder.set(.single(nil))
|
||
}
|
||
} else if let videoRecorderValue = self.videoRecorderValue {
|
||
if videoRecorderValue.stopVideo() {
|
||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
$0.updatedInputTextPanelState { panelState in
|
||
return panelState.withUpdatedMediaRecordingState(.video(status: .editing, isLocked: false))
|
||
}
|
||
})
|
||
} else {
|
||
self.videoRecorder.set(.single(nil))
|
||
}
|
||
}
|
||
}
|
||
|
||
private func lockMediaRecorder() {
|
||
if self.presentationInterfaceState.inputTextPanelState.mediaRecordingState != nil {
|
||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
return $0.updatedInputTextPanelState { panelState in
|
||
return panelState.withUpdatedMediaRecordingState(panelState.mediaRecordingState?.withLocked(true))
|
||
}
|
||
})
|
||
}
|
||
|
||
self.videoRecorderValue?.lockVideo()
|
||
}
|
||
|
||
private func deleteMediaRecording() {
|
||
self.chatDisplayNode.updateRecordedMediaDeleted(true)
|
||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
$0.updatedRecordedMediaPreview(nil)
|
||
})
|
||
}
|
||
|
||
private func sendMediaRecording(silentPosting: Bool? = nil, scheduleTime: Int32? = nil) {
|
||
self.chatDisplayNode.updateRecordedMediaDeleted(false)
|
||
if let recordedMediaPreview = self.presentationInterfaceState.recordedMediaPreview {
|
||
var isScheduledMessages = false
|
||
if case .scheduledMessages = self.presentationInterfaceState.subject {
|
||
isScheduledMessages = true
|
||
}
|
||
|
||
if let _ = self.presentationInterfaceState.slowmodeState, !isScheduledMessages {
|
||
if let rect = self.chatDisplayNode.frameForInputActionButton() {
|
||
self.interfaceInteraction?.displaySlowmodeTooltip(self.chatDisplayNode, rect)
|
||
}
|
||
return
|
||
}
|
||
|
||
let waveformBuffer = MemoryBuffer(data: recordedMediaPreview.waveform.makeBitstream())
|
||
|
||
self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||
$0.updatedRecordedMediaPreview(nil).updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
|
||
})
|
||
}
|
||
})
|
||
|
||
let messages: [EnqueueMessage] = [.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: arc4random64()), partialReference: nil, resource: recordedMediaPreview.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int(recordedMediaPreview.fileSize), attributes: [.Audio(isVoice: true, duration: Int(recordedMediaPreview.duration), title: nil, performer: nil, waveform: waveformBuffer)])), replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)]
|
||
|
||
let transformedMessages: [EnqueueMessage]
|
||
if let silentPosting = silentPosting {
|
||
transformedMessages = self.transformEnqueueMessages(messages, silentPosting: silentPosting)
|
||
} else if let scheduleTime = scheduleTime {
|
||
transformedMessages = self.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime)
|
||
} else {
|
||
transformedMessages = self.transformEnqueueMessages(messages)
|
||
}
|
||
|
||
let peerId = self.chatLocation.peerId
|
||
let _ = (enqueueMessages(account: self.context.account, peerId: peerId, messages: transformedMessages)
|
||
|> deliverOnMainQueue).start(next: { [weak self] _ in
|
||
if let strongSelf = self, strongSelf.presentationInterfaceState.subject != .scheduledMessages {
|
||
strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
||
}
|
||
})
|
||
|
||
donateSendMessageIntent(account: self.context.account, sharedContext: self.context.sharedContext, intentContext: .chat, peerIds: [peerId])
|
||
}
|
||
}
|
||
|
||
private func updateSearch(_ interfaceState: ChatPresentationInterfaceState) -> ChatPresentationInterfaceState? {
|
||
let limit: Int32 = 100
|
||
|
||
var derivedSearchState: ChatSearchState?
|
||
if let search = interfaceState.search {
|
||
func loadMoreStateFromResultsState(_ resultsState: ChatSearchResultsState?) -> SearchMessagesState? {
|
||
guard let resultsState = resultsState, let currentId = resultsState.currentId else {
|
||
return nil
|
||
}
|
||
if let index = resultsState.messageIndices.firstIndex(where: { $0.id == currentId }) {
|
||
if index <= limit / 2 {
|
||
return resultsState.state
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
var searchTopMsgId: MessageId?
|
||
switch self.chatLocation {
|
||
case .peer:
|
||
break
|
||
case let .replyThread(replyThreadMessage):
|
||
searchTopMsgId = replyThreadMessage.messageId
|
||
}
|
||
switch search.domain {
|
||
case .everything:
|
||
derivedSearchState = ChatSearchState(query: search.query, location: .peer(peerId: self.chatLocation.peerId, fromId: nil, tags: nil, topMsgId: searchTopMsgId, minDate: nil, maxDate: nil), loadMoreState: loadMoreStateFromResultsState(search.resultsState))
|
||
case .members:
|
||
derivedSearchState = nil
|
||
case let .member(peer):
|
||
derivedSearchState = ChatSearchState(query: search.query, location: .peer(peerId: self.chatLocation.peerId, fromId: peer.id, tags: nil, topMsgId: searchTopMsgId, minDate: nil, maxDate: nil), loadMoreState: loadMoreStateFromResultsState(search.resultsState))
|
||
}
|
||
}
|
||
|
||
if derivedSearchState != self.searchState {
|
||
let previousSearchState = self.searchState
|
||
self.searchState = derivedSearchState
|
||
if let searchState = derivedSearchState {
|
||
if previousSearchState?.query != searchState.query || previousSearchState?.location != searchState.location {
|
||
var queryIsEmpty = false
|
||
if searchState.query.isEmpty {
|
||
if case let .peer(_, fromId, _, _, _, _) = searchState.location {
|
||
if fromId == nil {
|
||
queryIsEmpty = true
|
||
}
|
||
} else {
|
||
queryIsEmpty = true
|
||
}
|
||
}
|
||
|
||
if queryIsEmpty {
|
||
self.searching.set(false)
|
||
self.searchDisposable?.set(nil)
|
||
self.searchResult.set(.single(nil))
|
||
if let data = interfaceState.search {
|
||
return interfaceState.updatedSearch(data.withUpdatedResultsState(nil))
|
||
}
|
||
} else {
|
||
self.searching.set(true)
|
||
let searchDisposable: MetaDisposable
|
||
if let current = self.searchDisposable {
|
||
searchDisposable = current
|
||
} else {
|
||
searchDisposable = MetaDisposable()
|
||
self.searchDisposable = searchDisposable
|
||
}
|
||
|
||
let search = searchMessages(account: self.context.account, location: searchState.location, query: searchState.query, state: nil, limit: limit)
|
||
|> delay(0.2, queue: Queue.mainQueue())
|
||
self.searchResult.set(search
|
||
|> map { (result, state) -> (SearchMessagesResult, SearchMessagesState, SearchMessagesLocation)? in
|
||
return (result, state, searchState.location)
|
||
})
|
||
|
||
searchDisposable.set((search
|
||
|> deliverOnMainQueue).start(next: { [weak self] results, updatedState in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
var navigateIndex: MessageIndex?
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in
|
||
if let data = current.search {
|
||
let messageIndices = results.messages.map({ $0.index }).sorted()
|
||
var currentIndex = messageIndices.last
|
||
if let previousResultId = data.resultsState?.currentId {
|
||
for index in messageIndices {
|
||
if index.id >= previousResultId {
|
||
currentIndex = index
|
||
break
|
||
}
|
||
}
|
||
}
|
||
navigateIndex = currentIndex
|
||
return current.updatedSearch(data.withUpdatedResultsState(ChatSearchResultsState(messageIndices: messageIndices, currentId: currentIndex?.id, state: updatedState, totalCount: results.totalCount, completed: results.completed)))
|
||
} else {
|
||
return current
|
||
}
|
||
})
|
||
if let navigateIndex = navigateIndex {
|
||
switch strongSelf.chatLocation {
|
||
case .peer, .replyThread:
|
||
strongSelf.navigateToMessage(from: nil, to: .index(navigateIndex), forceInCurrentChat: true)
|
||
}
|
||
}
|
||
strongSelf.updateItemNodesSearchTextHighlightStates()
|
||
}, completed: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.searching.set(false)
|
||
}
|
||
}))
|
||
}
|
||
} else if previousSearchState?.loadMoreState != searchState.loadMoreState {
|
||
if let loadMoreState = searchState.loadMoreState {
|
||
self.searching.set(true)
|
||
let searchDisposable: MetaDisposable
|
||
if let current = self.searchDisposable {
|
||
searchDisposable = current
|
||
} else {
|
||
searchDisposable = MetaDisposable()
|
||
self.searchDisposable = searchDisposable
|
||
}
|
||
searchDisposable.set((searchMessages(account: self.context.account, location: searchState.location, query: searchState.query, state: loadMoreState, limit: limit)
|
||
|> delay(0.2, queue: Queue.mainQueue())
|
||
|> deliverOnMainQueue).start(next: { [weak self] results, updatedState in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
let complete = results.completed
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in
|
||
if let data = current.search, let previousResultsState = data.resultsState {
|
||
let messageIndices = results.messages.map({ $0.index }).sorted()
|
||
return current.updatedSearch(data.withUpdatedResultsState(ChatSearchResultsState(messageIndices: messageIndices, currentId: previousResultsState.currentId, state: updatedState, totalCount: results.totalCount, completed: results.completed)))
|
||
} else {
|
||
return current
|
||
}
|
||
})
|
||
}, completed: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.searching.set(false)
|
||
}
|
||
}))
|
||
} else {
|
||
self.searching.set(false)
|
||
self.searchDisposable?.set(nil)
|
||
}
|
||
}
|
||
} else {
|
||
self.searching.set(false)
|
||
self.searchDisposable?.set(nil)
|
||
|
||
if let data = interfaceState.search {
|
||
return interfaceState.updatedSearch(data.withUpdatedResultsState(nil))
|
||
}
|
||
}
|
||
}
|
||
self.updateItemNodesSearchTextHighlightStates()
|
||
return nil
|
||
}
|
||
|
||
func scrollToEndOfHistory() {
|
||
let locationInput = ChatHistoryLocationInput(content: .Scroll(index: .upperBound, anchorIndex: .upperBound, sourceIndex: .lowerBound, scrollPosition: .top(0.0), animated: true, highlight: false), id: 0)
|
||
|
||
let historyView = preloadedChatHistoryViewForLocation(locationInput, context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: [])
|
||
let signal = historyView
|
||
|> mapToSignal { historyView -> Signal<(MessageIndex?, Bool), NoError> in
|
||
switch historyView {
|
||
case .Loading:
|
||
return .single((nil, true))
|
||
case .HistoryView:
|
||
return .single((nil, false))
|
||
}
|
||
}
|
||
|> take(until: { index in
|
||
return SignalTakeAction(passthrough: true, complete: !index.1)
|
||
})
|
||
|
||
var cancelImpl: (() -> Void)?
|
||
let presentationData = self.presentationData
|
||
let displayTime = CACurrentMediaTime()
|
||
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
|
||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||
if CACurrentMediaTime() - displayTime > 1.5 {
|
||
cancelImpl?()
|
||
}
|
||
}))
|
||
self?.present(controller, in: .window(.root))
|
||
return ActionDisposable { [weak controller] in
|
||
Queue.mainQueue().async() {
|
||
controller?.dismiss()
|
||
}
|
||
}
|
||
}
|
||
|> runOn(Queue.mainQueue())
|
||
|> delay(0.05, queue: Queue.mainQueue())
|
||
let progressDisposable = MetaDisposable()
|
||
var progressStarted = false
|
||
self.messageIndexDisposable.set((signal
|
||
|> afterDisposed {
|
||
Queue.mainQueue().async {
|
||
progressDisposable.dispose()
|
||
}
|
||
}
|
||
|> deliverOnMainQueue).start(next: { index in
|
||
if index.1 {
|
||
if !progressStarted {
|
||
progressStarted = true
|
||
progressDisposable.set(progressSignal.start())
|
||
}
|
||
}
|
||
}, completed: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.loadingMessage.set(.single(nil))
|
||
strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
||
}
|
||
}))
|
||
cancelImpl = { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.loadingMessage.set(.single(nil))
|
||
strongSelf.messageIndexDisposable.set(nil)
|
||
}
|
||
}
|
||
}
|
||
|
||
func scrollToStartOfHistory() {
|
||
let locationInput = ChatHistoryLocationInput(content: .Scroll(index: .lowerBound, anchorIndex: .lowerBound, sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true, highlight: false), id: 0)
|
||
|
||
let historyView = preloadedChatHistoryViewForLocation(locationInput, context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: [])
|
||
let signal = historyView
|
||
|> mapToSignal { historyView -> Signal<(MessageIndex?, Bool), NoError> in
|
||
switch historyView {
|
||
case .Loading:
|
||
return .single((nil, true))
|
||
case .HistoryView:
|
||
return .single((nil, false))
|
||
}
|
||
}
|
||
|> take(until: { index in
|
||
return SignalTakeAction(passthrough: true, complete: !index.1)
|
||
})
|
||
|
||
var cancelImpl: (() -> Void)?
|
||
let presentationData = self.presentationData
|
||
let displayTime = CACurrentMediaTime()
|
||
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
|
||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||
if CACurrentMediaTime() - displayTime > 1.5 {
|
||
cancelImpl?()
|
||
}
|
||
}))
|
||
self?.present(controller, in: .window(.root))
|
||
return ActionDisposable { [weak controller] in
|
||
Queue.mainQueue().async() {
|
||
controller?.dismiss()
|
||
}
|
||
}
|
||
}
|
||
|> runOn(Queue.mainQueue())
|
||
|> delay(0.05, queue: Queue.mainQueue())
|
||
let progressDisposable = MetaDisposable()
|
||
var progressStarted = false
|
||
self.messageIndexDisposable.set((signal
|
||
|> afterDisposed {
|
||
Queue.mainQueue().async {
|
||
progressDisposable.dispose()
|
||
}
|
||
}
|
||
|> deliverOnMainQueue).start(next: { index in
|
||
if index.1 {
|
||
if !progressStarted {
|
||
progressStarted = true
|
||
progressDisposable.set(progressSignal.start())
|
||
}
|
||
}
|
||
}, completed: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.loadingMessage.set(.single(nil))
|
||
strongSelf.chatDisplayNode.historyNode.scrollToStartOfHistory()
|
||
}
|
||
}))
|
||
cancelImpl = { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.loadingMessage.set(.single(nil))
|
||
strongSelf.messageIndexDisposable.set(nil)
|
||
}
|
||
}
|
||
}
|
||
|
||
func updateDownButtonVisibility() {
|
||
let recordingMediaMessage = self.audioRecorderValue != nil || self.videoRecorderValue != nil
|
||
self.chatDisplayNode.navigateButtons.displayDownButton = self.shouldDisplayDownButton && !recordingMediaMessage
|
||
}
|
||
|
||
func updateTextInputState(_ textInputState: ChatTextInputState) {
|
||
self.updateChatPresentationInterfaceState(interactive: false, { state in
|
||
state.updatedInterfaceState({ state in
|
||
state.withUpdatedComposeInputState(textInputState)
|
||
})
|
||
})
|
||
}
|
||
|
||
private func openMessageReplies(messageId: MessageId, displayProgressInMessage: MessageId?, isChannelPost: Bool, atMessage atMessageId: MessageId?, displayModalProgress: Bool) {
|
||
guard let navigationController = self.navigationController as? NavigationController else {
|
||
return
|
||
}
|
||
|
||
if let displayProgressInMessage = displayProgressInMessage, self.controllerInteraction?.currentMessageWithLoadingReplyThread == displayProgressInMessage {
|
||
return
|
||
}
|
||
|
||
let _ = self.presentVoiceMessageDiscardAlert(action: {
|
||
let progressSignal: Signal<Never, NoError> = Signal { [weak self] _ in
|
||
guard let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction else {
|
||
return EmptyDisposable
|
||
}
|
||
|
||
if let displayProgressInMessage = displayProgressInMessage, controllerInteraction.currentMessageWithLoadingReplyThread != displayProgressInMessage {
|
||
let previousId = controllerInteraction.currentMessageWithLoadingReplyThread
|
||
controllerInteraction.currentMessageWithLoadingReplyThread = displayProgressInMessage
|
||
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(displayProgressInMessage)
|
||
if let previousId = previousId {
|
||
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(previousId)
|
||
}
|
||
}
|
||
|
||
return ActionDisposable {
|
||
Queue.mainQueue().async {
|
||
guard let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction else {
|
||
return
|
||
}
|
||
if let displayProgressInMessage = displayProgressInMessage, controllerInteraction.currentMessageWithLoadingReplyThread == displayProgressInMessage {
|
||
controllerInteraction.currentMessageWithLoadingReplyThread = nil
|
||
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(displayProgressInMessage)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|> runOn(.mainQueue())
|
||
|
||
let progress = (progressSignal
|
||
|> delay(0.15, queue: .mainQueue())).start()
|
||
|
||
self.navigationActionDisposable.set((ChatControllerImpl.openMessageReplies(context: self.context, navigationController: navigationController, present: { [weak self] c, a in
|
||
self?.present(c, in: .window(.root), with: a)
|
||
}, messageId: messageId, isChannelPost: isChannelPost, atMessage: atMessageId, displayModalProgress: displayModalProgress)
|
||
|> afterDisposed {
|
||
progress.dispose()
|
||
}).start())
|
||
})
|
||
}
|
||
|
||
static func openMessageReplies(context: AccountContext, navigationController: NavigationController, present: @escaping (ViewController, Any?) -> Void, messageId: MessageId, isChannelPost: Bool, atMessage atMessageId: MessageId?, displayModalProgress: Bool) -> Signal<Never, NoError> {
|
||
return Signal { subscriber in
|
||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||
|
||
var cancelImpl: (() -> Void)?
|
||
let statusController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||
cancelImpl?()
|
||
}))
|
||
|
||
if displayModalProgress {
|
||
present(statusController, nil)
|
||
}
|
||
|
||
let disposable = (fetchAndPreloadReplyThreadInfo(context: context, subject: isChannelPost ? .channelPost(messageId) : .groupMessage(messageId), atMessageId: atMessageId)
|
||
|> deliverOnMainQueue).start(next: { [weak statusController] result in
|
||
if displayModalProgress {
|
||
statusController?.dismiss()
|
||
}
|
||
|
||
let chatLocation: ChatLocation = .replyThread(result.message)
|
||
|
||
let subject: ChatControllerSubject?
|
||
if let atMessageId = atMessageId {
|
||
subject = .message(id: atMessageId, highlight: true)
|
||
} else if let index = result.scrollToLowerBoundMessage {
|
||
subject = .message(id: index.id, highlight: false)
|
||
} else {
|
||
subject = nil
|
||
}
|
||
|
||
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: chatLocation, chatLocationContextHolder: result.contextHolder, subject: subject, activateInput: result.isEmpty, keepStack: .always))
|
||
subscriber.putCompletion()
|
||
}, error: { _ in
|
||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||
|
||
present(textAlertController(context: context, title: nil, text: presentationData.strings.Channel_DiscussionMessageUnavailable, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||
})
|
||
|
||
cancelImpl = { [weak statusController] in
|
||
disposable.dispose()
|
||
statusController?.dismiss()
|
||
subscriber.putCompletion()
|
||
}
|
||
|
||
return ActionDisposable {
|
||
cancelImpl?()
|
||
}
|
||
}
|
||
|> runOn(.mainQueue())
|
||
}
|
||
|
||
public func navigateToMessage(messageLocation: NavigateToMessageLocation, animated: Bool, forceInCurrentChat: Bool = false, dropStack: Bool = false, completion: (() -> Void)? = nil, customPresentProgress: ((ViewController, Any?) -> Void)? = nil) {
|
||
let scrollPosition: ListViewScrollPosition
|
||
if case .upperBound = messageLocation {
|
||
scrollPosition = .top(0.0)
|
||
} else {
|
||
scrollPosition = .center(.bottom)
|
||
}
|
||
self.navigateToMessage(from: nil, to: messageLocation, scrollPosition: scrollPosition, rememberInStack: false, forceInCurrentChat: forceInCurrentChat, dropStack: dropStack, animated: animated, completion: completion, customPresentProgress: customPresentProgress)
|
||
}
|
||
|
||
private func navigateToMessage(from fromId: MessageId?, to messageLocation: NavigateToMessageLocation, scrollPosition: ListViewScrollPosition = .center(.bottom), rememberInStack: Bool = true, forceInCurrentChat: Bool = false, dropStack: Bool = false, animated: Bool = true, completion: (() -> Void)? = nil, customPresentProgress: ((ViewController, Any?) -> Void)? = nil, statusSubject: ChatLoadingMessageSubject = .generic) {
|
||
if self.isNodeLoaded {
|
||
var fromIndex: MessageIndex?
|
||
|
||
if let fromId = fromId, let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(fromId) {
|
||
fromIndex = message.index
|
||
} else {
|
||
if let message = self.chatDisplayNode.historyNode.anchorMessageInCurrentHistoryView() {
|
||
fromIndex = message.index
|
||
}
|
||
}
|
||
|
||
var isScheduledMessages = false
|
||
var isPinnedMessages = false
|
||
if case .scheduledMessages = self.presentationInterfaceState.subject {
|
||
isScheduledMessages = true
|
||
} else if case .pinnedMessages = self.presentationInterfaceState.subject {
|
||
isPinnedMessages = true
|
||
}
|
||
|
||
var forceInCurrentChat = forceInCurrentChat
|
||
if case let .peer(peerId) = self.chatLocation, messageLocation.peerId == peerId, !isPinnedMessages, !isScheduledMessages {
|
||
forceInCurrentChat = true
|
||
}
|
||
|
||
if isPinnedMessages, let messageId = messageLocation.messageId {
|
||
if let navigationController = self.effectiveNavigationController {
|
||
self.dismiss()
|
||
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(messageId.peerId), subject: .message(id: messageId, highlight: true), keepStack: .always))
|
||
}
|
||
} else if case let .peer(peerId) = self.chatLocation, let messageId = messageLocation.messageId, (messageId.peerId != peerId && !forceInCurrentChat) || (isScheduledMessages && messageId.id != 0 && !Namespaces.Message.allScheduled.contains(messageId.namespace)) {
|
||
if let navigationController = self.effectiveNavigationController {
|
||
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(messageId.peerId), subject: .message(id: messageId, highlight: true), keepStack: .always))
|
||
}
|
||
} else if forceInCurrentChat {
|
||
if let _ = fromId, let fromIndex = fromIndex, rememberInStack {
|
||
self.historyNavigationStack.add(fromIndex)
|
||
}
|
||
|
||
let scrollFromIndex: MessageIndex?
|
||
if let fromIndex = fromIndex {
|
||
scrollFromIndex = fromIndex
|
||
} else if let message = self.chatDisplayNode.historyNode.lastVisbleMesssage() {
|
||
scrollFromIndex = message.index
|
||
} else {
|
||
scrollFromIndex = nil
|
||
}
|
||
|
||
if let scrollFromIndex = scrollFromIndex {
|
||
if let messageId = messageLocation.messageId, let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
|
||
self.loadingMessage.set(.single(nil))
|
||
self.messageIndexDisposable.set(nil)
|
||
self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: message.index, animated: animated, scrollPosition: scrollPosition)
|
||
completion?()
|
||
} else if case let .index(index) = messageLocation, index.id.id == 0, index.timestamp > 0, case .scheduledMessages = self.presentationInterfaceState.subject {
|
||
self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: index, animated: animated, scrollPosition: scrollPosition)
|
||
} else {
|
||
self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue()))
|
||
let searchLocation: ChatHistoryInitialSearchLocation
|
||
switch messageLocation {
|
||
case let .id(id):
|
||
searchLocation = .id(id)
|
||
case let .index(index):
|
||
searchLocation = .index(index)
|
||
case .upperBound:
|
||
searchLocation = .index(MessageIndex.upperBound(peerId: self.chatLocation.peerId))
|
||
}
|
||
var historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(location: searchLocation, count: 50, highlight: true), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: [])
|
||
#if DEBUG
|
||
historyView = historyView |> delay(1.0, queue: .mainQueue())
|
||
#endif
|
||
|
||
let signal = historyView
|
||
|> mapToSignal { historyView -> Signal<(MessageIndex?, Bool), NoError> in
|
||
switch historyView {
|
||
case .Loading:
|
||
return .single((nil, true))
|
||
case let .HistoryView(view, _, _, _, _, _, _):
|
||
for entry in view.entries {
|
||
if entry.message.id == messageLocation.messageId {
|
||
return .single((entry.message.index, false))
|
||
}
|
||
}
|
||
if case let .index(index) = searchLocation {
|
||
return .single((index, false))
|
||
}
|
||
return .single((nil, false))
|
||
}
|
||
}
|
||
|> take(until: { index in
|
||
return SignalTakeAction(passthrough: true, complete: !index.1)
|
||
})
|
||
|
||
var cancelImpl: (() -> Void)?
|
||
let presentationData = self.presentationData
|
||
let displayTime = CACurrentMediaTime()
|
||
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
|
||
if case .generic = statusSubject {
|
||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||
if CACurrentMediaTime() - displayTime > 1.5 {
|
||
cancelImpl?()
|
||
}
|
||
}))
|
||
if let customPresentProgress = customPresentProgress {
|
||
customPresentProgress(controller, nil)
|
||
} else {
|
||
self?.present(controller, in: .window(.root))
|
||
}
|
||
return ActionDisposable { [weak controller] in
|
||
Queue.mainQueue().async() {
|
||
controller?.dismiss()
|
||
}
|
||
}
|
||
} else {
|
||
return EmptyDisposable
|
||
}
|
||
}
|
||
|> runOn(Queue.mainQueue())
|
||
|> delay(0.05, queue: Queue.mainQueue())
|
||
let progressDisposable = MetaDisposable()
|
||
var progressStarted = false
|
||
self.messageIndexDisposable.set((signal
|
||
|> afterDisposed {
|
||
Queue.mainQueue().async {
|
||
progressDisposable.dispose()
|
||
}
|
||
}
|
||
|> deliverOnMainQueue).start(next: { [weak self] index in
|
||
if let strongSelf = self, let index = index.0 {
|
||
strongSelf.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: index, animated: animated, scrollPosition: scrollPosition)
|
||
completion?()
|
||
} else if index.1 {
|
||
if !progressStarted {
|
||
progressStarted = true
|
||
progressDisposable.set(progressSignal.start())
|
||
}
|
||
}
|
||
}, completed: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.loadingMessage.set(.single(nil))
|
||
}
|
||
}))
|
||
cancelImpl = { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.loadingMessage.set(.single(nil))
|
||
strongSelf.messageIndexDisposable.set(nil)
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
completion?()
|
||
}
|
||
} else {
|
||
if let fromIndex = fromIndex {
|
||
let searchLocation: ChatHistoryInitialSearchLocation
|
||
switch messageLocation {
|
||
case let .id(id):
|
||
searchLocation = .id(id)
|
||
case let .index(index):
|
||
searchLocation = .index(index)
|
||
case .upperBound:
|
||
return
|
||
}
|
||
if let _ = fromId, rememberInStack {
|
||
self.historyNavigationStack.add(fromIndex)
|
||
}
|
||
self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue()))
|
||
let historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(location: searchLocation, count: 50, highlight: true), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: [])
|
||
let signal = historyView
|
||
|> mapToSignal { historyView -> Signal<MessageIndex?, NoError> in
|
||
switch historyView {
|
||
case .Loading:
|
||
return .complete()
|
||
case let .HistoryView(view, _, _, _, _, _, _):
|
||
for entry in view.entries {
|
||
if entry.message.id == messageLocation.messageId {
|
||
return .single(entry.message.index)
|
||
}
|
||
}
|
||
return .single(nil)
|
||
}
|
||
}
|
||
|> take(1)
|
||
self.messageIndexDisposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] index in
|
||
if let strongSelf = self {
|
||
if let index = index {
|
||
strongSelf.chatDisplayNode.historyNode.scrollToMessage(from: fromIndex, to: index, animated: animated, scrollPosition: scrollPosition)
|
||
completion?()
|
||
} else {
|
||
if let navigationController = strongSelf.effectiveNavigationController {
|
||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(messageLocation.peerId), subject: messageLocation.messageId.flatMap { .message(id: $0, highlight: true) }))
|
||
}
|
||
completion?()
|
||
}
|
||
}
|
||
}, completed: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.loadingMessage.set(.single(nil))
|
||
}
|
||
}))
|
||
} else {
|
||
if let navigationController = self.effectiveNavigationController {
|
||
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(messageLocation.peerId), subject: messageLocation.messageId.flatMap { .message(id: $0, highlight: true) }))
|
||
}
|
||
completion?()
|
||
}
|
||
}
|
||
} else {
|
||
completion?()
|
||
}
|
||
}
|
||
|
||
private func forwardMessages(messageIds: [MessageId], resetCurrent: Bool = false) {
|
||
let _ = (self.context.account.postbox.transaction { transaction -> [Message] in
|
||
return messageIds.compactMap(transaction.getMessage)
|
||
}
|
||
|> deliverOnMainQueue).start(next: { [weak self] messages in
|
||
self?.forwardMessages(messages: messages, resetCurrent: resetCurrent)
|
||
})
|
||
}
|
||
|
||
private func forwardMessages(messages: [Message], resetCurrent: Bool) {
|
||
let _ = self.presentVoiceMessageDiscardAlert(action: {
|
||
var filter: ChatListNodePeersFilter = [.onlyWriteable, .includeSavedMessages, .excludeDisabled, .doNotSearchMessages]
|
||
var hasPublicPolls = false
|
||
var hasPublicQuiz = false
|
||
for message in messages {
|
||
for media in message.media {
|
||
if let poll = media as? TelegramMediaPoll, case .public = poll.publicity {
|
||
hasPublicPolls = true
|
||
if case .quiz = poll.kind {
|
||
hasPublicQuiz = true
|
||
}
|
||
filter.insert(.excludeChannels)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
var attemptSelectionImpl: ((Peer) -> Void)?
|
||
let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: filter, attemptSelection: { peer in
|
||
attemptSelectionImpl?(peer)
|
||
}))
|
||
let context = self.context
|
||
attemptSelectionImpl = { [weak controller] peer in
|
||
guard let controller = controller else {
|
||
return
|
||
}
|
||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||
if hasPublicPolls {
|
||
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
|
||
controller.present(textAlertController(context: context, title: nil, text: hasPublicQuiz ? presentationData.strings.Forward_ErrorPublicQuizDisabledInChannels : presentationData.strings.Forward_ErrorPublicPollDisabledInChannels, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||
return
|
||
}
|
||
}
|
||
controller.present(textAlertController(context: context, title: nil, text: presentationData.strings.Forward_ErrorDisabledForChat, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||
}
|
||
controller.peerSelected = { [weak self, weak controller] peer in
|
||
let peerId = peer.id
|
||
|
||
guard let strongSelf = self, let strongController = controller else {
|
||
return
|
||
}
|
||
|
||
if resetCurrent {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(nil) }) })
|
||
}
|
||
|
||
var isPinnedMessages = false
|
||
if case .pinnedMessages = strongSelf.presentationInterfaceState.subject {
|
||
isPinnedMessages = true
|
||
}
|
||
|
||
if case .peer(peerId) = strongSelf.chatLocation, strongSelf.parentController == nil, !isPinnedMessages {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(messages.map { $0.id }).withoutSelectionState() }).updatedSearch(nil) })
|
||
strongSelf.updateItemNodesSearchTextHighlightStates()
|
||
strongSelf.searchResultsController = nil
|
||
strongController.dismiss()
|
||
} else if peerId == strongSelf.context.account.peerId {
|
||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||
|
||
let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: messages.map { message -> EnqueueMessage in
|
||
return .forward(source: message.id, grouping: .auto, attributes: [])
|
||
})
|
||
|> deliverOnMainQueue).start(next: { messageIds in
|
||
if let strongSelf = self {
|
||
let signals: [Signal<Bool, NoError>] = messageIds.compactMap({ id -> Signal<Bool, NoError>? in
|
||
guard let id = id else {
|
||
return nil
|
||
}
|
||
return strongSelf.context.account.pendingMessageManager.pendingMessageStatus(id)
|
||
|> mapToSignal { status, _ -> Signal<Bool, NoError> in
|
||
if status != nil {
|
||
return .never()
|
||
} else {
|
||
return .single(true)
|
||
}
|
||
}
|
||
|> take(1)
|
||
})
|
||
if strongSelf.shareStatusDisposable == nil {
|
||
strongSelf.shareStatusDisposable = MetaDisposable()
|
||
}
|
||
strongSelf.shareStatusDisposable?.set((combineLatest(signals)
|
||
|> deliverOnMainQueue).start())
|
||
}
|
||
})
|
||
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) })
|
||
strongController.dismiss()
|
||
} else {
|
||
if let navigationController = strongSelf.navigationController as? NavigationController {
|
||
for controller in navigationController.viewControllers {
|
||
if let maybeChat = controller as? ChatControllerImpl {
|
||
if case .peer(peerId) = maybeChat.chatLocation {
|
||
var isChatPinnedMessages = false
|
||
if case .pinnedMessages = maybeChat.presentationInterfaceState.subject {
|
||
isChatPinnedMessages = true
|
||
}
|
||
if !isChatPinnedMessages {
|
||
maybeChat.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(messages.map { $0.id }).withoutSelectionState() }) })
|
||
strongSelf.dismiss()
|
||
strongController.dismiss()
|
||
return
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
let _ = (strongSelf.context.account.postbox.transaction({ transaction -> Void in
|
||
transaction.updatePeerChatInterfaceState(peerId, update: { currentState in
|
||
if let currentState = currentState as? ChatInterfaceState {
|
||
return currentState.withUpdatedForwardMessageIds(messages.map { $0.id })
|
||
} else {
|
||
return ChatInterfaceState().withUpdatedForwardMessageIds(messages.map { $0.id })
|
||
}
|
||
})
|
||
}) |> deliverOnMainQueue).start(completed: {
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) })
|
||
|
||
let ready = Promise<Bool>()
|
||
|
||
strongSelf.controllerNavigationDisposable.set((ready.get()
|
||
|> SwiftSignalKit.filter { $0 }
|
||
|> take(1)
|
||
|> deliverOnMainQueue).start(next: { _ in
|
||
if let strongController = controller {
|
||
strongController.dismiss()
|
||
}
|
||
}))
|
||
|
||
if let parentController = strongSelf.parentController {
|
||
(parentController.navigationController as? NavigationController)?.replaceTopController(ChatControllerImpl(context: strongSelf.context, chatLocation: .peer(peerId)), animated: false, ready: ready)
|
||
} else {
|
||
strongSelf.effectiveNavigationController?.replaceTopController(ChatControllerImpl(context: strongSelf.context, chatLocation: .peer(peerId)), animated: false, ready: ready)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
self.chatDisplayNode.dismissInput()
|
||
self.effectiveNavigationController?.pushViewController(controller)
|
||
})
|
||
}
|
||
|
||
private func openPeer(peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer, fromMessage: Message?, expandAvatar: Bool = false) {
|
||
let _ = self.presentVoiceMessageDiscardAlert(action: {
|
||
if case let .peer(currentPeerId) = self.chatLocation, peerId == currentPeerId {
|
||
switch navigation {
|
||
case .info:
|
||
self.navigationButtonAction(.openChatInfo(expandAvatar: expandAvatar))
|
||
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)
|
||
})
|
||
default:
|
||
break
|
||
}
|
||
} else {
|
||
if let peerId = peerId {
|
||
do {
|
||
let selfPeerId = self.chatLocation.peerId
|
||
switch navigation {
|
||
case .info, .default:
|
||
let peerSignal: Signal<Peer?, NoError>
|
||
if let fromMessage = fromMessage {
|
||
peerSignal = loadedPeerFromMessage(account: self.context.account, peerId: peerId, messageId: fromMessage.id)
|
||
} else {
|
||
peerSignal = self.context.account.postbox.loadedPeerWithId(peerId) |> map(Optional.init)
|
||
}
|
||
self.navigationActionDisposable.set((peerSignal |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peer in
|
||
if let strongSelf = self, let peer = peer {
|
||
var mode: PeerInfoControllerMode = .generic
|
||
if let _ = fromMessage {
|
||
mode = .group(selfPeerId)
|
||
}
|
||
var expandAvatar = expandAvatar
|
||
if peer.smallProfileImage == nil {
|
||
expandAvatar = false
|
||
}
|
||
if let validLayout = strongSelf.validLayout, validLayout.deviceMetrics.type == .tablet {
|
||
expandAvatar = false
|
||
}
|
||
if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: mode, avatarInitiallyExpanded: expandAvatar, fromChat: false) {
|
||
strongSelf.effectiveNavigationController?.pushViewController(infoController)
|
||
}
|
||
}
|
||
}))
|
||
case let .chat(textInputState, subject, peekData):
|
||
if let textInputState = textInputState {
|
||
let _ = (self.context.account.postbox.transaction({ transaction -> Void in
|
||
transaction.updatePeerChatInterfaceState(peerId, update: { currentState in
|
||
if let currentState = currentState as? ChatInterfaceState {
|
||
return currentState.withUpdatedComposeInputState(textInputState)
|
||
} else {
|
||
return ChatInterfaceState().withUpdatedComposeInputState(textInputState)
|
||
}
|
||
})
|
||
})
|
||
|> deliverOnMainQueue).start(completed: { [weak self] in
|
||
if let strongSelf = self, let navigationController = strongSelf.effectiveNavigationController {
|
||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peerId), subject: subject, updateTextInputState: textInputState, peekData: peekData))
|
||
}
|
||
})
|
||
} else {
|
||
self.effectiveNavigationController?.pushViewController(ChatControllerImpl(context: self.context, chatLocation: .peer(peerId), subject: subject))
|
||
}
|
||
case let .withBotStartPayload(botStart):
|
||
self.effectiveNavigationController?.pushViewController(ChatControllerImpl(context: self.context, chatLocation: .peer(peerId), botStart: botStart))
|
||
}
|
||
}
|
||
} else {
|
||
switch navigation {
|
||
case .info:
|
||
break
|
||
case let .chat(textInputState, _, _):
|
||
if let textInputState = textInputState {
|
||
let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context))
|
||
controller.peerSelected = { [weak self, weak controller] peer in
|
||
let peerId = peer.id
|
||
|
||
if let strongSelf = self, let strongController = controller {
|
||
if case let .peer(currentPeerId) = strongSelf.chatLocation, peerId == currentPeerId {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
return ($0.updatedInterfaceState {
|
||
return $0.withUpdatedComposeInputState(textInputState)
|
||
}).updatedInputMode({ _ in
|
||
return .text
|
||
})
|
||
})
|
||
strongController.dismiss()
|
||
} else {
|
||
let _ = (strongSelf.context.account.postbox.transaction({ transaction -> Void in
|
||
transaction.updatePeerChatInterfaceState(peerId, update: { currentState in
|
||
if let currentState = currentState as? ChatInterfaceState {
|
||
return currentState.withUpdatedComposeInputState(textInputState)
|
||
} else {
|
||
return ChatInterfaceState().withUpdatedComposeInputState(textInputState)
|
||
}
|
||
})
|
||
}) |> deliverOnMainQueue).start(completed: {
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) })
|
||
|
||
let ready = Promise<Bool>()
|
||
|
||
strongSelf.controllerNavigationDisposable.set((ready.get() |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { _ in
|
||
if let strongController = controller {
|
||
strongController.dismiss()
|
||
}
|
||
}))
|
||
|
||
strongSelf.effectiveNavigationController?.replaceTopController(ChatControllerImpl(context: strongSelf.context, chatLocation: .peer(peerId)), animated: false, ready: ready)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}
|
||
self.chatDisplayNode.dismissInput()
|
||
self.effectiveNavigationController?.pushViewController(controller)
|
||
}
|
||
default:
|
||
break
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
private func openPeerMention(_ name: String, navigation: ChatControllerInteractionNavigateToPeer = .default) {
|
||
let _ = self.presentVoiceMessageDiscardAlert(action: {
|
||
let disposable: MetaDisposable
|
||
if let resolvePeerByNameDisposable = self.resolvePeerByNameDisposable {
|
||
disposable = resolvePeerByNameDisposable
|
||
} else {
|
||
disposable = MetaDisposable()
|
||
self.resolvePeerByNameDisposable = disposable
|
||
}
|
||
var resolveSignal = resolvePeerByName(account: self.context.account, name: name, ageLimit: 10)
|
||
|
||
var cancelImpl: (() -> Void)?
|
||
let presentationData = self.presentationData
|
||
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
|
||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||
cancelImpl?()
|
||
}))
|
||
self?.present(controller, in: .window(.root))
|
||
return ActionDisposable { [weak controller] in
|
||
Queue.mainQueue().async() {
|
||
controller?.dismiss()
|
||
}
|
||
}
|
||
}
|
||
|> runOn(Queue.mainQueue())
|
||
|> delay(0.15, queue: Queue.mainQueue())
|
||
let progressDisposable = progressSignal.start()
|
||
|
||
resolveSignal = resolveSignal
|
||
|> afterDisposed {
|
||
Queue.mainQueue().async {
|
||
progressDisposable.dispose()
|
||
}
|
||
}
|
||
cancelImpl = { [weak self] in
|
||
self?.resolvePeerByNameDisposable?.set(nil)
|
||
}
|
||
let account = self.context.account
|
||
disposable.set((resolveSignal
|
||
|> take(1)
|
||
|> mapToSignal { peerId -> Signal<Peer?, NoError> in
|
||
return account.postbox.transaction { transaction -> Peer? in
|
||
if let peerId = peerId {
|
||
return transaction.getPeer(peerId)
|
||
} else {
|
||
return nil
|
||
}
|
||
}
|
||
}
|
||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||
if let strongSelf = self {
|
||
if let peer = peer {
|
||
var navigation = navigation
|
||
if case .default = navigation {
|
||
if let peer = peer as? TelegramUser, peer.botInfo != nil {
|
||
navigation = .chat(textInputState: nil, subject: nil, peekData: nil)
|
||
}
|
||
}
|
||
strongSelf.openResolved(.peer(peer.id, navigation))
|
||
} else {
|
||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Resolve_ErrorNotFound, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||
}
|
||
}
|
||
}))
|
||
})
|
||
}
|
||
|
||
private func unblockPeer() {
|
||
guard case let .peer(peerId) = self.chatLocation else {
|
||
return
|
||
}
|
||
let unblockingPeer = self.unblockingPeer
|
||
unblockingPeer.set(true)
|
||
|
||
var restartBot = false
|
||
if let user = self.presentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil {
|
||
restartBot = true
|
||
}
|
||
self.editMessageDisposable.set((requestUpdatePeerIsBlocked(account: self.context.account, peerId: peerId, isBlocked: false)
|
||
|> afterDisposed({ [weak self] in
|
||
Queue.mainQueue().async {
|
||
unblockingPeer.set(false)
|
||
if let strongSelf = self, restartBot {
|
||
let _ = enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: [.message(text: "/start", attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)]).start()
|
||
}
|
||
}
|
||
})).start())
|
||
}
|
||
|
||
private func reportPeer() {
|
||
guard let renderedPeer = self.presentationInterfaceState.renderedPeer, let peer = renderedPeer.chatMainPeer, let chatPeer = renderedPeer.peer else {
|
||
return
|
||
}
|
||
self.chatDisplayNode.dismissInput()
|
||
|
||
if let peer = peer as? TelegramChannel, let username = peer.username, !username.isEmpty {
|
||
let actionSheet = ActionSheetController(presentationData: self.presentationData)
|
||
|
||
var items: [ActionSheetItem] = []
|
||
items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ReportSpamAndLeave, color: .destructive, action: { [weak self, weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
if let strongSelf = self {
|
||
strongSelf.deleteChat(reportChatSpam: true)
|
||
}
|
||
}))
|
||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
})
|
||
])])
|
||
|
||
self.present(actionSheet, in: .window(.root))
|
||
} else if let _ = peer as? TelegramUser {
|
||
let presentationData = self.presentationData
|
||
let controller = ActionSheetController(presentationData: presentationData)
|
||
let dismissAction: () -> Void = { [weak controller] in
|
||
controller?.dismissAnimated()
|
||
}
|
||
var reportSpam = true
|
||
var deleteChat = true
|
||
var items: [ActionSheetItem] = []
|
||
if !peer.isDeleted {
|
||
items.append(ActionSheetTextItem(title: presentationData.strings.UserInfo_BlockConfirmationTitle(peer.compactDisplayTitle).0))
|
||
}
|
||
items.append(contentsOf: [
|
||
ActionSheetCheckboxItem(title: presentationData.strings.Conversation_Moderate_Report, label: "", value: reportSpam, action: { [weak controller] checkValue in
|
||
reportSpam = checkValue
|
||
controller?.updateItem(groupIndex: 0, itemIndex: 1, { item in
|
||
if let item = item as? ActionSheetCheckboxItem {
|
||
return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action)
|
||
}
|
||
return item
|
||
})
|
||
}),
|
||
ActionSheetCheckboxItem(title: presentationData.strings.ReportSpam_DeleteThisChat, label: "", value: deleteChat, action: { [weak controller] checkValue in
|
||
deleteChat = checkValue
|
||
controller?.updateItem(groupIndex: 0, itemIndex: 2, { item in
|
||
if let item = item as? ActionSheetCheckboxItem {
|
||
return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action)
|
||
}
|
||
return item
|
||
})
|
||
}),
|
||
ActionSheetButtonItem(title: presentationData.strings.UserInfo_BlockActionTitle(peer.compactDisplayTitle).0, color: .destructive, action: { [weak self] in
|
||
dismissAction()
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
let _ = requestUpdatePeerIsBlocked(account: strongSelf.context.account, peerId: peer.id, isBlocked: true).start()
|
||
if let _ = chatPeer as? TelegramSecretChat {
|
||
let _ = (strongSelf.context.account.postbox.transaction { transaction in
|
||
terminateSecretChat(transaction: transaction, peerId: chatPeer.id, requestRemoteHistoryRemoval: true)
|
||
}).start()
|
||
}
|
||
if deleteChat {
|
||
let _ = removePeerChat(account: strongSelf.context.account, peerId: chatPeer.id, reportChatSpam: reportSpam).start()
|
||
strongSelf.effectiveNavigationController?.filterController(strongSelf, animated: true)
|
||
} else if reportSpam {
|
||
let _ = TelegramCore.reportPeer(account: strongSelf.context.account, peerId: peer.id, reason: .spam, message: "").start()
|
||
}
|
||
})
|
||
] as [ActionSheetItem])
|
||
|
||
controller.setItemGroups([
|
||
ActionSheetItemGroup(items: items),
|
||
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
|
||
])
|
||
self.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||
} else {
|
||
let title: String
|
||
var infoString: String?
|
||
if let _ = peer as? TelegramGroup {
|
||
title = self.presentationData.strings.Conversation_ReportSpamAndLeave
|
||
infoString = self.presentationData.strings.Conversation_ReportSpamGroupConfirmation
|
||
} else if let channel = peer as? TelegramChannel {
|
||
title = self.presentationData.strings.Conversation_ReportSpamAndLeave
|
||
if case .group = channel.info {
|
||
infoString = self.presentationData.strings.Conversation_ReportSpamGroupConfirmation
|
||
} else {
|
||
infoString = self.presentationData.strings.Conversation_ReportSpamChannelConfirmation
|
||
}
|
||
} else {
|
||
title = self.presentationData.strings.Conversation_ReportSpam
|
||
infoString = self.presentationData.strings.Conversation_ReportSpamConfirmation
|
||
}
|
||
let actionSheet = ActionSheetController(presentationData: self.presentationData)
|
||
|
||
var items: [ActionSheetItem] = []
|
||
if let infoString = infoString {
|
||
items.append(ActionSheetTextItem(title: infoString))
|
||
}
|
||
items.append(ActionSheetButtonItem(title: title, color: .destructive, action: { [weak self, weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
if let strongSelf = self {
|
||
strongSelf.deleteChat(reportChatSpam: true)
|
||
}
|
||
}))
|
||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
})
|
||
])])
|
||
|
||
self.present(actionSheet, in: .window(.root))
|
||
}
|
||
}
|
||
|
||
private func shareAccountContact() {
|
||
let _ = (self.context.account.postbox.loadedPeerWithId(self.context.account.peerId)
|
||
|> deliverOnMainQueue).start(next: { [weak self] accountPeer in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
guard let user = accountPeer as? TelegramUser, let phoneNumber = user.phone else {
|
||
return
|
||
}
|
||
guard let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramUser else {
|
||
return
|
||
}
|
||
|
||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
|
||
var items: [ActionSheetItem] = []
|
||
items.append(ActionSheetTextItem(title: strongSelf.presentationData.strings.Conversation_ShareMyPhoneNumberConfirmation(formatPhoneNumber(phoneNumber), peer.compactDisplayTitle).0))
|
||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_ShareMyPhoneNumber, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
let _ = (acceptAndShareContact(account: strongSelf.context.account, peerId: peer.id)
|
||
|> deliverOnMainQueue).start(error: { _ in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||
}, completed: {
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.present(OverlayStatusController(theme: strongSelf.presentationData.theme, type: .genericSuccess(strongSelf.presentationData.strings.Conversation_ShareMyPhoneNumber_StatusSuccess(peer.compactDisplayTitle).0, true)), in: .window(.root))
|
||
})
|
||
}))
|
||
|
||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
})
|
||
])])
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
strongSelf.present(actionSheet, in: .window(.root))
|
||
})
|
||
}
|
||
|
||
private func addPeerContact() {
|
||
if let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramUser, let peerStatusSettings = self.presentationInterfaceState.contactStatus?.peerStatusSettings, let contactData = DeviceContactExtendedData(peer: peer) {
|
||
self.present(context.sharedContext.makeDeviceContactInfoController(context: context, subject: .create(peer: peer, contactData: contactData, isSharing: true, shareViaException: peerStatusSettings.contains(.addExceptionWhenAddingContact), completion: { [weak self] peer, stableId, contactData in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
if let peer = peer as? TelegramUser {
|
||
if let phone = peer.phone, !phone.isEmpty {
|
||
}
|
||
|
||
self?.present(OverlayStatusController(theme: strongSelf.presentationData.theme, type: .genericSuccess(strongSelf.presentationData.strings.AddContact_StatusSuccess(peer.compactDisplayTitle).0, true)), in: .window(.root))
|
||
}
|
||
}), completed: nil, cancelled: nil), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||
}
|
||
}
|
||
|
||
private func dismissPeerContactOptions() {
|
||
guard case let .peer(peerId) = self.chatLocation else {
|
||
return
|
||
}
|
||
let dismissPeerId: PeerId
|
||
if let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramUser {
|
||
dismissPeerId = peer.id
|
||
} else {
|
||
dismissPeerId = peerId
|
||
}
|
||
self.editMessageDisposable.set((dismissPeerStatusOptions(account: self.context.account, peerId: dismissPeerId)
|
||
|> afterDisposed({
|
||
Queue.mainQueue().async {
|
||
}
|
||
})).start())
|
||
}
|
||
|
||
private func deleteChat(reportChatSpam: Bool) {
|
||
guard case let .peer(peerId) = self.chatLocation else {
|
||
return
|
||
}
|
||
self.commitPurposefulAction()
|
||
self.chatDisplayNode.historyNode.disconnect()
|
||
let _ = removePeerChat(account: self.context.account, peerId: peerId, reportChatSpam: reportChatSpam).start()
|
||
self.effectiveNavigationController?.popToRoot(animated: true)
|
||
|
||
let _ = requestUpdatePeerIsBlocked(account: self.context.account, peerId: peerId, isBlocked: true).start()
|
||
}
|
||
|
||
private func startBot(_ payload: String?) {
|
||
guard case let .peer(peerId) = self.chatLocation else {
|
||
return
|
||
}
|
||
|
||
let startingBot = self.startingBot
|
||
startingBot.set(true)
|
||
self.editMessageDisposable.set((requestStartBot(account: self.context.account, botPeerId: 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 openResolved(_ result: ResolvedUrl) {
|
||
self.context.sharedContext.openResolvedUrl(result, context: self.context, urlContext: .chat, navigationController: self.effectiveNavigationController, openPeer: { [weak self] peerId, navigation in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
switch navigation {
|
||
case let .chat(_, subject, peekData):
|
||
if case .peer(peerId) = strongSelf.chatLocation {
|
||
if let subject = subject, case let .message(messageId, _) = subject {
|
||
strongSelf.navigateToMessage(from: nil, to: .id(messageId))
|
||
}
|
||
} else if let navigationController = strongSelf.effectiveNavigationController {
|
||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peerId), subject: subject, keepStack: .always, peekData: peekData))
|
||
}
|
||
case .info:
|
||
strongSelf.navigationActionDisposable.set((strongSelf.context.account.postbox.loadedPeerWithId(peerId)
|
||
|> take(1)
|
||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||
if let strongSelf = self, peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil {
|
||
if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) {
|
||
strongSelf.effectiveNavigationController?.pushViewController(infoController)
|
||
}
|
||
}
|
||
}))
|
||
case let .withBotStartPayload(startPayload):
|
||
if case .peer(peerId) = strongSelf.chatLocation {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
$0.updatedBotStartPayload(startPayload.payload)
|
||
})
|
||
} else if let navigationController = strongSelf.effectiveNavigationController {
|
||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peerId), botStart: startPayload))
|
||
}
|
||
default:
|
||
break
|
||
}
|
||
}, sendFile: nil,
|
||
sendSticker: { [weak self] f, sourceNode, sourceRect in
|
||
return self?.interfaceInteraction?.sendSticker(f, sourceNode, sourceRect) ?? false
|
||
}, present: { [weak self] c, a in
|
||
self?.present(c, in: .window(.root), with: a)
|
||
}, dismissInput: { [weak self] in
|
||
self?.chatDisplayNode.dismissInput()
|
||
}, contentContext: nil)
|
||
}
|
||
|
||
private func openUrl(_ url: String, concealed: Bool, message: Message? = nil) {
|
||
self.commitPurposefulAction()
|
||
|
||
let _ = self.presentVoiceMessageDiscardAlert(action: {
|
||
if self.context.sharedContext.immediateExperimentalUISettings.playlistPlayback {
|
||
if url.hasSuffix(".m3u8") {
|
||
let navigationController = self.navigationController as? NavigationController
|
||
|
||
let webPage = TelegramMediaWebpage(
|
||
webpageId: MediaId(namespace: 0, id: 0),
|
||
content: .Loaded(TelegramMediaWebpageLoadedContent(
|
||
url: url,
|
||
displayUrl: url,
|
||
hash: 0,
|
||
type: "video",
|
||
websiteName: nil,
|
||
title: nil,
|
||
text: nil,
|
||
embedUrl: url,
|
||
embedType: "video",
|
||
embedSize: nil,
|
||
duration: nil,
|
||
author: nil,
|
||
image: nil,
|
||
file: nil,
|
||
attributes: [],
|
||
instantPage: nil
|
||
))
|
||
)
|
||
let entry = InstantPageGalleryEntry(
|
||
index: 0,
|
||
pageId: webPage.webpageId,
|
||
media: InstantPageMedia(
|
||
index: 0,
|
||
media: webPage,
|
||
url: nil,
|
||
caption: nil,
|
||
credit: nil
|
||
),
|
||
caption: nil,
|
||
credit: nil,
|
||
location: nil
|
||
)
|
||
|
||
let gallery = InstantPageGalleryController(context: self.context, webPage: webPage, entries: [entry], centralIndex: 0, replaceRootController: { [weak navigationController] controller, ready in
|
||
if let navigationController = navigationController {
|
||
navigationController.replaceTopController(controller, animated: false, ready: ready)
|
||
}
|
||
}, baseNavigationController: navigationController)
|
||
self.present(gallery, in: .window(.root), with: InstantPageGalleryControllerPresentationArguments(transitionArguments: { entry -> GalleryTransitionArguments? in
|
||
return nil
|
||
}))
|
||
return;
|
||
}
|
||
}
|
||
|
||
openUserGeneratedUrl(context: self.context, url: url, concealed: concealed, present: { [weak self] c in
|
||
self?.present(c, in: .window(.root))
|
||
}, openResolved: { [weak self] resolved in
|
||
self?.openResolved(resolved)
|
||
})
|
||
}, performAction: true)
|
||
}
|
||
|
||
private func openUrlIn(_ url: String) {
|
||
let actionSheet = OpenInActionSheetController(context: self.context, item: .url(url: url), openUrl: { [weak self] url in
|
||
if let strongSelf = self, let navigationController = strongSelf.effectiveNavigationController {
|
||
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: strongSelf.presentationData, navigationController: navigationController, dismissInput: {
|
||
self?.chatDisplayNode.dismissInput()
|
||
})
|
||
}
|
||
})
|
||
self.chatDisplayNode.dismissInput()
|
||
self.present(actionSheet, in: .window(.root))
|
||
}
|
||
|
||
func avatarPreviewingController(from sourceView: UIView) -> (UIViewController, CGRect)? {
|
||
guard let layout = self.validLayout else {
|
||
return nil
|
||
}
|
||
guard let buttonView = (self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.view else {
|
||
return nil
|
||
}
|
||
if let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer, peer.smallProfileImage != nil {
|
||
let galleryController = AvatarGalleryController(context: self.context, peer: peer, remoteEntries: nil, replaceRootController: { controller, ready in
|
||
}, synchronousLoad: true)
|
||
galleryController.setHintWillBePresentedInPreviewingContext(true)
|
||
galleryController.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height), metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: .immediate)
|
||
return (galleryController, buttonView.convert(buttonView.bounds, to: sourceView))
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func previewingController(from sourceView: UIView, for location: CGPoint) -> (UIViewController, CGRect)? {
|
||
guard let layout = self.validLayout, case .phone = layout.deviceMetrics.type, let view = self.chatDisplayNode.view.hitTest(location, with: nil), view.isDescendant(of: self.chatDisplayNode.historyNode.view) else {
|
||
return nil
|
||
}
|
||
|
||
let historyPoint = sourceView.convert(location, to: self.chatDisplayNode.historyNode.view)
|
||
var result: (Message, ChatMessagePeekPreviewContent)?
|
||
self.chatDisplayNode.historyNode.forEachItemNode { itemNode in
|
||
if let itemNode = itemNode as? ChatMessageItemView {
|
||
if itemNode.frame.contains(historyPoint) {
|
||
if let value = itemNode.peekPreviewContent(at: self.chatDisplayNode.historyNode.view.convert(historyPoint, to: itemNode.view)) {
|
||
result = value
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if let (message, content) = result {
|
||
switch content {
|
||
case let .media(media):
|
||
var selectedTransitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
|
||
self.chatDisplayNode.historyNode.forEachItemNode { itemNode in
|
||
if let itemNode = itemNode as? ChatMessageItemView {
|
||
if let result = itemNode.transitionNode(id: message.id, media: media) {
|
||
selectedTransitionNode = result
|
||
}
|
||
}
|
||
}
|
||
|
||
if let selectedTransitionNode = selectedTransitionNode {
|
||
if let previewData = chatMessagePreviewControllerData(context: self.context, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: self.effectiveNavigationController) {
|
||
switch previewData {
|
||
case let .gallery(gallery):
|
||
gallery.setHintWillBePresentedInPreviewingContext(true)
|
||
let rect = selectedTransitionNode.0.view.convert(selectedTransitionNode.0.bounds, to: sourceView)
|
||
let sourceRect = rect.insetBy(dx: -2.0, dy: -2.0)
|
||
gallery.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height), metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: .immediate)
|
||
return (gallery, sourceRect)
|
||
case .instantPage:
|
||
break
|
||
}
|
||
}
|
||
}
|
||
case let .url(node, rect, string, concealed):
|
||
var parsedUrlValue: URL?
|
||
if let parsed = URL(string: string) {
|
||
parsedUrlValue = parsed
|
||
} else if let encoded = (string as NSString).addingPercentEscapes(using: String.Encoding.utf8.rawValue), let parsed = URL(string: encoded) {
|
||
parsedUrlValue = parsed
|
||
}
|
||
|
||
if let parsedUrlValue = parsedUrlValue {
|
||
if concealed, (parsedUrlValue.scheme == "http" || parsedUrlValue.scheme == "https"), !isConcealedUrlWhitelisted(parsedUrlValue) {
|
||
return nil
|
||
}
|
||
} else {
|
||
return nil
|
||
}
|
||
|
||
let targetRect = node.view.convert(rect, to: sourceView)
|
||
let sourceRect = CGRect(origin: CGPoint(x: floor(targetRect.midX), y: floor(targetRect.midY)), size: CGSize(width: 1.0, height: 1.0))
|
||
if let parsedUrl = parsedUrlValue {
|
||
if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" {
|
||
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
|
||
let controller = SFSafariViewController(url: parsedUrl)
|
||
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
|
||
controller.preferredBarTintColor = self.presentationData.theme.rootController.navigationBar.backgroundColor
|
||
controller.preferredControlTintColor = self.presentationData.theme.rootController.navigationBar.accentTextColor
|
||
}
|
||
return (controller, sourceRect)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func previewingCommit(_ viewControllerToCommit: UIViewController) {
|
||
if let gallery = viewControllerToCommit as? AvatarGalleryController {
|
||
self.chatDisplayNode.dismissInput()
|
||
gallery.setHintWillBePresentedInPreviewingContext(false)
|
||
self.present(gallery, in: .window(.root), with: AvatarGalleryControllerPresentationArguments(animated: false, transitionArguments: { _ in
|
||
return nil
|
||
}))
|
||
} else if let gallery = viewControllerToCommit as? GalleryController {
|
||
self.chatDisplayNode.dismissInput()
|
||
gallery.setHintWillBePresentedInPreviewingContext(false)
|
||
|
||
self.present(gallery, in: .window(.root), with: GalleryControllerPresentationArguments(animated: false, transitionArguments: { [weak self] messageId, media in
|
||
if let strongSelf = self {
|
||
var selectedTransitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
|
||
strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in
|
||
if let itemNode = itemNode as? ChatMessageItemView {
|
||
if let result = itemNode.transitionNode(id: messageId, media: media) {
|
||
selectedTransitionNode = result
|
||
}
|
||
}
|
||
}
|
||
if let selectedTransitionNode = selectedTransitionNode {
|
||
return GalleryTransitionArguments(transitionNode: selectedTransitionNode, addToTransitionSurface: { view in
|
||
if let strongSelf = self {
|
||
strongSelf.chatDisplayNode.historyNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.chatDisplayNode.historyNode.view)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
return nil
|
||
}))
|
||
}
|
||
|
||
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
|
||
if let safariController = viewControllerToCommit as? SFSafariViewController {
|
||
if let window = self.effectiveNavigationController?.view.window {
|
||
window.rootViewController?.present(safariController, animated: true)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private func presentBanMessageOptions(accountPeerId: PeerId, author: Peer, messageIds: Set<MessageId>, options: ChatAvailableMessageActionOptions) {
|
||
let peerId = self.chatLocation.peerId
|
||
do {
|
||
self.navigationActionDisposable.set((fetchChannelParticipant(account: self.context.account, peerId: peerId, participantId: author.id)
|
||
|> deliverOnMainQueue).start(next: { [weak self] participant in
|
||
if let strongSelf = self {
|
||
let canBan = participant?.canBeBannedBy(peerId: accountPeerId) ?? true
|
||
|
||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
|
||
var items: [ActionSheetItem] = []
|
||
|
||
var actions = Set<Int>([0])
|
||
|
||
let toggleCheck: (Int, Int) -> Void = { [weak actionSheet] category, itemIndex in
|
||
if actions.contains(category) {
|
||
actions.remove(category)
|
||
} else {
|
||
actions.insert(category)
|
||
}
|
||
actionSheet?.updateItem(groupIndex: 0, itemIndex: itemIndex, { item in
|
||
if let item = item as? ActionSheetCheckboxItem {
|
||
return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action)
|
||
}
|
||
return item
|
||
})
|
||
}
|
||
|
||
var itemIndex = 0
|
||
var categories: [Int] = [0]
|
||
if canBan {
|
||
categories.append(1)
|
||
}
|
||
categories.append(contentsOf: [2, 3])
|
||
|
||
for categoryId in categories as [Int] {
|
||
var title = ""
|
||
if categoryId == 0 {
|
||
title = strongSelf.presentationData.strings.Conversation_Moderate_Delete
|
||
} else if categoryId == 1 {
|
||
title = strongSelf.presentationData.strings.Conversation_Moderate_Ban
|
||
} else if categoryId == 2 {
|
||
title = strongSelf.presentationData.strings.Conversation_Moderate_Report
|
||
} else if categoryId == 3 {
|
||
title = strongSelf.presentationData.strings.Conversation_Moderate_DeleteAllMessages(author.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).0
|
||
}
|
||
let index = itemIndex
|
||
items.append(ActionSheetCheckboxItem(title: title, label: "", value: actions.contains(categoryId), action: { value in
|
||
toggleCheck(categoryId, index)
|
||
}))
|
||
itemIndex += 1
|
||
}
|
||
|
||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Done, action: { [weak self, weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
||
if actions.contains(3) {
|
||
let mediaBox = strongSelf.context.account.postbox.mediaBox
|
||
let _ = strongSelf.context.account.postbox.transaction({ transaction -> Void in
|
||
deleteAllMessagesWithAuthor(transaction: transaction, mediaBox: mediaBox, peerId: peerId, authorId: author.id, namespace: Namespaces.Message.Cloud)
|
||
}).start()
|
||
let _ = clearAuthorHistory(account: strongSelf.context.account, peerId: peerId, memberId: author.id).start()
|
||
} else if actions.contains(0) {
|
||
let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forEveryone).start()
|
||
}
|
||
if actions.contains(1) {
|
||
let _ = removePeerMember(account: strongSelf.context.account, peerId: peerId, memberId: author.id).start()
|
||
}
|
||
}
|
||
}))
|
||
|
||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
})
|
||
])])
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
strongSelf.present(actionSheet, in: .window(.root))
|
||
}
|
||
}))
|
||
}
|
||
}
|
||
|
||
private func presentDeleteMessageOptions(messageIds: Set<MessageId>, options: ChatAvailableMessageActionOptions, contextController: ContextController?, completion: @escaping (ContextMenuActionResult) -> Void) {
|
||
let actionSheet = ActionSheetController(presentationData: self.presentationData)
|
||
var items: [ActionSheetItem] = []
|
||
var personalPeerName: String?
|
||
var isChannel = false
|
||
if let user = self.presentationInterfaceState.renderedPeer?.peer as? TelegramUser {
|
||
personalPeerName = user.compactDisplayTitle
|
||
} else if let peer = self.presentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat, let associatedPeerId = peer.associatedPeerId, let user = self.presentationInterfaceState.renderedPeer?.peers[associatedPeerId] as? TelegramUser {
|
||
personalPeerName = user.compactDisplayTitle
|
||
} else if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, case .broadcast = channel.info {
|
||
isChannel = true
|
||
}
|
||
|
||
if options.contains(.cancelSending) {
|
||
items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ContextMenuCancelSending, color: .destructive, action: { [weak self, weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
||
let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forEveryone).start()
|
||
}
|
||
}))
|
||
}
|
||
|
||
var contextItems: [ContextMenuItem] = []
|
||
var canDisplayContextMenu = true
|
||
|
||
var unsendPersonalMessages = false
|
||
if options.contains(.unsendPersonal) {
|
||
canDisplayContextMenu = false
|
||
items.append(ActionSheetTextItem(title: self.presentationData.strings.Chat_UnsendMyMessagesAlertTitle(personalPeerName ?? "").0))
|
||
items.append(ActionSheetSwitchItem(title: self.presentationData.strings.Chat_UnsendMyMessages, isOn: false, action: { value in
|
||
unsendPersonalMessages = value
|
||
}))
|
||
} else if options.contains(.deleteGlobally) {
|
||
let globalTitle: String
|
||
if isChannel {
|
||
globalTitle = self.presentationData.strings.Conversation_DeleteMessagesForEveryone
|
||
} else if let personalPeerName = personalPeerName {
|
||
globalTitle = self.presentationData.strings.Conversation_DeleteMessagesFor(personalPeerName).0
|
||
} else {
|
||
globalTitle = self.presentationData.strings.Conversation_DeleteMessagesForEveryone
|
||
}
|
||
contextItems.append(.action(ContextMenuActionItem(text: globalTitle, textColor: .destructive, icon: { _ in nil }, action: { [weak self] _, f in
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
||
let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forEveryone).start()
|
||
f(.dismissWithoutContent)
|
||
}
|
||
})))
|
||
items.append(ActionSheetButtonItem(title: globalTitle, color: .destructive, action: { [weak self, weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
||
let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forEveryone).start()
|
||
}
|
||
}))
|
||
}
|
||
if options.contains(.deleteLocally) {
|
||
var localOptionText = self.presentationData.strings.Conversation_DeleteMessagesForMe
|
||
if case .scheduledMessages = self.presentationInterfaceState.subject {
|
||
localOptionText = messageIds.count > 1 ? self.presentationData.strings.ScheduledMessages_DeleteMany : self.presentationData.strings.ScheduledMessages_Delete
|
||
} else {
|
||
if options.contains(.unsendPersonal) {
|
||
localOptionText = self.presentationData.strings.Chat_DeleteMessagesConfirmation(Int32(messageIds.count))
|
||
} else if case .peer(self.context.account.peerId) = self.chatLocation {
|
||
if messageIds.count == 1 {
|
||
localOptionText = self.presentationData.strings.Conversation_Moderate_Delete
|
||
} else {
|
||
localOptionText = self.presentationData.strings.Conversation_DeleteManyMessages
|
||
}
|
||
}
|
||
}
|
||
contextItems.append(.action(ContextMenuActionItem(text: localOptionText, textColor: .destructive, icon: { _ in nil }, action: { [weak self] _, f in
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
||
let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).start()
|
||
f(.dismissWithoutContent)
|
||
}
|
||
})))
|
||
items.append(ActionSheetButtonItem(title: localOptionText, color: .destructive, action: { [weak self, weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
||
let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).start()
|
||
}
|
||
}))
|
||
}
|
||
|
||
if canDisplayContextMenu, let contextController = contextController {
|
||
contextController.setItems(.single(contextItems))
|
||
} else {
|
||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
})
|
||
])])
|
||
self.chatDisplayNode.dismissInput()
|
||
self.present(actionSheet, in: .window(.root))
|
||
completion(.default)
|
||
}
|
||
}
|
||
|
||
private func presentClearCacheSuggestion() {
|
||
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
|
||
return
|
||
}
|
||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) })
|
||
|
||
let actionSheet = ActionSheetController(presentationData: self.presentationData)
|
||
var items: [ActionSheetItem] = []
|
||
|
||
items.append(DeleteChatPeerActionSheetItem(context: self.context, peer: peer, chatPeer: peer, action: .clearCacheSuggestion, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder))
|
||
|
||
var presented = false
|
||
items.append(ActionSheetButtonItem(title: self.presentationData.strings.ClearCache_FreeSpace, color: .accent, action: { [weak self, weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
if let strongSelf = self, !presented {
|
||
presented = true
|
||
strongSelf.push(storageUsageController(context: strongSelf.context, isModal: true))
|
||
}
|
||
}))
|
||
|
||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||
actionSheet?.dismissAnimated()
|
||
})
|
||
])])
|
||
self.chatDisplayNode.dismissInput()
|
||
self.presentInGlobalOverlay(actionSheet)
|
||
}
|
||
|
||
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
|
||
public func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool {
|
||
return session.hasItemsConforming(toTypeIdentifiers: [kUTTypeImage as String])
|
||
}
|
||
|
||
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
|
||
public func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal {
|
||
if !canSendMessagesToChat(self.presentationInterfaceState) {
|
||
return UIDropProposal(operation: .cancel)
|
||
}
|
||
|
||
//let dropLocation = session.location(in: self.chatDisplayNode.view)
|
||
self.chatDisplayNode.updateDropInteraction(isActive: true)
|
||
|
||
let operation: UIDropOperation
|
||
operation = .copy
|
||
return UIDropProposal(operation: operation)
|
||
}
|
||
|
||
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
|
||
public func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
|
||
session.loadObjects(ofClass: UIImage.self) { [weak self] imageItems in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
let images = imageItems as! [UIImage]
|
||
|
||
strongSelf.chatDisplayNode.updateDropInteraction(isActive: false)
|
||
strongSelf.displayPasteMenu(images)
|
||
}
|
||
}
|
||
|
||
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
|
||
public func dropInteraction(_ interaction: UIDropInteraction, sessionDidExit session: UIDropSession) {
|
||
self.chatDisplayNode.updateDropInteraction(isActive: false)
|
||
}
|
||
|
||
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
|
||
public func dropInteraction(_ interaction: UIDropInteraction, sessionDidEnd session: UIDropSession) {
|
||
self.chatDisplayNode.updateDropInteraction(isActive: false)
|
||
}
|
||
|
||
public func beginMessageSearch(_ query: String) {
|
||
self.interfaceInteraction?.beginMessageSearch(.everything, query)
|
||
}
|
||
|
||
public func beginReportSelection(reason: ReportReason) {
|
||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedReportReason(reason).updatedInterfaceState { $0.withUpdatedSelectedMessages([]) } })
|
||
}
|
||
|
||
private func displayMediaRecordingTooltip() {
|
||
let rect: CGRect? = self.chatDisplayNode.frameForInputActionButton()
|
||
|
||
let updatedMode: ChatTextInputMediaRecordingButtonMode = self.presentationInterfaceState.interfaceState.mediaRecordingMode
|
||
|
||
let text: String
|
||
if updatedMode == .audio {
|
||
text = self.presentationData.strings.Conversation_HoldForAudio
|
||
} else {
|
||
text = self.presentationData.strings.Conversation_HoldForVideo
|
||
}
|
||
|
||
if let tooltipController = self.mediaRecordingModeTooltipController {
|
||
tooltipController.updateContent(.text(text), animated: true, extendTimer: true)
|
||
} else if let rect = rect {
|
||
let tooltipController = TooltipController(content: .text(text), baseFontSize: self.presentationData.listsFontSize.baseDisplaySize)
|
||
self.mediaRecordingModeTooltipController = tooltipController
|
||
tooltipController.dismissed = { [weak self, weak tooltipController] _ in
|
||
if let strongSelf = self, let tooltipController = tooltipController, strongSelf.mediaRecordingModeTooltipController === tooltipController {
|
||
strongSelf.mediaRecordingModeTooltipController = nil
|
||
}
|
||
}
|
||
self.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
|
||
if let strongSelf = self {
|
||
return (strongSelf.chatDisplayNode, rect)
|
||
}
|
||
return nil
|
||
}))
|
||
}
|
||
}
|
||
|
||
private func displaySendingOptionsTooltip() {
|
||
guard let rect = self.chatDisplayNode.frameForInputActionButton(), self.effectiveNavigationController?.topViewController === self else {
|
||
return
|
||
}
|
||
self.sendingOptionsTooltipController?.dismiss()
|
||
let tooltipController = TooltipController(content: .text(self.presentationData.strings.Conversation_SendingOptionsTooltip), baseFontSize: self.presentationData.listsFontSize.baseDisplaySize, timeout: 3.0, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true)
|
||
self.sendingOptionsTooltipController = tooltipController
|
||
tooltipController.dismissed = { [weak self, weak tooltipController] _ in
|
||
if let strongSelf = self, let tooltipController = tooltipController, strongSelf.sendingOptionsTooltipController === tooltipController {
|
||
strongSelf.sendingOptionsTooltipController = nil
|
||
}
|
||
}
|
||
self.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
|
||
if let strongSelf = self {
|
||
return (strongSelf.chatDisplayNode, rect)
|
||
}
|
||
return nil
|
||
}))
|
||
}
|
||
|
||
private func displayChecksTooltip() {
|
||
self.checksTooltipController?.dismiss()
|
||
|
||
var latestNode: (Int32, ASDisplayNode)?
|
||
self.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
|
||
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item, let statusNode = itemNode.getStatusNode() {
|
||
if let (latestTimestamp, _) = latestNode {
|
||
if item.message.timestamp > latestTimestamp {
|
||
latestNode = (item.message.timestamp, statusNode)
|
||
}
|
||
} else {
|
||
latestNode = (item.message.timestamp, statusNode)
|
||
}
|
||
}
|
||
}
|
||
|
||
if let (_, latestStatusNode) = latestNode {
|
||
let bounds = latestStatusNode.view.convert(latestStatusNode.view.bounds, to: self.chatDisplayNode.view)
|
||
let location = CGPoint(x: bounds.maxX - 7.0, y: bounds.minY + 2.0)
|
||
|
||
let contentNode = ChatStatusChecksTooltipContentNode(presentationData: self.presentationData)
|
||
let tooltipController = TooltipController(content: .custom(contentNode), baseFontSize: self.presentationData.listsFontSize.baseDisplaySize, timeout: 3.5, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true)
|
||
self.checksTooltipController = tooltipController
|
||
tooltipController.dismissed = { [weak self, weak tooltipController] _ in
|
||
if let strongSelf = self, let tooltipController = tooltipController, strongSelf.checksTooltipController === tooltipController {
|
||
strongSelf.checksTooltipController = nil
|
||
// ApplicationSpecificNotice.setVolumeButtonToUnmute(accountManager: strongSelf.context.sharedContext.accountManager)
|
||
}
|
||
}
|
||
self.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
|
||
if let strongSelf = self {
|
||
return (strongSelf.chatDisplayNode, CGRect(origin: location, size: CGSize()))
|
||
}
|
||
return nil
|
||
}))
|
||
}
|
||
}
|
||
|
||
private func dismissAllTooltips() {
|
||
self.sendingOptionsTooltipController?.dismiss()
|
||
self.searchResultsTooltipController?.dismiss()
|
||
self.messageTooltipController?.dismiss()
|
||
self.videoUnmuteTooltipController?.dismiss()
|
||
self.silentPostTooltipController?.dismiss()
|
||
self.mediaRecordingModeTooltipController?.dismiss()
|
||
self.mediaRestrictedTooltipController?.dismiss()
|
||
self.checksTooltipController?.dismiss()
|
||
|
||
self.window?.forEachController({ controller in
|
||
if let controller = controller as? UndoOverlayController {
|
||
controller.dismissWithCommitAction()
|
||
}
|
||
})
|
||
self.forEachController({ controller in
|
||
if let controller = controller as? UndoOverlayController {
|
||
controller.dismissWithCommitAction()
|
||
}
|
||
if let controller = controller as? TooltipScreen {
|
||
controller.dismiss()
|
||
}
|
||
return true
|
||
})
|
||
}
|
||
|
||
private func commitPurposefulAction() {
|
||
if let purposefulAction = self.purposefulAction {
|
||
self.purposefulAction = nil
|
||
purposefulAction()
|
||
}
|
||
}
|
||
|
||
public override var keyShortcuts: [KeyShortcut] {
|
||
if !self.traceVisibility() || !isTopmostChatController(self) {
|
||
return []
|
||
}
|
||
|
||
let strings = self.presentationData.strings
|
||
|
||
var inputShortcuts: [KeyShortcut]
|
||
if self.chatDisplayNode.isInputViewFocused {
|
||
inputShortcuts = [
|
||
KeyShortcut(title: strings.KeyCommand_SendMessage, input: "\r", action: {}),
|
||
KeyShortcut(input: "B", modifiers: [.command], action: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
||
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.bold), inputMode)
|
||
}
|
||
}
|
||
}),
|
||
KeyShortcut(input: "I", modifiers: [.command], action: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
||
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.italic), inputMode)
|
||
}
|
||
}
|
||
}),
|
||
KeyShortcut(input: "M", modifiers: [.shift, .command], action: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
||
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.monospace), inputMode)
|
||
}
|
||
}
|
||
}),
|
||
KeyShortcut(input: "K", modifiers: [.command], action: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.interfaceInteraction?.openLinkEditing()
|
||
}
|
||
}),
|
||
KeyShortcut(input: "N", modifiers: [.shift, .command], action: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
||
return (chatTextInputClearFormattingAttributes(current), inputMode)
|
||
}
|
||
}
|
||
})
|
||
]
|
||
} else if UIResponder.currentFirst() == nil {
|
||
inputShortcuts = [
|
||
KeyShortcut(title: strings.KeyCommand_FocusOnInputField, input: "\r", action: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
|
||
return state.updatedInterfaceState { interfaceState in
|
||
return interfaceState.withUpdatedEffectiveInputState(interfaceState.effectiveInputState)
|
||
}.updatedInputMode({ _ in .text })
|
||
})
|
||
}
|
||
}),
|
||
KeyShortcut(input: "/", modifiers: [], action: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
|
||
if state.interfaceState.effectiveInputState.inputText.length == 0 {
|
||
return state.updatedInterfaceState { interfaceState in
|
||
let effectiveInputState = ChatTextInputState(inputText: NSAttributedString(string: "/"))
|
||
return interfaceState.withUpdatedEffectiveInputState(effectiveInputState)
|
||
}.updatedInputMode({ _ in .text })
|
||
} else {
|
||
return state
|
||
}
|
||
})
|
||
}
|
||
}),
|
||
KeyShortcut(input: "2", modifiers: [.shift], action: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
|
||
if state.interfaceState.effectiveInputState.inputText.length == 0 {
|
||
return state.updatedInterfaceState { interfaceState in
|
||
let effectiveInputState = ChatTextInputState(inputText: NSAttributedString(string: "@"))
|
||
return interfaceState.withUpdatedEffectiveInputState(effectiveInputState)
|
||
}.updatedInputMode({ _ in .text })
|
||
} else {
|
||
return state
|
||
}
|
||
})
|
||
}
|
||
}),
|
||
KeyShortcut(input: "3", modifiers: [.shift], action: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
|
||
if state.interfaceState.effectiveInputState.inputText.length == 0 {
|
||
return state.updatedInterfaceState { interfaceState in
|
||
let effectiveInputState = ChatTextInputState(inputText: NSAttributedString(string: "#"))
|
||
return interfaceState.withUpdatedEffectiveInputState(effectiveInputState)
|
||
}.updatedInputMode({ _ in .text })
|
||
} else {
|
||
return state
|
||
}
|
||
})
|
||
}
|
||
})
|
||
]
|
||
} else {
|
||
inputShortcuts = []
|
||
}
|
||
|
||
var canEdit = false
|
||
if self.presentationInterfaceState.interfaceState.effectiveInputState.inputText.length == 0 && self.presentationInterfaceState.interfaceState.editMessage == nil {
|
||
canEdit = true
|
||
}
|
||
|
||
if canEdit, let message = self.chatDisplayNode.historyNode.firstMessageForEditInCurrentHistoryView() {
|
||
inputShortcuts.append(KeyShortcut(input: UIKeyCommand.inputUpArrow, action: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.interfaceInteraction?.setupEditMessage(message.id, { _ in })
|
||
}
|
||
}))
|
||
}
|
||
|
||
let otherShortcuts: [KeyShortcut] = [
|
||
KeyShortcut(title: strings.KeyCommand_ChatInfo, input: "I", modifiers: [.command, .control], action: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.interfaceInteraction?.openPeerInfo()
|
||
}
|
||
}),
|
||
KeyShortcut(input: "/", modifiers: [.command], action: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
|
||
return state.updatedInterfaceState { interfaceState in
|
||
return interfaceState.withUpdatedEffectiveInputState(interfaceState.effectiveInputState)
|
||
}.updatedInputMode({ _ in ChatInputMode.media(mode: .other, expanded: nil) })
|
||
})
|
||
}
|
||
}),
|
||
KeyShortcut(title: strings.KeyCommand_SearchInChat, input: "F", modifiers: [.command], action: { [weak self] in
|
||
if let strongSelf = self {
|
||
strongSelf.beginMessageSearch("")
|
||
}
|
||
})
|
||
]
|
||
|
||
return inputShortcuts + otherShortcuts
|
||
}
|
||
|
||
public override func joinGroupCall(peerId: PeerId, info: GroupCallInfo) {
|
||
let _ = self.presentVoiceMessageDiscardAlert(action: {
|
||
super.joinGroupCall(peerId: peerId, info: info)
|
||
})
|
||
}
|
||
|
||
public func getTransitionInfo(messageId: MessageId, media: Media) -> ((UIView) -> Void, ASDisplayNode, () -> (UIView?, UIView?))? {
|
||
var selectedNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
|
||
self.chatDisplayNode.historyNode.forEachItemNode { itemNode in
|
||
if let itemNode = itemNode as? ChatMessageItemView {
|
||
if let result = itemNode.transitionNode(id: messageId, media: media) {
|
||
selectedNode = result
|
||
}
|
||
}
|
||
}
|
||
if let (node, _, get) = selectedNode {
|
||
return ({ [weak self] view in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.chatDisplayNode.historyNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.chatDisplayNode.historyNode.view)
|
||
}, node, get)
|
||
} else {
|
||
return nil
|
||
}
|
||
}
|
||
|
||
func activateInput() {
|
||
if self.didAppear {
|
||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
|
||
return state.updatedInputMode({ _ in .text })
|
||
})
|
||
} else {
|
||
self.scheduledActivateInput = true
|
||
}
|
||
}
|
||
|
||
private func clearInputText() {
|
||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
|
||
if !state.interfaceState.effectiveInputState.inputText.string.isEmpty {
|
||
return state.updatedInterfaceState { interfaceState in
|
||
let effectiveInputState = ChatTextInputState(inputText: NSAttributedString(string: ""))
|
||
return interfaceState.withUpdatedEffectiveInputState(effectiveInputState)
|
||
}
|
||
} else {
|
||
return state
|
||
}
|
||
})
|
||
}
|
||
|
||
private func updateReminderActivity() {
|
||
if self.isReminderActivityEnabled && false {
|
||
if #available(iOS 9.0, *) {
|
||
if self.reminderActivity == nil, case let .peer(peerId) = self.chatLocation, let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer {
|
||
let reminderActivity = NSUserActivity(activityType: "RemindAboutChatIntent")
|
||
self.reminderActivity = reminderActivity
|
||
if peer is TelegramGroup {
|
||
reminderActivity.title = self.presentationData.strings.Activity_RemindAboutGroup(peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)).0
|
||
} else if let channel = peer as? TelegramChannel {
|
||
if case .broadcast = channel.info {
|
||
reminderActivity.title = self.presentationData.strings.Activity_RemindAboutChannel(peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)).0
|
||
} else {
|
||
reminderActivity.title = self.presentationData.strings.Activity_RemindAboutGroup(peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)).0
|
||
}
|
||
} else {
|
||
reminderActivity.title = self.presentationData.strings.Activity_RemindAboutUser(peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)).0
|
||
}
|
||
reminderActivity.userInfo = ["peerId": peerId.toInt64(), "peerTitle": peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)]
|
||
reminderActivity.isEligibleForHandoff = true
|
||
reminderActivity.becomeCurrent()
|
||
}
|
||
}
|
||
} else if let reminderActivity = self.reminderActivity {
|
||
self.reminderActivity = nil
|
||
reminderActivity.invalidate()
|
||
}
|
||
}
|
||
|
||
private func updateSlowmodeStatus() {
|
||
if let slowmodeState = self.presentationInterfaceState.slowmodeState, case let .timestamp(slowmodeActiveUntilTimestamp) = slowmodeState.variant {
|
||
let timestamp = Int32(Date().timeIntervalSince1970)
|
||
let remainingTime = max(0, slowmodeActiveUntilTimestamp - timestamp)
|
||
if remainingTime == 0 {
|
||
self.updateSlowmodeStatusTimerValue = nil
|
||
self.updateSlowmodeStatusDisposable.set(nil)
|
||
self.updateChatPresentationInterfaceState(interactive: false, {
|
||
$0.updatedSlowmodeState(nil)
|
||
})
|
||
} else {
|
||
if self.updateSlowmodeStatusTimerValue != slowmodeActiveUntilTimestamp {
|
||
self.updateSlowmodeStatusTimerValue = slowmodeActiveUntilTimestamp
|
||
self.updateSlowmodeStatusDisposable.set((Signal<Never, NoError>.complete()
|
||
|> suspendAwareDelay(Double(remainingTime), granularity: 1.0, queue: .mainQueue())
|
||
|> deliverOnMainQueue).start(completed: { [weak self] in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.updateSlowmodeStatusTimerValue = nil
|
||
strongSelf.updateSlowmodeStatus()
|
||
}))
|
||
}
|
||
}
|
||
} else if let _ = self.updateSlowmodeStatusTimerValue {
|
||
self.updateSlowmodeStatusTimerValue = nil
|
||
self.updateSlowmodeStatusDisposable.set(nil)
|
||
}
|
||
}
|
||
|
||
private func openScheduledMessages() {
|
||
guard let navigationController = self.effectiveNavigationController, navigationController.topViewController == self else {
|
||
return
|
||
}
|
||
let controller = ChatControllerImpl(context: self.context, chatLocation: self.chatLocation, subject: .scheduledMessages)
|
||
controller.navigationPresentation = .modal
|
||
navigationController.pushViewController(controller)
|
||
}
|
||
|
||
private func openPinnedMessages(at messageId: MessageId?) {
|
||
guard let navigationController = self.effectiveNavigationController, navigationController.topViewController == self else {
|
||
return
|
||
}
|
||
let controller = ChatControllerImpl(context: self.context, chatLocation: self.chatLocation, subject: .pinnedMessages(id: messageId))
|
||
controller.navigationPresentation = .modal
|
||
controller.updatedClosedPinnedMessageId = { [weak self] pinnedMessageId in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.performUpdatedClosedPinnedMessageId(pinnedMessageId: pinnedMessageId)
|
||
}
|
||
controller.requestedUnpinAllMessages = { [weak self] count, pinnedMessageId in
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
strongSelf.performRequestedUnpinAllMessages(count: count, pinnedMessageId: pinnedMessageId)
|
||
}
|
||
navigationController.pushViewController(controller)
|
||
}
|
||
|
||
private func performUpdatedClosedPinnedMessageId(pinnedMessageId: MessageId) {
|
||
let previousClosedPinnedMessageId = self.presentationInterfaceState.interfaceState.messageActionsState.closedPinnedMessageId
|
||
|
||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
return $0.updatedInterfaceState({ $0.withUpdatedMessageActionsState({ value in
|
||
var value = value
|
||
value.closedPinnedMessageId = pinnedMessageId
|
||
return value
|
||
}) })
|
||
})
|
||
|
||
self.present(
|
||
UndoOverlayController(
|
||
presentationData: self.presentationData,
|
||
content: .messagesUnpinned(
|
||
title: self.presentationData.strings.Chat_PinnedMessagesHiddenTitle,
|
||
text: self.presentationData.strings.Chat_PinnedMessagesHiddenText,
|
||
undo: true,
|
||
isHidden: true
|
||
),
|
||
elevatedLayout: false,
|
||
action: { [weak self] action in
|
||
guard let strongSelf = self else {
|
||
return true
|
||
}
|
||
|
||
switch action {
|
||
case .commit:
|
||
break
|
||
case .undo:
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
return $0.updatedInterfaceState({ $0.withUpdatedMessageActionsState({ value in
|
||
var value = value
|
||
value.closedPinnedMessageId = previousClosedPinnedMessageId
|
||
return value
|
||
}) })
|
||
})
|
||
default:
|
||
break
|
||
}
|
||
return true
|
||
}
|
||
),
|
||
in: .current
|
||
)
|
||
}
|
||
|
||
private func performRequestedUnpinAllMessages(count: Int, pinnedMessageId: MessageId) {
|
||
self.chatDisplayNode.historyNode.pendingUnpinnedAllMessages = true
|
||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
return $0.updatedPendingUnpinnedAllMessages(true)
|
||
})
|
||
|
||
self.present(
|
||
UndoOverlayController(
|
||
presentationData: self.presentationData,
|
||
content: .messagesUnpinned(
|
||
title: self.presentationData.strings.Chat_MessagesUnpinned(Int32(count)),
|
||
text: "",
|
||
undo: true,
|
||
isHidden: false
|
||
),
|
||
elevatedLayout: false,
|
||
action: { [weak self] action in
|
||
guard let strongSelf = self else {
|
||
return true
|
||
}
|
||
|
||
switch action {
|
||
case .commit:
|
||
let _ = (requestUnpinAllMessages(account: strongSelf.context.account, peerId: strongSelf.chatLocation.peerId)
|
||
|> deliverOnMainQueue).start(error: { _ in
|
||
}, completed: {
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
|
||
strongSelf.chatDisplayNode.historyNode.pendingUnpinnedAllMessages = false
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
return $0.updatedPendingUnpinnedAllMessages(false)
|
||
})
|
||
})
|
||
case .undo:
|
||
strongSelf.chatDisplayNode.historyNode.pendingUnpinnedAllMessages = false
|
||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||
return $0.updatedPendingUnpinnedAllMessages(false)
|
||
})
|
||
default:
|
||
break
|
||
}
|
||
return true
|
||
}
|
||
),
|
||
in: .current
|
||
)
|
||
}
|
||
|
||
private func presentScheduleTimePicker(style: ChatScheduleTimeControllerStyle = .default, selectedTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) {
|
||
guard case let .peer(peerId) = self.chatLocation else {
|
||
return
|
||
}
|
||
let _ = (self.context.account.viewTracker.peerView(peerId)
|
||
|> take(1)
|
||
|> deliverOnMainQueue).start(next: { [weak self] peerView in
|
||
guard let strongSelf = self, let peer = peerViewMainPeer(peerView) else {
|
||
return
|
||
}
|
||
var sendWhenOnlineAvailable = false
|
||
if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence, case .present = presence.status {
|
||
sendWhenOnlineAvailable = true
|
||
}
|
||
|
||
let mode: ChatScheduleTimeControllerMode
|
||
if peerId == strongSelf.context.account.peerId {
|
||
mode = .reminders
|
||
} else {
|
||
mode = .scheduledMessages(sendWhenOnlineAvailable: sendWhenOnlineAvailable)
|
||
}
|
||
let controller = ChatScheduleTimeController(context: strongSelf.context, peerId: peerId, mode: mode, style: style, currentTime: selectedTime, minimalTime: strongSelf.presentationInterfaceState.slowmodeState?.timeout, dismissByTapOutside: dismissByTapOutside, completion: { time in
|
||
completion(time)
|
||
})
|
||
strongSelf.chatDisplayNode.dismissInput()
|
||
strongSelf.present(controller, in: .window(.root))
|
||
})
|
||
}
|
||
|
||
private func presentTimerPicker(style: ChatTimerScreenStyle = .default, selectedTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) {
|
||
guard case let .peer(peerId) = self.chatLocation else {
|
||
return
|
||
}
|
||
let controller = ChatTimerScreen(context: self.context, peerId: peerId, style: style, currentTime: selectedTime, dismissByTapOutside: dismissByTapOutside, completion: { time in
|
||
completion(time)
|
||
})
|
||
self.chatDisplayNode.dismissInput()
|
||
self.present(controller, in: .window(.root))
|
||
}
|
||
|
||
private func presentVoiceMessageDiscardAlert(action: @escaping () -> Void = {}, alertAction: (() -> Void)? = nil, delay: Bool = false, performAction: Bool = true) -> Bool {
|
||
if let _ = self.presentationInterfaceState.inputTextPanelState.mediaRecordingState {
|
||
alertAction?()
|
||
Queue.mainQueue().after(delay ? 0.2 : 0.0) {
|
||
self.present(textAlertController(context: self.context, title: nil, text: self.presentationData.strings.Conversation_DiscardVoiceMessageDescription, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Conversation_DiscardVoiceMessageAction, action: { [weak self] in
|
||
self?.stopMediaRecorder()
|
||
action()
|
||
})]), in: .window(.root))
|
||
}
|
||
|
||
return true
|
||
} else if performAction {
|
||
action()
|
||
}
|
||
return false
|
||
}
|
||
|
||
private func presentAutoremoveSetup() {
|
||
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
|
||
return
|
||
}
|
||
|
||
let controller = peerAutoremoveSetupScreen(context: self.context, peerId: peer.id, completion: { [weak self] updatedValue in
|
||
if case let .updated(value) = updatedValue {
|
||
guard let strongSelf = self else {
|
||
return
|
||
}
|
||
|
||
var text: String?
|
||
if let myValue = value.myValue {
|
||
if let limitedByValue = value.limitedByValue, limitedByValue < myValue {
|
||
text = "\(peer.compactDisplayTitle) has set messages to auto-delete in \(timeIntervalString(strings: strongSelf.presentationData.strings, value: limitedByValue)). You can't cancel it or make this interval longer."
|
||
} else {
|
||
text = strongSelf.presentationData.strings.Conversation_AutoremoveChanged("\(timeIntervalString(strings: strongSelf.presentationData.strings, value: myValue))").0
|
||
}
|
||
} else if let limitedByValue = value.limitedByValue {
|
||
text = "\(peer.compactDisplayTitle) has set messages to auto-delete in \(timeIntervalString(strings: strongSelf.presentationData.strings, value: limitedByValue)). You can't cancel it or make this interval longer."
|
||
}
|
||
if let text = text {
|
||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .succeed(text: text), elevatedLayout: false, action: { _ in return false }), in: .current)
|
||
}
|
||
}
|
||
})
|
||
self.chatDisplayNode.dismissInput()
|
||
self.push(controller)
|
||
}
|
||
|
||
private var effectiveNavigationController: NavigationController? {
|
||
if let navigationController = self.navigationController as? NavigationController {
|
||
return navigationController
|
||
} else if case let .inline(navigationController) = self.presentationInterfaceState.mode {
|
||
return navigationController
|
||
} else if case let .overlay(navigationController) = self.presentationInterfaceState.mode {
|
||
return navigationController
|
||
} else {
|
||
return nil
|
||
}
|
||
}
|
||
|
||
func activateSearch(domain: ChatSearchDomain = .everything, query: String = "") {
|
||
self.focusOnSearchAfterAppearance = (domain, query)
|
||
self.interfaceInteraction?.beginMessageSearch(domain, query)
|
||
}
|
||
|
||
override public func updatePossibleControllerDropContent(content: NavigationControllerDropContent?) {
|
||
//self.chatDisplayNode.updateEmbeddedTitlePeekContent(content: content)
|
||
}
|
||
|
||
override public func acceptPossibleControllerDropContent(content: NavigationControllerDropContent) -> Bool {
|
||
//return self.chatDisplayNode.acceptEmbeddedTitlePeekContent(content: content)
|
||
return false
|
||
}
|
||
|
||
public var isSendButtonVisible: Bool {
|
||
if self.presentationInterfaceState.interfaceState.editMessage != nil || self.presentationInterfaceState.interfaceState.forwardMessageIds != nil || self.presentationInterfaceState.interfaceState.composeInputState.inputText.string.count > 0 {
|
||
return true
|
||
} else {
|
||
return false
|
||
}
|
||
}
|
||
|
||
private func speakText(_ text: String) {
|
||
let speechSynthesizer = AVSpeechSynthesizer()
|
||
let utterance = AVSpeechUtterance(string: text)
|
||
if #available(iOS 11.0, *), let language = NSLinguisticTagger.dominantLanguage(for: text) {
|
||
utterance.voice = AVSpeechSynthesisVoice(language: language)
|
||
}
|
||
speechSynthesizer.speak(utterance)
|
||
}
|
||
}
|
||
|
||
private final class ContextControllerContentSourceImpl: ContextControllerContentSource {
|
||
let controller: ViewController
|
||
weak var sourceNode: ASDisplayNode?
|
||
|
||
let navigationController: NavigationController? = nil
|
||
|
||
let passthroughTouches: Bool
|
||
|
||
init(controller: ViewController, sourceNode: ASDisplayNode?, passthroughTouches: Bool) {
|
||
self.controller = controller
|
||
self.sourceNode = sourceNode
|
||
self.passthroughTouches = passthroughTouches
|
||
}
|
||
|
||
func transitionInfo() -> ContextControllerTakeControllerInfo? {
|
||
let sourceNode = self.sourceNode
|
||
return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in
|
||
if let sourceNode = sourceNode {
|
||
return (sourceNode, sourceNode.bounds)
|
||
} else {
|
||
return nil
|
||
}
|
||
})
|
||
}
|
||
|
||
func animatedIn() {
|
||
}
|
||
}
|