mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
5194 lines
300 KiB
Swift
5194 lines
300 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Postbox
|
|
import SwiftSignalKit
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import TelegramCore
|
|
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 Emoji
|
|
import PeerAvatarGalleryUI
|
|
import PeerInfoUI
|
|
import RaiseToListen
|
|
import UrlHandling
|
|
import AvatarNode
|
|
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 Markdown
|
|
import TelegramPermissionsUI
|
|
import Speak
|
|
import TranslateUI
|
|
import UniversalMediaPlayer
|
|
import WallpaperBackgroundNode
|
|
import ChatListUI
|
|
import CalendarMessageScreen
|
|
import ReactionSelectionNode
|
|
import ReactionListContextMenuContent
|
|
import AttachmentUI
|
|
import AttachmentTextInputPanelNode
|
|
import MediaPickerUI
|
|
import ChatPresentationInterfaceState
|
|
import Pasteboard
|
|
import ChatSendMessageActionUI
|
|
import ChatTextLinkEditUI
|
|
import WebUI
|
|
import PremiumUI
|
|
import ImageTransparency
|
|
import StickerPackPreviewUI
|
|
import TextNodeWithEntities
|
|
import EntityKeyboard
|
|
import ChatTitleView
|
|
import EmojiStatusComponent
|
|
import ChatTimerScreen
|
|
import MediaPasteboardUI
|
|
import ChatListHeaderComponent
|
|
import ChatControllerInteraction
|
|
import FeaturedStickersScreen
|
|
import ChatEntityKeyboardInputNode
|
|
import StorageUsageScreen
|
|
import AvatarEditorScreen
|
|
import ChatScheduleTimeController
|
|
import ICloudResources
|
|
import StoryContainerScreen
|
|
import MoreHeaderButton
|
|
import VolumeButtons
|
|
import ChatAvatarNavigationNode
|
|
import ChatContextQuery
|
|
import PeerReportScreen
|
|
import PeerSelectionController
|
|
import SaveToCameraRoll
|
|
import ChatMessageDateAndStatusNode
|
|
import ReplyAccessoryPanelNode
|
|
import TextSelectionNode
|
|
import ChatMessagePollBubbleContentNode
|
|
import ChatMessageItem
|
|
import ChatMessageItemImpl
|
|
import ChatMessageItemView
|
|
import ChatMessageItemCommon
|
|
import ChatMessageAnimatedStickerItemNode
|
|
import ChatMessageBubbleItemNode
|
|
import ChatNavigationButton
|
|
import WebsiteType
|
|
import ChatQrCodeScreen
|
|
import PeerInfoScreen
|
|
import MediaEditorScreen
|
|
import WallpaperGalleryScreen
|
|
import WallpaperGridScreen
|
|
import VideoMessageCameraScreen
|
|
import TopMessageReactions
|
|
import AudioWaveform
|
|
import PeerNameColorScreen
|
|
import ChatEmptyNode
|
|
import ChatMediaInputStickerGridItem
|
|
import AdsInfoScreen
|
|
import PostSuggestionsSettingsScreen
|
|
import ChatSendStarsScreen
|
|
|
|
extension ChatControllerImpl {
|
|
func loadDisplayNodeImpl() {
|
|
if #available(iOS 18.0, *) {
|
|
if self.context.sharedContext.immediateExperimentalUISettings.enableLocalTranslation {
|
|
if engineExperimentalInternalTranslationService == nil, let hostView = self.context.sharedContext.mainWindow?.hostView {
|
|
let translationService = ExperimentalInternalTranslationServiceImpl(view: hostView.containerView)
|
|
engineExperimentalInternalTranslationService = translationService
|
|
}
|
|
} else {
|
|
if engineExperimentalInternalTranslationService != nil {
|
|
engineExperimentalInternalTranslationService = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
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, statusBar: self.statusBar, backgroundNode: self.chatBackgroundNode, controller: self)
|
|
|
|
if let currentItem = self.tempVoicePlaylistCurrentItem {
|
|
self.chatDisplayNode.historyNode.voicePlaylistItemChanged(nil, currentItem)
|
|
}
|
|
|
|
self.chatDisplayNode.historyNode.beganDragging = { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if self.presentationInterfaceState.search != nil && self.presentationInterfaceState.historyFilter != nil {
|
|
self.chatDisplayNode.historyNode.addAfterTransactionsCompleted { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
self.chatDisplayNode.dismissInput()
|
|
}
|
|
}
|
|
}
|
|
|
|
self.chatDisplayNode.historyNode.didScrollWithOffset = { [weak self] offset, transition, itemNode, isTracking in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
//print("didScrollWithOffset offset: \(offset), itemNode: \(String(describing: itemNode))")
|
|
|
|
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 let currentPinchSourceItemNode = strongSelf.currentPinchSourceItemNode {
|
|
if let itemNode = itemNode {
|
|
if itemNode === currentPinchSourceItemNode {
|
|
strongSelf.currentPinchController?.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition)
|
|
}
|
|
} else {
|
|
strongSelf.currentPinchController?.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition)
|
|
}
|
|
}
|
|
|
|
if isTracking {
|
|
strongSelf.chatDisplayNode.loadingPlaceholderNode?.addContentOffset(offset: offset, transition: transition)
|
|
}
|
|
strongSelf.chatDisplayNode.messageTransitionNode.addExternalOffset(offset: offset, transition: transition, itemNode: itemNode, isRotated: strongSelf.chatDisplayNode.historyNode.rotated)
|
|
|
|
}
|
|
|
|
self.chatDisplayNode.historyNode.hasAtLeast3MessagesUpdated = { [weak self] hasAtLeast3Messages in
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(interactive: false, { $0.updatedHasAtLeast3Messages(hasAtLeast3Messages) })
|
|
}
|
|
}
|
|
self.chatDisplayNode.historyNode.hasPlentyOfMessagesUpdated = { [weak self] hasPlentyOfMessages in
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(interactive: false, { $0.updatedHasPlentyOfMessages(hasPlentyOfMessages) })
|
|
}
|
|
}
|
|
|
|
if case .peer(self.context.account.peerId) = self.chatLocation {
|
|
var didDisplayTooltip = false
|
|
if "".isEmpty {
|
|
didDisplayTooltip = true
|
|
}
|
|
self.chatDisplayNode.historyNode.hasLotsOfMessagesUpdated = { [weak self] hasLotsOfMessages in
|
|
guard let self, hasLotsOfMessages else {
|
|
return
|
|
}
|
|
if didDisplayTooltip {
|
|
return
|
|
}
|
|
didDisplayTooltip = true
|
|
|
|
let _ = (ApplicationSpecificNotice.getSavedMessagesChatsSuggestion(accountManager: self.context.sharedContext.accountManager)
|
|
|> deliverOnMainQueue).startStandalone(next: { [weak self] counter in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if counter >= 3 {
|
|
return
|
|
}
|
|
guard let navigationBar = self.navigationBar else {
|
|
return
|
|
}
|
|
|
|
let tooltipScreen = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: self.presentationData.strings.Chat_SavedMessagesChatsTooltip), location: .point(navigationBar.frame, .top), displayDuration: .manual, shouldDismissOnTouch: { point, _ in
|
|
return .ignore
|
|
})
|
|
self.present(tooltipScreen, in: .current)
|
|
|
|
let _ = ApplicationSpecificNotice.incrementSavedMessagesChatsSuggestion(accountManager: self.context.sharedContext.accountManager).startStandalone()
|
|
})
|
|
}
|
|
}
|
|
|
|
self.chatDisplayNode.historyNode.addContentOffset = { [weak self] offset, itemNode in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.chatDisplayNode.messageTransitionNode.addContentOffset(offset: offset, itemNode: itemNode)
|
|
}
|
|
|
|
var closeOnEmpty = false
|
|
if case .pinnedMessages = self.presentationInterfaceState.subject {
|
|
closeOnEmpty = true
|
|
} else if self.chatLocation.peerId == self.context.account.peerId {
|
|
if let data = self.context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_close_empty_saved"] {
|
|
} else {
|
|
closeOnEmpty = true
|
|
}
|
|
}
|
|
|
|
if closeOnEmpty {
|
|
self.chatDisplayNode.historyNode.addSetLoadStateUpdated({ [weak self] state, _ in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if case .empty = state {
|
|
if self.chatLocation.peerId == self.context.account.peerId {
|
|
if self.chatDisplayNode.historyNode.tag != nil {
|
|
self.updateChatPresentationInterfaceState(animated: true, interactive: false, { state in
|
|
return state.updatedSearch(nil).updatedHistoryFilter(nil)
|
|
})
|
|
} else if case .replyThread = self.chatLocation {
|
|
self.dismiss()
|
|
}
|
|
} else {
|
|
self.dismiss()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
self.chatDisplayNode.overlayTitle = self.overlayTitle
|
|
|
|
let currentAccountPeer = self.context.account.postbox.loadedPeerWithId(self.context.account.peerId)
|
|
|> map { peer in
|
|
return SendAsPeer(peer: peer, subscribers: nil, isPremiumRequired: false)
|
|
}
|
|
|
|
if let peerId = self.chatLocation.peerId, [Namespaces.Peer.CloudChannel, Namespaces.Peer.CloudGroup].contains(peerId.namespace) {
|
|
self.sendAsPeersDisposable = (combineLatest(
|
|
queue: Queue.mainQueue(),
|
|
currentAccountPeer,
|
|
self.context.account.postbox.peerView(id: peerId),
|
|
self.context.engine.peers.sendAsAvailablePeers(peerId: peerId))
|
|
).startStrict(next: { [weak self] currentAccountPeer, peerView, peers in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
let isPremium = strongSelf.presentationInterfaceState.isPremium
|
|
|
|
var allPeers: [SendAsPeer]?
|
|
if !peers.isEmpty {
|
|
if let channel = peerViewMainPeer(peerView) as? TelegramChannel, case .group = channel.info, channel.hasPermission(.canBeAnonymous) {
|
|
allPeers = peers
|
|
|
|
var hasAnonymousPeer = false
|
|
for peer in peers {
|
|
if peer.peer.id == channel.id {
|
|
hasAnonymousPeer = true
|
|
break
|
|
}
|
|
}
|
|
if !hasAnonymousPeer {
|
|
allPeers?.insert(SendAsPeer(peer: channel, subscribers: 0, isPremiumRequired: false), at: 0)
|
|
}
|
|
} else if let channel = peerViewMainPeer(peerView) as? TelegramChannel, case let .broadcast(info) = channel.info, (info.flags.contains(.messagesShouldHaveSignatures) || info.flags.contains(.messagesShouldHaveProfiles)) {
|
|
allPeers = peers
|
|
|
|
var hasAnonymousPeer = false
|
|
var hasSelfPeer = false
|
|
for peer in peers {
|
|
if peer.peer.id == channel.id {
|
|
hasAnonymousPeer = true
|
|
} else if peer.peer.id == strongSelf.context.account.peerId {
|
|
hasSelfPeer = true
|
|
}
|
|
}
|
|
if !hasSelfPeer {
|
|
allPeers?.insert(currentAccountPeer, at: 0)
|
|
}
|
|
if !hasAnonymousPeer {
|
|
allPeers?.insert(SendAsPeer(peer: channel, subscribers: 0, isPremiumRequired: false), at: 0)
|
|
}
|
|
} else {
|
|
allPeers = peers.filter { $0.peer.id != peerViewMainPeer(peerView)?.id }
|
|
allPeers?.insert(currentAccountPeer, at: 0)
|
|
}
|
|
}
|
|
if allPeers?.count == 1 {
|
|
allPeers = nil
|
|
}
|
|
|
|
var currentSendAsPeerId = strongSelf.presentationInterfaceState.currentSendAsPeerId
|
|
if let peerId = currentSendAsPeerId, let peer = allPeers?.first(where: { $0.peer.id == peerId }) {
|
|
if !isPremium && peer.isPremiumRequired {
|
|
currentSendAsPeerId = nil
|
|
}
|
|
} else {
|
|
currentSendAsPeerId = nil
|
|
}
|
|
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
|
return $0.updatedSendAsPeers(allPeers).updatedCurrentSendAsPeerId(currentSendAsPeerId)
|
|
})
|
|
})
|
|
}
|
|
|
|
let initialData = self.chatDisplayNode.historyNode.initialData
|
|
|> take(1)
|
|
|> beforeNext { [weak self] combinedInitialData in
|
|
guard let strongSelf = self, let combinedInitialData = combinedInitialData else {
|
|
return
|
|
}
|
|
|
|
if let opaqueState = (combinedInitialData.initialData?.storedInterfaceState).flatMap(_internal_decodeStoredChatInterfaceState) {
|
|
var interfaceState = ChatInterfaceState.parse(opaqueState)
|
|
|
|
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
|
|
|
|
var canBypassRestrictions = false
|
|
if let boostsToUnrestrict = cachedData.boostsToUnrestrict, let appliedBoosts = cachedData.appliedBoosts, appliedBoosts >= boostsToUnrestrict {
|
|
canBypassRestrictions = true
|
|
}
|
|
if !canBypassRestrictions, 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 let channel = combinedInitialData.initialData?.peer as? TelegramChannel {
|
|
if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil {
|
|
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio)
|
|
} else if channel.hasBannedPermission(.banSendVoice) != nil {
|
|
if channel.hasBannedPermission(.banSendInstantVideos) == nil {
|
|
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video)
|
|
}
|
|
} else if channel.hasBannedPermission(.banSendInstantVideos) != nil {
|
|
if channel.hasBannedPermission(.banSendVoice) == nil {
|
|
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio)
|
|
}
|
|
}
|
|
} else if let group = combinedInitialData.initialData?.peer as? TelegramGroup {
|
|
if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) {
|
|
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio)
|
|
} else if group.hasBannedPermission(.banSendVoice) {
|
|
if !group.hasBannedPermission(.banSendInstantVideos) {
|
|
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video)
|
|
}
|
|
} else if group.hasBannedPermission(.banSendInstantVideos) {
|
|
if !group.hasBannedPermission(.banSendVoice) {
|
|
interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio)
|
|
}
|
|
}
|
|
}
|
|
|
|
if case let .replyThread(replyThreadMessageId) = strongSelf.chatLocation {
|
|
if let channel = combinedInitialData.initialData?.peer as? TelegramChannel, channel.flags.contains(.isForum) {
|
|
pinnedMessageId = nil
|
|
} else {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
var buttonKeyboardMessage = combinedInitialData.buttonKeyboardMessage
|
|
if let buttonKeyboardMessageValue = buttonKeyboardMessage, buttonKeyboardMessageValue.isRestricted(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with({ $0 })) {
|
|
buttonKeyboardMessage = nil
|
|
}
|
|
|
|
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { updated in
|
|
var updated = updated
|
|
|
|
updated = updated.updatedInterfaceState({ _ in return interfaceState })
|
|
|
|
updated = updated.updatedKeyboardButtonsMessage(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] {
|
|
let (updatedState, updatedPreviewQueryState) = updatedChatEditInterfaceMessageState(context: strongSelf.context, state: updated, message: message)
|
|
updated = updatedState
|
|
strongSelf.editingUrlPreviewQueryState?.1.dispose()
|
|
strongSelf.editingUrlPreviewQueryState = updatedPreviewQueryState
|
|
}
|
|
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.startStrict(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
|
|
|
|
if let chatLocationPeerId = chatLocationPeerId {
|
|
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)
|
|
}
|
|
}
|
|
|> distinctUntilChanged
|
|
} else {
|
|
hasPendingMessages = .single(false)
|
|
}
|
|
|
|
let isTopReplyThreadMessageShown: Signal<Bool, NoError> = self.chatDisplayNode.historyNode.isTopReplyThreadMessageShown.get()
|
|
|> distinctUntilChanged
|
|
|
|
let topPinnedMessage: Signal<ChatPinnedMessage?, NoError>
|
|
if let subject = self.subject {
|
|
switch subject {
|
|
case .messageOptions, .pinnedMessages, .scheduledMessages:
|
|
topPinnedMessage = .single(nil)
|
|
default:
|
|
topPinnedMessage = self.topPinnedMessageSignal(latest: false)
|
|
}
|
|
} else {
|
|
topPinnedMessage = self.topPinnedMessageSignal(latest: false)
|
|
}
|
|
|
|
if let peerId = self.chatLocation.peerId {
|
|
self.chatThemeEmoticonPromise.set(self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.ThemeEmoticon(id: peerId)))
|
|
let chatWallpaper = self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Wallpaper(id: peerId))
|
|
|> take(1)
|
|
self.chatWallpaperPromise.set(chatWallpaper)
|
|
} else {
|
|
self.chatThemeEmoticonPromise.set(.single(nil))
|
|
self.chatWallpaperPromise.set(.single(nil))
|
|
}
|
|
|
|
if let peerId = self.chatLocation.peerId {
|
|
let customEmojiAvailable: Signal<Bool, NoError> = self.context.engine.data.subscribe(
|
|
TelegramEngine.EngineData.Item.Peer.SecretChatLayer(id: peerId)
|
|
)
|
|
|> map { layer -> Bool in
|
|
guard let layer = layer else {
|
|
return true
|
|
}
|
|
|
|
return layer >= 144
|
|
}
|
|
|> distinctUntilChanged
|
|
|
|
let isForum = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|
|
|> map { peer -> Bool in
|
|
if case let .channel(channel) = peer {
|
|
return channel.flags.contains(.isForum)
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|> distinctUntilChanged
|
|
|
|
let context = self.context
|
|
let threadData: Signal<ChatPresentationInterfaceState.ThreadData?, NoError>
|
|
let forumTopicData: Signal<ChatPresentationInterfaceState.ThreadData?, NoError>
|
|
if let threadId = self.chatLocation.threadId {
|
|
let viewKey: PostboxViewKey = .messageHistoryThreadInfo(peerId: peerId, threadId: threadId)
|
|
threadData = context.account.postbox.combinedView(keys: [viewKey])
|
|
|> map { views -> ChatPresentationInterfaceState.ThreadData? in
|
|
guard let view = views.views[viewKey] as? MessageHistoryThreadInfoView else {
|
|
return nil
|
|
}
|
|
guard let data = view.info?.data.get(MessageHistoryThreadData.self) else {
|
|
return nil
|
|
}
|
|
return ChatPresentationInterfaceState.ThreadData(title: data.info.title, icon: data.info.icon, iconColor: data.info.iconColor, isOwnedByMe: data.isOwnedByMe, isClosed: data.isClosed)
|
|
}
|
|
|> distinctUntilChanged
|
|
forumTopicData = .single(nil)
|
|
} else {
|
|
forumTopicData = isForum
|
|
|> mapToSignal { isForum -> Signal<ChatPresentationInterfaceState.ThreadData?, NoError> in
|
|
if isForum {
|
|
let viewKey: PostboxViewKey = .messageHistoryThreadInfo(peerId: peerId, threadId: 1)
|
|
return context.account.postbox.combinedView(keys: [viewKey])
|
|
|> map { views -> ChatPresentationInterfaceState.ThreadData? in
|
|
guard let view = views.views[viewKey] as? MessageHistoryThreadInfoView else {
|
|
return nil
|
|
}
|
|
guard let data = view.info?.data.get(MessageHistoryThreadData.self) else {
|
|
return nil
|
|
}
|
|
return ChatPresentationInterfaceState.ThreadData(title: data.info.title, icon: data.info.icon, iconColor: data.info.iconColor, isOwnedByMe: data.isOwnedByMe, isClosed: data.isClosed)
|
|
}
|
|
|> distinctUntilChanged
|
|
} else {
|
|
return .single(nil)
|
|
}
|
|
}
|
|
threadData = .single(nil)
|
|
}
|
|
|
|
if case .standard(.previewing) = self.presentationInterfaceState.mode {
|
|
|
|
} else if peerId.namespace != Namespaces.Peer.SecretChat && peerId != context.account.peerId && self.subject != .scheduledMessages {
|
|
self.premiumGiftSuggestionDisposable = (ApplicationSpecificNotice.dismissedPremiumGiftSuggestion(accountManager: self.context.sharedContext.accountManager, peerId: peerId)
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self] timestamp in
|
|
if let strongSelf = self {
|
|
let currentTime = Int32(Date().timeIntervalSince1970)
|
|
strongSelf.updateChatPresentationInterfaceState(animated: strongSelf.willAppear, interactive: strongSelf.willAppear, { state in
|
|
var suggest = true
|
|
if let timestamp, currentTime < timestamp + 60 * 60 * 24 {
|
|
suggest = false
|
|
}
|
|
return state.updatedSuggestPremiumGift(suggest)
|
|
})
|
|
}
|
|
})
|
|
|
|
var baseLanguageCode = self.presentationData.strings.baseLanguageCode
|
|
if baseLanguageCode.contains("-") {
|
|
baseLanguageCode = baseLanguageCode.components(separatedBy: "-").first ?? baseLanguageCode
|
|
}
|
|
let isPremium = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|
|
|> map { peer -> Bool in
|
|
return peer?.isPremium ?? false
|
|
} |> distinctUntilChanged
|
|
|
|
let isHidden = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.TranslationHidden(id: peerId))
|
|
|> distinctUntilChanged
|
|
|
|
let hasAutoTranslate = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.AutoTranslateEnabled(id: peerId))
|
|
|> distinctUntilChanged
|
|
|
|
let chatLocation = self.chatLocation
|
|
self.translationStateDisposable = (combineLatest(
|
|
queue: .concurrentDefaultQueue(),
|
|
isPremium,
|
|
isHidden,
|
|
hasAutoTranslate,
|
|
ApplicationSpecificNotice.translationSuggestion(accountManager: self.context.sharedContext.accountManager)
|
|
) |> mapToSignal { isPremium, isHidden, hasAutoTranslate, counterAndTimestamp -> Signal<ChatPresentationTranslationState?, NoError> in
|
|
var maybeSuggestPremium = false
|
|
if counterAndTimestamp.0 >= 3 {
|
|
maybeSuggestPremium = true
|
|
}
|
|
if (isPremium || maybeSuggestPremium || hasAutoTranslate || true /* MARK: Swiftgram */) && !isHidden {
|
|
return chatTranslationState(context: context, peerId: peerId, threadId: chatLocation.threadId)
|
|
|> map { translationState -> ChatPresentationTranslationState? in
|
|
if let translationState, !translationState.fromLang.isEmpty && (translationState.fromLang != baseLanguageCode || translationState.isEnabled) {
|
|
return ChatPresentationTranslationState(isEnabled: translationState.isEnabled, fromLang: translationState.fromLang, toLang: translationState.toLang ?? baseLanguageCode)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|> distinctUntilChanged
|
|
} else {
|
|
return .single(nil)
|
|
}
|
|
}
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self] chatTranslationState in
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: strongSelf.willAppear, interactive: strongSelf.willAppear, { state in
|
|
return state.updatedTranslationState(chatTranslationState)
|
|
})
|
|
}
|
|
})
|
|
|
|
// MARK: Swiftgram
|
|
self.chatLanguagePredictionDisposable = (
|
|
chatTranslationState(context: context, peerId: peerId, threadId: chatLocation.threadId, forcePredict: true)
|
|
|> map { translationState -> ChatPresentationTranslationState? in
|
|
if let translationState, !translationState.fromLang.isEmpty {
|
|
return ChatPresentationTranslationState(isEnabled: translationState.isEnabled, fromLang: translationState.fromLang, toLang: translationState.toLang ?? baseLanguageCode)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|> distinctUntilChanged).startStrict(next: { [weak self] translationState in
|
|
if let strongSelf = self, let translationState = translationState, strongSelf.predictedChatLanguage == nil {
|
|
strongSelf.predictedChatLanguage = translationState.fromLang
|
|
}
|
|
})
|
|
}
|
|
|
|
let premiumGiftOptions: Signal<[CachedPremiumGiftOption], NoError> = .single([])
|
|
|> then(
|
|
self.context.engine.payments.premiumGiftCodeOptions(peerId: peerId, onlyCached: true)
|
|
|> map { options in
|
|
return options.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) }
|
|
}
|
|
)
|
|
|
|
self.cachedDataDisposable = combineLatest(queue: .mainQueue(), self.chatDisplayNode.historyNode.cachedPeerDataAndMessages |> debug_measureTimeToFirstEvent(label: "cachedData_cachedPeerDataAndMessages"),
|
|
hasPendingMessages |> debug_measureTimeToFirstEvent(label: "cachedData_hasPendingMessages"),
|
|
isTopReplyThreadMessageShown |> debug_measureTimeToFirstEvent(label: "cachedData_isTopReplyThreadMessageShown"),
|
|
topPinnedMessage |> debug_measureTimeToFirstEvent(label: "cachedData_topPinnedMessage"),
|
|
customEmojiAvailable |> debug_measureTimeToFirstEvent(label: "cachedData_customEmojiAvailable"),
|
|
isForum |> debug_measureTimeToFirstEvent(label: "cachedData_isForum"),
|
|
threadData |> debug_measureTimeToFirstEvent(label: "cachedData_threadData"),
|
|
forumTopicData |> debug_measureTimeToFirstEvent(label: "cachedData_forumTopicData"),
|
|
premiumGiftOptions |> debug_measureTimeToFirstEvent(label: "cachedData_premiumGiftOptions")
|
|
).startStrict(next: { [weak self] cachedDataAndMessages, hasPendingMessages, isTopReplyThreadMessageShown, topPinnedMessage, customEmojiAvailable, isForum, threadData, forumTopicData, premiumGiftOptions in
|
|
if let strongSelf = self {
|
|
let (cachedData, messages) = cachedDataAndMessages
|
|
|
|
if cachedData != nil {
|
|
var themeEmoticon: String? = nil
|
|
var chatWallpaper: TelegramWallpaper?
|
|
if let cachedData = cachedData as? CachedUserData {
|
|
themeEmoticon = cachedData.themeEmoticon
|
|
chatWallpaper = cachedData.wallpaper
|
|
} else if let cachedData = cachedData as? CachedGroupData {
|
|
themeEmoticon = cachedData.themeEmoticon
|
|
} else if let cachedData = cachedData as? CachedChannelData {
|
|
themeEmoticon = cachedData.themeEmoticon
|
|
chatWallpaper = cachedData.wallpaper
|
|
}
|
|
|
|
strongSelf.chatThemeEmoticonPromise.set(.single(themeEmoticon))
|
|
strongSelf.chatWallpaperPromise.set(.single(chatWallpaper))
|
|
}
|
|
|
|
var pinnedMessageId: MessageId?
|
|
var peerIsBlocked: Bool = false
|
|
var callsAvailable: Bool = false
|
|
var callsPrivate: Bool = false
|
|
var voiceMessagesAvailable: Bool = true
|
|
var slowmodeState: ChatSlowmodeState?
|
|
var activeGroupCallInfo: ChatActiveGroupCallInfo?
|
|
var inviteRequestsPending: Int32?
|
|
if let cachedData = cachedData as? CachedChannelData {
|
|
pinnedMessageId = cachedData.pinnedMessageId
|
|
if !canBypassRestrictions(chatPresentationInterfaceState: strongSelf.presentationInterfaceState) {
|
|
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)
|
|
}
|
|
inviteRequestsPending = cachedData.inviteRequestsPending
|
|
} else if let cachedData = cachedData as? CachedUserData {
|
|
peerIsBlocked = cachedData.isBlocked
|
|
callsAvailable = cachedData.voiceCallsAvailable
|
|
callsPrivate = cachedData.callsPrivate
|
|
pinnedMessageId = cachedData.pinnedMessageId
|
|
voiceMessagesAvailable = cachedData.voiceMessagesAvailable
|
|
} else if let cachedData = cachedData as? CachedGroupData {
|
|
pinnedMessageId = cachedData.pinnedMessageId
|
|
if let activeCall = cachedData.activeCall {
|
|
activeGroupCallInfo = ChatActiveGroupCallInfo(activeCall: activeCall)
|
|
}
|
|
inviteRequestsPending = cachedData.inviteRequestsPending
|
|
} else if let _ = cachedData as? CachedSecretChatData {
|
|
}
|
|
|
|
var pinnedMessage: ChatPinnedMessage?
|
|
switch strongSelf.chatLocation {
|
|
case let .replyThread(replyThreadMessage):
|
|
if isForum {
|
|
pinnedMessageId = topPinnedMessage?.message.id
|
|
pinnedMessage = topPinnedMessage
|
|
} else {
|
|
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
|
|
case .customChatContents:
|
|
pinnedMessageId = nil
|
|
pinnedMessage = nil
|
|
}
|
|
|
|
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
|
|
|
|
let voiceMessagesAvailableUpdated = strongSelf.presentationInterfaceState.voiceMessagesAvailable != voiceMessagesAvailable
|
|
|
|
var canManageInvitations = false
|
|
if let channel = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isCreator) || (channel.adminRights?.rights.contains(.canInviteUsers) == true) {
|
|
canManageInvitations = true
|
|
} else if let group = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramGroup {
|
|
if case .creator = group.role {
|
|
canManageInvitations = true
|
|
} else if case let .admin(rights, _) = group.role, rights.rights.contains(.canInviteUsers) {
|
|
canManageInvitations = true
|
|
}
|
|
}
|
|
|
|
if canManageInvitations, let inviteRequestsPending = inviteRequestsPending, inviteRequestsPending >= 0 {
|
|
if strongSelf.inviteRequestsContext == nil {
|
|
let inviteRequestsContext = strongSelf.context.engine.peers.peerInvitationImporters(peerId: peerId, subject: .requests(query: nil))
|
|
strongSelf.inviteRequestsContext = inviteRequestsContext
|
|
|
|
strongSelf.inviteRequestsDisposable.set((combineLatest(queue: Queue.mainQueue(), inviteRequestsContext.state, ApplicationSpecificNotice.dismissedInvitationRequests(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId))).startStrict(next: { [weak self] requestsState, dismissedInvitationRequests in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { state in
|
|
return state
|
|
.updatedTitlePanelContext({ context in
|
|
let peers: [EnginePeer] = Array(requestsState.importers.compactMap({ $0.peer.peer.flatMap({ EnginePeer($0) }) }).prefix(3))
|
|
|
|
var peersDismissed = false
|
|
if let dismissedInvitationRequests = dismissedInvitationRequests, Set(peers.map({ $0.id.toInt64() })) == Set(dismissedInvitationRequests) {
|
|
peersDismissed = true
|
|
}
|
|
|
|
if requestsState.count > 0 && !peersDismissed {
|
|
if !context.contains(where: {
|
|
switch $0 {
|
|
case .inviteRequests(peers, requestsState.count):
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}) {
|
|
var updatedContexts = context.filter { c in
|
|
if case .inviteRequests = c {
|
|
return false
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
updatedContexts.append(.inviteRequests(peers, requestsState.count))
|
|
return updatedContexts.sorted()
|
|
} else {
|
|
return context
|
|
}
|
|
} else {
|
|
if let index = context.firstIndex(where: {
|
|
switch $0 {
|
|
case .inviteRequests:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}) {
|
|
var updatedContexts = context
|
|
updatedContexts.remove(at: index)
|
|
return updatedContexts
|
|
} else {
|
|
return context
|
|
}
|
|
}
|
|
})
|
|
.updatedSlowmodeState(slowmodeState)
|
|
})
|
|
}))
|
|
} else if let inviteRequestsContext = strongSelf.inviteRequestsContext {
|
|
let _ = (inviteRequestsContext.state
|
|
|> take(1)
|
|
|> deliverOnMainQueue).startStandalone(next: { [weak inviteRequestsContext] state in
|
|
if state.count != inviteRequestsPending {
|
|
inviteRequestsContext?.loadMore()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
if strongSelf.presentationInterfaceState.pinnedMessageId != pinnedMessageId || strongSelf.presentationInterfaceState.pinnedMessage != pinnedMessage || strongSelf.presentationInterfaceState.peerIsBlocked != peerIsBlocked || pinnedMessageUpdated || callsDataUpdated || voiceMessagesAvailableUpdated || strongSelf.presentationInterfaceState.slowmodeState != slowmodeState || strongSelf.presentationInterfaceState.activeGroupCallInfo != activeGroupCallInfo || customEmojiAvailable != strongSelf.presentationInterfaceState.customEmojiAvailable || threadData != strongSelf.presentationInterfaceState.threadData || forumTopicData != strongSelf.presentationInterfaceState.forumTopicData || premiumGiftOptions != strongSelf.presentationInterfaceState.premiumGiftOptions {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: strongSelf.willAppear, interactive: strongSelf.willAppear, { state in
|
|
return state
|
|
.updatedPinnedMessageId(pinnedMessageId)
|
|
.updatedActiveGroupCallInfo(activeGroupCallInfo)
|
|
.updatedPinnedMessage(pinnedMessage)
|
|
.updatedPeerIsBlocked(peerIsBlocked)
|
|
.updatedCallsAvailable(callsAvailable)
|
|
.updatedCallsPrivate(callsPrivate)
|
|
.updatedVoiceMessagesAvailable(voiceMessagesAvailable)
|
|
.updatedCustomEmojiAvailable(customEmojiAvailable)
|
|
.updatedThreadData(threadData)
|
|
.updatedForumTopicData(forumTopicData)
|
|
.updatedIsGeneralThreadClosed(forumTopicData?.isClosed)
|
|
.updatedPremiumGiftOptions(premiumGiftOptions)
|
|
.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))
|
|
}
|
|
}
|
|
})
|
|
} else {
|
|
if !self.didSetCachedDataReady {
|
|
self.didSetCachedDataReady = true
|
|
self.cachedDataReady.set(.single(true))
|
|
}
|
|
}
|
|
|
|
self.historyStateDisposable = self.chatDisplayNode.historyNode.historyState.get().startStrict(next: { [weak self] state in
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: strongSelf.isViewLoaded && strongSelf.view.window != nil, {
|
|
$0.updatedChatHistoryState(state)
|
|
})
|
|
|
|
if let botStart = strongSelf.botStart, case let .loaded(isEmpty, _) = state {
|
|
strongSelf.botStart = nil
|
|
if !isEmpty {
|
|
strongSelf.startBot(botStart.payload)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
let effectiveCachedDataReady: Signal<Bool, NoError>
|
|
if case .replyThread = self.chatLocation {
|
|
effectiveCachedDataReady = self.cachedDataReady.get()
|
|
} else {
|
|
effectiveCachedDataReady = self.cachedDataReady.get()
|
|
}
|
|
var measure_isFirstTime = true
|
|
let initTimestamp = self.initTimestamp
|
|
|
|
let mapped_chatLocationInfoReady = self._chatLocationInfoReady.get() |> filter { $0 } |> debug_measureTimeToFirstEvent(label: "chatLocationInfoReady")
|
|
let mapped_effectiveCachedDataReady = effectiveCachedDataReady |> filter { $0 } |> debug_measureTimeToFirstEvent(label: "effectiveCachedDataReady")
|
|
let mapped_initialDataReady = initialData |> map { $0 != nil } |> filter { $0 } |> debug_measureTimeToFirstEvent(label: "initialDataReady")
|
|
let mapped_wallpaperReady = self.wallpaperReady.get() |> filter { $0 } |> debug_measureTimeToFirstEvent(label: "wallpaperReady")
|
|
let mapped_presentationReady = self.presentationReady.get() |> filter { $0 } |> debug_measureTimeToFirstEvent(label: "presentationReady")
|
|
|
|
self.ready.set(combineLatest(queue: .mainQueue(),
|
|
mapped_chatLocationInfoReady,
|
|
mapped_effectiveCachedDataReady,
|
|
mapped_initialDataReady,
|
|
mapped_wallpaperReady,
|
|
mapped_presentationReady
|
|
)
|
|
|> map { chatLocationInfoReady, cachedDataReady, initialData, wallpaperReady, presentationReady in
|
|
return chatLocationInfoReady && cachedDataReady && initialData && wallpaperReady && presentationReady
|
|
}
|
|
|> distinctUntilChanged
|
|
|> beforeNext { value in
|
|
if measure_isFirstTime {
|
|
measure_isFirstTime = false
|
|
#if DEBUG
|
|
let deltaTime = (CFAbsoluteTimeGetCurrent() - initTimestamp) * 1000.0
|
|
print("Chat controller init to ready: \(deltaTime) ms")
|
|
#endif
|
|
}
|
|
})
|
|
#if DEBUG
|
|
//self.ready.set(.single(true))
|
|
#endif
|
|
|
|
if self.context.sharedContext.immediateExperimentalUISettings.crashOnLongQueries {
|
|
let _ = (self.ready.get()
|
|
|> filter({ $0 })
|
|
|> take(1)
|
|
|> timeout(0.8, queue: .concurrentDefaultQueue(), alternate: Signal { _ in
|
|
preconditionFailure()
|
|
})).startStandalone()
|
|
}
|
|
|
|
self.chatDisplayNode.historyNode.contentPositionChanged = { [weak self] offset in
|
|
guard let strongSelf = self else { return }
|
|
|
|
var minOffsetForNavigation: CGFloat = 40.0
|
|
strongSelf.chatDisplayNode.historyNode.enumerateItemNodes { itemNode in
|
|
if let itemNode = itemNode as? ChatMessageBubbleItemNode {
|
|
if let message = itemNode.item?.content.firstMessage, let adAttribute = message.adAttribute {
|
|
minOffsetForNavigation += itemNode.bounds.height
|
|
|
|
switch offset {
|
|
case let .known(offset):
|
|
if offset <= 50.0 {
|
|
strongSelf.chatDisplayNode.historyNode.markAdAsSeen(opaqueId: adAttribute.opaqueId)
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
let offsetAlpha: CGFloat
|
|
let plainInputSeparatorAlpha: CGFloat
|
|
switch offset {
|
|
case let .known(offset):
|
|
if offset < minOffsetForNavigation {
|
|
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.controllerInteraction?.recommendedChannelsOpenUp = !strongSelf.shouldDisplayDownButton
|
|
strongSelf.updateDownButtonVisibility()
|
|
strongSelf.chatDisplayNode.updatePlainInputSeparatorAlpha(plainInputSeparatorAlpha, transition: .animated(duration: 0.2, curve: .easeInOut))
|
|
}
|
|
|
|
self.chatDisplayNode.historyNode.scrolledToIndex = { [weak self] toSubject, initial in
|
|
if let strongSelf = self, case let .message(index) = toSubject.index {
|
|
if case let .message(messageSubject, _, _, _) = strongSelf.subject, initial, case let .id(messageId) = messageSubject, messageId != index.id {
|
|
if messageId.peerId == index.id.peerId {
|
|
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Conversation_MessageDoesntExist, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return true }), in: .current)
|
|
}
|
|
} else if let controllerInteraction = strongSelf.controllerInteraction {
|
|
var mappedId = index.id
|
|
if index.timestamp == 0 {
|
|
if case let .replyThread(message) = strongSelf.chatLocation, let channelMessageId = message.channelMessageId {
|
|
mappedId = channelMessageId
|
|
}
|
|
}
|
|
|
|
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(mappedId) {
|
|
if toSubject.setupReply {
|
|
Queue.mainQueue().after(0.1) {
|
|
strongSelf.interfaceInteraction?.setupReplyMessage(mappedId, { _, f in f() })
|
|
}
|
|
}
|
|
|
|
let highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId, quote: toSubject.quote.flatMap { quote in ChatInterfaceHighlightedState.Quote(string: quote.string, offset: quote.offset) })
|
|
controllerInteraction.highlightedState = highlightedState
|
|
strongSelf.updateItemNodesHighlightedStates(animated: initial)
|
|
strongSelf.scrolledToMessageIdValue = ScrolledToMessageId(id: mappedId, allowedReplacementDirection: [])
|
|
|
|
var hasQuote = false
|
|
if let quote = toSubject.quote {
|
|
if message.text.contains(quote.string) {
|
|
hasQuote = true
|
|
} else {
|
|
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Chat_ToastQuoteNotFound, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return true }), in: .current)
|
|
}
|
|
}
|
|
|
|
strongSelf.messageContextDisposable.set((Signal<Void, NoError>.complete() |> delay(hasQuote ? 1.5 : 0.7, queue: Queue.mainQueue())).startStrict(completed: {
|
|
if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction {
|
|
if controllerInteraction.highlightedState == highlightedState {
|
|
controllerInteraction.highlightedState = nil
|
|
strongSelf.updateItemNodesHighlightedStates(animated: true)
|
|
}
|
|
}
|
|
}))
|
|
|
|
if let (messageId, params) = strongSelf.scheduledScrollToMessageId {
|
|
strongSelf.scheduledScrollToMessageId = nil
|
|
if let timecode = params.timestamp, message.id == messageId {
|
|
Queue.mainQueue().after(0.2) {
|
|
let _ = strongSelf.controllerInteraction?.openMessage(message, OpenMessageParams(mode: .timecode(timecode)))
|
|
}
|
|
}
|
|
} else if case let .message(_, _, maybeTimecode, _) = strongSelf.subject, let timecode = maybeTimecode, initial {
|
|
Queue.mainQueue().after(0.2) {
|
|
let _ = strongSelf.controllerInteraction?.openMessage(message, OpenMessageParams(mode: .timecode(timecode)))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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, messageCorrelationId in
|
|
//print("setup layoutActionOnViewTransition")
|
|
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.layoutActionOnViewTransitionAction = f
|
|
|
|
self.chatDisplayNode.historyNode.layoutActionOnViewTransition = ({ [weak self] transition in
|
|
f()
|
|
if let strongSelf = self, let validLayout = strongSelf.validLayout {
|
|
strongSelf.layoutActionOnViewTransitionAction = nil
|
|
|
|
var mappedTransition: (ChatHistoryListViewTransition, ListViewUpdateSizeAndInsets?)?
|
|
|
|
let isScheduledMessages: Bool
|
|
if case .scheduledMessages = strongSelf.presentationInterfaceState.subject {
|
|
isScheduledMessages = true
|
|
} else {
|
|
isScheduledMessages = false
|
|
}
|
|
let duration: Double = strongSelf.chatDisplayNode.messageTransitionNode.hasScheduledTransitions ? ChatMessageTransitionNodeImpl.animationDuration : 0.18
|
|
let curve: ContainedViewLayoutTransitionCurve = strongSelf.chatDisplayNode.messageTransitionNode.hasScheduledTransitions ? ChatMessageTransitionNodeImpl.verticalAnimationCurve : .easeInOut
|
|
let controlPoints: (Float, Float, Float, Float) = strongSelf.chatDisplayNode.messageTransitionNode.hasScheduledTransitions ? ChatMessageTransitionNodeImpl.verticalAnimationControlPoints : (0.5, 0.33, 0.0, 0.0)
|
|
|
|
let shouldUseFastMessageSendAnimation = strongSelf.chatDisplayNode.shouldUseFastMessageSendAnimation
|
|
|
|
strongSelf.chatDisplayNode.containerLayoutUpdated(validLayout, navigationBarHeight: strongSelf.navigationLayout(layout: validLayout).navigationFrame.maxY, transition: .animated(duration: duration, curve: curve), listViewTransaction: { updateSizeAndInsets, _, _, _ in
|
|
|
|
var options = transition.options
|
|
let _ = options.insert(.Synchronous)
|
|
let _ = options.insert(.LowLatency)
|
|
let _ = options.insert(.PreferSynchronousResourceLoading)
|
|
|
|
var deleteItems = transition.deleteItems
|
|
var insertItems: [ListViewInsertItem] = []
|
|
var stationaryItemRange: (Int, Int)?
|
|
var scrollToItem: ListViewScrollToItem?
|
|
|
|
if shouldUseFastMessageSendAnimation {
|
|
options.remove(.AnimateInsertion)
|
|
options.insert(.RequestItemInsertionAnimations)
|
|
|
|
deleteItems = transition.deleteItems.map({ item in
|
|
return ListViewDeleteItem(index: item.index, directionHint: nil)
|
|
})
|
|
|
|
var maxInsertedItem: Int?
|
|
var insertedIndex: Int?
|
|
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))
|
|
}
|
|
|
|
if isScheduledMessages, let insertedIndex = insertedIndex {
|
|
scrollToItem = ListViewScrollToItem(index: insertedIndex, position: .visible, animated: true, curve: .Custom(duration: duration, controlPoints.0, controlPoints.1, controlPoints.2, controlPoints.3), directionHint: .Down)
|
|
} else if transition.historyView.originalView.laterId == nil {
|
|
scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Custom(duration: duration, controlPoints.0, controlPoints.1, controlPoints.2, controlPoints.3), directionHint: .Up)
|
|
}
|
|
|
|
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, animateFromPreviousFilter: false), updateSizeAndInsets)
|
|
}, updateExtraNavigationBarBackgroundHeight: { value, hitTestSlop, _ in
|
|
strongSelf.additionalNavigationBarBackgroundHeight = value
|
|
strongSelf.additionalNavigationBarHitTestSlop = hitTestSlop
|
|
})
|
|
|
|
if let mappedTransition = mappedTransition {
|
|
return mappedTransition
|
|
}
|
|
}
|
|
return (transition, nil)
|
|
}, messageCorrelationId)
|
|
}
|
|
|
|
self.chatDisplayNode.sendMessages = { [weak self] messages, silentPosting, scheduleTime, isAnyMessageTextPartitioned, postpone in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
var correlationIds: [Int64] = []
|
|
for message in messages {
|
|
switch message {
|
|
case let .message(_, _, _, _, _, _, _, _, correlationId, _):
|
|
if let correlationId = correlationId {
|
|
correlationIds.append(correlationId)
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
strongSelf.commitPurposefulAction()
|
|
|
|
if let peerId = strongSelf.chatLocation.peerId {
|
|
var hasDisabledContent = false
|
|
if "".isEmpty {
|
|
hasDisabledContent = false
|
|
}
|
|
|
|
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
|
|
} else if hasDisabledContent {
|
|
errorText = strongSelf.restrictedSendingContentsText()
|
|
}
|
|
|
|
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 = strongSelf.transformEnqueueMessages(messages, silentPosting: silentPosting ?? false, scheduleTime: scheduleTime, postpone: postpone)
|
|
|
|
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 _ = (strongSelf.shouldDivertMessagesToScheduled(messages: transformedMessages)
|
|
|> deliverOnMainQueue).start(next: { shouldDivert in
|
|
let signal: Signal<[MessageId?], NoError>
|
|
var shouldOpenScheduledMessages = false
|
|
if forwardSourcePeerIds.count > 1 {
|
|
var forwardedMessages = forwardedMessages
|
|
if shouldDivert {
|
|
forwardedMessages = forwardedMessages.map { messageGroup -> [EnqueueMessage] in
|
|
return messageGroup.map { message -> EnqueueMessage in
|
|
return message.withUpdatedAttributes { attributes in
|
|
var attributes = attributes
|
|
attributes.removeAll(where: { $0 is OutgoingScheduleInfoMessageAttribute })
|
|
attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: Int32(Date().timeIntervalSince1970) + 10 * 24 * 60 * 60))
|
|
return attributes
|
|
}
|
|
}
|
|
}
|
|
shouldOpenScheduledMessages = true
|
|
}
|
|
|
|
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 {
|
|
var transformedMessages = transformedMessages
|
|
if shouldDivert {
|
|
transformedMessages = transformedMessages.map { message -> EnqueueMessage in
|
|
return message.withUpdatedAttributes { attributes in
|
|
var attributes = attributes
|
|
attributes.removeAll(where: { $0 is OutgoingScheduleInfoMessageAttribute })
|
|
attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: Int32(Date().timeIntervalSince1970) + 10 * 24 * 60 * 60))
|
|
return attributes
|
|
}
|
|
}
|
|
shouldOpenScheduledMessages = true
|
|
}
|
|
|
|
signal = enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: transformedMessages)
|
|
}
|
|
|
|
let _ = (signal
|
|
|> deliverOnMainQueue).startStandalone(next: { messageIds in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if case .scheduledMessages = strongSelf.presentationInterfaceState.subject {
|
|
} else {
|
|
strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
|
|
|
if shouldOpenScheduledMessages {
|
|
if let layoutActionOnViewTransitionAction = strongSelf.layoutActionOnViewTransitionAction {
|
|
strongSelf.layoutActionOnViewTransitionAction = nil
|
|
layoutActionOnViewTransitionAction()
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
donateSendMessageIntent(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, intentContext: .chat, peerIds: [peerId])
|
|
})
|
|
} else if case let .customChatContents(customChatContents) = strongSelf.subject {
|
|
switch customChatContents.kind {
|
|
case .hashTagSearch:
|
|
break
|
|
case .quickReplyMessageInput:
|
|
customChatContents.enqueueMessages(messages: messages)
|
|
strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
|
case let .businessLinkSetup(link):
|
|
if messages.count > 1 {
|
|
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.BusinessLink_AlertTextLimitText, actions: [
|
|
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})
|
|
]), in: .window(.root))
|
|
|
|
return
|
|
}
|
|
|
|
var text: String = ""
|
|
var entities: [MessageTextEntity] = []
|
|
if let message = messages.first {
|
|
if case let .message(textValue, attributes, _, _, _, _, _, _, _, _) = message {
|
|
text = textValue
|
|
for attribute in attributes {
|
|
if let attribute = attribute as? TextEntitiesMessageAttribute {
|
|
entities = attribute.entities
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let _ = strongSelf.context.engine.accountData.editBusinessChatLink(url: link.url, message: text, entities: entities, title: link.title).start()
|
|
if case let .customChatContents(customChatContents) = strongSelf.subject {
|
|
customChatContents.businessLinkUpdate(message: text, entities: entities, title: link.title)
|
|
}
|
|
|
|
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .succeed(text: strongSelf.presentationData.strings.Business_Links_EditLinkToastSaved, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false }), in: .current)
|
|
case let .postSuggestions(postSuggestions):
|
|
if let customChatContents = customChatContents as? PostSuggestionsChatContents {
|
|
//TODO:release
|
|
strongSelf.chatDisplayNode.dismissInput()
|
|
|
|
let _ = (ChatSendStarsScreen.initialData(context: strongSelf.context, peerId: customChatContents.peerId, suggestMessageAmount: postSuggestions, completion: { [weak strongSelf] amount, timestamp in
|
|
guard let strongSelf else {
|
|
return
|
|
}
|
|
guard case let .customChatContents(customChatContents) = strongSelf.subject else {
|
|
return
|
|
}
|
|
if amount == 0 {
|
|
return
|
|
}
|
|
let messages = messages.map { message in
|
|
return message.withUpdatedAttributes { attributes in
|
|
var attributes = attributes
|
|
attributes.removeAll(where: { $0 is OutgoingSuggestedPostMessageAttribute })
|
|
attributes.append(OutgoingSuggestedPostMessageAttribute(
|
|
price: StarsAmount(value: amount, nanos: 0),
|
|
timestamp: timestamp
|
|
))
|
|
return attributes
|
|
}
|
|
}
|
|
customChatContents.enqueueMessages(messages: messages)
|
|
strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
|
})
|
|
|> deliverOnMainQueue).startStandalone(next: { [weak strongSelf] initialData in
|
|
guard let strongSelf, let initialData else {
|
|
return
|
|
}
|
|
let sendStarsScreen = ChatSendStarsScreen(
|
|
context: strongSelf.context,
|
|
initialData: initialData
|
|
)
|
|
strongSelf.push(sendStarsScreen)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
strongSelf.updateChatPresentationInterfaceState(interactive: true, { $0.updatedShowCommands(false) })
|
|
}
|
|
|
|
if case let .customChatContents(customChatContents) = self.subject {
|
|
customChatContents.hashtagSearchResultsUpdate = { [weak self] searchResult in
|
|
guard let self else {
|
|
return
|
|
}
|
|
let (results, state) = searchResult
|
|
let isEmpty = results.totalCount == 0
|
|
if isEmpty {
|
|
self.alwaysShowSearchResultsAsList = true
|
|
}
|
|
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in
|
|
var updatedState = current
|
|
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
|
|
}
|
|
}
|
|
}
|
|
updatedState = updatedState.updatedSearch(data.withUpdatedResultsState(ChatSearchResultsState(messageIndices: messageIndices, currentId: currentIndex?.id, state: state, totalCount: results.totalCount, completed: results.completed)))
|
|
}
|
|
if isEmpty {
|
|
updatedState = updatedState.updatedDisplayHistoryFilterAsList(true)
|
|
}
|
|
return updatedState
|
|
})
|
|
self.searchResult.set(.single((results, state, .general(scope: .channels, tags: nil, minDate: nil, maxDate: nil))))
|
|
}
|
|
}
|
|
|
|
self.chatDisplayNode.requestUpdateChatInterfaceState = { [weak self] transition, saveInterfaceState, f in
|
|
self?.updateChatPresentationInterfaceState(transition: transition, 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
|
|
}
|
|
strongSelf.interfaceInteraction?.updateShowWebView { _ in
|
|
return false
|
|
}
|
|
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.view, rect)
|
|
}
|
|
return
|
|
}
|
|
if let messageId = strongSelf.presentationInterfaceState.interfaceState.editMessage?.messageId {
|
|
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId))
|
|
|> deliverOnMainQueue).startStandalone(next: { message in
|
|
guard let strongSelf = self, let editMessageState = strongSelf.presentationInterfaceState.editMessageState 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._asMessage()), media: image)
|
|
} else if let file = media as? TelegramMediaFile {
|
|
if file.isVideo || file.isAnimated {
|
|
originalMediaReference = .message(message: MessageReference(message._asMessage()), media: file)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
var editMediaOptions: MessageMediaEditingOptions?
|
|
if case let .media(options) = editMessageState.content {
|
|
editMediaOptions = options
|
|
}
|
|
strongSelf.presentEditingAttachmentMenu(editMediaOptions: editMediaOptions, editMediaReference: originalMediaReference)
|
|
})
|
|
} else {
|
|
strongSelf.presentAttachmentMenu(subject: .default)
|
|
}
|
|
}
|
|
self.chatDisplayNode.paste = { [weak self] data in
|
|
switch data {
|
|
case let .images(images):
|
|
self?.displayPasteMenu(images.map { .image($0) })
|
|
case let .video(data):
|
|
let tempFilePath = NSTemporaryDirectory() + "\(Int64.random(in: 0...Int64.max)).mp4"
|
|
let url = NSURL(fileURLWithPath: tempFilePath) as URL
|
|
try? data.write(to: url)
|
|
self?.displayPasteMenu([.video(url)])
|
|
case let .gif(data):
|
|
self?.enqueueGifData(data)
|
|
case let .sticker(image, isMemoji):
|
|
self?.enqueueStickerImage(image, isMemoji: isMemoji)
|
|
case let .animatedSticker(data):
|
|
self?.enqueueAnimatedStickerData(data)
|
|
}
|
|
}
|
|
self.chatDisplayNode.updateTypingActivity = { [weak self] value in
|
|
if let strongSelf = self {
|
|
if value {
|
|
strongSelf.typingActivityPromise.set(Signal<Bool, NoError>.single(true)
|
|
|> then(
|
|
Signal<Bool, NoError>.single(false)
|
|
|> delay(4.0, queue: Queue.mainQueue())
|
|
))
|
|
|
|
if !strongSelf.didDisplayGroupEmojiTip, value {
|
|
strongSelf.didDisplayGroupEmojiTip = true
|
|
|
|
Queue.mainQueue().after(2.0) {
|
|
strongSelf.displayGroupEmojiTooltip()
|
|
}
|
|
}
|
|
|
|
if !strongSelf.didDisplaySendWhenOnlineTip, value {
|
|
strongSelf.didDisplaySendWhenOnlineTip = true
|
|
|
|
strongSelf.displaySendWhenOnlineTipDisposable.set(
|
|
(strongSelf.typingActivityPromise.get()
|
|
|> filter { !$0 }
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { [weak self] _ in
|
|
if let strongSelf = self {
|
|
Queue.mainQueue().after(2.0) {
|
|
strongSelf.displaySendWhenOnlineTooltip()
|
|
}
|
|
}
|
|
})
|
|
)
|
|
}
|
|
} 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?.url {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { presentationInterfaceState in
|
|
return presentationInterfaceState.updatedInterfaceState { interfaceState in
|
|
return interfaceState.withUpdatedEditMessage(interfaceState.editMessage.flatMap { editMessage in
|
|
var editMessage = editMessage
|
|
if !editMessage.disableUrlPreviews.contains(link) {
|
|
editMessage.disableUrlPreviews.append(link)
|
|
}
|
|
return editMessage
|
|
})
|
|
}
|
|
})
|
|
}
|
|
} else {
|
|
if let link = strongSelf.presentationInterfaceState.urlPreview?.url {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { presentationInterfaceState in
|
|
return presentationInterfaceState.updatedInterfaceState { interfaceState in
|
|
var composeDisableUrlPreviews = interfaceState.composeDisableUrlPreviews
|
|
if !composeDisableUrlPreviews.contains(link) {
|
|
composeDisableUrlPreviews.append(link)
|
|
}
|
|
return interfaceState.withUpdatedComposeDisableUrlPreviews(composeDisableUrlPreviews)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.chatDisplayNode.navigateButtons.downPressed = { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
if case let .customChatContents(contents) = self.presentationInterfaceState.subject, case .hashTagSearch = contents.kind {
|
|
self.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
|
} else if let resultsState = self.presentationInterfaceState.search?.resultsState, !resultsState.messageIndices.isEmpty {
|
|
if let currentId = resultsState.currentId, let index = resultsState.messageIndices.firstIndex(where: { $0.id == currentId }) {
|
|
if index != resultsState.messageIndices.count - 1 {
|
|
self.interfaceInteraction?.navigateMessageSearch(.later)
|
|
} else {
|
|
self.scrollToEndOfHistory()
|
|
}
|
|
} else {
|
|
self.scrollToEndOfHistory()
|
|
}
|
|
} else {
|
|
if let messageId = self.historyNavigationStack.removeLast() {
|
|
self.navigateToMessage(from: nil, to: .id(messageId.id, NavigateToMessageParams(timestamp: nil, quote: nil)), rememberInStack: false)
|
|
} else {
|
|
if case .known = self.chatDisplayNode.historyNode.visibleContentOffset() {
|
|
self.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
|
} else if case .peer = self.chatLocation {
|
|
self.scrollToEndOfHistory()
|
|
} else if case .replyThread = self.chatLocation {
|
|
self.scrollToEndOfHistory()
|
|
} else {
|
|
self.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
self.chatDisplayNode.navigateButtons.upPressed = { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
if self.presentationInterfaceState.search?.resultsState != nil {
|
|
self.interfaceInteraction?.navigateMessageSearch(.earlier)
|
|
}
|
|
}
|
|
|
|
self.chatDisplayNode.navigateButtons.mentionsPressed = { [weak self] in
|
|
if let strongSelf = self, strongSelf.isNodeLoaded, let peerId = strongSelf.chatLocation.peerId {
|
|
let signal = strongSelf.context.engine.messages.earliestUnseenPersonalMentionMessage(peerId: peerId, threadId: strongSelf.chatLocation.threadId)
|
|
strongSelf.navigationActionDisposable.set((signal |> deliverOnMainQueue).startStrict(next: { result in
|
|
if let strongSelf = self {
|
|
switch result {
|
|
case let .result(messageId):
|
|
if let messageId = messageId {
|
|
strongSelf.navigateToMessage(from: nil, to: .id(messageId, NavigateToMessageParams(timestamp: nil, quote: nil)))
|
|
}
|
|
case .loading:
|
|
break
|
|
}
|
|
}
|
|
}))
|
|
}
|
|
}
|
|
|
|
self.chatDisplayNode.navigateButtons.mentionsButton.activated = { [weak self] gesture, _ in
|
|
guard let strongSelf = self else {
|
|
gesture.cancel()
|
|
return
|
|
}
|
|
|
|
strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts()
|
|
|
|
var menuItems: [ContextMenuItem] = []
|
|
menuItems.append(.action(ContextMenuActionItem(
|
|
id: nil,
|
|
text: strongSelf.presentationData.strings.WebSearch_RecentSectionClear,
|
|
textColor: .primary,
|
|
textLayout: .singleLine,
|
|
icon: { theme in
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Read"), color: theme.contextMenu.primaryColor)
|
|
},
|
|
action: { _, f in
|
|
f(.dismissWithoutContent)
|
|
|
|
guard let strongSelf = self, let peerId = strongSelf.chatLocation.peerId else {
|
|
return
|
|
}
|
|
let _ = clearPeerUnseenPersonalMessagesInteractively(account: strongSelf.context.account, peerId: peerId, threadId: strongSelf.chatLocation.threadId).startStandalone()
|
|
}
|
|
)))
|
|
let items = ContextController.Items(content: .list(menuItems))
|
|
|
|
let controller = ContextController(presentationData: strongSelf.presentationData, source: .extracted(ChatMessageNavigationButtonContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, contentNode: strongSelf.chatDisplayNode.navigateButtons.mentionsButton.containerNode)), items: .single(items), recognizer: nil, gesture: gesture)
|
|
|
|
strongSelf.forEachController({ controller in
|
|
if let controller = controller as? TooltipScreen {
|
|
controller.dismiss()
|
|
}
|
|
return true
|
|
})
|
|
strongSelf.window?.presentInGlobalOverlay(controller)
|
|
}
|
|
|
|
self.chatDisplayNode.navigateButtons.reactionsPressed = { [weak self] in
|
|
if let strongSelf = self, strongSelf.isNodeLoaded, let peerId = strongSelf.chatLocation.peerId {
|
|
let signal = strongSelf.context.engine.messages.earliestUnseenPersonalReactionMessage(peerId: peerId, threadId: strongSelf.chatLocation.threadId)
|
|
strongSelf.navigationActionDisposable.set((signal |> deliverOnMainQueue).startStrict(next: { result in
|
|
if let strongSelf = self {
|
|
switch result {
|
|
case let .result(messageId):
|
|
if let messageId = messageId {
|
|
strongSelf.chatDisplayNode.historyNode.suspendReadingReactions = true
|
|
strongSelf.navigateToMessage(from: nil, to: .id(messageId, NavigateToMessageParams(timestamp: nil, quote: nil)), scrollPosition: .center(.top), completion: {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in
|
|
guard let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item else {
|
|
return
|
|
}
|
|
guard item.message.id == messageId else {
|
|
return
|
|
}
|
|
var maybeUpdatedReaction: (MessageReaction.Reaction, Bool, EnginePeer?)?
|
|
if let attribute = item.message.reactionsAttribute {
|
|
for recentPeer in attribute.recentPeers {
|
|
if recentPeer.isUnseen {
|
|
maybeUpdatedReaction = (recentPeer.value, recentPeer.isLarge, item.message.peers[recentPeer.peerId].flatMap(EnginePeer.init))
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
guard let (updatedReaction, updatedReactionIsLarge, updatedReactionPeer) = maybeUpdatedReaction else {
|
|
return
|
|
}
|
|
|
|
guard let availableReactions = item.associatedData.availableReactions else {
|
|
return
|
|
}
|
|
|
|
var avatarPeers: [EnginePeer] = []
|
|
if item.message.id.peerId.namespace != Namespaces.Peer.CloudUser, let updatedReactionPeer = updatedReactionPeer {
|
|
avatarPeers.append(updatedReactionPeer)
|
|
}
|
|
|
|
var reactionItem: ReactionItem?
|
|
|
|
switch updatedReaction {
|
|
case .builtin, .stars:
|
|
for reaction in availableReactions.reactions {
|
|
guard let centerAnimation = reaction.centerAnimation else {
|
|
continue
|
|
}
|
|
guard let aroundAnimation = reaction.aroundAnimation else {
|
|
continue
|
|
}
|
|
if reaction.value == updatedReaction {
|
|
reactionItem = ReactionItem(
|
|
reaction: ReactionItem.Reaction(rawValue: reaction.value),
|
|
appearAnimation: reaction.appearAnimation,
|
|
stillAnimation: reaction.selectAnimation,
|
|
listAnimation: centerAnimation,
|
|
largeListAnimation: reaction.activateAnimation,
|
|
applicationAnimation: aroundAnimation,
|
|
largeApplicationAnimation: reaction.effectAnimation,
|
|
isCustom: false
|
|
)
|
|
break
|
|
}
|
|
}
|
|
case let .custom(fileId):
|
|
if let itemFile = item.message.associatedMedia[MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile {
|
|
let itemFile = TelegramMediaFile.Accessor(itemFile)
|
|
reactionItem = ReactionItem(
|
|
reaction: ReactionItem.Reaction(rawValue: updatedReaction),
|
|
appearAnimation: itemFile,
|
|
stillAnimation: itemFile,
|
|
listAnimation: itemFile,
|
|
largeListAnimation: itemFile,
|
|
applicationAnimation: nil,
|
|
largeApplicationAnimation: nil,
|
|
isCustom: true
|
|
)
|
|
}
|
|
}
|
|
|
|
guard let targetView = itemNode.targetReactionView(value: updatedReaction) else {
|
|
return
|
|
}
|
|
if let reactionItem = reactionItem {
|
|
let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: strongSelf.chatDisplayNode.historyNode.takeGenericReactionEffect())
|
|
|
|
strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation)
|
|
|
|
strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation)
|
|
standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds
|
|
standaloneReactionAnimation.animateReactionSelection(
|
|
context: strongSelf.context,
|
|
theme: strongSelf.presentationData.theme,
|
|
animationCache: strongSelf.controllerInteraction!.presentationContext.animationCache,
|
|
reaction: reactionItem,
|
|
avatarPeers: avatarPeers,
|
|
playHaptic: true,
|
|
isLarge: updatedReactionIsLarge,
|
|
targetView: targetView,
|
|
addStandaloneReactionAnimation: { standaloneReactionAnimation in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation)
|
|
standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds
|
|
strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation)
|
|
},
|
|
completion: { [weak standaloneReactionAnimation] in
|
|
standaloneReactionAnimation?.removeFromSupernode()
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
strongSelf.chatDisplayNode.historyNode.suspendReadingReactions = false
|
|
})
|
|
}
|
|
case .loading:
|
|
break
|
|
}
|
|
}
|
|
}))
|
|
}
|
|
}
|
|
|
|
self.chatDisplayNode.navigateButtons.reactionsButton.activated = { [weak self] gesture, _ in
|
|
guard let strongSelf = self else {
|
|
gesture.cancel()
|
|
return
|
|
}
|
|
|
|
strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts()
|
|
|
|
var menuItems: [ContextMenuItem] = []
|
|
menuItems.append(.action(ContextMenuActionItem(
|
|
id: nil,
|
|
text: strongSelf.presentationData.strings.Conversation_ReadAllReactions,
|
|
textColor: .primary,
|
|
textLayout: .singleLine,
|
|
icon: { theme in
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Read"), color: theme.contextMenu.primaryColor)
|
|
},
|
|
action: { _, f in
|
|
f(.dismissWithoutContent)
|
|
|
|
guard let strongSelf = self, let peerId = strongSelf.chatLocation.peerId else {
|
|
return
|
|
}
|
|
let _ = clearPeerUnseenReactionsInteractively(account: strongSelf.context.account, peerId: peerId, threadId: strongSelf.chatLocation.threadId).startStandalone()
|
|
}
|
|
)))
|
|
let items = ContextController.Items(content: .list(menuItems))
|
|
|
|
let controller = ContextController(presentationData: strongSelf.presentationData, source: .extracted(ChatMessageNavigationButtonContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, contentNode: strongSelf.chatDisplayNode.navigateButtons.reactionsButton.containerNode)), items: .single(items), recognizer: nil, gesture: gesture)
|
|
|
|
strongSelf.forEachController({ controller in
|
|
if let controller = controller as? TooltipScreen {
|
|
controller.dismiss()
|
|
}
|
|
return true
|
|
})
|
|
strongSelf.window?.presentInGlobalOverlay(controller)
|
|
}
|
|
|
|
let interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { [weak self] messageId, completion in
|
|
guard let strongSelf = self, strongSelf.isNodeLoaded else {
|
|
return
|
|
}
|
|
|
|
guard !strongSelf.presentAccountFrozenInfoIfNeeded(delay: true) else {
|
|
completion(.immediate, {})
|
|
return
|
|
}
|
|
|
|
if let messageId = messageId {
|
|
let intrinsicCanSendMessagesHere = canSendMessagesToChat(strongSelf.presentationInterfaceState)
|
|
var canSendMessagesHere = intrinsicCanSendMessagesHere
|
|
if case .standard(.embedded) = strongSelf.presentationInterfaceState.mode {
|
|
canSendMessagesHere = false
|
|
}
|
|
if case .inline = strongSelf.presentationInterfaceState.mode {
|
|
canSendMessagesHere = false
|
|
}
|
|
|
|
if canSendMessagesHere {
|
|
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
|
|
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({
|
|
$0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject(
|
|
messageId: message.id,
|
|
quote: nil
|
|
))
|
|
}).updatedReplyMessage(message).updatedSearch(nil).updatedShowCommands(false) }, completion: { t in
|
|
completion(t, {})
|
|
})
|
|
strongSelf.updateItemNodesSearchTextHighlightStates()
|
|
strongSelf.chatDisplayNode.ensureInputViewFocused()
|
|
} else {
|
|
completion(.immediate, {})
|
|
}
|
|
}, alertAction: {
|
|
completion(.immediate, {})
|
|
}, delay: true)
|
|
} else {
|
|
let replySubject = ChatInterfaceState.ReplyMessageSubject(
|
|
messageId: messageId,
|
|
quote: nil
|
|
)
|
|
|
|
completion(.immediate, {
|
|
guard let self else {
|
|
return
|
|
}
|
|
if intrinsicCanSendMessagesHere {
|
|
if let peerId = self.chatLocation.peerId {
|
|
moveReplyToChat(selfController: self, peerId: peerId, threadId: self.chatLocation.threadId, replySubject: replySubject, completion: {})
|
|
}
|
|
} else {
|
|
moveReplyMessageToAnotherChat(selfController: self, replySubject: replySubject)
|
|
}
|
|
})
|
|
}
|
|
} else {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) }) }, completion: { t in
|
|
completion(t, {})
|
|
})
|
|
}
|
|
}, 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 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.userLimits.maxCaptionLength
|
|
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
|
|
webpageUrl = content.url
|
|
}
|
|
}
|
|
|
|
let inputText = chatInputStateStringWithAppliedEntities(message.text, entities: entities)
|
|
var disableUrlPreviews: [String] = []
|
|
if webpageUrl == nil {
|
|
disableUrlPreviews = detectUrls(inputText)
|
|
}
|
|
|
|
var updated = state.updatedInterfaceState { interfaceState in
|
|
return interfaceState.withUpdatedEditMessage(ChatEditMessageState(messageId: messageId, inputState: ChatTextInputState(inputText: inputText), disableUrlPreviews: disableUrlPreviews, inputTextMaxLength: inputTextMaxLength, mediaCaptionIsAbove: nil))
|
|
}
|
|
|
|
let (updatedState, updatedPreviewQueryState) = updatedChatEditInterfaceMessageState(context: strongSelf.context, state: updated, message: message)
|
|
updated = updatedState
|
|
strongSelf.editingUrlPreviewQueryState?.1.dispose()
|
|
strongSelf.editingUrlPreviewQueryState = updatedPreviewQueryState
|
|
|
|
updated = updated.updatedInputMode({ _ in
|
|
return .text
|
|
})
|
|
updated = updated.updatedShowCommands(false)
|
|
|
|
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) }.updatedShowCommands(false) }, 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)
|
|
}
|
|
}, cancelMessageSelection: { [weak self] transition in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.updateChatPresentationInterfaceState(transition: transition, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
|
}, 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(engine: strongSelf.context.engine, accountPeerId: strongSelf.context.account.peerId, messageIds: messageIds, keepUpdated: false)
|
|
|> deliverOnMainQueue).startStrict(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.banAuthors.isEmpty {
|
|
strongSelf.presentMultiBanMessageOptions(accountPeerId: strongSelf.context.account.peerId, authors: actions.banAuthors, 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 (_, option, message) = strongSelf.presentationInterfaceState.reportReason {
|
|
let presentationData = strongSelf.presentationData
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }, completion: { _ in
|
|
let _ = (strongSelf.context.engine.messages.reportContent(subject: .messages(Array(messageIds)), option: option, message: message)
|
|
|> deliverOnMainQueue).startStandalone(completed: {
|
|
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .emoji(name: "PoliceCar", text: presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return false }), in: .current)
|
|
})
|
|
})
|
|
} else {
|
|
strongSelf.context.sharedContext.makeContentReportScreen(
|
|
context: strongSelf.context,
|
|
subject: .messages(Array(messageIds).sorted()),
|
|
forceDark: false,
|
|
present: { [weak self] controller in
|
|
self?.push(controller)
|
|
},
|
|
completion: { [weak self] in
|
|
self?.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
|
},
|
|
requestSelectMessages: nil
|
|
)
|
|
}
|
|
}
|
|
}, reportMessages: { [weak self] messages, contextController in
|
|
guard let self, !messages.isEmpty else {
|
|
return
|
|
}
|
|
contextController?.dismiss()
|
|
self.context.sharedContext.makeContentReportScreen(
|
|
context: self.context,
|
|
subject: .messages(messages.map({ $0.id }).sorted()),
|
|
forceDark: false,
|
|
present: { [weak self] controller in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.push(controller)
|
|
},
|
|
completion: {},
|
|
requestSelectMessages: nil
|
|
)
|
|
}, 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(EnginePeer(peer).compactDisplayTitle).string))
|
|
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 _ = strongSelf.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peer.id, isBlocked: true).startStandalone()
|
|
let context = strongSelf.context
|
|
let _ = context.engine.messages.deleteAllMessagesWithForwardAuthor(peerId: message.id.peerId, forwardAuthorId: peer.id, namespace: Namespaces.Message.Cloud).startStandalone()
|
|
let _ = strongSelf.context.engine.peers.reportRepliesMessage(messageId: message.id, deleteMessage: true, deleteHistory: true, reportSpam: reportSpam).startStandalone()
|
|
})
|
|
] 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 {
|
|
guard !strongSelf.presentAccountFrozenInfoIfNeeded(delay: true) else {
|
|
completion(.default)
|
|
return
|
|
}
|
|
|
|
let messageIds = Set(messages.map { $0.id })
|
|
strongSelf.messageContextDisposable.set((strongSelf.context.sharedContext.chatAvailableMessageActions(engine: strongSelf.context.engine, accountPeerId: strongSelf.context.account.peerId, messageIds: messageIds, keepUpdated: false)
|
|
|> deliverOnMainQueue).startStrict(next: { actions in
|
|
if let strongSelf = self, !actions.options.isEmpty {
|
|
if let banAuthor = actions.banAuthor {
|
|
if let contextController = contextController {
|
|
contextController.dismiss(completion: {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.presentBanMessageOptions(accountPeerId: strongSelf.context.account.peerId, author: banAuthor, messageIds: messageIds, options: actions.options)
|
|
})
|
|
} else {
|
|
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 _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: actions.options == .deleteLocally ? .forLocalPeer : .forEveryone).startStandalone()
|
|
completion(.dismissWithoutContent)
|
|
} else if (messages.first?.flags.isSending ?? false) {
|
|
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone, deleteAllInGroup: true).startStandalone()
|
|
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] mode in
|
|
if let strongSelf = self {
|
|
strongSelf.commitPurposefulAction()
|
|
if let forwardMessageIdsSet = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds {
|
|
let forwardMessageIds = Array(forwardMessageIdsSet).sorted()
|
|
// MARK: Swiftgram
|
|
if let mode = mode {
|
|
switch (mode) {
|
|
case "toCloud":
|
|
strongSelf.forwardMessagesToCloud(messageIds: forwardMessageIds, removeNames: false, openCloud: false, resetCurrent: true)
|
|
case "hideNames":
|
|
strongSelf.forwardMessages(forceHideNames: true, messageIds: forwardMessageIds, options: ChatInterfaceForwardOptionsState(hideNames: true, hideCaptions: false, unhideNamesOnCaptionChange: false))
|
|
default:
|
|
strongSelf.forwardMessages(messageIds: forwardMessageIds)
|
|
}
|
|
} else {
|
|
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, options: strongSelf.presentationInterfaceState.interfaceState.forwardOptionsState, resetCurrent: true)
|
|
}
|
|
}
|
|
}, forwardMessages: { [weak self] messages, mode in
|
|
if let strongSelf = self, !messages.isEmpty {
|
|
guard !strongSelf.presentAccountFrozenInfoIfNeeded(delay: true) else {
|
|
return
|
|
}
|
|
|
|
strongSelf.commitPurposefulAction()
|
|
let forwardMessageIds = messages.map { $0.id }.sorted()
|
|
// MARK: Swiftgram
|
|
if let mode = mode {
|
|
switch (mode) {
|
|
case "forwardMessagesToCloudWithNoNamesAndOpen":
|
|
strongSelf.forwardMessagesToCloud(messageIds: forwardMessageIds, removeNames: true, openCloud: true)
|
|
case "forwardMessagesToCloud":
|
|
strongSelf.forwardMessagesToCloud(messageIds: forwardMessageIds, removeNames: false, openCloud: false)
|
|
case "forwardMessagesWithNoNames":
|
|
strongSelf.forwardMessages(forceHideNames: true, messageIds: forwardMessageIds, options: ChatInterfaceForwardOptionsState(hideNames: true, hideCaptions: false, unhideNamesOnCaptionChange: false))
|
|
default:
|
|
strongSelf.forwardMessages(messageIds: forwardMessageIds)
|
|
}
|
|
} else {
|
|
strongSelf.forwardMessages(messageIds: forwardMessageIds)
|
|
}
|
|
|
|
}
|
|
}, updateForwardOptionsState: { [weak self] f in
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardOptionsState(f($0.forwardOptionsState ?? ChatInterfaceForwardOptionsState(hideNames: false, hideCaptions: false, unhideNamesOnCaptionChange: false))) }) })
|
|
}
|
|
}, presentForwardOptions: { [weak self] sourceNode in
|
|
guard let self else {
|
|
return
|
|
}
|
|
presentChatForwardOptions(selfController: self, sourceNode: sourceNode)
|
|
}, presentReplyOptions: { [weak self] sourceNode in
|
|
guard let self else {
|
|
return
|
|
}
|
|
presentChatReplyOptions(selfController: self, sourceNode: sourceNode)
|
|
}, presentLinkOptions: { [weak self] sourceNode in
|
|
guard let self else {
|
|
return
|
|
}
|
|
presentChatLinkOptions(selfController: self, sourceNode: sourceNode)
|
|
}, shareSelectedMessages: { [weak self] in
|
|
if let strongSelf = self, let selectedIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !selectedIds.isEmpty {
|
|
strongSelf.commitPurposefulAction()
|
|
let _ = (strongSelf.context.engine.data.get(EngineDataMap(
|
|
selectedIds.map(TelegramEngine.EngineData.Item.Messages.Message.init)
|
|
))
|
|
|> map { messages -> [EngineMessage] in
|
|
return messages.values.compactMap { $0 }
|
|
}
|
|
|> deliverOnMainQueue).startStandalone(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
|
|
}).map { $0._asMessage() }), externalShare: true, immediateExternalShare: true, updatedPresentationData: strongSelf.updatedPresentationData)
|
|
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 })
|
|
})
|
|
|
|
if !strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText.string.isEmpty {
|
|
strongSelf.silentPostTooltipController?.dismiss()
|
|
}
|
|
}
|
|
}, updateInputModeAndDismissedButtonKeyboardMessageId: { [weak self] f in
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
|
let (updatedInputMode, updatedClosedButtonKeyboardMessageId) = f($0)
|
|
var updated = $0.updatedInputMode({ _ in return updatedInputMode }).updatedInterfaceState({
|
|
$0.withUpdatedMessageActionsState({ value in
|
|
var value = value
|
|
value.closedButtonKeyboardMessageId = updatedClosedButtonKeyboardMessageId
|
|
return value
|
|
})
|
|
})
|
|
var dismissWebView = false
|
|
switch updatedInputMode {
|
|
case .text, .media, .inputButtons:
|
|
dismissWebView = true
|
|
default:
|
|
break
|
|
}
|
|
if dismissWebView {
|
|
updated = updated.updatedShowWebView(false)
|
|
}
|
|
return updated
|
|
})
|
|
}
|
|
}, openStickers: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.chatDisplayNode.openStickers(beginWithEmoji: false)
|
|
strongSelf.mediaRecordingModeTooltipController?.dismissImmediately()
|
|
}, editMessage: { [weak self] in
|
|
guard let strongSelf = self, let editMessage = strongSelf.presentationInterfaceState.interfaceState.editMessage else {
|
|
return
|
|
}
|
|
|
|
let sourceMessage: Signal<EngineMessage?, NoError>
|
|
sourceMessage = strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: editMessage.messageId))
|
|
|
|
let _ = (sourceMessage
|
|
|> deliverOnMainQueue).start(next: { [weak strongSelf] message in
|
|
guard let strongSelf, let message else {
|
|
return
|
|
}
|
|
|
|
var disableUrlPreview = false
|
|
|
|
var webpage: TelegramMediaWebpage?
|
|
var webpagePreviewAttribute: WebpagePreviewMessageAttribute?
|
|
if let urlPreview = strongSelf.presentationInterfaceState.editingUrlPreview {
|
|
if editMessage.disableUrlPreviews.contains(urlPreview.url) {
|
|
disableUrlPreview = true
|
|
} else {
|
|
webpage = urlPreview.webPage
|
|
webpagePreviewAttribute = WebpagePreviewMessageAttribute(leadingPreview: !urlPreview.positionBelowText, forceLargeMedia: urlPreview.largeMedia, isManuallyAdded: true, isSafe: false)
|
|
}
|
|
}
|
|
|
|
var invertedMediaAttribute: InvertMediaMessageAttribute?
|
|
if let attribute = message.attributes.first(where: { $0 is InvertMediaMessageAttribute }) {
|
|
invertedMediaAttribute = attribute as? InvertMediaMessageAttribute
|
|
}
|
|
|
|
if let mediaCaptionIsAbove = editMessage.mediaCaptionIsAbove {
|
|
if mediaCaptionIsAbove {
|
|
invertedMediaAttribute = InvertMediaMessageAttribute()
|
|
} else {
|
|
invertedMediaAttribute = nil
|
|
}
|
|
}
|
|
|
|
let text = trimChatInputText(convertMarkdownToAttributes(expandedInputStateAttributedString(editMessage.inputState.inputText)))
|
|
|
|
let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text))
|
|
var entitiesAttribute: TextEntitiesMessageAttribute?
|
|
if !entities.isEmpty {
|
|
entitiesAttribute = TextEntitiesMessageAttribute(entities: entities)
|
|
}
|
|
|
|
var inlineStickers: [MediaId: TelegramMediaFile] = [:]
|
|
var firstLockedPremiumEmoji: TelegramMediaFile?
|
|
text.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: NSRange(location: 0, length: text.length), using: { value, _, _ in
|
|
if let value = value as? ChatTextInputTextCustomEmojiAttribute {
|
|
if let file = value.file {
|
|
inlineStickers[file.fileId] = file
|
|
if file.isPremiumEmoji && !strongSelf.presentationInterfaceState.isPremium && strongSelf.chatLocation.peerId != strongSelf.context.account.peerId {
|
|
if firstLockedPremiumEmoji == nil {
|
|
firstLockedPremiumEmoji = file
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
if let firstLockedPremiumEmoji = firstLockedPremiumEmoji {
|
|
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
|
strongSelf.controllerInteraction?.displayUndo(.sticker(context: strongSelf.context, file: firstLockedPremiumEmoji, loop: true, title: nil, text: presentationData.strings.EmojiInput_PremiumEmojiToast_Text, undoText: presentationData.strings.EmojiInput_PremiumEmojiToast_Action, customAction: {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.chatDisplayNode.dismissTextInput()
|
|
|
|
let context = strongSelf.context
|
|
var replaceImpl: ((ViewController) -> Void)?
|
|
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .animatedEmoji, forceDark: false, action: {
|
|
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .animatedEmoji, forceDark: false, dismissed: nil)
|
|
replaceImpl?(controller)
|
|
}, dismissed: nil)
|
|
replaceImpl = { [weak controller] c in
|
|
controller?.replace(with: c)
|
|
}
|
|
strongSelf.push(controller)
|
|
}))
|
|
|
|
return
|
|
}
|
|
|
|
if text.length == 0 {
|
|
if strongSelf.presentationInterfaceState.editMessageState?.mediaReference != nil {
|
|
} else if message.media.contains(where: { media in
|
|
switch media {
|
|
case _ as TelegramMediaImage, _ as TelegramMediaFile, _ as TelegramMediaMap:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}) {
|
|
} else {
|
|
if strongSelf.recordingModeFeedback == nil {
|
|
strongSelf.recordingModeFeedback = HapticFeedback()
|
|
strongSelf.recordingModeFeedback?.prepareError()
|
|
}
|
|
strongSelf.recordingModeFeedback?.error()
|
|
return
|
|
}
|
|
}
|
|
|
|
var updatingMedia = false
|
|
let media: RequestEditMessageMedia
|
|
if let editMediaReference = strongSelf.presentationInterfaceState.editMessageState?.mediaReference {
|
|
media = .update(editMediaReference)
|
|
updatingMedia = true
|
|
} else if let webpage {
|
|
media = .update(.standalone(media: webpage))
|
|
} else {
|
|
media = .keep
|
|
}
|
|
|
|
let _ = (strongSelf.context.account.postbox.messageAtId(editMessage.messageId)
|
|
|> deliverOnMainQueue).startStandalone(next: { [weak self] currentMessage in
|
|
if let strongSelf = self {
|
|
if let currentMessage = currentMessage {
|
|
let currentEntities = currentMessage.textEntitiesAttribute?.entities ?? []
|
|
let currentWebpagePreviewAttribute = currentMessage.webpagePreviewAttribute ?? WebpagePreviewMessageAttribute(leadingPreview: false, forceLargeMedia: nil, isManuallyAdded: true, isSafe: false)
|
|
|
|
if currentMessage.text != text.string || currentEntities != entities || updatingMedia || webpagePreviewAttribute != currentWebpagePreviewAttribute || disableUrlPreview {
|
|
strongSelf.context.account.pendingUpdateMessageManager.add(messageId: editMessage.messageId, text: text.string, media: media, entities: entitiesAttribute, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, invertMediaAttribute: invertedMediaAttribute, 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.updatedSearch(current.search == nil ? ChatSearchData(domain: domain).withUpdatedQuery(query) : current.search?.withUpdatedDomain(domain).withUpdatedQuery(query))
|
|
}, completion: { [weak strongSelf] _ in
|
|
guard let strongSelf else {
|
|
return
|
|
}
|
|
strongSelf.chatDisplayNode.searchNavigationNode?.activate()
|
|
})
|
|
strongSelf.updateItemNodesSearchTextHighlightStates()
|
|
})
|
|
}, dismissMessageSearch: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
if let customDismissSearch = self.customDismissSearch {
|
|
customDismissSearch()
|
|
return
|
|
}
|
|
|
|
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in
|
|
return current.updatedSearch(nil).updatedHistoryFilter(nil)
|
|
})
|
|
self.updateItemNodesSearchTextHighlightStates()
|
|
self.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).startStandalone(next: { [weak self] searchResult in
|
|
if let strongSelf = self, let (searchResult, searchState, searchLocation) = searchResult {
|
|
let controller = ChatSearchResultsController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, 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, .customChatContents:
|
|
strongSelf.navigateToMessage(from: nil, to: .index(navigateIndex), forceInCurrentChat: true)
|
|
}
|
|
}
|
|
}
|
|
}, openCalendarSearch: { [weak self] in
|
|
self?.openCalendarSearch(timestamp: Int32(Date().timeIntervalSince1970))
|
|
}, 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, .tag:
|
|
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, NavigateToMessageParams(timestamp: nil, quote: nil)), forceInCurrentChat: forceInCurrentChat, dropStack: dropStack, statusSubject: statusSubject)
|
|
}, navigateToChat: { [weak self] peerId in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|
|
|> deliverOnMainQueue).startStandalone(next: { peer in
|
|
guard let peer = peer else {
|
|
return
|
|
}
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
if let navigationController = strongSelf.effectiveNavigationController {
|
|
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), subject: nil, keepStack: .always))
|
|
}
|
|
})
|
|
}, navigateToProfile: { [weak self] peerId in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|
|
|> deliverOnMainQueue).startStandalone(next: { peer in
|
|
if let strongSelf = self, let peer = peer {
|
|
strongSelf.openPeer(peer: peer, navigation: .default, fromMessage: nil)
|
|
}
|
|
})
|
|
}, openPeerInfo: { [weak self] in
|
|
self?.navigationButtonAction(.openChatInfo(expandAvatar: false, section: nil))
|
|
}, togglePeerNotifications: { [weak self] in
|
|
if let strongSelf = self, let peerId = strongSelf.chatLocation.peerId {
|
|
let _ = strongSelf.context.engine.peers.togglePeerMuted(peerId: peerId, threadId: strongSelf.chatLocation.threadId).startStandalone()
|
|
}
|
|
}, 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.view, rect)
|
|
return false
|
|
}
|
|
strongSelf.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.enqueueChatContextResult(results, result, postpone: postpone)
|
|
})
|
|
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 replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject
|
|
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
|
if let strongSelf = self {
|
|
strongSelf.chatDisplayNode.collapseInput()
|
|
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
|
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreviews([]) }
|
|
})
|
|
}
|
|
}, 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, inlineStickers: [:], mediaReference: nil, threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])])
|
|
strongSelf.interfaceInteraction?.updateShowCommands { _ in
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}, sendShortcut: { [weak self] shortcutId in
|
|
guard let self else {
|
|
return
|
|
}
|
|
guard let peerId = self.chatLocation.peerId else {
|
|
return
|
|
}
|
|
|
|
self.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
|
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreviews([]) }
|
|
})
|
|
|
|
if !self.presentationInterfaceState.isPremium {
|
|
let controller = PremiumIntroScreen(context: self.context, source: .settings)
|
|
self.push(controller)
|
|
return
|
|
}
|
|
|
|
self.context.engine.accountData.sendMessageShortcut(peerId: peerId, id: shortcutId)
|
|
|
|
/*self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
|
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreviews([]) }
|
|
})
|
|
}, nil)
|
|
|
|
var messages: [EnqueueMessage] = []
|
|
do {
|
|
let message = shortcut.topMessage
|
|
var attributes: [MessageAttribute] = []
|
|
let entities = generateTextEntities(message.text, enabledTypes: .all)
|
|
if !entities.isEmpty {
|
|
attributes.append(TextEntitiesMessageAttribute(entities: entities))
|
|
}
|
|
|
|
messages.append(.message(
|
|
text: message.text,
|
|
attributes: attributes,
|
|
inlineStickers: [:],
|
|
mediaReference: message.media.first.flatMap { AnyMediaReference.standalone(media: $0) },
|
|
threadId: self.chatLocation.threadId,
|
|
replyToMessageId: nil,
|
|
replyToStoryId: nil,
|
|
localGroupingKey: nil,
|
|
correlationId: nil,
|
|
bubbleUpEmojiOrStickersets: []
|
|
))
|
|
}
|
|
|
|
self.sendMessages(messages)*/
|
|
}, openEditShortcuts: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
let _ = (self.context.sharedContext.makeQuickReplySetupScreenInitialData(context: self.context)
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { [weak self] initialData in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
let controller = self.context.sharedContext.makeQuickReplySetupScreen(context: self.context, initialData: initialData)
|
|
controller.navigationPresentation = .modal
|
|
self.push(controller)
|
|
})
|
|
}, 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
|
|
}
|
|
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|
|
|> deliverOnMainQueue).startStandalone(next: { peer in
|
|
if let strongSelf = self, let peer = peer {
|
|
strongSelf.openPeer(peer: peer, 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.dismissAllTooltips()
|
|
|
|
strongSelf.mediaRecordingModeTooltipController?.dismiss()
|
|
strongSelf.interfaceInteraction?.updateShowWebView { _ in
|
|
return false
|
|
}
|
|
|
|
var bannedMediaInput = false
|
|
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
|
|
if let channel = peer as? TelegramChannel {
|
|
if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil {
|
|
bannedMediaInput = true
|
|
} else if channel.hasBannedPermission(.banSendVoice) != nil {
|
|
if !isVideo {
|
|
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
|
|
return
|
|
}
|
|
} else if channel.hasBannedPermission(.banSendInstantVideos) != nil {
|
|
if isVideo {
|
|
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
|
|
return
|
|
}
|
|
}
|
|
} else if let group = peer as? TelegramGroup {
|
|
if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) {
|
|
bannedMediaInput = true
|
|
} else if group.hasBannedPermission(.banSendVoice) {
|
|
if !isVideo {
|
|
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
|
|
return
|
|
}
|
|
} else if group.hasBannedPermission(.banSendInstantVideos) {
|
|
if isVideo {
|
|
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil, customUndoText: nil))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if bannedMediaInput {
|
|
strongSelf.controllerInteraction?.displayUndo(.universal(animation: "premium_unlock", scale: 1.0, colors: ["__allcolors__": UIColor(white: 1.0, alpha: 1.0)], title: nil, text: strongSelf.restrictedSendingContentsText(), customUndoText: nil, timeout: nil))
|
|
return
|
|
}
|
|
|
|
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?.present(c, in: .window(.root))
|
|
}) else {
|
|
return
|
|
}
|
|
let hasOngoingCall: Signal<Bool, NoError> = strongSelf.context.sharedContext.hasOngoingCall.get()
|
|
let _ = (hasOngoingCall
|
|
|> take(1)
|
|
|> deliverOnMainQueue).startStandalone(next: { hasOngoingCall in
|
|
guard let strongSelf = self, strongSelf.beginMediaRecordingRequestId == requestId else {
|
|
return
|
|
}
|
|
if hasOngoingCall {
|
|
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, 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(pause: true)
|
|
}, lockMediaRecording: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.lockMediaRecordingRequestId = strongSelf.beginMediaRecordingRequestId
|
|
strongSelf.lockMediaRecorder()
|
|
}, resumeMediaRecording: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.resumeMediaRecorder()
|
|
}, deleteRecordedMedia: { [weak self] in
|
|
self?.deleteMediaRecording()
|
|
}, sendRecordedMedia: { [weak self] silentPosting, viewOnce in
|
|
self?.presentPaidMessageAlertIfNeeded(count: 1, completion: { [weak self] postpone in
|
|
self?.sendMediaRecording(silentPosting: silentPosting, viewOnce: viewOnce, postpone: postpone)
|
|
})
|
|
}, displayRestrictedInfo: { [weak self] subject, displayType in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
|
|
let canBypassRestrictions = canBypassRestrictions(chatPresentationInterfaceState: strongSelf.presentationInterfaceState)
|
|
|
|
let subjectFlags: [TelegramChatBannedRightsFlags]
|
|
switch subject {
|
|
case .stickers:
|
|
subjectFlags = [.banSendStickers]
|
|
case .mediaRecording, .premiumVoiceMessages:
|
|
subjectFlags = [.banSendVoice, .banSendInstantVideos]
|
|
}
|
|
|
|
var bannedPermission: (Int32, Bool)? = nil
|
|
if let channel = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel {
|
|
for subjectFlag in subjectFlags {
|
|
if let value = channel.hasBannedPermission(subjectFlag, ignoreDefault: canBypassRestrictions) {
|
|
bannedPermission = value
|
|
break
|
|
}
|
|
}
|
|
} else if let group = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramGroup {
|
|
for subjectFlag in subjectFlags {
|
|
if group.hasBannedPermission(subjectFlag) {
|
|
bannedPermission = (Int32.max, false)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if let boostsToUnrestrict = (strongSelf.peerView?.cachedData as? CachedChannelData)?.boostsToUnrestrict, boostsToUnrestrict > 0, let bannedPermission, !bannedPermission.1 {
|
|
strongSelf.interfaceInteraction?.openBoostToUnrestrict()
|
|
return
|
|
}
|
|
|
|
var displayToast = false
|
|
|
|
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)).string
|
|
} 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)).string
|
|
} else if personal {
|
|
banDescription = strongSelf.presentationInterfaceState.strings.Conversation_RestrictedMedia
|
|
} else {
|
|
banDescription = strongSelf.restrictedSendingContentsText()
|
|
displayToast = true
|
|
}
|
|
case .premiumVoiceMessages:
|
|
banDescription = ""
|
|
}
|
|
if strongSelf.recordingModeFeedback == nil {
|
|
strongSelf.recordingModeFeedback = HapticFeedback()
|
|
strongSelf.recordingModeFeedback?.prepareError()
|
|
}
|
|
|
|
strongSelf.recordingModeFeedback?.error()
|
|
|
|
switch displayType {
|
|
case .tooltip:
|
|
if displayToast {
|
|
strongSelf.controllerInteraction?.displayUndo(.universal(animation: "premium_unlock", scale: 1.0, colors: ["__allcolors__": UIColor(white: 1.0, alpha: 1.0)], title: nil, text: banDescription, customUndoText: nil, timeout: nil))
|
|
} else {
|
|
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, .premiumVoiceMessages:
|
|
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, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: banDescription, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
|
}
|
|
}
|
|
|
|
if case .premiumVoiceMessages = subject {
|
|
let text: String
|
|
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer.flatMap({ EnginePeer($0) }) {
|
|
text = strongSelf.presentationInterfaceState.strings.Conversation_VoiceMessagesRestricted(peer.compactDisplayTitle).string
|
|
} else {
|
|
text = ""
|
|
}
|
|
switch displayType {
|
|
case .tooltip:
|
|
let rect = strongSelf.chatDisplayNode.frameForInputActionButton()
|
|
if let rect = rect {
|
|
strongSelf.mediaRestrictedTooltipController?.dismiss()
|
|
let tooltipController = TooltipController(content: .text(text), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize, padding: 2.0)
|
|
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
|
|
}))
|
|
}
|
|
case .alert:
|
|
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
|
}
|
|
} else if case .mediaRecording = subject, strongSelf.presentationInterfaceState.hasActiveGroupCall {
|
|
let rect = strongSelf.chatDisplayNode.frameForInputActionButton()
|
|
if let rect = rect {
|
|
strongSelf.mediaRestrictedTooltipController?.dismiss()
|
|
let text: String
|
|
if let channel = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, case .broadcast = channel.info {
|
|
text = strongSelf.presentationInterfaceState.strings.Conversation_LiveStreamMediaRecordingRestricted
|
|
} else {
|
|
text = strongSelf.presentationInterfaceState.strings.Conversation_VoiceChatMediaRecordingRestricted
|
|
}
|
|
let tooltipController = TooltipController(content: .text(text), 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
|
|
}))
|
|
}
|
|
}
|
|
}, displayVideoUnmuteTip: { [weak self] location in
|
|
guard let strongSelf = self, !strongSelf.didDisplayVideoUnmuteTooltip, let layout = strongSelf.validLayout, strongSelf.traceVisibility() && isTopmostChatController(strongSelf) else {
|
|
return
|
|
}
|
|
if let location = location, location.y < strongSelf.navigationLayout(layout: layout).navigationFrame.maxY {
|
|
return
|
|
}
|
|
let icon: UIImage?
|
|
if layout.deviceMetrics.hasTopNotch || layout.deviceMetrics.hasDynamicIsland {
|
|
icon = UIImage(bundleImageName: "Chat/Message/VolumeButtonIconX")
|
|
} else {
|
|
icon = UIImage(bundleImageName: "Chat/Message/VolumeButtonIcon")
|
|
}
|
|
if let location = location, let icon = icon {
|
|
strongSelf.didDisplayVideoUnmuteTooltip = true
|
|
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
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
var bannedMediaInput = false
|
|
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
|
|
if let channel = peer as? TelegramChannel {
|
|
if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil {
|
|
bannedMediaInput = true
|
|
} else if channel.hasBannedPermission(.banSendVoice) != nil {
|
|
if channel.hasBannedPermission(.banSendInstantVideos) == nil {
|
|
strongSelf.displayMediaRecordingTooltip()
|
|
return
|
|
}
|
|
} else if channel.hasBannedPermission(.banSendInstantVideos) != nil {
|
|
if channel.hasBannedPermission(.banSendVoice) == nil {
|
|
strongSelf.displayMediaRecordingTooltip()
|
|
return
|
|
}
|
|
}
|
|
} else if let group = peer as? TelegramGroup {
|
|
if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) {
|
|
bannedMediaInput = true
|
|
} else if group.hasBannedPermission(.banSendVoice) {
|
|
if !group.hasBannedPermission(.banSendInstantVideos) {
|
|
strongSelf.displayMediaRecordingTooltip()
|
|
return
|
|
}
|
|
} else if group.hasBannedPermission(.banSendInstantVideos) {
|
|
if !group.hasBannedPermission(.banSendVoice) {
|
|
strongSelf.displayMediaRecordingTooltip()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if bannedMediaInput {
|
|
strongSelf.controllerInteraction?.displayUndo(.universal(animation: "premium_unlock", scale: 1.0, colors: ["__allcolors__": UIColor(white: 1.0, alpha: 1.0)], title: nil, text: strongSelf.restrictedSendingContentsText(), customUndoText: nil, timeout: nil))
|
|
return
|
|
}
|
|
|
|
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)
|
|
}).updatedShowWebView(false)
|
|
})
|
|
|
|
if let updatedMode = updatedMode, updatedMode == .video {
|
|
let _ = ApplicationSpecificNotice.incrementChatMediaMediaRecordingTips(accountManager: strongSelf.context.sharedContext.accountManager, count: 3).startStandalone()
|
|
}
|
|
|
|
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 _ = strongSelf.context.engine.peers.setChatMessageAutoremoveTimeoutInteractively(peerId: peer.id, timeout: value == 0 ? nil : value).startStandalone()
|
|
}
|
|
})
|
|
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 !group.hasBannedPermission(.banChangeInfo) {
|
|
canSetupAutoremoveTimeout = true
|
|
}
|
|
} else if let user = peer as? TelegramUser {
|
|
if user.id != strongSelf.context.account.peerId && user.botInfo == nil {
|
|
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)) {
|
|
|
|
let intervalText = timeIntervalString(strings: strongSelf.presentationData.strings, value: currentAutoremoveTimeout)
|
|
let text: String = strongSelf.presentationData.strings.Conversation_AutoremoveTimerSetToastText(intervalText).string
|
|
|
|
strongSelf.mediaRecordingModeTooltipController?.dismiss()
|
|
|
|
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, timeout: 4.0)
|
|
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, clearInput, sourceView, sourceRect, sourceLayer, bubbleUpEmojiOrStickersets in
|
|
if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) {
|
|
return strongSelf.controllerInteraction?.sendSticker(file, false, false, nil, clearInput, sourceView, sourceRect, sourceLayer, bubbleUpEmojiOrStickersets) ?? false
|
|
} else {
|
|
return false
|
|
}
|
|
}, unblockPeer: { [weak self] in
|
|
self?.unblockPeer()
|
|
}, pinMessage: { [weak self] messageId, contextController in
|
|
if let strongSelf = self, let currentPeerId = strongSelf.chatLocation.peerId {
|
|
guard !strongSelf.presentAccountFrozenInfoIfNeeded(delay: true) else {
|
|
contextController?.dismiss(completion: nil)
|
|
return
|
|
}
|
|
|
|
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(strongSelf.context.engine.messages.requestUpdatePinnedMessage(peerId: currentPeerId, update: .pin(id: messageId, silent: !notify, forThisPeerOnlyIfPossible: forThisPeerOnlyIfPossible)).startStrict(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(EnginePeer(peer).compactDisplayTitle).string, 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(ContextController.Items(content: .list(contextItems))), minHeight: nil, animated: true)
|
|
}
|
|
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(ContextController.Items(content: .list(contextItems))), minHeight: nil, animated: true)
|
|
|
|
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).startStandalone(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, updatedPresentationData: strongSelf.updatedPresentationData, 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((strongSelf.context.engine.messages.requestUpdatePinnedMessage(peerId: peer.id, update: .clear(id: id))
|
|
|> deliverOnMainQueue).startStrict(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 _ = (strongSelf.context.engine.messages.requestUpdatePinnedMessage(peerId: peer.id, update: .clear(id: id))
|
|
|> deliverOnMainQueue).startStandalone(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((strongSelf.context.engine.messages.requestUpdatePinnedMessage(peerId: peer.id, update: .clear(id: id))
|
|
|> deliverOnMainQueue).startStrict())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if askForConfirmation {
|
|
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, 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).startStandalone(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 context = strongSelf.context
|
|
let _ = (context.engine.stickers.isStickerSaved(id: stickerFile.fileId)
|
|
|> castError(AddSavedStickerError.self)
|
|
|> mapToSignal { isSaved -> Signal<(SavedStickerResult, Bool), AddSavedStickerError> in
|
|
return context.engine.stickers.toggleStickerSaved(file: stickerFile, saved: !isSaved)
|
|
|> map { result -> (SavedStickerResult, Bool) in
|
|
return (result, !isSaved)
|
|
}
|
|
}
|
|
|> deliverOnMainQueue).startStandalone(next: { [weak self] result, added in
|
|
if let strongSelf = self {
|
|
switch result {
|
|
case .generic:
|
|
strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: stickerFile, loop: true, title: nil, text: added ? strongSelf.presentationData.strings.Conversation_StickerAddedToFavorites : strongSelf.presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: true, action: { _ in return false }), with: nil)
|
|
case let .limitExceeded(limit, premiumLimit):
|
|
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
|
|
let text: String
|
|
if limit == premiumLimit || premiumConfiguration.isPremiumDisabled {
|
|
text = strongSelf.presentationData.strings.Premium_MaxFavedStickersFinalText
|
|
} else {
|
|
text = strongSelf.presentationData.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string
|
|
}
|
|
strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: stickerFile, loop: true, title: strongSelf.presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: true, action: { [weak self] action in
|
|
if let strongSelf = self {
|
|
if case .info = action {
|
|
let controller = PremiumIntroScreen(context: strongSelf.context, source: .savedStickers)
|
|
strongSelf.push(controller)
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}), with: nil)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}, presentController: { [weak self] controller, arguments in
|
|
self?.present(controller, in: .window(.root), with: arguments)
|
|
}, presentControllerInCurrent: { [weak self] controller, arguments in
|
|
if controller is UndoOverlayController {
|
|
self?.dismissAllTooltips()
|
|
}
|
|
self?.present(controller, in: .current, 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
|
|
}
|
|
|
|
var signal = strongSelf.context.engine.messages.requestMessageSelectPollOption(messageId: id, opaqueIdentifiers: [])
|
|
let disposables: DisposableDict<MessageId>
|
|
if let current = strongSelf.selectMessagePollOptionDisposables {
|
|
disposables = current
|
|
} else {
|
|
disposables = DisposableDict()
|
|
strongSelf.selectMessagePollOptionDisposables = disposables
|
|
}
|
|
|
|
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.3, queue: Queue.mainQueue())
|
|
let progressDisposable = progressSignal.startStrict()
|
|
|
|
signal = signal
|
|
|> afterDisposed {
|
|
Queue.mainQueue().async {
|
|
progressDisposable.dispose()
|
|
}
|
|
}
|
|
cancelImpl = {
|
|
disposables.set(nil, forKey: id)
|
|
}
|
|
|
|
disposables.set((signal
|
|
|> deliverOnMainQueue).startStrict(completed: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if self.selectPollOptionFeedback == nil {
|
|
self.selectPollOptionFeedback = HapticFeedback()
|
|
}
|
|
self.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 = strongSelf.context.engine.messages.requestClosePoll(messageId: id)
|
|
|> afterDisposed { [weak controller] in
|
|
Queue.mainQueue().async {
|
|
controller?.dismiss()
|
|
}
|
|
}
|
|
disposables.set((signal
|
|
|> deliverOnMainQueue).startStrict(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
|
|
}
|
|
let _ = (strongSelf.context.engine.peers.updatePeersGroupIdInteractively(peerIds: [peerId], groupId: .root)
|
|
|> deliverOnMainQueue).startStandalone()
|
|
}, openLinkEditing: { [weak self] in
|
|
if let strongSelf = self {
|
|
var selectionRange: Range<Int>?
|
|
var text: NSAttributedString?
|
|
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))
|
|
}
|
|
inputMode = state.inputMode
|
|
return state
|
|
})
|
|
|
|
var link: String?
|
|
if let text {
|
|
text.enumerateAttributes(in: NSMakeRange(0, text.length)) { attributes, _, _ in
|
|
if let linkAttribute = attributes[ChatTextInputAttributes.textUrl] as? ChatTextInputTextUrlAttribute {
|
|
link = linkAttribute.url
|
|
}
|
|
}
|
|
}
|
|
|
|
let controller = chatTextLinkEditController(sharedContext: strongSelf.context.sharedContext, updatedPresentationData: strongSelf.updatedPresentationData, account: strongSelf.context.account, text: text?.string ?? "", link: link, allowEmpty: true, apply: { [weak self] link in
|
|
if let strongSelf = self, let inputMode = inputMode, let selectionRange = selectionRange {
|
|
if let link {
|
|
if !link.isEmpty {
|
|
strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
|
return (chatTextInputAddLinkAttribute(current, selectionRange: selectionRange, url: link), inputMode)
|
|
}
|
|
} else {
|
|
strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
|
return (chatTextInputRemoveLinkAttribute(current, selectionRange: selectionRange), inputMode)
|
|
}
|
|
}
|
|
}
|
|
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 = (strongSelf.context.engine.peers.reportPeer(peerId: peerId, reason: .irrelevantLocation, message: "")
|
|
|> deliverOnMainQueue).startStrict(completed: { [weak self] in
|
|
if let strongSelf = self {
|
|
strongSelf.reportIrrelvantGeoNoticePromise.set(.single(true))
|
|
let _ = ApplicationSpecificNotice.setIrrelevantPeerGeoReport(engine: strongSelf.context.engine, peerId: peerId).startStandalone()
|
|
|
|
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .emoji(name: "PoliceCar", text: strongSelf.presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return false }), in: .current)
|
|
}
|
|
})
|
|
})]
|
|
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: strongSelf.presentationData.strings.ReportGroupLocation_Title, text: strongSelf.presentationData.strings.ReportGroupLocation_Text, actions: actions), in: .window(.root))
|
|
}, displaySlowmodeTooltip: { [weak self] sourceView, nodeRect in
|
|
guard let strongSelf = self, let slowmodeState = strongSelf.presentationInterfaceState.slowmodeState else {
|
|
return
|
|
}
|
|
|
|
if let boostsToUnrestrict = (strongSelf.peerView?.cachedData as? CachedChannelData)?.boostsToUnrestrict, boostsToUnrestrict > 0 {
|
|
strongSelf.interfaceInteraction?.openBoostToUnrestrict()
|
|
return
|
|
}
|
|
|
|
let rect = sourceView.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
|
|
guard let self else {
|
|
return
|
|
}
|
|
chatMessageDisplaySendMessageOptions(selfController: self, node: node, gesture: gesture)
|
|
}, 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, timeout: nil, customUndoText: nil), 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: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false) }
|
|
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 let peerId = strongSelf.chatLocation.peerId 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 chatLocation: ChatLocation
|
|
if let _ = strongSelf.chatLocation.threadId {
|
|
chatLocation = strongSelf.chatLocation
|
|
} else {
|
|
chatLocation = .peer(id: peerId)
|
|
}
|
|
|
|
let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: chatLocation, subject: .pinnedMessages(id: pinnedMessage.message.id), botStart: nil, mode: .standard(.previewing), params: nil)
|
|
chatController.canReadHistory.set(false)
|
|
|
|
strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts()
|
|
|
|
let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceNode: node, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
|
|
strongSelf.presentInGlobalOverlay(contextController)
|
|
}, joinGroupCall: { [weak self] activeCall in
|
|
guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
|
|
return
|
|
}
|
|
strongSelf.joinGroupCall(peerId: peer.id, invite: nil, activeCall: EngineGroupCallDescription(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
|
|
}
|
|
presentAddMembersImpl(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, parentController: strongSelf, groupPeer: peer, selectAddMemberDisposable: strongSelf.selectAddMemberDisposable, addMemberDisposable: strongSelf.addMemberDisposable)
|
|
}, presentGigagroupHelp: { [weak self] in
|
|
if let strongSelf = self {
|
|
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Conversation_GigagroupDescription, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return true }), in: .current)
|
|
}
|
|
}, openSuggestPost: { [weak self] in
|
|
let _ = self
|
|
/*guard let self else {
|
|
return
|
|
}
|
|
guard let peerId = self.chatLocation.peerId else {
|
|
return
|
|
}
|
|
|
|
let contents = PostSuggestionsChatContents(
|
|
context: self.context,
|
|
peerId: peerId
|
|
)
|
|
let chatController = self.context.sharedContext.makeChatController(
|
|
context: self.context,
|
|
chatLocation: .customChatContents,
|
|
subject: .customChatContents(contents: contents),
|
|
botStart: nil,
|
|
mode: .standard(.default),
|
|
params: nil
|
|
)
|
|
chatController.navigationPresentation = .modal
|
|
|
|
self.push(chatController)*/
|
|
}, editMessageMedia: { [weak self] messageId, draw in
|
|
if let strongSelf = self {
|
|
strongSelf.controllerInteraction?.editMessageMedia(messageId, draw)
|
|
}
|
|
}, updateShowCommands: { [weak self] f in
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(interactive: true, {
|
|
return $0.updatedShowCommands(f($0.showCommands))
|
|
})
|
|
}
|
|
}, updateShowSendAsPeers: { [weak self] f in
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(interactive: true, {
|
|
return $0.updatedShowSendAsPeers(f($0.showSendAsPeers))
|
|
})
|
|
}
|
|
}, openInviteRequests: { [weak self] in
|
|
if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
|
|
let controller = inviteRequestsController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: peer.id, existingContext: strongSelf.inviteRequestsContext)
|
|
controller.navigationPresentation = .modal
|
|
strongSelf.push(controller)
|
|
}
|
|
}, openSendAsPeer: { [weak self] node, gesture in
|
|
guard let strongSelf = self, let peerId = strongSelf.chatLocation.peerId, let node = node as? ContextReferenceContentNode, let peers = strongSelf.presentationInterfaceState.sendAsPeers, let layout = strongSelf.validLayout else {
|
|
return
|
|
}
|
|
|
|
let isPremium = strongSelf.presentationInterfaceState.isPremium
|
|
|
|
let cleanInsets = layout.intrinsicInsets
|
|
let insets = layout.insets(options: .input)
|
|
let bottomInset = max(insets.bottom, cleanInsets.bottom) + 43.0
|
|
|
|
let defaultMyPeerId: PeerId
|
|
if let channel = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramChannel, case .group = channel.info, channel.hasPermission(.canBeAnonymous) {
|
|
defaultMyPeerId = channel.id
|
|
} else if let channel = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramChannel, case let .broadcast(info) = channel.info, info.flags.contains(.messagesShouldHaveProfiles) {
|
|
defaultMyPeerId = channel.id
|
|
} else {
|
|
defaultMyPeerId = strongSelf.context.account.peerId
|
|
}
|
|
let myPeerId = strongSelf.presentationInterfaceState.currentSendAsPeerId ?? defaultMyPeerId
|
|
|
|
var items: [ContextMenuItem] = []
|
|
items.append(.custom(ChatSendAsPeerTitleContextItem(text: strongSelf.presentationInterfaceState.strings.Conversation_SendMesageAs.uppercased()), false))
|
|
items.append(.custom(ChatSendAsPeerListContextItem(context: strongSelf.context, chatPeerId: peerId, peers: peers, selectedPeerId: myPeerId, isPremium: isPremium, presentToast: { [weak self] peer in
|
|
if let strongSelf = self {
|
|
HapticFeedback().impact()
|
|
|
|
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: strongSelf.presentationData.strings.Conversation_SendMesageAsPremiumInfo, action: strongSelf.presentationData.strings.EmojiInput_PremiumEmojiToast_Action, duration: 3), elevatedLayout: false, action: { [weak self] action in
|
|
guard let strongSelf = self else {
|
|
return true
|
|
}
|
|
if case .undo = action {
|
|
strongSelf.chatDisplayNode.dismissTextInput()
|
|
|
|
let controller = PremiumIntroScreen(context: strongSelf.context, source: .settings)
|
|
strongSelf.push(controller)
|
|
}
|
|
return true
|
|
}), in: .current)
|
|
}
|
|
|
|
}), false))
|
|
|
|
strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts()
|
|
|
|
let contextController = ContextController(presentationData: strongSelf.presentationData, source: .reference(ChatControllerContextReferenceContentSource(controller: strongSelf, sourceView: node.view, insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0))), items: .single(ContextController.Items(content: .list(items))), gesture: gesture, workaroundUseLegacyImplementation: true)
|
|
contextController.dismissed = { [weak self] in
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(interactive: true, {
|
|
return $0.updatedShowSendAsPeers(false)
|
|
})
|
|
}
|
|
}
|
|
strongSelf.presentInGlobalOverlay(contextController)
|
|
|
|
strongSelf.updateChatPresentationInterfaceState(interactive: true, {
|
|
return $0.updatedShowSendAsPeers(true)
|
|
})
|
|
}, presentChatRequestAdminInfo: { [weak self] in
|
|
self?.presentChatRequestAdminInfo()
|
|
}, displayCopyProtectionTip: { [weak self] node, save in
|
|
if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer, let messageIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds {
|
|
let _ = (strongSelf.context.engine.data.get(EngineDataMap(
|
|
messageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init)
|
|
))
|
|
|> map { messages -> [EngineMessage] in
|
|
return messages.values.compactMap { $0 }
|
|
}
|
|
|> deliverOnMainQueue).startStandalone(next: { [weak self] messages in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
enum PeerType {
|
|
case group
|
|
case channel
|
|
case bot
|
|
case user
|
|
}
|
|
var isBot = false
|
|
for message in messages {
|
|
if let author = message.author, case let .user(user) = author, user.botInfo != nil && !user.id.isVerificationCodes {
|
|
isBot = true
|
|
break
|
|
}
|
|
}
|
|
let type: PeerType
|
|
if isBot {
|
|
type = .bot
|
|
} else if let user = peer as? TelegramUser {
|
|
if user.botInfo != nil && !user.id.isVerificationCodes {
|
|
type = .bot
|
|
} else {
|
|
type = .user
|
|
}
|
|
} else if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
|
|
type = .channel
|
|
} else {
|
|
type = .group
|
|
}
|
|
|
|
let text: String
|
|
switch type {
|
|
case .group:
|
|
text = save ? strongSelf.presentationInterfaceState.strings.Conversation_CopyProtectionSavingDisabledGroup : strongSelf.presentationInterfaceState.strings.Conversation_CopyProtectionForwardingDisabledGroup
|
|
case .channel:
|
|
text = save ? strongSelf.presentationInterfaceState.strings.Conversation_CopyProtectionSavingDisabledChannel : strongSelf.presentationInterfaceState.strings.Conversation_CopyProtectionForwardingDisabledChannel
|
|
case .bot:
|
|
text = save ? strongSelf.presentationInterfaceState.strings.Conversation_CopyProtectionSavingDisabledBot : strongSelf.presentationInterfaceState.strings.Conversation_CopyProtectionForwardingDisabledBot
|
|
case .user:
|
|
text = save ? strongSelf.presentationData.strings.Conversation_CopyProtectionSavingDisabledSecret : strongSelf.presentationData.strings.Conversation_CopyProtectionForwardingDisabledSecret
|
|
}
|
|
|
|
strongSelf.copyProtectionTooltipController?.dismiss()
|
|
let tooltipController = TooltipController(content: .text(text), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true)
|
|
strongSelf.copyProtectionTooltipController = tooltipController
|
|
tooltipController.dismissed = { [weak tooltipController] _ in
|
|
if let strongSelf = self, let tooltipController = tooltipController, strongSelf.copyProtectionTooltipController === tooltipController {
|
|
strongSelf.copyProtectionTooltipController = nil
|
|
}
|
|
}
|
|
strongSelf.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: {
|
|
if let strongSelf = self {
|
|
let rect = node.view.convert(node.view.bounds, to: strongSelf.chatDisplayNode.view).offsetBy(dx: 0.0, dy: 3.0)
|
|
return (strongSelf.chatDisplayNode, rect)
|
|
}
|
|
return nil
|
|
}))
|
|
})
|
|
}
|
|
}, openWebView: { [weak self] buttonText, url, simple, source in
|
|
if let strongSelf = self {
|
|
strongSelf.controllerInteraction?.openWebView(buttonText, url, simple, source)
|
|
}
|
|
}, updateShowWebView: { [weak self] f in
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(interactive: true, {
|
|
return $0.updatedShowWebView(f($0.showWebView))
|
|
})
|
|
}
|
|
}, insertText: { [weak self] text in
|
|
guard let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction else {
|
|
return
|
|
}
|
|
if !strongSelf.chatDisplayNode.isTextInputPanelActive {
|
|
return
|
|
}
|
|
|
|
interfaceInteraction.updateTextInputStateAndMode { textInputState, inputMode in
|
|
let inputText = NSMutableAttributedString(attributedString: textInputState.inputText)
|
|
|
|
let range = textInputState.selectionRange
|
|
|
|
let updatedText = NSMutableAttributedString(attributedString: text)
|
|
if range.lowerBound < inputText.length {
|
|
if let quote = inputText.attribute(ChatTextInputAttributes.block, at: range.lowerBound, effectiveRange: nil) {
|
|
updatedText.addAttribute(ChatTextInputAttributes.block, value: quote, range: NSRange(location: 0, length: updatedText.length))
|
|
}
|
|
}
|
|
inputText.replaceCharacters(in: NSMakeRange(range.lowerBound, range.count), with: updatedText)
|
|
|
|
let selectionPosition = range.lowerBound + (updatedText.string as NSString).length
|
|
|
|
return (ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition), inputMode)
|
|
}
|
|
|
|
strongSelf.chatDisplayNode.updateTypingActivity(true)
|
|
}, backwardsDeleteText: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if !strongSelf.chatDisplayNode.isTextInputPanelActive {
|
|
return
|
|
}
|
|
guard let textInputPanelNode = strongSelf.chatDisplayNode.textInputPanelNode else {
|
|
return
|
|
}
|
|
textInputPanelNode.backwardsDeleteText()
|
|
}, restartTopic: { [weak self] in
|
|
guard let strongSelf = self, let peerId = strongSelf.chatLocation.peerId, let threadId = strongSelf.chatLocation.threadId else {
|
|
return
|
|
}
|
|
let _ = strongSelf.context.engine.peers.setForumChannelTopicClosed(id: peerId, threadId: threadId, isClosed: false).startStandalone()
|
|
}, toggleTranslation: { [weak self] type in
|
|
guard let self, let peerId = self.chatLocation.peerId else {
|
|
return
|
|
}
|
|
let _ = (updateChatTranslationStateInteractively(engine: self.context.engine, peerId: peerId, threadId: self.chatLocation.threadId, { current in
|
|
return current?.withIsEnabled(type == .translated)
|
|
})
|
|
|> deliverOnMainQueue).startStandalone(completed: { [weak self] in
|
|
if let self, type == .translated {
|
|
Queue.mainQueue().after(0.15) {
|
|
self.chatDisplayNode.historyNode.refreshPollActionsForVisibleMessages()
|
|
}
|
|
}
|
|
})
|
|
}, changeTranslationLanguage: { [weak self] langCode in
|
|
guard let self, let peerId = self.chatLocation.peerId else {
|
|
return
|
|
}
|
|
let langCode = normalizeTranslationLanguage(langCode)
|
|
let _ = updateChatTranslationStateInteractively(engine: self.context.engine, peerId: peerId, threadId: self.chatLocation.threadId, { current in
|
|
return current?.withToLang(langCode).withIsEnabled(true)
|
|
}).startStandalone()
|
|
}, addDoNotTranslateLanguage: { [weak self] langCode in
|
|
guard let self, let peerId = self.chatLocation.peerId else {
|
|
return
|
|
}
|
|
let _ = updateTranslationSettingsInteractively(accountManager: self.context.sharedContext.accountManager, { current in
|
|
var updated = current
|
|
if var ignoredLanguages = updated.ignoredLanguages {
|
|
if !ignoredLanguages.contains(langCode) {
|
|
ignoredLanguages.append(langCode)
|
|
}
|
|
updated.ignoredLanguages = ignoredLanguages
|
|
} else {
|
|
var ignoredLanguages = Set<String>()
|
|
ignoredLanguages.insert(self.presentationData.strings.baseLanguageCode)
|
|
for language in systemLanguageCodes() {
|
|
ignoredLanguages.insert(language)
|
|
}
|
|
ignoredLanguages.insert(langCode)
|
|
updated.ignoredLanguages = Array(ignoredLanguages)
|
|
}
|
|
return updated
|
|
}).startStandalone()
|
|
let _ = updateChatTranslationStateInteractively(engine: self.context.engine, peerId: peerId, threadId: self.chatLocation.threadId, { current in
|
|
return nil
|
|
}).startStandalone()
|
|
|
|
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
|
var languageCode = presentationData.strings.baseLanguageCode
|
|
let rawSuffix = "-raw"
|
|
if languageCode.hasSuffix(rawSuffix) {
|
|
languageCode = String(languageCode.dropLast(rawSuffix.count))
|
|
}
|
|
let locale = Locale(identifier: languageCode)
|
|
let fromLanguage: String = locale.localizedString(forLanguageCode: langCode) ?? ""
|
|
|
|
self.present(UndoOverlayController(presentationData: presentationData, content: .image(image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Title Panels/Translate"), color: .white)!, title: nil, text: presentationData.strings.Conversation_Translation_AddedToDoNotTranslateText(fromLanguage).string, round: false, undoText: presentationData.strings.Conversation_Translation_Settings), elevatedLayout: false, animateInAsReplacement: false, action: { [weak self] action in
|
|
if case .undo = action, let self {
|
|
let controller = translationSettingsController(context: self.context)
|
|
controller.navigationPresentation = .modal
|
|
self.push(controller)
|
|
}
|
|
return true
|
|
}), in: .current)
|
|
}, hideTranslationPanel: { [weak self] in
|
|
guard let strongSelf = self, let peerId = strongSelf.chatLocation.peerId else {
|
|
return
|
|
}
|
|
let context = strongSelf.context
|
|
let presentationData = strongSelf.presentationData
|
|
let _ = context.engine.messages.togglePeerMessagesTranslationHidden(peerId: peerId, hidden: true).startStandalone()
|
|
|
|
var text: String = ""
|
|
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
|
|
if peer is TelegramGroup {
|
|
text = presentationData.strings.Conversation_Translation_TranslationBarHiddenGroupText
|
|
} else if let peer = peer as? TelegramChannel {
|
|
switch peer.info {
|
|
case .group:
|
|
text = presentationData.strings.Conversation_Translation_TranslationBarHiddenGroupText
|
|
case .broadcast:
|
|
text = presentationData.strings.Conversation_Translation_TranslationBarHiddenChannelText
|
|
}
|
|
} else {
|
|
text = presentationData.strings.Conversation_Translation_TranslationBarHiddenChatText
|
|
}
|
|
}
|
|
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .image(image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Title Panels/Translate"), color: .white)!, title: nil, text: text, round: false, undoText: presentationData.strings.Undo_Undo), elevatedLayout: false, animateInAsReplacement: false, action: { action in
|
|
if case .undo = action {
|
|
let _ = context.engine.messages.togglePeerMessagesTranslationHidden(peerId: peerId, hidden: false).startStandalone()
|
|
}
|
|
return true
|
|
}), in: .current)
|
|
}, openPremiumGift: { [weak self] in
|
|
guard let self, let peerId = self.chatLocation.peerId else {
|
|
return
|
|
}
|
|
|
|
if peerId.namespace == Namespaces.Peer.CloudUser {
|
|
self.presentAttachmentMenu(subject: .gift)
|
|
Queue.mainQueue().after(0.5) {
|
|
let _ = ApplicationSpecificNotice.incrementDismissedPremiumGiftSuggestion(accountManager: self.context.sharedContext.accountManager, peerId: peerId, timestamp: Int32(Date().timeIntervalSince1970)).startStandalone()
|
|
}
|
|
} else {
|
|
let controller = self.context.sharedContext.makeGiftOptionsController(context: self.context, peerId: peerId, premiumOptions: [], hasBirthday: false, completion: { [weak self] in
|
|
guard let self, let peer = self.presentationInterfaceState.renderedPeer?.peer else {
|
|
return
|
|
}
|
|
if let controller = self.context.sharedContext.makePeerInfoController(
|
|
context: self.context,
|
|
updatedPresentationData: nil,
|
|
peer: peer,
|
|
mode: .gifts,
|
|
avatarInitiallyExpanded: false,
|
|
fromChat: false,
|
|
requestsContext: nil
|
|
) {
|
|
self.push(controller)
|
|
}
|
|
})
|
|
self.push(controller)
|
|
}
|
|
}, openPremiumRequiredForMessaging: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .messageTags, forceDark: false, dismissed: nil)
|
|
self.push(controller)
|
|
}, openStarsPurchase: { [weak self] requiredStars in
|
|
guard let self, let starsContext = self.context.starsContext else {
|
|
return
|
|
}
|
|
let _ = (self.context.engine.payments.starsTopUpOptions()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).startStandalone(next: { [weak self] options in
|
|
guard let self else {
|
|
return
|
|
}
|
|
let controller = self.context.sharedContext.makeStarsPurchaseScreen(context: self.context, starsContext: starsContext, options: options, purpose: .generic, completion: { _ in
|
|
})
|
|
self.push(controller)
|
|
})
|
|
}, openMessagePayment: {
|
|
|
|
}, openBoostToUnrestrict: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
guard !self.presentAccountFrozenInfoIfNeeded() else {
|
|
return
|
|
}
|
|
|
|
guard let peerId = self.chatLocation.peerId, let cachedData = self.peerView?.cachedData as? CachedChannelData, let boostToUnrestrict = cachedData.boostsToUnrestrict else {
|
|
return
|
|
}
|
|
|
|
HapticFeedback().impact()
|
|
|
|
let _ = combineLatest(queue: Queue.mainQueue(),
|
|
context.engine.peers.getChannelBoostStatus(peerId: peerId),
|
|
context.engine.peers.getMyBoostStatus()
|
|
).startStandalone(next: { [weak self] boostStatus, myBoostStatus in
|
|
guard let self, let boostStatus, let myBoostStatus else {
|
|
return
|
|
}
|
|
let boostController = PremiumBoostLevelsScreen(
|
|
context: self.context,
|
|
peerId: peerId,
|
|
mode: .user(mode: .unrestrict(Int(boostToUnrestrict))),
|
|
status: boostStatus,
|
|
myBoostStatus: myBoostStatus
|
|
)
|
|
self.push(boostController)
|
|
})
|
|
}, updateVideoTrimRange: { [weak self] start, end, updatedEnd, apply in
|
|
if let videoRecorder = self?.videoRecorderValue {
|
|
videoRecorder.updateTrimRange(start: start, end: end, updatedEnd: updatedEnd, apply: apply)
|
|
}
|
|
}, updateHistoryFilter: { [weak self] update in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
let updatedFilter = update(self.presentationInterfaceState.historyFilter)
|
|
|
|
let apply: () -> Void = { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
|
|
var state = state.updatedHistoryFilter(updatedFilter)
|
|
if let updatedFilter, let reaction = ReactionsMessageAttribute.reactionFromMessageTag(tag: updatedFilter.customTag) {
|
|
if let search = state.search, search.domain != .tag(reaction) {
|
|
state = state.updatedSearch(ChatSearchData())
|
|
} else if state.search == nil {
|
|
state = state.updatedSearch(ChatSearchData())
|
|
}
|
|
}
|
|
return state
|
|
})
|
|
}
|
|
|
|
if let updatedFilter, let reaction = ReactionsMessageAttribute.reactionFromMessageTag(tag: updatedFilter.customTag) {
|
|
let tag = updatedFilter.customTag
|
|
|
|
let _ = (self.context.engine.data.get(
|
|
TelegramEngine.EngineData.Item.Messages.ReactionTagMessageCount(peerId: self.context.account.peerId, threadId: self.chatLocation.threadId, reaction: reaction)
|
|
)
|
|
|> deliverOnMainQueue).start(next: { [weak self] count in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
var tagSearchInputPanelNode: ChatTagSearchInputPanelNode?
|
|
if let panelNode = self.chatDisplayNode.inputPanelNode as? ChatTagSearchInputPanelNode {
|
|
tagSearchInputPanelNode = panelNode
|
|
} else if let panelNode = self.chatDisplayNode.secondaryInputPanelNode as? ChatTagSearchInputPanelNode {
|
|
tagSearchInputPanelNode = panelNode
|
|
}
|
|
|
|
if let tagSearchInputPanelNode, let count {
|
|
tagSearchInputPanelNode.prepareSwitchToFilter(tag: tag, count: count)
|
|
}
|
|
|
|
apply()
|
|
})
|
|
} else {
|
|
apply()
|
|
}
|
|
}, updateDisplayHistoryFilterAsList: { [weak self] displayAsList in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
if !displayAsList {
|
|
self.alwaysShowSearchResultsAsList = false
|
|
self.chatDisplayNode.alwaysShowSearchResultsAsList = false
|
|
}
|
|
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
|
|
return state.updatedDisplayHistoryFilterAsList(displayAsList)
|
|
})
|
|
}, requestLayout: { [weak self] transition in
|
|
if let strongSelf = self, let layout = strongSelf.validLayout {
|
|
strongSelf.containerLayoutUpdated(layout, transition: transition)
|
|
}
|
|
}, chatController: { [weak self] in
|
|
return self
|
|
}, 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 {
|
|
let throttledUnreadCountSignal = self.context.chatLocationUnreadCount(for: self.chatLocation, contextHolder: self.chatLocationContextHolder)
|
|
|> mapToThrottled { value -> Signal<Int, NoError> in
|
|
return .single(value) |> then(.complete() |> delay(0.2, queue: Queue.mainQueue()))
|
|
}
|
|
self.buttonUnreadCountDisposable = (throttledUnreadCountSignal
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self] count in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.chatDisplayNode.navigateButtons.unreadCount = Int32(count)
|
|
})
|
|
|
|
if case let .peer(peerId) = self.chatLocation {
|
|
self.chatUnreadCountDisposable = (self.context.engine.data.subscribe(
|
|
TelegramEngine.EngineData.Item.Messages.PeerUnreadCount(id: peerId),
|
|
TelegramEngine.EngineData.Item.Messages.TotalReadCounters(),
|
|
TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peerId)
|
|
)
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self] peerUnreadCount, totalReadCounters, notificationSettings in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
let unreadCount: Int32 = Int32(peerUnreadCount)
|
|
|
|
let inAppSettings = strongSelf.context.sharedContext.currentInAppNotificationSettings.with { $0 }
|
|
let totalChatCount: Int32 = renderedTotalUnreadCount(inAppSettings: inAppSettings, totalUnreadState: totalReadCounters._asCounters()).0
|
|
|
|
var globalRemainingUnreadChatCount = totalChatCount
|
|
if !notificationSettings._asNotificationSettings().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.unseenPersonalMessagesAndReactionCount(peerId: peerId, threadId: nil) |> deliverOnMainQueue).startStrict(next: { [weak self] mentionCount, reactionCount in
|
|
if let strongSelf = self {
|
|
if case .standard(.previewing) = strongSelf.presentationInterfaceState.mode {
|
|
strongSelf.chatDisplayNode.navigateButtons.mentionCount = 0
|
|
strongSelf.chatDisplayNode.navigateButtons.reactionsCount = 0
|
|
} else {
|
|
strongSelf.chatDisplayNode.navigateButtons.mentionCount = mentionCount
|
|
strongSelf.chatDisplayNode.navigateButtons.reactionsCount = reactionCount
|
|
}
|
|
}
|
|
})
|
|
} else if let peerId = self.chatLocation.peerId, let threadId = self.chatLocation.threadId {
|
|
self.chatUnreadMentionCountDisposable = (self.context.account.viewTracker.unseenPersonalMessagesAndReactionCount(peerId: peerId, threadId: threadId) |> deliverOnMainQueue).startStrict(next: { [weak self] mentionCount, reactionCount in
|
|
if let strongSelf = self {
|
|
if case .standard(.previewing) = strongSelf.presentationInterfaceState.mode {
|
|
strongSelf.chatDisplayNode.navigateButtons.mentionCount = 0
|
|
strongSelf.chatDisplayNode.navigateButtons.reactionsCount = 0
|
|
} else {
|
|
strongSelf.chatDisplayNode.navigateButtons.mentionCount = mentionCount
|
|
strongSelf.chatDisplayNode.navigateButtons.reactionsCount = reactionCount
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
let engine = self.context.engine
|
|
let previousPeerCache = Atomic<[PeerId: Peer]>(value: [:])
|
|
|
|
let activitySpace: PeerActivitySpace?
|
|
switch self.chatLocation {
|
|
case let .peer(peerId):
|
|
activitySpace = PeerActivitySpace(peerId: peerId, category: .global)
|
|
case let .replyThread(replyThreadMessage):
|
|
activitySpace = PeerActivitySpace(peerId: replyThreadMessage.peerId, category: .thread(replyThreadMessage.threadId))
|
|
case .customChatContents:
|
|
activitySpace = nil
|
|
}
|
|
|
|
if let activitySpace = activitySpace, let peerId = peerId {
|
|
self.peerInputActivitiesDisposable = (self.context.account.peerInputActivities(peerId: activitySpace)
|
|
|> 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 engine.data.get(EngineDataMap(
|
|
activities.map { TelegramEngine.EngineData.Item.Peer.Peer(id: $0.0) }
|
|
))
|
|
|> map { peerMap -> [(Peer, PeerInputActivity)] in
|
|
var result: [(Peer, PeerInputActivity)] = []
|
|
var peerCache: [PeerId: Peer] = [:]
|
|
for (peerId, activity) in activities {
|
|
if let maybePeer = peerMap[peerId], let peer = maybePeer {
|
|
result.append((peer._asPeer(), activity))
|
|
peerCache[peerId] = peer._asPeer()
|
|
}
|
|
}
|
|
let _ = previousPeerCache.swap(peerCache)
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self] activities in
|
|
if let strongSelf = self {
|
|
let displayActivities = activities.filter({
|
|
switch $0.1 {
|
|
case .speakingInGroupCall, .interactingWithEmoji:
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
})
|
|
strongSelf.chatTitleView?.inputActivities = (peerId, displayActivities)
|
|
|
|
strongSelf.peerInputActivitiesPromise.set(.single(activities))
|
|
|
|
for activity in activities {
|
|
if case let .interactingWithEmoji(emoticon, messageId, maybeInteraction) = activity.1, let interaction = maybeInteraction {
|
|
var found = false
|
|
strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode({ itemNode in
|
|
if !found, let itemNode = itemNode as? ChatMessageAnimatedStickerItemNode, let item = itemNode.item {
|
|
if item.message.id == messageId {
|
|
itemNode.playEmojiInteraction(interaction)
|
|
found = true
|
|
}
|
|
}
|
|
})
|
|
|
|
if found {
|
|
let _ = strongSelf.context.account.updateLocalInputActivity(peerId: activitySpace, activity: .seeingEmojiInteraction(emoticon: emoticon), isPresent: true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
if let peerId = peerId {
|
|
self.sentMessageEventsDisposable.set((self.context.account.pendingMessageManager.deliveredMessageEvents(peerId: peerId)
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self] eventGroup in
|
|
guard let self else {
|
|
return
|
|
}
|
|
let inAppNotificationSettings = self.context.sharedContext.currentInAppNotificationSettings.with { $0 }
|
|
if inAppNotificationSettings.playSounds, let firstEvent = eventGroup.first, !firstEvent.isSilent {
|
|
serviceSoundManager.playMessageDeliveredSound()
|
|
}
|
|
if self.presentationInterfaceState.subject != .scheduledMessages, let firstEvent = eventGroup.first, firstEvent.id.namespace == Namespaces.Message.ScheduledCloud {
|
|
if eventGroup.contains(where: { $0.isPendingProcessing }) {
|
|
self.openScheduledMessages(completion: { [weak self] c in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
c.dismissAllUndoControllers()
|
|
|
|
Queue.mainQueue().after(0.5) { [weak c] in
|
|
c?.displayProcessingVideoTooltip(messageId: firstEvent.id)
|
|
}
|
|
|
|
c.present(
|
|
UndoOverlayController(
|
|
presentationData: self.presentationData,
|
|
content: .universalImage(
|
|
image: generateTintedImage(image: UIImage(bundleImageName: "Chat/ToastImprovingVideo"), color: .white)!,
|
|
size: nil,
|
|
title: self.presentationData.strings.Chat_ToastImprovingVideo_Title,
|
|
text: self.presentationData.strings.Chat_ToastImprovingVideo_Text,
|
|
customUndoText: nil,
|
|
timeout: 5.0
|
|
),
|
|
elevatedLayout: false,
|
|
position: .top,
|
|
action: { _ in
|
|
return true
|
|
}
|
|
),
|
|
in: .current
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
if self.shouldDisplayChecksTooltip {
|
|
Queue.mainQueue().after(1.0) { [weak self] in
|
|
self?.displayChecksTooltip()
|
|
}
|
|
self.shouldDisplayChecksTooltip = false
|
|
self.checksTooltipDisposable.set(self.context.engine.notices.dismissServerProvidedSuggestion(suggestion: ServerProvidedSuggestion.newcomerTicks.id).startStrict())
|
|
}
|
|
|
|
if let shouldDisplayProcessingVideoTooltip = self.shouldDisplayProcessingVideoTooltip {
|
|
self.shouldDisplayProcessingVideoTooltip = nil
|
|
Queue.mainQueue().after(1.0) { [weak self] in
|
|
self?.displayProcessingVideoTooltip(messageId: shouldDisplayProcessingVideoTooltip)
|
|
}
|
|
}
|
|
}))
|
|
|
|
self.failedMessageEventsDisposable.set((self.context.account.pendingMessageManager.failedMessageEvents(peerId: peerId)
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self] reason in
|
|
if let strongSelf = self, strongSelf.currentFailedMessagesAlertController == nil {
|
|
let text: String
|
|
var title: String?
|
|
let moreInfo: Bool
|
|
switch reason {
|
|
case .flood:
|
|
text = strongSelf.presentationData.strings.Conversation_SendMessageErrorFlood
|
|
moreInfo = true
|
|
case .sendingTooFast:
|
|
text = strongSelf.presentationData.strings.Conversation_SendMessageErrorTooFast
|
|
title = strongSelf.presentationData.strings.Conversation_SendMessageErrorTooFastTitle
|
|
moreInfo = false
|
|
case .publicBan:
|
|
text = strongSelf.presentationData.strings.Conversation_SendMessageErrorGroupRestricted
|
|
moreInfo = true
|
|
case .mediaRestricted:
|
|
text = strongSelf.restrictedSendingContentsText()
|
|
moreInfo = false
|
|
case .slowmodeActive:
|
|
text = strongSelf.presentationData.strings.Chat_SlowmodeSendError
|
|
moreInfo = false
|
|
case .tooMuchScheduled:
|
|
text = strongSelf.presentationData.strings.Conversation_SendMessageErrorTooMuchScheduled
|
|
moreInfo = false
|
|
case .voiceMessagesForbidden:
|
|
strongSelf.interfaceInteraction?.displayRestrictedInfo(.premiumVoiceMessages, .alert)
|
|
return
|
|
case .nonPremiumMessagesForbidden:
|
|
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer {
|
|
text = strongSelf.presentationData.strings.Conversation_SendMessageErrorNonPremiumForbidden(EnginePeer(peer).compactDisplayTitle).string
|
|
moreInfo = false
|
|
} else {
|
|
return
|
|
}
|
|
}
|
|
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, updatedPresentationData: strongSelf.updatedPresentationData, title: title, text: text, actions: actions)
|
|
strongSelf.currentFailedMessagesAlertController = controller
|
|
strongSelf.present(controller, in: .window(.root))
|
|
}
|
|
}))
|
|
|
|
self.sentPeerMediaMessageEventsDisposable.set(
|
|
(self.context.account.pendingPeerMediaUploadManager.sentMessageEvents(peerId: peerId)
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self] _ in
|
|
if let self {
|
|
self.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
|
}
|
|
})
|
|
)
|
|
}
|
|
}
|
|
|
|
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().startStrict(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).startStrict(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)
|
|
}
|
|
}
|
|
|
|
let buttonAction = { [weak self] in
|
|
guard let self, self.traceVisibility() && isTopmostChatController(self) else {
|
|
return
|
|
}
|
|
self.videoUnmuteTooltipController?.dismiss()
|
|
|
|
var actions: [(Bool, (Double?) -> Void)] = []
|
|
var hasUnconsumed = false
|
|
self.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.volumeButtonsListener = VolumeButtonsListener(
|
|
sharedContext: self.context.sharedContext,
|
|
isCameraSpecific: false,
|
|
shouldBeActive: shouldBeActive,
|
|
upPressed: buttonAction,
|
|
downPressed: buttonAction
|
|
)
|
|
|
|
self.chatDisplayNode.historyNode.openNextChannelToRead = { [weak self] peer, threadData, location in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if let navigationController = strongSelf.effectiveNavigationController {
|
|
let _ = ApplicationSpecificNotice.incrementNextChatSuggestionTip(accountManager: strongSelf.context.sharedContext.accountManager).startStandalone()
|
|
|
|
let snapshotState = strongSelf.chatDisplayNode.prepareSnapshotState(
|
|
titleViewSnapshotState: strongSelf.chatTitleView?.prepareSnapshotState(),
|
|
avatarSnapshotState: (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.prepareSnapshotState()
|
|
)
|
|
|
|
var nextFolderId: Int32?
|
|
switch location {
|
|
case let .folder(id, _):
|
|
nextFolderId = id
|
|
case .same:
|
|
nextFolderId = strongSelf.currentChatListFilter
|
|
default:
|
|
nextFolderId = nil
|
|
}
|
|
|
|
var updatedChatNavigationStack = strongSelf.chatNavigationStack
|
|
updatedChatNavigationStack.removeAll(where: { $0 == ChatNavigationStackItem(peerId: peer.id, threadId: threadData?.id) })
|
|
if let peerId = strongSelf.chatLocation.peerId {
|
|
updatedChatNavigationStack.insert(ChatNavigationStackItem(peerId: peerId, threadId: strongSelf.chatLocation.threadId), at: 0)
|
|
}
|
|
|
|
let chatLocation: NavigateToChatControllerParams.Location
|
|
if let threadData {
|
|
chatLocation = .replyThread(ChatReplyThreadMessage(
|
|
peerId: peer.id,
|
|
threadId: threadData.id,
|
|
channelMessageId: nil,
|
|
isChannelPost: false,
|
|
isForumPost: true,
|
|
maxMessage: nil,
|
|
maxReadIncomingMessageId: nil,
|
|
maxReadOutgoingMessageId: nil,
|
|
unreadCount: 0,
|
|
initialFilledHoles: IndexSet(),
|
|
initialAnchor: .automatic,
|
|
isNotAvailable: false
|
|
))
|
|
} else {
|
|
chatLocation = .peer(peer)
|
|
}
|
|
|
|
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: chatLocation, animated: false, chatListFilter: nextFolderId, chatNavigationStack: updatedChatNavigationStack, completion: { nextController in
|
|
(nextController as! ChatControllerImpl).animateFromPreviousController(snapshotState: snapshotState)
|
|
}, customChatNavigationStack: strongSelf.customChatNavigationStack))
|
|
}
|
|
}
|
|
|
|
var lastEventTimestamp: Double = 0.0
|
|
self.networkSpeedEventsDisposable = (self.context.account.network.networkSpeedLimitedEvents
|
|
|> deliverOnMainQueue).start(next: { [weak self] event in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
switch event {
|
|
case let .download(subject):
|
|
if case let .message(messageId) = subject {
|
|
var isVisible = false
|
|
self.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
|
|
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item {
|
|
for (message, _) in item.content {
|
|
if message.id == messageId {
|
|
isVisible = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !isVisible {
|
|
return
|
|
}
|
|
}
|
|
case .upload:
|
|
break
|
|
}
|
|
|
|
let timestamp = CFAbsoluteTimeGetCurrent()
|
|
if lastEventTimestamp + 10.0 < timestamp {
|
|
lastEventTimestamp = timestamp
|
|
} else {
|
|
return
|
|
}
|
|
|
|
let title: String
|
|
let text: String
|
|
switch event {
|
|
case .download:
|
|
var speedIncreaseFactor = 10
|
|
if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["upload_premium_speedup_download"] as? Double {
|
|
speedIncreaseFactor = Int(value)
|
|
}
|
|
title = self.presentationData.strings.Chat_SpeedLimitAlert_Download_Title
|
|
text = self.presentationData.strings.Chat_SpeedLimitAlert_Download_Text("\(speedIncreaseFactor)").string
|
|
case .upload:
|
|
var speedIncreaseFactor = 10
|
|
if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["upload_premium_speedup_upload"] as? Double {
|
|
speedIncreaseFactor = Int(value)
|
|
}
|
|
title = self.presentationData.strings.Chat_SpeedLimitAlert_Upload_Title
|
|
text = self.presentationData.strings.Chat_SpeedLimitAlert_Upload_Text("\(speedIncreaseFactor)").string
|
|
}
|
|
let content: UndoOverlayContent = .universal(animation: "anim_speed_low", scale: 0.066, colors: [:], title: title, text: text, customUndoText: nil, timeout: 5.0)
|
|
|
|
self.context.account.network.markNetworkSpeedLimitDisplayed()
|
|
|
|
self.present(UndoOverlayController(presentationData: self.presentationData, content: content, elevatedLayout: false, position: .top, action: { [weak self] action in
|
|
guard let self else {
|
|
return false
|
|
}
|
|
switch action {
|
|
case .info:
|
|
let context = self.context
|
|
var replaceImpl: ((ViewController) -> Void)?
|
|
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .fasterDownload, forceDark: false, action: {
|
|
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .fasterDownload, forceDark: false, dismissed: nil)
|
|
replaceImpl?(controller)
|
|
}, dismissed: nil)
|
|
replaceImpl = { [weak controller] c in
|
|
controller?.replace(with: c)
|
|
}
|
|
self.push(controller)
|
|
return true
|
|
default:
|
|
break
|
|
}
|
|
return false
|
|
}), in: .current)
|
|
})
|
|
|
|
if case .scheduledMessages = self.subject {
|
|
self.postedScheduledMessagesEventsDisposable = (self.context.account.stateManager.sentScheduledMessageIds
|
|
|> deliverOnMainQueue).start(next: { [weak self] ids in
|
|
guard let self, let peerId = self.chatLocation.peerId else {
|
|
return
|
|
}
|
|
let filteredIds = Array(ids).filter({ $0.peerId == peerId })
|
|
if filteredIds.isEmpty {
|
|
return
|
|
}
|
|
self.displayPostedScheduledMessagesToast(ids: filteredIds)
|
|
})
|
|
}
|
|
|
|
self.displayNodeDidLoad()
|
|
}
|
|
} |