diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index a2edb10793..2373353de5 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -39,6 +39,7 @@ public enum PremiumIntroSource { case readTime case messageTags case folderTags + case animatedEmoji } public enum PremiumGiftSource: Equatable { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 3cfd6892cf..6f1e12f55b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -937,6 +937,32 @@ public extension TelegramEngine { } } } + + public func getNextUnreadForumTopic(peerId: PeerId, topicId: Int32) -> Signal<(id: Int64, data: MessageHistoryThreadData)?, NoError> { + return self.account.postbox.transaction { transaction -> (id: Int64, data: MessageHistoryThreadData)? in + var unreadThreads: [(id: Int64, data: MessageHistoryThreadData, index: MessageIndex)] = [] + for item in transaction.getMessageHistoryThreadIndex(peerId: peerId, limit: 100) { + if item.threadId == Int64(topicId) { + continue + } + guard let data = item.info.data.get(MessageHistoryThreadData.self) else { + continue + } + if data.incomingUnreadCount <= 0 { + continue + } + guard let messageIndex = transaction.getMessageHistoryThreadTopMessage(peerId: peerId, threadId: item.threadId, namespaces: Set([Namespaces.Message.Cloud])) else { + continue + } + unreadThreads.append((item.threadId, data, messageIndex)) + } + if let result = unreadThreads.min(by: { $0.index > $1.index }) { + return (result.id, result.data) + } else { + return nil + } + } + } public func getOpaqueChatInterfaceState(peerId: PeerId, threadId: Int64?) -> Signal { return self.account.postbox.transaction { transaction -> OpaqueChatInterfaceState? in diff --git a/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/BUILD b/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/BUILD index f716408639..6596b91582 100644 --- a/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/BUILD @@ -20,6 +20,8 @@ swift_library( "//submodules/TextFormat", "//submodules/Markdown", "//submodules/WallpaperBackgroundNode", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUI/Components/EmojiStatusComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/Sources/ChatOverscrollControl.swift b/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/Sources/ChatOverscrollControl.swift index 8bdcac9712..b505081b99 100644 --- a/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/Sources/ChatOverscrollControl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/Sources/ChatOverscrollControl.swift @@ -9,6 +9,8 @@ import AvatarNode import TextFormat import Markdown import WallpaperBackgroundNode +import EmojiStatusComponent +import TelegramPresentationData final class BlurredRoundedRectangle: Component { let color: UIColor @@ -403,6 +405,16 @@ final class BadgeComponent: CombinedComponent { } } +public struct ChatOverscrollThreadData: Equatable { + public var id: Int64 + public var data: MessageHistoryThreadData + + public init(id: Int64, data: MessageHistoryThreadData) { + self.id = id + self.data = data + } +} + final class AvatarComponent: Component { final class Badge: Equatable { let count: Int @@ -431,6 +443,7 @@ final class AvatarComponent: Component { let context: AccountContext let peer: EnginePeer + let threadData: ChatOverscrollThreadData? let badge: Badge? let rect: CGRect let withinSize: CGSize @@ -439,6 +452,7 @@ final class AvatarComponent: Component { init( context: AccountContext, peer: EnginePeer, + threadData: ChatOverscrollThreadData?, badge: Badge?, rect: CGRect, withinSize: CGSize, @@ -446,6 +460,7 @@ final class AvatarComponent: Component { ) { self.context = context self.peer = peer + self.threadData = threadData self.badge = badge self.rect = rect self.withinSize = withinSize @@ -459,6 +474,9 @@ final class AvatarComponent: Component { if lhs.peer != rhs.peer { return false } + if lhs.threadData != rhs.threadData { + return false + } if lhs.badge != rhs.badge { return false } @@ -475,17 +493,20 @@ final class AvatarComponent: Component { } final class View: UIView { - private let avatarNode: AvatarNode + private let avatarContainer: UIView + private var avatarNode: AvatarNode? + private var avatarIcon: ComponentView? + private let avatarMask: CAShapeLayer private var badgeView: ComponentHostView? init() { - self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) + self.avatarContainer = UIView() self.avatarMask = CAShapeLayer() super.init(frame: CGRect()) - self.addSubview(self.avatarNode.view) + self.addSubview(self.avatarContainer) } required init?(coder aDecoder: NSCoder) { @@ -493,9 +514,73 @@ final class AvatarComponent: Component { } func update(component: AvatarComponent, availableSize: CGSize, transition: Transition) -> CGSize { - self.avatarNode.frame = CGRect(origin: CGPoint(), size: availableSize) + self.avatarContainer.frame = CGRect(origin: CGPoint(), size: availableSize) let theme = component.context.sharedContext.currentPresentationData.with({ $0 }).theme - self.avatarNode.setPeer(context: component.context, theme: theme, peer: component.peer, emptyColor: theme.list.mediaPlaceholderColor, synchronousLoad: true) + + if let threadData = component.threadData { + if let avatarNode = self.avatarNode { + self.avatarNode = nil + avatarNode.view.removeFromSuperview() + } + + let avatarIconContent: EmojiStatusComponent.Content + if threadData.id == 1 { + avatarIconContent = .image(image: PresentationResourcesChatList.generalTopicIcon(theme)) + } else if let fileId = threadData.data.info.icon, fileId != 0 { + avatarIconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 48.0, height: 48.0), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: theme.list.itemAccentColor, loopMode: .count(0)) + } else { + avatarIconContent = .topic(title: String(threadData.data.info.title.prefix(1)), color: threadData.data.info.iconColor, size: CGSize(width: 32.0, height: 32.0)) + } + + let avatarIcon: ComponentView + if let current = self.avatarIcon { + avatarIcon = current + } else { + avatarIcon = ComponentView() + self.avatarIcon = avatarIcon + } + + let avatarIconComponent = EmojiStatusComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + content: avatarIconContent, + isVisibleForAnimations: true, + action: nil + ) + + let iconSize = avatarIcon.update( + transition: .immediate, + component: AnyComponent(avatarIconComponent), + environment: {}, + containerSize: availableSize + ) + + let avatarIconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) / 2.0), y: floor((availableSize.height - iconSize.height) / 2.0)), size: iconSize) + if let avatarIconView = avatarIcon.view { + if avatarIconView.superview == nil { + self.avatarContainer.addSubview(avatarIconView) + } + avatarIconView.frame = avatarIconFrame + } + } else { + if let avatarIcon = self.avatarIcon { + self.avatarIcon = nil + avatarIcon.view?.removeFromSuperview() + } + + let avatarNode: AvatarNode + if let current = self.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) + self.avatarNode = avatarNode + self.avatarContainer.addSubview(avatarNode.view) + } + + avatarNode.frame = CGRect(origin: CGPoint(), size: availableSize) + avatarNode.setPeer(context: component.context, theme: theme, peer: component.peer, emptyColor: theme.list.mediaPlaceholderColor, synchronousLoad: true) + } if let badge = component.badge { let badgeView: ComponentHostView @@ -528,14 +613,14 @@ final class AvatarComponent: Component { ) badgeView.frame = CGRect(origin: CGPoint(x: circlePoint.x - badgeDiameter / 2.0, y: circlePoint.y - badgeDiameter / 2.0), size: badgeSize) - self.avatarMask.frame = self.avatarNode.bounds + self.avatarMask.frame = self.avatarContainer.bounds self.avatarMask.fillRule = .evenOdd let path = UIBezierPath(rect: self.avatarMask.bounds) path.append(UIBezierPath(roundedRect: badgeView.frame.insetBy(dx: -2.0, dy: -2.0), cornerRadius: badgeDiameter / 2.0)) self.avatarMask.path = path.cgPath - self.avatarNode.view.layer.mask = self.avatarMask + self.avatarContainer.layer.mask = self.avatarMask if animateIn { badgeView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.14) @@ -547,7 +632,7 @@ final class AvatarComponent: Component { badgeView?.removeFromSuperview() }) - self.avatarNode.view.layer.mask = nil + self.avatarContainer.layer.mask = nil } return availableSize @@ -666,6 +751,7 @@ final class OverscrollContentsComponent: Component { let backgroundColor: UIColor let foregroundColor: UIColor let peer: EnginePeer? + let threadData: ChatOverscrollThreadData? let unreadCount: Int let location: TelegramEngine.NextUnreadChannelLocation let expandOffset: CGFloat @@ -679,6 +765,7 @@ final class OverscrollContentsComponent: Component { backgroundColor: UIColor, foregroundColor: UIColor, peer: EnginePeer?, + threadData: ChatOverscrollThreadData?, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation, expandOffset: CGFloat, @@ -691,6 +778,7 @@ final class OverscrollContentsComponent: Component { self.backgroundColor = backgroundColor self.foregroundColor = foregroundColor self.peer = peer + self.threadData = threadData self.unreadCount = unreadCount self.location = location self.expandOffset = expandOffset @@ -713,6 +801,9 @@ final class OverscrollContentsComponent: Component { if lhs.peer != rhs.peer { return false } + if lhs.threadData != rhs.threadData { + return false + } if lhs.unreadCount != rhs.unreadCount { return false } @@ -870,7 +961,9 @@ final class OverscrollContentsComponent: Component { transition.setSublayerTransform(view: self.avatarScalingContainer.view, transform: CATransform3DMakeScale(avatarExpandProgress, avatarExpandProgress, 1.0)) let titleText: String - if let peer = component.peer { + if let threadData = component.threadData { + titleText = threadData.data.info.title + } else if let peer = component.peer { titleText = peer.compactDisplayTitle } else { titleText = component.context.sharedContext.currentPresentationData.with({ $0 }).strings.Chat_NavigationNoChannels @@ -949,6 +1042,7 @@ final class OverscrollContentsComponent: Component { component: AnyComponent(AvatarComponent( context: component.context, peer: peer, + threadData: component.threadData, badge: (isFullyExpanded && component.unreadCount != 0) ? AvatarComponent.Badge(count: component.unreadCount, backgroundColor: component.backgroundColor, foregroundColor: component.foregroundColor) : nil, rect: avatarFrame.offsetBy(dx: self.avatarExtraScalingContainer.frame.midX + component.absoluteRect.minX, dy: self.avatarExtraScalingContainer.frame.midY + component.absoluteRect.minY), withinSize: component.absoluteSize, @@ -988,6 +1082,7 @@ public final class ChatOverscrollControl: CombinedComponent { let backgroundColor: UIColor let foregroundColor: UIColor let peer: EnginePeer? + let threadData: ChatOverscrollThreadData? let unreadCount: Int let location: TelegramEngine.NextUnreadChannelLocation let context: AccountContext @@ -1001,6 +1096,7 @@ public final class ChatOverscrollControl: CombinedComponent { backgroundColor: UIColor, foregroundColor: UIColor, peer: EnginePeer?, + threadData: ChatOverscrollThreadData?, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation, context: AccountContext, @@ -1013,6 +1109,7 @@ public final class ChatOverscrollControl: CombinedComponent { self.backgroundColor = backgroundColor self.foregroundColor = foregroundColor self.peer = peer + self.threadData = threadData self.unreadCount = unreadCount self.location = location self.context = context @@ -1033,6 +1130,9 @@ public final class ChatOverscrollControl: CombinedComponent { if lhs.peer != rhs.peer { return false } + if lhs.threadData != rhs.threadData { + return false + } if lhs.unreadCount != rhs.unreadCount { return false } @@ -1070,6 +1170,7 @@ public final class ChatOverscrollControl: CombinedComponent { backgroundColor: context.component.backgroundColor, foregroundColor: context.component.foregroundColor, peer: context.component.peer, + threadData: context.component.threadData, unreadCount: context.component.unreadCount, location: context.component.location, expandOffset: context.component.expandDistance, diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift new file mode 100644 index 0000000000..38210a39b0 --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -0,0 +1,4771 @@ +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 + +extension ChatControllerImpl { + func loadDisplayNodeImpl() { + 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.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 { + 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 + let chatLocationPeerId = self.chatLocation.peerId + + if let chatLocationPeerId = chatLocationPeerId { + hasPendingMessages = self.context.account.pendingMessageManager.hasPendingMessages + |> mapToSignal { peerIds -> Signal in + let value = peerIds.contains(chatLocationPeerId) + if value { + return .single(true) + } else { + return .single(false) + |> delay(0.1, queue: .mainQueue()) + } + } + |> distinctUntilChanged + } else { + hasPendingMessages = .single(false) + } + + let isTopReplyThreadMessageShown: Signal = self.chatDisplayNode.historyNode.isTopReplyThreadMessageShown.get() + |> distinctUntilChanged + + let topPinnedMessage: Signal + 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 = 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 + let forumTopicData: Signal + 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 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 + self.translationStateDisposable = (combineLatest( + queue: .concurrentDefaultQueue(), + isPremium, + isHidden, + ApplicationSpecificNotice.translationSuggestion(accountManager: self.context.sharedContext.accountManager) + ) |> mapToSignal { isPremium, isHidden, counterAndTimestamp -> Signal in + var maybeSuggestPremium = false + if counterAndTimestamp.0 >= 3 { + maybeSuggestPremium = true + } + if (isPremium || maybeSuggestPremium) && !isHidden { + return chatTranslationState(context: context, peerId: peerId) + |> 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) + }) + } + }) + } + + self.cachedDataDisposable = combineLatest(queue: .mainQueue(), self.chatDisplayNode.historyNode.cachedPeerDataAndMessages, + hasPendingMessages, + isTopReplyThreadMessageShown, + topPinnedMessage, + customEmojiAvailable, + isForum, + threadData, + forumTopicData + ).startStrict(next: { [weak self] cachedDataAndMessages, hasPendingMessages, isTopReplyThreadMessageShown, topPinnedMessage, customEmojiAvailable, isForum, threadData, forumTopicData 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? + var premiumGiftOptions: [CachedPremiumGiftOption] = [] + 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 + premiumGiftOptions = cachedData.premiumGiftOptions + } 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 + if case .replyThread = self.chatLocation { + effectiveCachedDataReady = self.cachedDataReady.get() + } else { + //effectiveCachedDataReady = .single(true) + effectiveCachedDataReady = self.cachedDataReady.get() + } + self.ready.set(combineLatest(queue: .mainQueue(), + self.chatDisplayNode.historyNode.historyState.get(), + self._chatLocationInfoReady.get(), + effectiveCachedDataReady, + initialData, + self.wallpaperReady.get(), + self.presentationReady.get() + ) + |> map { _, chatLocationInfoReady, cachedDataReady, _, wallpaperReady, presentationReady in + return chatLocationInfoReady && cachedDataReady && wallpaperReady && presentationReady + } + |> distinctUntilChanged) + + 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) { + 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.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") + + self?.chatDisplayNode.historyNode.layoutActionOnViewTransition = ({ [weak self] transition in + f() + if let strongSelf = self, let validLayout = strongSelf.validLayout { + var mappedTransition: (ChatHistoryListViewTransition, ListViewUpdateSizeAndInsets?)? + + let isScheduledMessages: Bool + if case .scheduledMessages = strongSelf.presentationInterfaceState.subject { + isScheduledMessages = true + } else { + isScheduledMessages = false + } + 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 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: [EnqueueMessage] + if let silentPosting = silentPosting { + transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: silentPosting) + } else if let scheduleTime = scheduleTime { + transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime) + } else { + transformedMessages = strongSelf.transformEnqueueMessages(messages) + } + + var forwardedMessages: [[EnqueueMessage]] = [] + var forwardSourcePeerIds = Set() + for message in transformedMessages { + if case let .forward(source, _, _, _, _) = message { + forwardSourcePeerIds.insert(source.peerId) + + var added = false + if var last = forwardedMessages.last { + if let currentMessage = last.first, case let .forward(currentSource, _, _, _, _) = currentMessage, currentSource.peerId == source.peerId { + last.append(message) + added = true + } + } + if !added { + forwardedMessages.append([message]) + } + } + } + + let signal: Signal<[MessageId?], NoError> + if forwardSourcePeerIds.count > 1 { + var signals: [Signal<[MessageId?], NoError>] = [] + for messagesGroup in forwardedMessages { + signals.append(enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: messagesGroup)) + } + signal = combineLatest(signals) + |> map { results in + var ids: [MessageId?] = [] + for result in results { + ids.append(contentsOf: result) + } + return ids + } + } else { + signal = enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: transformedMessages) + } + + let _ = (signal + |> deliverOnMainQueue).startStandalone(next: { messageIds in + if let strongSelf = self { + if case .scheduledMessages = strongSelf.presentationInterfaceState.subject { + } else { + strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() + } + } + }) + + donateSendMessageIntent(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, intentContext: .chat, peerIds: [peerId]) + } else if case let .customChatContents(customChatContents) = strongSelf.subject { + switch customChatContents.kind { + 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: "The message text limit is 4096 characters", 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) + } + } + + strongSelf.updateChatPresentationInterfaceState(interactive: true, { $0.updatedShowCommands(false) }) + } + + 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, case let .media(options) = editMessageState.content else { + return + } + var originalMediaReference: AnyMediaReference? + if let message = message { + for media in message.media { + if let image = media as? TelegramMediaImage { + originalMediaReference = .message(message: MessageReference(message._asMessage()), media: image) + } else if let file = media as? TelegramMediaFile { + if file.isVideo || file.isAnimated { + originalMediaReference = .message(message: MessageReference(message._asMessage()), media: file) + } + } + } + } + strongSelf.oldPresentAttachmentMenu(editMediaOptions: options, 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) + } + } + self.chatDisplayNode.updateTypingActivity = { [weak self] value in + if let strongSelf = self { + if value { + strongSelf.typingActivityPromise.set(Signal.single(true) + |> then( + Signal.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 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: + 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 { + 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 + } + if let messageId = messageId { + if canSendMessagesToChat(strongSelf.presentationInterfaceState) { + 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 + } + moveReplyMessageToAnotherChat(selfController: self, replySubject: replySubject) + }) + } + } else { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(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)) + } + + 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 reportReason = strongSelf.presentationInterfaceState.reportReason { + let presentationData = strongSelf.presentationData + let controller = ActionSheetController(presentationData: presentationData, allowInputInset: true) + let dismissAction: () -> Void = { [weak self, weak controller] in + self?.view.window?.endEditing(true) + controller?.dismissAnimated() + } + var message = "" + var items: [ActionSheetItem] = [] + items.append(ReportPeerHeaderActionSheetItem(context: strongSelf.context, text: presentationData.strings.Report_AdditionalDetailsText)) + items.append(ReportPeerDetailsActionSheetItem(context: strongSelf.context, theme: presentationData.theme, placeholderText: presentationData.strings.Report_AdditionalDetailsPlaceholder, textUpdated: { text in + message = text + })) + items.append(ActionSheetButtonItem(title: presentationData.strings.Report_Report, color: .accent, font: .bold, enabled: true, action: { + dismissAction() + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }, completion: { _ in + let _ = (strongSelf.context.engine.peers.reportPeerMessages(messageIds: Array(messageIds), reason: reportReason, 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) + }) + }) + })) + + controller.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + strongSelf.present(controller, in: .window(.root)) + } else { + strongSelf.present(peerReportOptionsController(context: strongSelf.context, subject: .messages(Array(messageIds).sorted()), passthrough: false, present: { c, a in + self?.present(c, in: .window(.root), with: a) + }, push: { c in + self?.push(c) + }, completion: { _, done in + if done { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + } + }), in: .window(.root)) + } + } + }, reportMessages: { [weak self] messages, contextController in + if let strongSelf = self, !messages.isEmpty { + let options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .illegalDrugs, .personalDetails, .other] + presentPeerReportOptions(context: strongSelf.context, parent: strongSelf, contextController: contextController, subject: .messages(messages.map({ $0.id }).sorted()), options: options, completion: { _, _ in }) + } + }, blockMessageAuthor: { [weak self] message, contextController in + contextController?.dismiss(completion: { + guard let strongSelf = self else { + return + } + + let author = message.forwardInfo?.author + + guard let peer = author else { + return + } + + let presentationData = strongSelf.presentationData + let controller = ActionSheetController(presentationData: presentationData) + let dismissAction: () -> Void = { [weak controller] in + controller?.dismissAnimated() + } + var reportSpam = true + var items: [ActionSheetItem] = [] + items.append(ActionSheetTextItem(title: presentationData.strings.UserInfo_BlockConfirmationTitle(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 { + 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] in + if let strongSelf = self { + strongSelf.commitPurposefulAction() + if let forwardMessageIdsSet = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds { + let forwardMessageIds = Array(forwardMessageIdsSet).sorted() + strongSelf.forwardMessages(messageIds: forwardMessageIds) + } + } + }, forwardCurrentForwardMessages: { [weak self] in + if let strongSelf = self { + strongSelf.commitPurposefulAction() + if let forwardMessageIds = strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds { + strongSelf.forwardMessages(messageIds: forwardMessageIds, options: strongSelf.presentationInterfaceState.interfaceState.forwardOptionsState, resetCurrent: true) + } + } + }, forwardMessages: { [weak self] messages in + if let strongSelf = self, !messages.isEmpty { + strongSelf.commitPurposefulAction() + let forwardMessageIds = messages.map { $0.id }.sorted() + strongSelf.forwardMessages(messageIds: forwardMessageIds) + } + }, 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 + 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) + } + } + + let text = trimChatInputText(convertMarkdownToAttributes(editMessage.inputState.inputText)) + + let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text)) + var entitiesAttribute: TextEntitiesMessageAttribute? + if !entities.isEmpty { + entitiesAttribute = TextEntitiesMessageAttribute(entities: entities) + } + + 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, 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)) + }) + 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, recommendedChannels: false)) + }, 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.enqueueChatContextResult(results, result) + return true + }, sendBotCommand: { [weak self] botPeer, command in + if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) { + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { + let messageText: String + if let addressName = botPeer.addressName { + if peer is TelegramUser { + messageText = command + } else { + messageText = command + "@" + addressName + } + } else { + messageText = command + } + let 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).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).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).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 = 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?.sendMediaRecording(silentPosting: silentPosting, viewOnce: viewOnce) + }, 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 { + 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 = 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 = 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 + 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 { 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 + 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? + 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, apply: { [weak self] link in + if let strongSelf = self, let inputMode = inputMode, let selectionRange = selectionRange { + if let link = link { + strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in + return (chatTextInputAddLinkAttribute(current, selectionRange: selectionRange, url: link), inputMode) + } + } else { + + } + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { + return $0.updatedInputMode({ _ in return inputMode }).updatedInterfaceState({ + $0.withUpdatedEffectiveInputState(ChatTextInputState(inputText: $0.effectiveInputState.inputText, selectionRange: selectionRange.endIndex ..< selectionRange.endIndex)) + }) + }) + } + }) + strongSelf.present(controller, in: .window(.root)) + + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { $0.updatedInputMode({ _ in return .none }) }) + } + }, reportPeerIrrelevantGeoLocation: { [weak self] in + guard let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation else { + return + } + + strongSelf.chatDisplayNode.dismissInput() + + let actions = [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.ReportGroupLocation_Report, action: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.reportIrrelvantGeoDisposable = (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) } + 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)) + 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) + } + }, 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 { + 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, 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 { + isBot = true + break + } + } + let type: PeerType + if isBot { + type = .bot + } else if let user = peer as? TelegramUser { + if user.botInfo != nil { + 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 strongSelf = self, let peerId = strongSelf.chatLocation.peerId else { + return + } + let _ = (updateChatTranslationStateInteractively(engine: strongSelf.context.engine, peerId: peerId, { current in + return current?.withIsEnabled(type == .translated) + }) + |> deliverOnMainQueue).startStandalone(completed: { [weak self] in + if let strongSelf = self, type == .translated { + Queue.mainQueue().after(0.15) { + strongSelf.chatDisplayNode.historyNode.refreshPollActionsForVisibleMessages() + } + } + }) + }, changeTranslationLanguage: { [weak self] langCode in + guard let strongSelf = self, let peerId = strongSelf.chatLocation.peerId else { + return + } + var langCode = langCode + if langCode == "nb" { + langCode = "no" + } else if langCode == "pt-br" { + langCode = "pt" + } + let _ = updateChatTranslationStateInteractively(engine: strongSelf.context.engine, peerId: peerId, { current in + return current?.withToLang(langCode).withIsEnabled(true) + }).startStandalone() + }, addDoNotTranslateLanguage: { [weak self] langCode in + guard let strongSelf = self, let peerId = strongSelf.chatLocation.peerId else { + return + } + let _ = updateTranslationSettingsInteractively(accountManager: strongSelf.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() + ignoredLanguages.insert(strongSelf.presentationData.strings.baseLanguageCode) + for language in systemLanguageCodes() { + ignoredLanguages.insert(language) + } + ignoredLanguages.insert(langCode) + updated.ignoredLanguages = Array(ignoredLanguages) + } + return updated + }).startStandalone() + let _ = updateChatTranslationStateInteractively(engine: strongSelf.context.engine, peerId: peerId, { current in + return nil + }).startStandalone() + + let presentationData = strongSelf.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) ?? "" + + strongSelf.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 strongSelf = self { + let controller = translationSettingsController(context: strongSelf.context) + controller.navigationPresentation = .modal + strongSelf.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 strongSelf = self, let peerId = strongSelf.chatLocation.peerId else { + return + } + strongSelf.presentAttachmentMenu(subject: .gift) + Queue.mainQueue().after(0.5) { + let _ = ApplicationSpecificNotice.incrementDismissedPremiumGiftSuggestion(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId, timestamp: Int32(Date().timeIntervalSince1970)).startStandalone() + } + }, openPremiumRequiredForMessaging: { [weak self] in + guard let self else { + return + } + let controller = PremiumIntroScreen(context: self.context, source: .settings) + self.push(controller) + }, openBoostToUnrestrict: { [weak self] in + guard let self, 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 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] namespace, silent in + if let strongSelf = self { + let inAppNotificationSettings = strongSelf.context.sharedContext.currentInAppNotificationSettings.with { $0 } + if inAppNotificationSettings.playSounds && !silent { + serviceSoundManager.playMessageDeliveredSound() + } + if strongSelf.presentationInterfaceState.subject != .scheduledMessages && namespace == Namespaces.Message.ScheduledCloud { + strongSelf.openScheduledMessages() + } + + if strongSelf.shouldDisplayChecksTooltip { + Queue.mainQueue().after(1.0) { + strongSelf.displayChecksTooltip() + } + strongSelf.shouldDisplayChecksTooltip = false + strongSelf.checksTooltipDisposable.set(strongSelf.context.engine.notices.dismissServerProvidedSuggestion(suggestion: .newcomerTicks).startStrict()) + } + } + })) + + 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 + 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 in + if hasVisiblePlayableItemNodes && !isPlaybackActive && !hasActiveCalls { + return Signal { [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) + }) + + self.displayNodeDidLoad() + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift new file mode 100644 index 0000000000..611e1b9896 --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift @@ -0,0 +1,548 @@ +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 + +extension ChatControllerImpl { + func requestAudioRecorder(beginWithTone: Bool) { + if self.audioRecorderValue == nil { + if self.recorderFeedback == nil { + self.recorderFeedback = HapticFeedback() + self.recorderFeedback?.prepareImpact(.light) + } + + self.audioRecorder.set(self.context.sharedContext.mediaManager.audioRecorder(beginWithTone: beginWithTone, applicationBindings: self.context.sharedContext.applicationBindings, beganWithTone: { _ in + })) + } + } + + func requestVideoRecorder() { + if self.videoRecorderValue == nil { + if let currentInputPanelFrame = self.chatDisplayNode.currentInputPanelFrame() { + if self.recorderFeedback == nil { + self.recorderFeedback = HapticFeedback() + self.recorderFeedback?.prepareImpact(.light) + } + + var isScheduledMessages = false + if case .scheduledMessages = self.presentationInterfaceState.subject { + isScheduledMessages = true + } + + var isBot = false + + var allowLiveUpload = false + var viewOnceAvailable = false + if let peerId = self.chatLocation.peerId { + allowLiveUpload = peerId.namespace != Namespaces.Peer.SecretChat + viewOnceAvailable = !isScheduledMessages && peerId.namespace == Namespaces.Peer.CloudUser && peerId != self.context.account.peerId && !isBot + } else if case .customChatContents = self.chatLocation { + allowLiveUpload = true + } + + if let user = self.presentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil { + isBot = true + } + + let controller = VideoMessageCameraScreen( + context: self.context, + updatedPresentationData: self.updatedPresentationData, + allowLiveUpload: allowLiveUpload, + viewOnceAvailable: viewOnceAvailable, + inputPanelFrame: (currentInputPanelFrame, self.chatDisplayNode.inputNode != nil), + chatNode: self.chatDisplayNode.historyNode, + completion: { [weak self] message, silentPosting, scheduleTime in + guard let self, let videoController = self.videoRecorderValue else { + return + } + + guard var message else { + self.recorderFeedback?.error() + self.recorderFeedback = nil + self.videoRecorder.set(.single(nil)) + return + } + + let replyMessageSubject = self.presentationInterfaceState.interfaceState.replyMessageSubject + let correlationId = Int64.random(in: 0 ..< Int64.max) + message = message + .withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) + .withUpdatedCorrelationId(correlationId) + + var usedCorrelationId = false + if scheduleTime == nil, self.chatDisplayNode.shouldAnimateMessageTransition, let extractedView = videoController.extractVideoSnapshot() { + usedCorrelationId = true + self.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .videoMessage(ChatMessageTransitionNodeImpl.Source.VideoMessage(view: extractedView)), initiated: { [weak videoController, weak self] in + videoController?.hideVideoSnapshot() + guard let self else { + return + } + self.videoRecorder.set(.single(nil)) + }) + } else { + self.videoRecorder.set(.single(nil)) + } + + self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in + if let self { + self.chatDisplayNode.collapseInput() + + self.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedMediaDraftState(nil) } + }) + } + }, usedCorrelationId ? correlationId : nil) + + let messages = [message] + let transformedMessages: [EnqueueMessage] + if let silentPosting { + transformedMessages = self.transformEnqueueMessages(messages, silentPosting: silentPosting) + } else if let scheduleTime { + transformedMessages = self.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime) + } else { + transformedMessages = self.transformEnqueueMessages(messages) + } + + self.sendMessages(transformedMessages) + } + ) + controller.onResume = { [weak self] in + guard let self else { + return + } + self.resumeMediaRecorder() + } + self.videoRecorder.set(.single(controller)) + } + } + } + + func dismissMediaRecorder(_ action: ChatFinishMediaRecordingAction) { + var updatedAction = action + var isScheduledMessages = false + if case .scheduledMessages = self.presentationInterfaceState.subject { + isScheduledMessages = true + } + + if let _ = self.presentationInterfaceState.slowmodeState, !isScheduledMessages { + updatedAction = .preview + } + + if let audioRecorderValue = self.audioRecorderValue { + switch action { + case .pause: + audioRecorderValue.pause() + default: + audioRecorderValue.stop() + } + + switch updatedAction { + case .dismiss: + self.recorderDataDisposable.set(nil) + self.chatDisplayNode.updateRecordedMediaDeleted(true) + self.audioRecorder.set(.single(nil)) + case .preview, .pause: + if case .preview = updatedAction { + self.audioRecorder.set(.single(nil)) + } + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { + $0.updatedInputTextPanelState { panelState in + return panelState.withUpdatedMediaRecordingState(.waitingForPreview) + } + }) + self.recorderDataDisposable.set((audioRecorderValue.takenRecordedData() + |> deliverOnMainQueue).startStrict(next: { [weak self] data in + if let strongSelf = self, let data = data { + if data.duration < 0.5 { + strongSelf.recorderFeedback?.error() + strongSelf.recorderFeedback = nil + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + $0.updatedInputTextPanelState { panelState in + return panelState.withUpdatedMediaRecordingState(nil) + } + }) + strongSelf.recorderDataDisposable.set(nil) + } else if let waveform = data.waveform { + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max), size: Int64(data.compressedData.count)) + + strongSelf.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data.compressedData) + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + $0.updatedInterfaceState { $0.withUpdatedMediaDraftState(.audio(ChatInterfaceMediaDraftState.Audio(resource: resource, fileSize: Int32(data.compressedData.count), duration: Int32(data.duration), waveform: AudioWaveform(bitstream: waveform, bitsPerSample: 5)))) }.updatedInputTextPanelState { panelState in + return panelState.withUpdatedMediaRecordingState(nil) + } + }) + strongSelf.recorderFeedback = nil + strongSelf.updateDownButtonVisibility() + strongSelf.recorderDataDisposable.set(nil) + } + } + })) + case let .send(viewOnce): + self.chatDisplayNode.updateRecordedMediaDeleted(false) + self.recorderDataDisposable.set((audioRecorderValue.takenRecordedData() + |> deliverOnMainQueue).startStrict(next: { [weak self] data in + if let strongSelf = self, let data = data { + if data.duration < 0.5 { + strongSelf.recorderFeedback?.error() + strongSelf.recorderFeedback = nil + strongSelf.audioRecorder.set(.single(nil)) + } else { + let randomId = Int64.random(in: Int64.min ... Int64.max) + + let resource = LocalFileMediaResource(fileId: randomId) + strongSelf.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data.compressedData) + + let waveformBuffer: Data? = data.waveform + + let correlationId = Int64.random(in: 0 ..< Int64.max) + var usedCorrelationId = false + + if strongSelf.chatDisplayNode.shouldAnimateMessageTransition, let textInputPanelNode = strongSelf.chatDisplayNode.textInputPanelNode, let micButton = textInputPanelNode.micButton { + usedCorrelationId = true + strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .audioMicInput(ChatMessageTransitionNodeImpl.Source.AudioMicInput(micButton: micButton)), initiated: { + guard let strongSelf = self else { + return + } + strongSelf.audioRecorder.set(.single(nil)) + }) + } else { + strongSelf.audioRecorder.set(.single(nil)) + } + + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.chatDisplayNode.collapseInput() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } + }) + } + }, usedCorrelationId ? correlationId : nil) + + var attributes: [MessageAttribute] = [] + if viewOnce { + attributes.append(AutoremoveTimeoutMessageAttribute(timeout: viewOnceTimeout, countdownBeginTime: nil)) + } + + strongSelf.sendMessages([.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)])), threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])]) + + strongSelf.recorderFeedback?.tap() + strongSelf.recorderFeedback = nil + strongSelf.recorderDataDisposable.set(nil) + } + } + })) + } + } else if let videoRecorderValue = self.videoRecorderValue { + if case .send = updatedAction { + self.chatDisplayNode.updateRecordedMediaDeleted(false) + videoRecorderValue.sendVideoRecording() + self.recorderDataDisposable.set(nil) + } else { + if case .dismiss = updatedAction { + self.chatDisplayNode.updateRecordedMediaDeleted(true) + self.recorderDataDisposable.set(nil) + } + + switch updatedAction { + case .preview, .pause: + if videoRecorderValue.stopVideoRecording() { + self.recorderDataDisposable.set((videoRecorderValue.takenRecordedData() + |> deliverOnMainQueue).startStrict(next: { [weak self] data in + if let strongSelf = self, let data = data { + if data.duration < 1.0 { + strongSelf.recorderFeedback?.error() + strongSelf.recorderFeedback = nil + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + $0.updatedInputTextPanelState { panelState in + return panelState.withUpdatedMediaRecordingState(nil) + } + }) + strongSelf.recorderDataDisposable.set(nil) + strongSelf.videoRecorder.set(.single(nil)) + } else { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + $0.updatedInterfaceState { + $0.withUpdatedMediaDraftState(.video( + ChatInterfaceMediaDraftState.Video( + duration: Int32(data.duration), + frames: data.frames, + framesUpdateTimestamp: data.framesUpdateTimestamp, + trimRange: data.trimRange + ) + )) + }.updatedInputTextPanelState { panelState in + return panelState.withUpdatedMediaRecordingState(nil) + } + }) + strongSelf.recorderFeedback = nil + strongSelf.updateDownButtonVisibility() + } + } + })) + } + default: + self.recorderDataDisposable.set(nil) + self.videoRecorder.set(.single(nil)) + } + } + } + } + + func stopMediaRecorder(pause: Bool = false) { + if let audioRecorderValue = self.audioRecorderValue { + if let _ = self.presentationInterfaceState.inputTextPanelState.mediaRecordingState { + self.dismissMediaRecorder(pause ? .pause : .preview) + } else { + audioRecorderValue.stop() + self.audioRecorder.set(.single(nil)) + } + } else if let _ = self.videoRecorderValue { + if let _ = self.presentationInterfaceState.inputTextPanelState.mediaRecordingState { + self.dismissMediaRecorder(pause ? .pause : .preview) + } else { + self.videoRecorder.set(.single(nil)) + } + } + } + + func resumeMediaRecorder() { + self.context.sharedContext.mediaManager.playlistControl(.playback(.pause), type: nil) + + if let audioRecorderValue = self.audioRecorderValue { + audioRecorderValue.resume() + + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { + $0.updatedInputTextPanelState { panelState in + return panelState.withUpdatedMediaRecordingState(.audio(recorder: audioRecorderValue, isLocked: true)) + }.updatedInterfaceState { $0.withUpdatedMediaDraftState(nil) } + }) + } else if let videoRecorderValue = self.videoRecorderValue { + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { + $0.updatedInputTextPanelState { panelState in + let recordingStatus = videoRecorderValue.recordingStatus + return panelState.withUpdatedMediaRecordingState(.video(status: .recording(InstantVideoControllerRecordingStatus(micLevel: recordingStatus.micLevel, duration: recordingStatus.duration)), isLocked: true)) + }.updatedInterfaceState { $0.withUpdatedMediaDraftState(nil) } + }) + } + } + + func lockMediaRecorder() { + if self.presentationInterfaceState.inputTextPanelState.mediaRecordingState != nil { + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedInputTextPanelState { panelState in + return panelState.withUpdatedMediaRecordingState(panelState.mediaRecordingState?.withLocked(true)) + } + }) + } + + self.videoRecorderValue?.lockVideoRecording() + } + + func deleteMediaRecording() { + if let _ = self.audioRecorderValue { + self.audioRecorder.set(.single(nil)) + } else if let _ = self.videoRecorderValue { + self.videoRecorder.set(.single(nil)) + } + + self.recorderDataDisposable.set(nil) + self.chatDisplayNode.updateRecordedMediaDeleted(true) + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { + $0.updatedInterfaceState { $0.withUpdatedMediaDraftState(nil) } + }) + self.updateDownButtonVisibility() + } + + func sendMediaRecording(silentPosting: Bool? = nil, scheduleTime: Int32? = nil, viewOnce: Bool = false) { + self.chatDisplayNode.updateRecordedMediaDeleted(false) + + guard let recordedMediaPreview = self.presentationInterfaceState.interfaceState.mediaDraftState else { + return + } + + switch recordedMediaPreview { + case let .audio(audio): + self.audioRecorder.set(.single(nil)) + + var isScheduledMessages = false + if case .scheduledMessages = self.presentationInterfaceState.subject { + isScheduledMessages = true + } + + if let _ = self.presentationInterfaceState.slowmodeState, !isScheduledMessages { + if let rect = self.chatDisplayNode.frameForInputActionButton() { + self.interfaceInteraction?.displaySlowmodeTooltip(self.chatDisplayNode.view, rect) + } + return + } + + let waveformBuffer = audio.waveform.makeBitstream() + + self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in + if let strongSelf = self { + strongSelf.chatDisplayNode.collapseInput() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedMediaDraftState(nil) } + }) + + strongSelf.updateDownButtonVisibility() + } + }, nil) + + var attributes: [MessageAttribute] = [] + if viewOnce { + attributes.append(AutoremoveTimeoutMessageAttribute(timeout: viewOnceTimeout, countdownBeginTime: nil)) + } + + let messages: [EnqueueMessage] = [.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: audio.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(audio.fileSize), attributes: [.Audio(isVoice: true, duration: Int(audio.duration), title: nil, performer: nil, waveform: waveformBuffer)])), threadId: self.chatLocation.threadId, replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] + + let transformedMessages: [EnqueueMessage] + if let silentPosting = silentPosting { + transformedMessages = self.transformEnqueueMessages(messages, silentPosting: silentPosting) + } else if let scheduleTime = scheduleTime { + transformedMessages = self.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime) + } else { + transformedMessages = self.transformEnqueueMessages(messages) + } + + guard let peerId = self.chatLocation.peerId else { + return + } + + let _ = (enqueueMessages(account: self.context.account, peerId: peerId, messages: transformedMessages) + |> deliverOnMainQueue).startStandalone(next: { [weak self] _ in + if let strongSelf = self, strongSelf.presentationInterfaceState.subject != .scheduledMessages { + strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() + } + }) + + donateSendMessageIntent(account: self.context.account, sharedContext: self.context.sharedContext, intentContext: .chat, peerIds: [peerId]) + case .video: + self.videoRecorderValue?.sendVideoRecording(silentPosting: silentPosting, scheduleTime: scheduleTime) + } + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift new file mode 100644 index 0000000000..ba2854682a --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift @@ -0,0 +1,639 @@ +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 + +extension ChatControllerImpl { + func navigationButtonAction(_ action: ChatNavigationButtonAction) { + switch action { + case .spacer, .toggleInfoPanel: + break + case .cancelMessageSelection: + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + case .clearHistory: + if case let .peer(peerId) = self.chatLocation { + let beginClear: (InteractiveHistoryClearingType) -> Void = { [weak self] type in + self?.beginClearHistory(type: type) + } + + let context = self.context + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.ParticipantCount(id: peerId), + TelegramEngine.EngineData.Item.Peer.CanDeleteHistory(id: peerId) + ) + |> map { participantCount, canDeleteHistory -> (isLargeGroupOrChannel: Bool, canClearChannel: Bool) in + if let participantCount = participantCount { + return (participantCount > 1000, canDeleteHistory) + } else { + return (false, false) + } + } + |> deliverOnMainQueue).startStandalone(next: { [weak self] parameters in + guard let strongSelf = self else { + return + } + + let (isLargeGroupOrChannel, canClearChannel) = parameters + + guard let peer = strongSelf.presentationInterfaceState.renderedPeer, let chatPeer = peer.peers[peer.peerId], let mainPeer = peer.chatMainPeer else { + return + } + + enum ClearType { + case savedMessages + case secretChat + case group + case channel + case user + } + + let canClearCache: Bool + let canClearForMyself: ClearType? + let canClearForEveryone: ClearType? + + if peerId == strongSelf.context.account.peerId { + canClearCache = false + canClearForMyself = .savedMessages + canClearForEveryone = nil + } else if chatPeer is TelegramSecretChat { + canClearCache = false + canClearForMyself = .secretChat + canClearForEveryone = nil + } else if let group = chatPeer as? TelegramGroup { + canClearCache = false + + switch group.role { + case .creator: + canClearForMyself = .group + canClearForEveryone = nil + case .admin, .member: + canClearForMyself = .group + canClearForEveryone = nil + } + } else if let channel = chatPeer as? TelegramChannel { + if let username = channel.addressName, !username.isEmpty { + if isLargeGroupOrChannel { + canClearCache = true + canClearForMyself = nil + canClearForEveryone = canClearChannel ? .channel : nil + } else { + canClearCache = true + canClearForMyself = nil + + switch channel.info { + case .broadcast: + if channel.flags.contains(.isCreator) { + canClearForEveryone = canClearChannel ? .channel : nil + } else { + canClearForEveryone = canClearChannel ? .channel : nil + } + case .group: + if channel.flags.contains(.isCreator) { + canClearForEveryone = canClearChannel ? .channel : nil + } else { + canClearForEveryone = canClearChannel ? .channel : nil + } + } + } + } else { + if isLargeGroupOrChannel { + switch channel.info { + case .broadcast: + canClearCache = true + + canClearForMyself = .channel + canClearForEveryone = nil + case .group: + canClearCache = false + + canClearForMyself = .channel + canClearForEveryone = nil + } + } else { + switch channel.info { + case .broadcast: + canClearCache = true + + if channel.flags.contains(.isCreator) { + canClearForMyself = .channel + canClearForEveryone = nil + } else { + canClearForMyself = .channel + canClearForEveryone = nil + } + case .group: + canClearCache = false + + if channel.flags.contains(.isCreator) { + canClearForMyself = .group + canClearForEveryone = nil + } else { + canClearForMyself = .group + canClearForEveryone = nil + } + } + } + } + } else { + canClearCache = false + canClearForMyself = .user + + if let user = chatPeer as? TelegramUser, user.botInfo != nil { + canClearForEveryone = nil + } else { + canClearForEveryone = .user + } + } + + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) + var items: [ActionSheetItem] = [] + + if case .scheduledMessages = strongSelf.presentationInterfaceState.subject { + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ScheduledMessages_ClearAllConfirmation, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + + guard let strongSelf = self else { + return + } + + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + }), + TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationAction, action: { + beginClear(.scheduledMessages) + }) + ], parseMarkdown: true), in: .window(.root)) + })) + } else { + if let _ = canClearForMyself ?? canClearForEveryone { + items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: EnginePeer(mainPeer), chatPeer: EnginePeer(chatPeer), action: .clearHistory(canClearCache: canClearCache), strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder)) + + if let canClearForEveryone = canClearForEveryone { + let text: String + let confirmationText: String + switch canClearForEveryone { + case .user: + text = strongSelf.presentationData.strings.ChatList_DeleteForEveryone(EnginePeer(mainPeer).compactDisplayTitle).string + confirmationText = strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationText + default: + text = strongSelf.presentationData.strings.Conversation_DeleteMessagesForEveryone + confirmationText = strongSelf.presentationData.strings.ChatList_DeleteForAllMembersConfirmationText + } + items.append(ActionSheetButtonItem(title: text, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + + guard let strongSelf = self else { + return + } + + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: confirmationText, actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + }), + TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationAction, action: { + beginClear(.forEveryone) + }) + ], parseMarkdown: true), in: .window(.root)) + })) + } + if let canClearForMyself = canClearForMyself { + let text: String + switch canClearForMyself { + case .savedMessages, .secretChat: + text = strongSelf.presentationData.strings.Conversation_ClearAll + default: + text = strongSelf.presentationData.strings.ChatList_DeleteForCurrentUser + } + items.append(ActionSheetButtonItem(title: text, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + if mainPeer.id == context.account.peerId, let strongSelf = self { + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + }), + TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationAction, action: { + beginClear(.forLocalPeer) + }) + ], parseMarkdown: true), in: .window(.root)) + } else { + beginClear(.forLocalPeer) + } + })) + } + } + + if canClearCache { + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_ClearCache, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + + guard let strongSelf = self else { + return + } + + strongSelf.navigationButtonAction(.clearCache) + })) + } + + if chatPeer.canSetupAutoremoveTimeout(accountPeerId: strongSelf.context.account.peerId) { + items.append(ActionSheetButtonItem(title: strongSelf.presentationInterfaceState.autoremoveTimeout == nil ? strongSelf.presentationData.strings.Conversation_AutoremoveActionEnable : strongSelf.presentationData.strings.Conversation_AutoremoveActionEdit, color: .accent, action: { [weak actionSheet] in + guard let actionSheet = actionSheet else { + return + } + guard let strongSelf = self else { + return + } + + actionSheet.dismissAnimated() + + strongSelf.presentAutoremoveSetup() + })) + } + } + + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + + strongSelf.chatDisplayNode.dismissInput() + strongSelf.present(actionSheet, in: .window(.root)) + }) + } + case let .openChatInfo(expandAvatar, recommendedChannels): + let _ = self.presentVoiceMessageDiscardAlert(action: { + switch self.chatLocationInfoData { + case let .peer(peerView): + self.navigationActionDisposable.set((peerView.get() + |> take(1) + |> deliverOnMainQueue).startStrict(next: { [weak self] peerView in + if let strongSelf = self, let peer = peerView.peers[peerView.peerId], peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil && !strongSelf.presentationInterfaceState.isNotAccessible { + if peer.id == strongSelf.context.account.peerId { + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer, let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: true, requestsContext: nil) { + strongSelf.effectiveNavigationController?.pushViewController(infoController) + } + } else { + var expandAvatar = expandAvatar + if peer.smallProfileImage == nil { + expandAvatar = false + } + if let validLayout = strongSelf.validLayout, validLayout.deviceMetrics.type == .tablet { + expandAvatar = false + } + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer, mode: recommendedChannels ? .recommendedChannels : .generic, avatarInitiallyExpanded: expandAvatar, fromChat: true, requestsContext: strongSelf.inviteRequestsContext) { + strongSelf.effectiveNavigationController?.pushViewController(infoController) + } + } + } + })) + case .replyThread: + if let peer = self.presentationInterfaceState.renderedPeer?.peer, case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.peerId == self.context.account.peerId { + if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: peer, mode: .forumTopic(thread: replyThreadMessage), avatarInitiallyExpanded: false, fromChat: true, requestsContext: nil) { + self.effectiveNavigationController?.pushViewController(infoController) + } + } else if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum), case let .replyThread(message) = self.chatLocation { + if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: channel, mode: .forumTopic(thread: message), avatarInitiallyExpanded: false, fromChat: true, requestsContext: self.inviteRequestsContext) { + self.effectiveNavigationController?.pushViewController(infoController) + } + } + case .customChatContents: + break + } + }) + case .search: + self.interfaceInteraction?.beginMessageSearch(.everything, "") + case .dismiss: + if self.attemptNavigation({}) { + self.dismiss() + } + case .clearCache: + let controller = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: nil)) + self.present(controller, in: .window(.root)) + + let disposable: MetaDisposable + if let currentDisposable = self.clearCacheDisposable { + disposable = currentDisposable + } else { + disposable = MetaDisposable() + self.clearCacheDisposable = disposable + } + + switch self.chatLocationInfoData { + case let .peer(peerView): + self.navigationActionDisposable.set((peerView.get() + |> take(1) + |> deliverOnMainQueue).startStrict(next: { [weak self] peerView in + guard let strongSelf = self, let peer = peerView.peers[peerView.peerId] else { + return + } + let peerId = peer.id + + let _ = (strongSelf.context.engine.resources.collectCacheUsageStats(peerId: peer.id) + |> deliverOnMainQueue).startStandalone(next: { [weak self, weak controller] result in + controller?.dismiss() + + guard let strongSelf = self, case let .result(stats) = result, let categories = stats.media[peer.id] else { + return + } + let presentationData = strongSelf.presentationData + let controller = ActionSheetController(presentationData: presentationData) + let dismissAction: () -> Void = { [weak controller] in + controller?.dismissAnimated() + } + + var sizeIndex: [PeerCacheUsageCategory: (Bool, Int64)] = [:] + + var itemIndex = 1 + + var selectedSize: Int64 = 0 + let updateTotalSize: () -> Void = { [weak controller] in + controller?.updateItem(groupIndex: 0, itemIndex: itemIndex, { item in + let title: String + let filteredSize = sizeIndex.values.reduce(0, { $0 + ($1.0 ? $1.1 : 0) }) + selectedSize = filteredSize + + if filteredSize == 0 { + title = presentationData.strings.Cache_ClearNone + } else { + title = presentationData.strings.Cache_Clear("\(dataSizeString(filteredSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))").string + } + + if let item = item as? ActionSheetButtonItem { + return ActionSheetButtonItem(title: title, color: filteredSize != 0 ? .accent : .disabled, enabled: filteredSize != 0, action: item.action) + } + return item + }) + } + + let toggleCheck: (PeerCacheUsageCategory, Int) -> Void = { [weak controller] category, itemIndex in + if let (value, size) = sizeIndex[category] { + sizeIndex[category] = (!value, size) + } + controller?.updateItem(groupIndex: 0, itemIndex: itemIndex, { item in + if let item = item as? ActionSheetCheckboxItem { + return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action) + } + return item + }) + updateTotalSize() + } + var items: [ActionSheetItem] = [] + + items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: EnginePeer(peer), chatPeer: EnginePeer(peer), action: .clearCache, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder)) + + let validCategories: [PeerCacheUsageCategory] = [.image, .video, .audio, .file] + + var totalSize: Int64 = 0 + + func stringForCategory(strings: PresentationStrings, category: PeerCacheUsageCategory) -> String { + switch category { + case .image: + return strings.Cache_Photos + case .video: + return strings.Cache_Videos + case .audio: + return strings.Cache_Music + case .file: + return strings.Cache_Files + } + } + + for categoryId in validCategories { + if let media = categories[categoryId] { + var categorySize: Int64 = 0 + for (_, size) in media { + categorySize += size + } + sizeIndex[categoryId] = (true, categorySize) + totalSize += categorySize + if categorySize > 1024 { + let index = itemIndex + items.append(ActionSheetCheckboxItem(title: stringForCategory(strings: presentationData.strings, category: categoryId), label: dataSizeString(categorySize, formatting: DataSizeStringFormatting(presentationData: presentationData)), value: true, action: { value in + toggleCheck(categoryId, index) + })) + itemIndex += 1 + } + } + } + selectedSize = totalSize + + if items.isEmpty { + strongSelf.presentClearCacheSuggestion() + } else { + items.append(ActionSheetButtonItem(title: presentationData.strings.Cache_Clear("\(dataSizeString(totalSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))").string, action: { + let clearCategories = sizeIndex.keys.filter({ sizeIndex[$0]!.0 }) + var clearMediaIds = Set() + + var media = stats.media + if var categories = media[peerId] { + for category in clearCategories { + if let contents = categories[category] { + for (mediaId, _) in contents { + clearMediaIds.insert(mediaId) + } + } + categories.removeValue(forKey: category) + } + + media[peerId] = categories + } + + var clearResourceIds = Set() + for id in clearMediaIds { + if let ids = stats.mediaResourceIds[id] { + for resourceId in ids { + clearResourceIds.insert(resourceId) + } + } + } + + var signal = strongSelf.context.engine.resources.clearCachedMediaResources(mediaResourceIds: clearResourceIds) + + var cancelImpl: (() -> Void)? + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + let progressSignal = Signal { subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.startStrict() + + signal = signal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + cancelImpl = { + disposable.set(nil) + } + disposable.set((signal + |> deliverOnMainQueue).startStrict(completed: { [weak self] in + if let strongSelf = self, let _ = strongSelf.validLayout { + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))", stringForDeviceType()).string, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false }), in: .current) + } + })) + + dismissAction() + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) + })) + + items.append(ActionSheetButtonItem(title: presentationData.strings.ClearCache_StorageUsage, action: { [weak self] in + dismissAction() + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) + + if let strongSelf = self { + let context = strongSelf.context + let controller = StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { category in + return storageUsageExceptionsScreen(context: context, category: category) + }) + strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + })) + + controller.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + strongSelf.chatDisplayNode.dismissInput() + strongSelf.present(controller, in: .window(.root)) + } + }) + })) + case .replyThread: + break + case .customChatContents: + break + } + case .edit: + self.editChat() + } + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift index 448e33897c..a5c4f4b52e 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift @@ -453,3 +453,64 @@ extension ChatControllerImpl { } } } + +final class ChatContextControllerContentSourceImpl: ContextControllerContentSource { + let controller: ViewController + weak var sourceNode: ASDisplayNode? + weak var sourceView: UIView? + let sourceRect: CGRect? + + let navigationController: NavigationController? = nil + + let passthroughTouches: Bool + + init(controller: ViewController, sourceNode: ASDisplayNode?, sourceRect: CGRect? = nil, passthroughTouches: Bool) { + self.controller = controller + self.sourceNode = sourceNode + self.sourceRect = sourceRect + self.passthroughTouches = passthroughTouches + } + + init(controller: ViewController, sourceView: UIView?, sourceRect: CGRect? = nil, passthroughTouches: Bool) { + self.controller = controller + self.sourceView = sourceView + self.sourceRect = sourceRect + self.passthroughTouches = passthroughTouches + } + + func transitionInfo() -> ContextControllerTakeControllerInfo? { + let sourceView = self.sourceView + let sourceNode = self.sourceNode + let sourceRect = self.sourceRect + return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in + if let sourceView = sourceView { + return (sourceView, sourceRect ?? sourceView.bounds) + } else if let sourceNode = sourceNode { + return (sourceNode.view, sourceRect ?? sourceNode.bounds) + } else { + return nil + } + }) + } + + func animatedIn() { + } +} + +final class ChatControllerContextReferenceContentSource: ContextReferenceContentSource { + let controller: ViewController + let sourceView: UIView + let insets: UIEdgeInsets + let contentInsets: UIEdgeInsets + + init(controller: ViewController, sourceView: UIView, insets: UIEdgeInsets, contentInsets: UIEdgeInsets = UIEdgeInsets()) { + self.controller = controller + self.sourceView = sourceView + self.insets = insets + self.contentInsets = contentInsets + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds.inset(by: self.insets), insets: self.contentInsets) + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift new file mode 100644 index 0000000000..ae57377d9c --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift @@ -0,0 +1,304 @@ +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 + +extension ChatControllerImpl { + func openPeer(peer: EnginePeer?, navigation: ChatControllerInteractionNavigateToPeer, fromMessage: MessageReference?, fromReactionMessageId: MessageId? = nil, expandAvatar: Bool = false, peerTypes: ReplyMarkupButtonAction.PeerTypes? = nil) { + let _ = self.presentVoiceMessageDiscardAlert(action: { + if case let .peer(currentPeerId) = self.chatLocation, peer?.id == currentPeerId { + switch navigation { + case let .info(params): + var recommendedChannels = false + if let params, params.switchToRecommendedChannels { + recommendedChannels = true + } + self.navigationButtonAction(.openChatInfo(expandAvatar: expandAvatar, recommendedChannels: recommendedChannels)) + case let .chat(textInputState, _, _): + if let textInputState = textInputState { + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return ($0.updatedInterfaceState { + return $0.withUpdatedComposeInputState(textInputState) + }).updatedInputMode({ _ in + return .text + }) + }) + } else { + self.playShakeAnimation() + } + case let .withBotStartPayload(botStart): + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { + $0.updatedBotStartPayload(botStart.payload) + }) + case .withAttachBot: + self.presentAttachmentMenu(subject: .default) + default: + break + } + } else { + if let peer = peer { + do { + var chatPeerId: PeerId? + if let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramGroup { + chatPeerId = peer.id + } else if let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramChannel, case .group = peer.info, case .member = peer.participationStatus { + chatPeerId = peer.id + } + + switch navigation { + case .info, .default: + let peerSignal: Signal + if let messageId = fromMessage?.id { + peerSignal = loadedPeerFromMessage(account: self.context.account, peerId: peer.id, messageId: messageId) + } else { + peerSignal = self.context.account.postbox.loadedPeerWithId(peer.id) |> map(Optional.init) + } + self.navigationActionDisposable.set((peerSignal |> take(1) |> deliverOnMainQueue).startStrict(next: { [weak self] peer in + if let strongSelf = self, let peer = peer { + var mode: PeerInfoControllerMode = .generic + if let _ = fromMessage, let chatPeerId = chatPeerId { + mode = .group(chatPeerId) + } + if let fromReactionMessageId = fromReactionMessageId { + mode = .reaction(fromReactionMessageId) + } + if case let .info(params) = navigation, let params, params.switchToRecommendedChannels { + mode = .recommendedChannels + } + var expandAvatar = expandAvatar + if peer.smallProfileImage == nil { + expandAvatar = false + } + if let validLayout = strongSelf.validLayout, validLayout.deviceMetrics.type == .tablet { + expandAvatar = false + } + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer, mode: mode, avatarInitiallyExpanded: expandAvatar, fromChat: false, requestsContext: nil) { + strongSelf.effectiveNavigationController?.pushViewController(infoController) + } + } + })) + case let .chat(textInputState, subject, peekData): + if let textInputState = textInputState { + let _ = (ChatInterfaceState.update(engine: self.context.engine, peerId: peer.id, threadId: nil, { currentState in + return currentState.withUpdatedComposeInputState(textInputState) + }) + |> deliverOnMainQueue).startStandalone(completed: { [weak self] in + if let strongSelf = self, let navigationController = strongSelf.effectiveNavigationController { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), subject: subject, updateTextInputState: textInputState, peekData: peekData)) + } + }) + } else { + if case let .channel(channel) = peer, channel.flags.contains(.isForum) { + self.effectiveNavigationController?.pushViewController(ChatListControllerImpl(context: self.context, location: .forum(peerId: channel.id), controlsHistoryPreload: false, enableDebugActions: false)) + } else { + self.effectiveNavigationController?.pushViewController(ChatControllerImpl(context: self.context, chatLocation: .peer(id: peer.id), subject: subject)) + } + } + case let .withBotStartPayload(botStart): + self.effectiveNavigationController?.pushViewController(ChatControllerImpl(context: self.context, chatLocation: .peer(id: peer.id), botStart: botStart)) + case let .withAttachBot(attachBotStart): + if let navigationController = self.effectiveNavigationController { + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), attachBotStart: attachBotStart)) + } + case let .withBotApp(botAppStart): + if let navigationController = self.effectiveNavigationController { + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), botAppStart: botAppStart)) + } + } + } + } else { + switch navigation { + case .info: + break + case let .chat(textInputState, _, _): + if let textInputState = textInputState { + let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, updatedPresentationData: self.updatedPresentationData, requestPeerType: peerTypes.flatMap { $0.requestPeerTypes }, selectForumThreads: true)) + controller.peerSelected = { [weak self, weak controller] peer, threadId in + let peerId = peer.id + + if let strongSelf = self, let strongController = controller { + if case let .peer(currentPeerId) = strongSelf.chatLocation, peerId == currentPeerId { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return ($0.updatedInterfaceState { + return $0.withUpdatedComposeInputState(textInputState) + }).updatedInputMode({ _ in + return .text + }) + }) + strongController.dismiss() + } else { + let _ = (ChatInterfaceState.update(engine: strongSelf.context.engine, peerId: peerId, threadId: threadId, { currentState in + return currentState.withUpdatedComposeInputState(textInputState) + }) + |> deliverOnMainQueue).startStandalone(completed: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) + + if let navigationController = strongSelf.effectiveNavigationController { + let chatController: Signal + if let threadId { + chatController = chatControllerForForumThreadImpl(context: strongSelf.context, peerId: peerId, threadId: threadId) + } else { + chatController = .single(ChatControllerImpl(context: strongSelf.context, chatLocation: .peer(id: peerId))) + } + + let _ = (chatController + |> deliverOnMainQueue).start(next: { [weak self, weak navigationController] chatController in + guard let strongSelf = self, let navigationController else { + return + } + var viewControllers = navigationController.viewControllers + let lastController = viewControllers.last as! ViewController + if threadId != nil { + viewControllers.remove(at: viewControllers.count - 2) + lastController.navigationPresentation = .modal + } + viewControllers.insert(chatController, at: viewControllers.count - 1) + navigationController.setViewControllers(viewControllers, animated: false) + + strongSelf.controllerNavigationDisposable.set((chatController.ready.get() + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).startStrict(next: { [weak lastController] _ in + lastController?.dismiss() + })) + }) + } + }) + } + } + } + self.chatDisplayNode.dismissInput() + self.effectiveNavigationController?.pushViewController(controller) + } + default: + break + } + } + } + }) + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenStorySharing.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenStorySharing.swift new file mode 100644 index 0000000000..77353f1fad --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenStorySharing.swift @@ -0,0 +1,195 @@ +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 + +extension ChatControllerImpl { + func openStorySharing(messages: [Message]) { + let context = self.context + let subject: Signal = .single(.message(messages.map { $0.id })) + + let externalState = MediaEditorTransitionOutExternalState( + storyTarget: nil, + isForcedTarget: false, + isPeerArchived: false, + transitionOut: nil + ) + + let controller = MediaEditorScreen( + context: context, + mode: .storyEditor, + subject: subject, + transitionIn: nil, + transitionOut: { _, _ in + return nil + }, + completion: { [weak self] result, commit in + guard let self else { + return + } + let targetPeerId: EnginePeer.Id + let target: Stories.PendingTarget + if let sendAsPeerId = result.options.sendAsPeerId { + target = .peer(sendAsPeerId) + targetPeerId = sendAsPeerId + } else { + target = .myStories + targetPeerId = self.context.account.peerId + } + externalState.storyTarget = target + + if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { + rootController.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) + } + + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: targetPeerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + let text: String + if case .channel = peer { + text = self.presentationData.strings.Story_MessageReposted_Channel(peer.compactDisplayTitle).string + } else { + text = self.presentationData.strings.Story_MessageReposted_Personal + } + Queue.mainQueue().after(0.25) { + self.present(UndoOverlayController( + presentationData: self.presentationData, + content: .forward(savedMessages: false, text: text), + elevatedLayout: false, + action: { _ in return false } + ), in: .current) + + Queue.mainQueue().after(0.1) { + self.chatDisplayNode.hapticFeedback.success() + } + } + }) + + } + ) + self.push(controller) + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenViewOnceMediaMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenViewOnceMediaMessage.swift new file mode 100644 index 0000000000..ada64473b4 --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenViewOnceMediaMessage.swift @@ -0,0 +1,182 @@ +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 + +extension ChatControllerImpl { + func openViewOnceMediaMessage(_ message: Message) { + if self.screenCaptureManager?.isRecordingActive == true { + let controller = textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, text: self.presentationData.strings.Chat_PlayOnceMesasge_DisableScreenCapture, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { + })]) + self.present(controller, in: .window(.root)) + return + } + + let isIncoming = message.effectivelyIncoming(self.context.account.peerId) + + var presentImpl: ((ViewController) -> Void)? + let configuration = ContextController.Configuration( + sources: [ + ContextController.Source( + id: 0, + title: "", + source: .extracted(ChatViewOnceMessageContextExtractedContentSource( + context: self.context, + presentationData: self.presentationData, + chatNode: self.chatDisplayNode, + backgroundNode: self.chatBackgroundNode, + engine: self.context.engine, + message: message, + present: { c in + presentImpl?(c) + } + )), + items: .single(ContextController.Items(content: .list([]))), + closeActionTitle: isIncoming ? self.presentationData.strings.Chat_PlayOnceMesasgeCloseAndDelete : self.presentationData.strings.Chat_PlayOnceMesasgeClose, + closeAction: { [weak self] in + if let self { + self.context.sharedContext.mediaManager.setPlaylist(nil, type: .voice, control: .playback(.pause)) + } + } + ) + ], initialId: 0 + ) + + let contextController = ContextController(presentationData: self.presentationData, configuration: configuration) + contextController.getOverlayViews = { [weak self] in + guard let self else { + return [] + } + return [self.chatDisplayNode.navigateButtons.view] + } + self.currentContextController = contextController + self.presentInGlobalOverlay(contextController) + + presentImpl = { [weak contextController] c in + contextController?.present(c, in: .current) + } + + let _ = self.context.sharedContext.openChatMessage(OpenChatMessageParams(context: self.context, chatLocation: nil, chatFilterTag: nil, chatLocationContextHolder: nil, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: nil, dismissInput: { }, present: { _, _ in }, transitionNode: { _, _, _ in return nil }, addToTransitionSurface: { _ in }, openUrl: { _ in }, openPeer: { _, _ in }, callPeer: { _, _ in }, enqueueMessage: { _ in }, sendSticker: nil, sendEmoji: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in }, playlistLocation: .singleMessage(message.id))) + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerReport.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerReport.swift new file mode 100644 index 0000000000..496c26e8f9 --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerReport.swift @@ -0,0 +1,265 @@ +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 + +extension ChatControllerImpl { + func unblockPeer() { + guard case let .peer(peerId) = self.chatLocation else { + return + } + let unblockingPeer = self.unblockingPeer + unblockingPeer.set(true) + + var restartBot = false + if let user = self.presentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil { + restartBot = true + } + self.editMessageDisposable.set((self.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peerId, isBlocked: false) + |> afterDisposed({ [weak self] in + Queue.mainQueue().async { + unblockingPeer.set(false) + if let strongSelf = self, restartBot { + strongSelf.startBot(strongSelf.presentationInterfaceState.botStartPayload) + } + } + })).startStrict()) + } + + func reportPeer() { + guard let renderedPeer = self.presentationInterfaceState.renderedPeer, let peer = renderedPeer.chatMainPeer, let chatPeer = renderedPeer.peer else { + return + } + self.chatDisplayNode.dismissInput() + + if let peer = peer as? TelegramChannel, let username = peer.addressName, !username.isEmpty { + let actionSheet = ActionSheetController(presentationData: self.presentationData) + + var items: [ActionSheetItem] = [] + items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ReportSpamAndLeave, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.deleteChat(reportChatSpam: true) + } + })) + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + + self.present(actionSheet, in: .window(.root)) + } else if let _ = peer as? TelegramUser { + let presentationData = self.presentationData + let controller = ActionSheetController(presentationData: presentationData) + let dismissAction: () -> Void = { [weak controller] in + controller?.dismissAnimated() + } + var reportSpam = true + var deleteChat = true + var items: [ActionSheetItem] = [] + if !peer.isDeleted { + items.append(ActionSheetTextItem(title: presentationData.strings.UserInfo_BlockConfirmationTitle(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 + }) + }), + ActionSheetCheckboxItem(title: presentationData.strings.ReportSpam_DeleteThisChat, label: "", value: deleteChat, action: { [weak controller] checkValue in + deleteChat = checkValue + controller?.updateItem(groupIndex: 0, itemIndex: 2, { item in + if let item = item as? ActionSheetCheckboxItem { + return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action) + } + return item + }) + }), + ActionSheetButtonItem(title: presentationData.strings.UserInfo_BlockActionTitle(EnginePeer(peer).compactDisplayTitle).string, color: .destructive, action: { [weak self] in + dismissAction() + guard let strongSelf = self else { + return + } + let _ = strongSelf.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peer.id, isBlocked: true).startStandalone() + if let _ = chatPeer as? TelegramSecretChat { + let _ = strongSelf.context.engine.peers.terminateSecretChat(peerId: chatPeer.id, requestRemoteHistoryRemoval: true).startStandalone() + } + if deleteChat { + let _ = strongSelf.context.engine.peers.removePeerChat(peerId: chatPeer.id, reportChatSpam: reportSpam).startStandalone() + strongSelf.effectiveNavigationController?.filterController(strongSelf, animated: true) + } else if reportSpam { + let _ = strongSelf.context.engine.peers.reportPeer(peerId: peer.id, reason: .spam, message: "").startStandalone() + } + }) + ] as [ActionSheetItem]) + + controller.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + self.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } else { + let title: String + var infoString: String? + if let _ = peer as? TelegramGroup { + title = self.presentationData.strings.Conversation_ReportSpamAndLeave + infoString = self.presentationData.strings.Conversation_ReportSpamGroupConfirmation + } else if let channel = peer as? TelegramChannel { + title = self.presentationData.strings.Conversation_ReportSpamAndLeave + if case .group = channel.info { + infoString = self.presentationData.strings.Conversation_ReportSpamGroupConfirmation + } else { + infoString = self.presentationData.strings.Conversation_ReportSpamChannelConfirmation + } + } else { + title = self.presentationData.strings.Conversation_ReportSpam + infoString = self.presentationData.strings.Conversation_ReportSpamConfirmation + } + let actionSheet = ActionSheetController(presentationData: self.presentationData) + + var items: [ActionSheetItem] = [] + if let infoString = infoString { + items.append(ActionSheetTextItem(title: infoString)) + } + items.append(ActionSheetButtonItem(title: title, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.deleteChat(reportChatSpam: true) + } + })) + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + + self.present(actionSheet, in: .window(.root)) + } + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift new file mode 100644 index 0000000000..d21126fadb --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift @@ -0,0 +1,362 @@ +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 + +extension ChatControllerImpl { + public func presentThemeSelection() { + guard self.themeScreen == nil else { + return + } + let context = self.context + let peerId = self.chatLocation.peerId + + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in + var updated = state + updated = updated.updatedInputMode({ _ in + return .none + }) + updated = updated.updatedShowCommands(false) + return updated + }) + + let animatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false) + |> map { animatedEmoji -> [String: [StickerPackItem]] in + var animatedEmojiStickers: [String: [StickerPackItem]] = [:] + switch animatedEmoji { + case let .result(_, items, _): + for item in items { + if let emoji = item.getStringRepresentationsOfIndexKeys().first { + animatedEmojiStickers[emoji.basicEmoji.0] = [item] + let strippedEmoji = emoji.basicEmoji.0.strippedEmoji + if animatedEmojiStickers[strippedEmoji] == nil { + animatedEmojiStickers[strippedEmoji] = [item] + } + } + } + default: + break + } + return animatedEmojiStickers + } + + let _ = (combineLatest(queue: Queue.mainQueue(), self.chatThemeEmoticonPromise.get(), animatedEmojiStickers) + |> take(1)).startStandalone(next: { [weak self] themeEmoticon, animatedEmojiStickers in + guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { + return + } + + var canResetWallpaper = false + if let cachedUserData = strongSelf.peerView?.cachedData as? CachedUserData { + canResetWallpaper = cachedUserData.wallpaper != nil + } + + let controller = ChatThemeScreen( + context: context, + updatedPresentationData: strongSelf.updatedPresentationData, + animatedEmojiStickers: animatedEmojiStickers, + initiallySelectedEmoticon: themeEmoticon, + peerName: strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer.flatMap(EnginePeer.init)?.compactDisplayTitle ?? "", + canResetWallpaper: canResetWallpaper, + previewTheme: { [weak self] emoticon, dark in + if let strongSelf = self { + strongSelf.presentCrossfadeSnapshot() + strongSelf.themeEmoticonAndDarkAppearancePreviewPromise.set(.single((emoticon, dark))) + } + }, + changeWallpaper: { [weak self] in + guard let strongSelf = self, let peerId else { + return + } + if let themeController = strongSelf.themeScreen { + strongSelf.themeScreen = nil + themeController.dimTapped() + } + let dismissControllers = { [weak self] in + if let self, let navigationController = self.navigationController as? NavigationController { + let controllers = navigationController.viewControllers.filter({ controller in + if controller is WallpaperGalleryController || controller is AttachmentController { + return false + } + return true + }) + navigationController.setViewControllers(controllers, animated: true) + } + } + var openWallpaperPickerImpl: ((Bool) -> Void)? + let openWallpaperPicker = { [weak self] animateAppearance in + guard let strongSelf = self else { + return + } + let controller = wallpaperMediaPickerController( + context: strongSelf.context, + updatedPresentationData: strongSelf.updatedPresentationData, + peer: EnginePeer(peer), + animateAppearance: animateAppearance, + completion: { [weak self] _, result in + guard let strongSelf = self, let asset = result as? PHAsset else { + return + } + let controller = WallpaperGalleryController(context: strongSelf.context, source: .asset(asset), mode: .peer(EnginePeer(peer), false)) + controller.navigationPresentation = .modal + controller.apply = { [weak self] wallpaper, options, editedImage, cropRect, brightness, forBoth in + if let strongSelf = self { + uploadCustomPeerWallpaper(context: strongSelf.context, wallpaper: wallpaper, mode: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness, peerId: peerId, forBoth: forBoth, completion: { + Queue.mainQueue().after(0.3, { + dismissControllers() + }) + }) + } + } + strongSelf.push(controller) + }, + openColors: { [weak self] in + guard let strongSelf = self else { + return + } + let controller = standaloneColorPickerController(context: strongSelf.context, peer: EnginePeer(peer), push: { [weak self] controller in + if let strongSelf = self { + strongSelf.push(controller) + } + }, openGallery: { + openWallpaperPickerImpl?(false) + }) + controller.navigationPresentation = .flatModal + strongSelf.push(controller) + } + ) + controller.navigationPresentation = .flatModal + strongSelf.push(controller) + } + openWallpaperPickerImpl = openWallpaperPicker + openWallpaperPicker(true) + }, + resetWallpaper: { [weak self] in + guard let strongSelf = self, let peerId else { + return + } + let _ = strongSelf.context.engine.themes.setChatWallpaper(peerId: peerId, wallpaper: nil, forBoth: false).startStandalone() + }, + completion: { [weak self] emoticon in + guard let strongSelf = self, let peerId else { + return + } + if canResetWallpaper && emoticon != nil { + let _ = context.engine.themes.setChatWallpaper(peerId: peerId, wallpaper: nil, forBoth: false).startStandalone() + } + strongSelf.themeEmoticonAndDarkAppearancePreviewPromise.set(.single((emoticon ?? "", nil))) + let _ = context.engine.themes.setChatTheme(peerId: peerId, emoticon: emoticon).startStandalone(completed: { [weak self] in + if let strongSelf = self { + strongSelf.themeEmoticonAndDarkAppearancePreviewPromise.set(.single((nil, nil))) + } + }) + } + ) + controller.navigationPresentation = .flatModal + controller.passthroughHitTestImpl = { [weak self] _ in + if let strongSelf = self { + return strongSelf.chatDisplayNode.historyNode.view + } else { + return nil + } + } + controller.dismissed = { [weak self] in + if let strongSelf = self { + strongSelf.chatDisplayNode.historyNode.tapped = nil + } + } + strongSelf.chatDisplayNode.historyNode.tapped = { [weak controller] in + controller?.dimTapped() + } + strongSelf.push(controller) + strongSelf.themeScreen = controller + }) + } + + func presentEmojiList(references: [StickerPackReference]) { + guard let packReference = references.first else { + return + } + self.chatDisplayNode.dismissTextInput() + + let presentationData = self.presentationData + let controller = StickerPackScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, mainStickerPack: packReference, stickerPacks: Array(references), parentNavigationController: self.effectiveNavigationController, sendEmoji: canSendMessagesToChat(self.presentationInterfaceState) ? { [weak self] text, attribute in + if let strongSelf = self { + strongSelf.controllerInteraction?.sendEmoji(text, attribute, false) + } + } : nil, actionPerformed: { [weak self] actions in + guard let strongSelf = self else { + return + } + let context = strongSelf.context + if actions.count > 1, let first = actions.first { + if case .add = first.2 { + strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.EmojiPackActionInfo_AddedTitle, text: presentationData.strings.EmojiPackActionInfo_MultipleAddedText(Int32(actions.count)), undo: false, info: first.0, topItem: first.1.first, context: context), elevatedLayout: true, animateInAsReplacement: false, action: { _ in + return true + })) + } else if actions.allSatisfy({ + if case .remove = $0.2 { + return true + } else { + return false + } + }) { + let isEmoji = actions[0].0.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks + strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: isEmoji ? presentationData.strings.EmojiPackActionInfo_RemovedTitle : presentationData.strings.StickerPackActionInfo_RemovedTitle, text: isEmoji ? presentationData.strings.EmojiPackActionInfo_MultipleRemovedText(Int32(actions.count)) : presentationData.strings.StickerPackActionInfo_MultipleRemovedText(Int32(actions.count)), undo: true, info: actions[0].0, topItem: actions[0].1.first, context: context), elevatedLayout: true, animateInAsReplacement: false, action: { action in + if case .undo = action { + var itemsAndIndices: [(StickerPackCollectionInfo, [StickerPackItem], Int)] = actions.compactMap { action -> (StickerPackCollectionInfo, [StickerPackItem], Int)? in + if case let .remove(index) = action.2 { + return (action.0, action.1, index) + } else { + return nil + } + } + itemsAndIndices.sort(by: { $0.2 < $1.2 }) + for (info, items, index) in itemsAndIndices.reversed() { + let _ = context.engine.stickers.addStickerPackInteractively(info: info, items: items, positionInList: index).startStandalone() + } + } + return true + })) + } + } else if let (info, items, action) = actions.first { + let isEmoji = info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks + switch action { + case .add: + strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: isEmoji ? presentationData.strings.EmojiPackActionInfo_AddedTitle : presentationData.strings.StickerPackActionInfo_AddedTitle, text: isEmoji ? presentationData.strings.EmojiPackActionInfo_AddedText(info.title).string : presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: items.first, context: context), elevatedLayout: true, animateInAsReplacement: false, action: { _ in + return true + })) + case let .remove(positionInList): + strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: isEmoji ? presentationData.strings.EmojiPackActionInfo_RemovedTitle : presentationData.strings.StickerPackActionInfo_RemovedTitle, text: isEmoji ? presentationData.strings.EmojiPackActionInfo_RemovedText(info.title).string : presentationData.strings.StickerPackActionInfo_RemovedText(info.title).string, undo: true, info: info, topItem: items.first, context: context), elevatedLayout: true, animateInAsReplacement: false, action: { action in + if case .undo = action { + let _ = context.engine.stickers.addStickerPackInteractively(info: info, items: items, positionInList: positionInList).startStandalone() + } + return true + })) + } + } + }) + self.present(controller, in: .window(.root)) + } +} diff --git a/submodules/TelegramUI/Sources/Chat/PeerMessageSelectedReactions.swift b/submodules/TelegramUI/Sources/Chat/PeerMessageSelectedReactions.swift new file mode 100644 index 0000000000..138e33de1c --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/PeerMessageSelectedReactions.swift @@ -0,0 +1,34 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Postbox +import TelegramCore +import AccountContext + +func peerMessageSelectedReactions(context: AccountContext, message: Message) -> Signal<(reactions: Set, files: Set), NoError> { + return context.engine.stickers.availableReactions() + |> take(1) + |> map { availableReactions -> (reactions: Set, files: Set) in + var result = Set() + var reactions = Set() + + if let effectiveReactions = message.effectiveReactions(isTags: message.areReactionsTags(accountPeerId: context.account.peerId)) { + for reaction in effectiveReactions { + if !reaction.isSelected { + continue + } + reactions.insert(reaction.value) + switch reaction.value { + case .builtin: + if let availableReaction = availableReactions?.reactions.first(where: { $0.value == reaction.value }) { + result.insert(availableReaction.selectAnimation.fileId) + } + case let .custom(fileId): + result.insert(MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)) + } + } + } + + return (reactions, result) + } +} diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index a31dc472f4..fb1de9b97b 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -4409,11 +4409,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.removeAd(opaqueId: adOpaqueId) } } else { + let context = self.context var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumDemoScreen(context: self.context, subject: .noAds, action: { - let controller = PremiumIntroScreen(context: self.context, source: .ads) + let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .noAds, forceDark: false, action: { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: false, dismissed: nil) replaceImpl?(controller) - }) + }, dismissed: nil) replaceImpl = { [weak controller] c in controller?.replace(with: c) } @@ -5707,8 +5708,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } strongSelf.offerNextChannelToRead = true - strongSelf.chatDisplayNode.historyNode.nextChannelToRead = nextPeer.flatMap { nextPeer -> (peer: EnginePeer, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation) in - return (peer: nextPeer, unreadCount: 0, location: .same) + strongSelf.chatDisplayNode.historyNode.nextChannelToRead = nextPeer.flatMap { nextPeer -> (peer: EnginePeer, threadData: (id: Int64, data: MessageHistoryThreadData)?, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation) in + return (peer: nextPeer, threadData: nil, unreadCount: 0, location: .same) } strongSelf.chatDisplayNode.historyNode.nextChannelToReadDisplayName = nextChatSuggestionTip >= 3 @@ -5745,8 +5746,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } strongSelf.offerNextChannelToRead = true - strongSelf.chatDisplayNode.historyNode.nextChannelToRead = nextPeer.flatMap { nextPeer -> (peer: EnginePeer, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation) in - return (peer: nextPeer.peer, unreadCount: nextPeer.unreadCount, location: nextPeer.location) + strongSelf.chatDisplayNode.historyNode.nextChannelToRead = nextPeer.flatMap { nextPeer -> (peer: EnginePeer, threadData: (id: Int64, data: MessageHistoryThreadData)?, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation) in + return (peer: nextPeer.peer, threadData: nil, unreadCount: nextPeer.unreadCount, location: nextPeer.location) } strongSelf.chatDisplayNode.historyNode.nextChannelToReadDisplayName = nextChatSuggestionTip >= 3 @@ -6310,6 +6311,27 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return interfaceState } }) + + if let replyThreadId, let channel = renderedPeer?.peer as? TelegramChannel, channel.isForum, strongSelf.nextChannelToReadDisposable == nil { + strongSelf.nextChannelToReadDisposable = (combineLatest(queue: .mainQueue(), + strongSelf.context.engine.peers.getNextUnreadForumTopic(peerId: channel.id, topicId: Int32(clamping: replyThreadId)), + ApplicationSpecificNotice.getNextChatSuggestionTip(accountManager: strongSelf.context.sharedContext.accountManager) + ) + |> then(.complete() |> delay(1.0, queue: .mainQueue())) + |> restart).startStrict(next: { nextThreadData, nextChatSuggestionTip in + guard let strongSelf = self else { + return + } + + strongSelf.offerNextChannelToRead = true + strongSelf.chatDisplayNode.historyNode.nextChannelToRead = nextThreadData.flatMap { nextThreadData -> (peer: EnginePeer, threadData: (id: Int64, data: MessageHistoryThreadData)?, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation) in + return (peer: EnginePeer(channel), threadData: nextThreadData, unreadCount: Int(nextThreadData.data.incomingUnreadCount), location: .same) + } + strongSelf.chatDisplayNode.historyNode.nextChannelToReadDisplayName = nextChatSuggestionTip >= 3 + + strongSelf.updateNextChannelToReadVisibility() + }) + } } if !strongSelf.didSetChatLocationInfoReady { strongSelf.didSetChatLocationInfoReady = true @@ -7325,4629 +7347,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return .single(nil) } } - - override public func loadDisplayNode() { - self.displayNode = ChatControllerNode(context: self.context, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, subject: self.subject, controllerInteraction: self.controllerInteraction!, chatPresentationInterfaceState: self.presentationInterfaceState, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, navigationBar: self.navigationBar, 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.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 { - 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 - let chatLocationPeerId = self.chatLocation.peerId - - if let chatLocationPeerId = chatLocationPeerId { - hasPendingMessages = self.context.account.pendingMessageManager.hasPendingMessages - |> mapToSignal { peerIds -> Signal in - let value = peerIds.contains(chatLocationPeerId) - if value { - return .single(true) - } else { - return .single(false) - |> delay(0.1, queue: .mainQueue()) - } - } - |> distinctUntilChanged - } else { - hasPendingMessages = .single(false) - } - - let isTopReplyThreadMessageShown: Signal = self.chatDisplayNode.historyNode.isTopReplyThreadMessageShown.get() - |> distinctUntilChanged - - let topPinnedMessage: Signal - 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 = 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 - let forumTopicData: Signal - 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 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 - self.translationStateDisposable = (combineLatest( - queue: .concurrentDefaultQueue(), - isPremium, - isHidden, - ApplicationSpecificNotice.translationSuggestion(accountManager: self.context.sharedContext.accountManager) - ) |> mapToSignal { isPremium, isHidden, counterAndTimestamp -> Signal in - var maybeSuggestPremium = false - if counterAndTimestamp.0 >= 3 { - maybeSuggestPremium = true - } - if (isPremium || maybeSuggestPremium) && !isHidden { - return chatTranslationState(context: context, peerId: peerId) - |> 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) - }) - } - }) - } - - self.cachedDataDisposable = combineLatest(queue: .mainQueue(), self.chatDisplayNode.historyNode.cachedPeerDataAndMessages, - hasPendingMessages, - isTopReplyThreadMessageShown, - topPinnedMessage, - customEmojiAvailable, - isForum, - threadData, - forumTopicData - ).startStrict(next: { [weak self] cachedDataAndMessages, hasPendingMessages, isTopReplyThreadMessageShown, topPinnedMessage, customEmojiAvailable, isForum, threadData, forumTopicData 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? - var premiumGiftOptions: [CachedPremiumGiftOption] = [] - 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 - premiumGiftOptions = cachedData.premiumGiftOptions - } 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 - if case .replyThread = self.chatLocation { - effectiveCachedDataReady = self.cachedDataReady.get() - } else { - //effectiveCachedDataReady = .single(true) - effectiveCachedDataReady = self.cachedDataReady.get() - } - self.ready.set(combineLatest(queue: .mainQueue(), - self.chatDisplayNode.historyNode.historyState.get(), - self._chatLocationInfoReady.get(), - effectiveCachedDataReady, - initialData, - self.wallpaperReady.get(), - self.presentationReady.get() - ) - |> map { _, chatLocationInfoReady, cachedDataReady, _, wallpaperReady, presentationReady in - return chatLocationInfoReady && cachedDataReady && wallpaperReady && presentationReady - } - |> distinctUntilChanged) - - 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) { - 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.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") - - self?.chatDisplayNode.historyNode.layoutActionOnViewTransition = ({ [weak self] transition in - f() - if let strongSelf = self, let validLayout = strongSelf.validLayout { - var mappedTransition: (ChatHistoryListViewTransition, ListViewUpdateSizeAndInsets?)? - - let isScheduledMessages: Bool - if case .scheduledMessages = strongSelf.presentationInterfaceState.subject { - isScheduledMessages = true - } else { - isScheduledMessages = false - } - 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 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: [EnqueueMessage] - if let silentPosting = silentPosting { - transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: silentPosting) - } else if let scheduleTime = scheduleTime { - transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime) - } else { - transformedMessages = strongSelf.transformEnqueueMessages(messages) - } - - var forwardedMessages: [[EnqueueMessage]] = [] - var forwardSourcePeerIds = Set() - for message in transformedMessages { - if case let .forward(source, _, _, _, _) = message { - forwardSourcePeerIds.insert(source.peerId) - - var added = false - if var last = forwardedMessages.last { - if let currentMessage = last.first, case let .forward(currentSource, _, _, _, _) = currentMessage, currentSource.peerId == source.peerId { - last.append(message) - added = true - } - } - if !added { - forwardedMessages.append([message]) - } - } - } - - let signal: Signal<[MessageId?], NoError> - if forwardSourcePeerIds.count > 1 { - var signals: [Signal<[MessageId?], NoError>] = [] - for messagesGroup in forwardedMessages { - signals.append(enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: messagesGroup)) - } - signal = combineLatest(signals) - |> map { results in - var ids: [MessageId?] = [] - for result in results { - ids.append(contentsOf: result) - } - return ids - } - } else { - signal = enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: transformedMessages) - } - - let _ = (signal - |> deliverOnMainQueue).startStandalone(next: { messageIds in - if let strongSelf = self { - if case .scheduledMessages = strongSelf.presentationInterfaceState.subject { - } else { - strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() - } - } - }) - - donateSendMessageIntent(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, intentContext: .chat, peerIds: [peerId]) - } else if case let .customChatContents(customChatContents) = strongSelf.subject { - switch customChatContents.kind { - 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: "The message text limit is 4096 characters", 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) - } - } - - strongSelf.updateChatPresentationInterfaceState(interactive: true, { $0.updatedShowCommands(false) }) - } - - 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, case let .media(options) = editMessageState.content else { - return - } - var originalMediaReference: AnyMediaReference? - if let message = message { - for media in message.media { - if let image = media as? TelegramMediaImage { - originalMediaReference = .message(message: MessageReference(message._asMessage()), media: image) - } else if let file = media as? TelegramMediaFile { - if file.isVideo || file.isAnimated { - originalMediaReference = .message(message: MessageReference(message._asMessage()), media: file) - } - } - } - } - strongSelf.oldPresentAttachmentMenu(editMediaOptions: options, 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) - } - } - self.chatDisplayNode.updateTypingActivity = { [weak self] value in - if let strongSelf = self { - if value { - strongSelf.typingActivityPromise.set(Signal.single(true) - |> then( - Signal.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 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: - 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 { - 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 - } - if let messageId = messageId { - if canSendMessagesToChat(strongSelf.presentationInterfaceState) { - 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 - } - moveReplyMessageToAnotherChat(selfController: self, replySubject: replySubject) - }) - } - } else { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(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)) - } - - 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 reportReason = strongSelf.presentationInterfaceState.reportReason { - let presentationData = strongSelf.presentationData - let controller = ActionSheetController(presentationData: presentationData, allowInputInset: true) - let dismissAction: () -> Void = { [weak self, weak controller] in - self?.view.window?.endEditing(true) - controller?.dismissAnimated() - } - var message = "" - var items: [ActionSheetItem] = [] - items.append(ReportPeerHeaderActionSheetItem(context: strongSelf.context, text: presentationData.strings.Report_AdditionalDetailsText)) - items.append(ReportPeerDetailsActionSheetItem(context: strongSelf.context, theme: presentationData.theme, placeholderText: presentationData.strings.Report_AdditionalDetailsPlaceholder, textUpdated: { text in - message = text - })) - items.append(ActionSheetButtonItem(title: presentationData.strings.Report_Report, color: .accent, font: .bold, enabled: true, action: { - dismissAction() - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }, completion: { _ in - let _ = (strongSelf.context.engine.peers.reportPeerMessages(messageIds: Array(messageIds), reason: reportReason, 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) - }) - }) - })) - - controller.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) - ]) - strongSelf.present(controller, in: .window(.root)) - } else { - strongSelf.present(peerReportOptionsController(context: strongSelf.context, subject: .messages(Array(messageIds).sorted()), passthrough: false, present: { c, a in - self?.present(c, in: .window(.root), with: a) - }, push: { c in - self?.push(c) - }, completion: { _, done in - if done { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - } - }), in: .window(.root)) - } - } - }, reportMessages: { [weak self] messages, contextController in - if let strongSelf = self, !messages.isEmpty { - let options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .illegalDrugs, .personalDetails, .other] - presentPeerReportOptions(context: strongSelf.context, parent: strongSelf, contextController: contextController, subject: .messages(messages.map({ $0.id }).sorted()), options: options, completion: { _, _ in }) - } - }, blockMessageAuthor: { [weak self] message, contextController in - contextController?.dismiss(completion: { - guard let strongSelf = self else { - return - } - - let author = message.forwardInfo?.author - - guard let peer = author else { - return - } - - let presentationData = strongSelf.presentationData - let controller = ActionSheetController(presentationData: presentationData) - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() - } - var reportSpam = true - var items: [ActionSheetItem] = [] - items.append(ActionSheetTextItem(title: presentationData.strings.UserInfo_BlockConfirmationTitle(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 { - 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] in - if let strongSelf = self { - strongSelf.commitPurposefulAction() - if let forwardMessageIdsSet = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds { - let forwardMessageIds = Array(forwardMessageIdsSet).sorted() - strongSelf.forwardMessages(messageIds: forwardMessageIds) - } - } - }, forwardCurrentForwardMessages: { [weak self] in - if let strongSelf = self { - strongSelf.commitPurposefulAction() - if let forwardMessageIds = strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds { - strongSelf.forwardMessages(messageIds: forwardMessageIds, options: strongSelf.presentationInterfaceState.interfaceState.forwardOptionsState, resetCurrent: true) - } - } - }, forwardMessages: { [weak self] messages in - if let strongSelf = self, !messages.isEmpty { - strongSelf.commitPurposefulAction() - let forwardMessageIds = messages.map { $0.id }.sorted() - strongSelf.forwardMessages(messageIds: forwardMessageIds) - } - }, 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 - 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) - } - } - - let text = trimChatInputText(convertMarkdownToAttributes(editMessage.inputState.inputText)) - - let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text)) - var entitiesAttribute: TextEntitiesMessageAttribute? - if !entities.isEmpty { - entitiesAttribute = TextEntitiesMessageAttribute(entities: entities) - } - - 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() - - var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumDemoScreen(context: strongSelf.context, subject: .animatedEmoji, action: { - let controller = PremiumIntroScreen(context: strongSelf.context, source: .animatedEmoji) - replaceImpl?(controller) - }) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) - } - strongSelf.present(controller, in: .window(.root), with: nil) - })) - - 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, 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)) - }) - 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, recommendedChannels: false)) - }, 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.enqueueChatContextResult(results, result) - return true - }, sendBotCommand: { [weak self] botPeer, command in - if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) { - if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { - let messageText: String - if let addressName = botPeer.addressName { - if peer is TelegramUser { - messageText = command - } else { - messageText = command + "@" + addressName - } - } else { - messageText = command - } - let 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).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).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).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 = 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?.sendMediaRecording(silentPosting: silentPosting, viewOnce: viewOnce) - }, 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 { - 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 = 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 = 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 - 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 { 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 - 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? - 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, apply: { [weak self] link in - if let strongSelf = self, let inputMode = inputMode, let selectionRange = selectionRange { - if let link = link { - strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in - return (chatTextInputAddLinkAttribute(current, selectionRange: selectionRange, url: link), inputMode) - } - } else { - - } - strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { - return $0.updatedInputMode({ _ in return inputMode }).updatedInterfaceState({ - $0.withUpdatedEffectiveInputState(ChatTextInputState(inputText: $0.effectiveInputState.inputText, selectionRange: selectionRange.endIndex ..< selectionRange.endIndex)) - }) - }) - } - }) - strongSelf.present(controller, in: .window(.root)) - - strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { $0.updatedInputMode({ _ in return .none }) }) - } - }, reportPeerIrrelevantGeoLocation: { [weak self] in - guard let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation else { - return - } - - strongSelf.chatDisplayNode.dismissInput() - - let actions = [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { - }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.ReportGroupLocation_Report, action: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.reportIrrelvantGeoDisposable = (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) } - 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)) - 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) - } - }, 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 { - 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, 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 { - isBot = true - break - } - } - let type: PeerType - if isBot { - type = .bot - } else if let user = peer as? TelegramUser { - if user.botInfo != nil { - 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 strongSelf = self, let peerId = strongSelf.chatLocation.peerId else { - return - } - let _ = (updateChatTranslationStateInteractively(engine: strongSelf.context.engine, peerId: peerId, { current in - return current?.withIsEnabled(type == .translated) - }) - |> deliverOnMainQueue).startStandalone(completed: { [weak self] in - if let strongSelf = self, type == .translated { - Queue.mainQueue().after(0.15) { - strongSelf.chatDisplayNode.historyNode.refreshPollActionsForVisibleMessages() - } - } - }) - }, changeTranslationLanguage: { [weak self] langCode in - guard let strongSelf = self, let peerId = strongSelf.chatLocation.peerId else { - return - } - var langCode = langCode - if langCode == "nb" { - langCode = "no" - } else if langCode == "pt-br" { - langCode = "pt" - } - let _ = updateChatTranslationStateInteractively(engine: strongSelf.context.engine, peerId: peerId, { current in - return current?.withToLang(langCode).withIsEnabled(true) - }).startStandalone() - }, addDoNotTranslateLanguage: { [weak self] langCode in - guard let strongSelf = self, let peerId = strongSelf.chatLocation.peerId else { - return - } - let _ = updateTranslationSettingsInteractively(accountManager: strongSelf.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() - ignoredLanguages.insert(strongSelf.presentationData.strings.baseLanguageCode) - for language in systemLanguageCodes() { - ignoredLanguages.insert(language) - } - ignoredLanguages.insert(langCode) - updated.ignoredLanguages = Array(ignoredLanguages) - } - return updated - }).startStandalone() - let _ = updateChatTranslationStateInteractively(engine: strongSelf.context.engine, peerId: peerId, { current in - return nil - }).startStandalone() - - let presentationData = strongSelf.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) ?? "" - - strongSelf.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 strongSelf = self { - let controller = translationSettingsController(context: strongSelf.context) - controller.navigationPresentation = .modal - strongSelf.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 strongSelf = self, let peerId = strongSelf.chatLocation.peerId else { - return - } - strongSelf.presentAttachmentMenu(subject: .gift) - Queue.mainQueue().after(0.5) { - let _ = ApplicationSpecificNotice.incrementDismissedPremiumGiftSuggestion(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId, timestamp: Int32(Date().timeIntervalSince1970)).startStandalone() - } - }, openPremiumRequiredForMessaging: { [weak self] in - guard let self else { - return - } - let controller = PremiumIntroScreen(context: self.context, source: .settings) - self.push(controller) - }, openBoostToUnrestrict: { [weak self] in - guard let self, 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 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] namespace, silent in - if let strongSelf = self { - let inAppNotificationSettings = strongSelf.context.sharedContext.currentInAppNotificationSettings.with { $0 } - if inAppNotificationSettings.playSounds && !silent { - serviceSoundManager.playMessageDeliveredSound() - } - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && namespace == Namespaces.Message.ScheduledCloud { - strongSelf.openScheduledMessages() - } - - if strongSelf.shouldDisplayChecksTooltip { - Queue.mainQueue().after(1.0) { - strongSelf.displayChecksTooltip() - } - strongSelf.shouldDisplayChecksTooltip = false - strongSelf.checksTooltipDisposable.set(strongSelf.context.engine.notices.dismissServerProvidedSuggestion(suggestion: .newcomerTicks).startStrict()) - } - } - })) - - 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 - 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 in - if hasVisiblePlayableItemNodes && !isPlaybackActive && !hasActiveCalls { - return Signal { [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, 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: nil) }) - if let peerId = strongSelf.chatLocation.peerId { - updatedChatNavigationStack.insert(ChatNavigationStackItem(peerId: peerId, threadId: strongSelf.chatLocation.threadId), at: 0) - } - - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), 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) - }) - - self.displayNodeDidLoad() - } var storedAnimateFromSnapshotState: ChatControllerNode.SnapshotState? @@ -11955,6 +7354,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.storedAnimateFromSnapshotState = snapshotState } + override public func loadDisplayNode() { + self.loadDisplayNodeImpl() + } + override public func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) @@ -12820,518 +8223,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.navigationButtonAction(.cancelMessageSelection) } - func navigationButtonAction(_ action: ChatNavigationButtonAction) { - switch action { - case .spacer, .toggleInfoPanel: - break - case .cancelMessageSelection: - self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - case .clearHistory: - if case let .peer(peerId) = self.chatLocation { - let beginClear: (InteractiveHistoryClearingType) -> Void = { [weak self] type in - self?.beginClearHistory(type: type) - } - - let context = self.context - let _ = (self.context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.ParticipantCount(id: peerId), - TelegramEngine.EngineData.Item.Peer.CanDeleteHistory(id: peerId) - ) - |> map { participantCount, canDeleteHistory -> (isLargeGroupOrChannel: Bool, canClearChannel: Bool) in - if let participantCount = participantCount { - return (participantCount > 1000, canDeleteHistory) - } else { - return (false, false) - } - } - |> deliverOnMainQueue).startStandalone(next: { [weak self] parameters in - guard let strongSelf = self else { - return - } - - let (isLargeGroupOrChannel, canClearChannel) = parameters - - guard let peer = strongSelf.presentationInterfaceState.renderedPeer, let chatPeer = peer.peers[peer.peerId], let mainPeer = peer.chatMainPeer else { - return - } - - enum ClearType { - case savedMessages - case secretChat - case group - case channel - case user - } - - let canClearCache: Bool - let canClearForMyself: ClearType? - let canClearForEveryone: ClearType? - - if peerId == strongSelf.context.account.peerId { - canClearCache = false - canClearForMyself = .savedMessages - canClearForEveryone = nil - } else if chatPeer is TelegramSecretChat { - canClearCache = false - canClearForMyself = .secretChat - canClearForEveryone = nil - } else if let group = chatPeer as? TelegramGroup { - canClearCache = false - - switch group.role { - case .creator: - canClearForMyself = .group - canClearForEveryone = nil - case .admin, .member: - canClearForMyself = .group - canClearForEveryone = nil - } - } else if let channel = chatPeer as? TelegramChannel { - if let username = channel.addressName, !username.isEmpty { - if isLargeGroupOrChannel { - canClearCache = true - canClearForMyself = nil - canClearForEveryone = canClearChannel ? .channel : nil - } else { - canClearCache = true - canClearForMyself = nil - - switch channel.info { - case .broadcast: - if channel.flags.contains(.isCreator) { - canClearForEveryone = canClearChannel ? .channel : nil - } else { - canClearForEveryone = canClearChannel ? .channel : nil - } - case .group: - if channel.flags.contains(.isCreator) { - canClearForEveryone = canClearChannel ? .channel : nil - } else { - canClearForEveryone = canClearChannel ? .channel : nil - } - } - } - } else { - if isLargeGroupOrChannel { - switch channel.info { - case .broadcast: - canClearCache = true - - canClearForMyself = .channel - canClearForEveryone = nil - case .group: - canClearCache = false - - canClearForMyself = .channel - canClearForEveryone = nil - } - } else { - switch channel.info { - case .broadcast: - canClearCache = true - - if channel.flags.contains(.isCreator) { - canClearForMyself = .channel - canClearForEveryone = nil - } else { - canClearForMyself = .channel - canClearForEveryone = nil - } - case .group: - canClearCache = false - - if channel.flags.contains(.isCreator) { - canClearForMyself = .group - canClearForEveryone = nil - } else { - canClearForMyself = .group - canClearForEveryone = nil - } - } - } - } - } else { - canClearCache = false - canClearForMyself = .user - - if let user = chatPeer as? TelegramUser, user.botInfo != nil { - canClearForEveryone = nil - } else { - canClearForEveryone = .user - } - } - - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) - var items: [ActionSheetItem] = [] - - if case .scheduledMessages = strongSelf.presentationInterfaceState.subject { - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ScheduledMessages_ClearAllConfirmation, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - - guard let strongSelf = self else { - return - } - - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [ - TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { - }), - TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationAction, action: { - beginClear(.scheduledMessages) - }) - ], parseMarkdown: true), in: .window(.root)) - })) - } else { - if let _ = canClearForMyself ?? canClearForEveryone { - items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: EnginePeer(mainPeer), chatPeer: EnginePeer(chatPeer), action: .clearHistory(canClearCache: canClearCache), strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder)) - - if let canClearForEveryone = canClearForEveryone { - let text: String - let confirmationText: String - switch canClearForEveryone { - case .user: - text = strongSelf.presentationData.strings.ChatList_DeleteForEveryone(EnginePeer(mainPeer).compactDisplayTitle).string - confirmationText = strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationText - default: - text = strongSelf.presentationData.strings.Conversation_DeleteMessagesForEveryone - confirmationText = strongSelf.presentationData.strings.ChatList_DeleteForAllMembersConfirmationText - } - items.append(ActionSheetButtonItem(title: text, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - - guard let strongSelf = self else { - return - } - - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: confirmationText, actions: [ - TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { - }), - TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationAction, action: { - beginClear(.forEveryone) - }) - ], parseMarkdown: true), in: .window(.root)) - })) - } - if let canClearForMyself = canClearForMyself { - let text: String - switch canClearForMyself { - case .savedMessages, .secretChat: - text = strongSelf.presentationData.strings.Conversation_ClearAll - default: - text = strongSelf.presentationData.strings.ChatList_DeleteForCurrentUser - } - items.append(ActionSheetButtonItem(title: text, color: .destructive, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - if mainPeer.id == context.account.peerId, let strongSelf = self { - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [ - TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { - }), - TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationAction, action: { - beginClear(.forLocalPeer) - }) - ], parseMarkdown: true), in: .window(.root)) - } else { - beginClear(.forLocalPeer) - } - })) - } - } - - if canClearCache { - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_ClearCache, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - - guard let strongSelf = self else { - return - } - - strongSelf.navigationButtonAction(.clearCache) - })) - } - - if chatPeer.canSetupAutoremoveTimeout(accountPeerId: strongSelf.context.account.peerId) { - items.append(ActionSheetButtonItem(title: strongSelf.presentationInterfaceState.autoremoveTimeout == nil ? strongSelf.presentationData.strings.Conversation_AutoremoveActionEnable : strongSelf.presentationData.strings.Conversation_AutoremoveActionEdit, color: .accent, action: { [weak actionSheet] in - guard let actionSheet = actionSheet else { - return - } - guard let strongSelf = self else { - return - } - - actionSheet.dismissAnimated() - - strongSelf.presentAutoremoveSetup() - })) - } - } - - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(actionSheet, in: .window(.root)) - }) - } - case let .openChatInfo(expandAvatar, recommendedChannels): - let _ = self.presentVoiceMessageDiscardAlert(action: { - switch self.chatLocationInfoData { - case let .peer(peerView): - self.navigationActionDisposable.set((peerView.get() - |> take(1) - |> deliverOnMainQueue).startStrict(next: { [weak self] peerView in - if let strongSelf = self, let peer = peerView.peers[peerView.peerId], peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil && !strongSelf.presentationInterfaceState.isNotAccessible { - if peer.id == strongSelf.context.account.peerId { - if let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer, let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: true, requestsContext: nil) { - strongSelf.effectiveNavigationController?.pushViewController(infoController) - } - } else { - var expandAvatar = expandAvatar - if peer.smallProfileImage == nil { - expandAvatar = false - } - if let validLayout = strongSelf.validLayout, validLayout.deviceMetrics.type == .tablet { - expandAvatar = false - } - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer, mode: recommendedChannels ? .recommendedChannels : .generic, avatarInitiallyExpanded: expandAvatar, fromChat: true, requestsContext: strongSelf.inviteRequestsContext) { - strongSelf.effectiveNavigationController?.pushViewController(infoController) - } - } - } - })) - case .replyThread: - if let peer = self.presentationInterfaceState.renderedPeer?.peer, case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.peerId == self.context.account.peerId { - if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: peer, mode: .forumTopic(thread: replyThreadMessage), avatarInitiallyExpanded: false, fromChat: true, requestsContext: nil) { - self.effectiveNavigationController?.pushViewController(infoController) - } - } else if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum), case let .replyThread(message) = self.chatLocation { - if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: channel, mode: .forumTopic(thread: message), avatarInitiallyExpanded: false, fromChat: true, requestsContext: self.inviteRequestsContext) { - self.effectiveNavigationController?.pushViewController(infoController) - } - } - case .customChatContents: - break - } - }) - case .search: - self.interfaceInteraction?.beginMessageSearch(.everything, "") - case .dismiss: - if self.attemptNavigation({}) { - self.dismiss() - } - case .clearCache: - let controller = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: nil)) - self.present(controller, in: .window(.root)) - - let disposable: MetaDisposable - if let currentDisposable = self.clearCacheDisposable { - disposable = currentDisposable - } else { - disposable = MetaDisposable() - self.clearCacheDisposable = disposable - } - - switch self.chatLocationInfoData { - case let .peer(peerView): - self.navigationActionDisposable.set((peerView.get() - |> take(1) - |> deliverOnMainQueue).startStrict(next: { [weak self] peerView in - guard let strongSelf = self, let peer = peerView.peers[peerView.peerId] else { - return - } - let peerId = peer.id - - let _ = (strongSelf.context.engine.resources.collectCacheUsageStats(peerId: peer.id) - |> deliverOnMainQueue).startStandalone(next: { [weak self, weak controller] result in - controller?.dismiss() - - guard let strongSelf = self, case let .result(stats) = result, let categories = stats.media[peer.id] else { - return - } - let presentationData = strongSelf.presentationData - let controller = ActionSheetController(presentationData: presentationData) - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() - } - - var sizeIndex: [PeerCacheUsageCategory: (Bool, Int64)] = [:] - - var itemIndex = 1 - - var selectedSize: Int64 = 0 - let updateTotalSize: () -> Void = { [weak controller] in - controller?.updateItem(groupIndex: 0, itemIndex: itemIndex, { item in - let title: String - let filteredSize = sizeIndex.values.reduce(0, { $0 + ($1.0 ? $1.1 : 0) }) - selectedSize = filteredSize - - if filteredSize == 0 { - title = presentationData.strings.Cache_ClearNone - } else { - title = presentationData.strings.Cache_Clear("\(dataSizeString(filteredSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))").string - } - - if let item = item as? ActionSheetButtonItem { - return ActionSheetButtonItem(title: title, color: filteredSize != 0 ? .accent : .disabled, enabled: filteredSize != 0, action: item.action) - } - return item - }) - } - - let toggleCheck: (PeerCacheUsageCategory, Int) -> Void = { [weak controller] category, itemIndex in - if let (value, size) = sizeIndex[category] { - sizeIndex[category] = (!value, size) - } - controller?.updateItem(groupIndex: 0, itemIndex: itemIndex, { item in - if let item = item as? ActionSheetCheckboxItem { - return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action) - } - return item - }) - updateTotalSize() - } - var items: [ActionSheetItem] = [] - - items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: EnginePeer(peer), chatPeer: EnginePeer(peer), action: .clearCache, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder)) - - let validCategories: [PeerCacheUsageCategory] = [.image, .video, .audio, .file] - - var totalSize: Int64 = 0 - - func stringForCategory(strings: PresentationStrings, category: PeerCacheUsageCategory) -> String { - switch category { - case .image: - return strings.Cache_Photos - case .video: - return strings.Cache_Videos - case .audio: - return strings.Cache_Music - case .file: - return strings.Cache_Files - } - } - - for categoryId in validCategories { - if let media = categories[categoryId] { - var categorySize: Int64 = 0 - for (_, size) in media { - categorySize += size - } - sizeIndex[categoryId] = (true, categorySize) - totalSize += categorySize - if categorySize > 1024 { - let index = itemIndex - items.append(ActionSheetCheckboxItem(title: stringForCategory(strings: presentationData.strings, category: categoryId), label: dataSizeString(categorySize, formatting: DataSizeStringFormatting(presentationData: presentationData)), value: true, action: { value in - toggleCheck(categoryId, index) - })) - itemIndex += 1 - } - } - } - selectedSize = totalSize - - if items.isEmpty { - strongSelf.presentClearCacheSuggestion() - } else { - items.append(ActionSheetButtonItem(title: presentationData.strings.Cache_Clear("\(dataSizeString(totalSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))").string, action: { - let clearCategories = sizeIndex.keys.filter({ sizeIndex[$0]!.0 }) - var clearMediaIds = Set() - - var media = stats.media - if var categories = media[peerId] { - for category in clearCategories { - if let contents = categories[category] { - for (mediaId, _) in contents { - clearMediaIds.insert(mediaId) - } - } - categories.removeValue(forKey: category) - } - - media[peerId] = categories - } - - var clearResourceIds = Set() - for id in clearMediaIds { - if let ids = stats.mediaResourceIds[id] { - for resourceId in ids { - clearResourceIds.insert(resourceId) - } - } - } - - var signal = strongSelf.context.engine.resources.clearCachedMediaResources(mediaResourceIds: clearResourceIds) - - var cancelImpl: (() -> Void)? - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - let progressSignal = Signal { subscriber in - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { - cancelImpl?() - })) - strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - return ActionDisposable { [weak controller] in - Queue.mainQueue().async() { - controller?.dismiss() - } - } - } - |> runOn(Queue.mainQueue()) - |> delay(0.15, queue: Queue.mainQueue()) - let progressDisposable = progressSignal.startStrict() - - signal = signal - |> afterDisposed { - Queue.mainQueue().async { - progressDisposable.dispose() - } - } - cancelImpl = { - disposable.set(nil) - } - disposable.set((signal - |> deliverOnMainQueue).startStrict(completed: { [weak self] in - if let strongSelf = self, let _ = strongSelf.validLayout { - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))", stringForDeviceType()).string, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false }), in: .current) - } - })) - - dismissAction() - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) - })) - - items.append(ActionSheetButtonItem(title: presentationData.strings.ClearCache_StorageUsage, action: { [weak self] in - dismissAction() - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) - - if let strongSelf = self { - let context = strongSelf.context - let controller = StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { category in - return storageUsageExceptionsScreen(context: context, category: category) - }) - strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - } - })) - - controller.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) - ]) - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(controller, in: .window(.root)) - } - }) - })) - case .replyThread: - break - case .customChatContents: - break - } - case .edit: - self.editChat() - } - } - func editMessageMediaWithMessages(_ messages: [EnqueueMessage]) { if let message = messages.first, case let .message(text, attributes, _, maybeMediaReference, _, _, _, _, _, _) = message, let mediaReference = maybeMediaReference { self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in @@ -14485,427 +9376,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.dismissMediaRecorder(.pause) } - func requestAudioRecorder(beginWithTone: Bool) { - if self.audioRecorderValue == nil { - if self.recorderFeedback == nil { - self.recorderFeedback = HapticFeedback() - self.recorderFeedback?.prepareImpact(.light) - } - - self.audioRecorder.set(self.context.sharedContext.mediaManager.audioRecorder(beginWithTone: beginWithTone, applicationBindings: self.context.sharedContext.applicationBindings, beganWithTone: { _ in - })) - } - } - - func requestVideoRecorder() { - if self.videoRecorderValue == nil { - if let currentInputPanelFrame = self.chatDisplayNode.currentInputPanelFrame() { - if self.recorderFeedback == nil { - self.recorderFeedback = HapticFeedback() - self.recorderFeedback?.prepareImpact(.light) - } - - var isScheduledMessages = false - if case .scheduledMessages = self.presentationInterfaceState.subject { - isScheduledMessages = true - } - - var isBot = false - - var allowLiveUpload = false - var viewOnceAvailable = false - if let peerId = self.chatLocation.peerId { - allowLiveUpload = peerId.namespace != Namespaces.Peer.SecretChat - viewOnceAvailable = !isScheduledMessages && peerId.namespace == Namespaces.Peer.CloudUser && peerId != self.context.account.peerId && !isBot - } else if case .customChatContents = self.chatLocation { - allowLiveUpload = true - } - - if let user = self.presentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil { - isBot = true - } - - let controller = VideoMessageCameraScreen( - context: self.context, - updatedPresentationData: self.updatedPresentationData, - allowLiveUpload: allowLiveUpload, - viewOnceAvailable: viewOnceAvailable, - inputPanelFrame: (currentInputPanelFrame, self.chatDisplayNode.inputNode != nil), - chatNode: self.chatDisplayNode.historyNode, - completion: { [weak self] message, silentPosting, scheduleTime in - guard let self, let videoController = self.videoRecorderValue else { - return - } - - guard var message else { - self.recorderFeedback?.error() - self.recorderFeedback = nil - self.videoRecorder.set(.single(nil)) - return - } - - let replyMessageSubject = self.presentationInterfaceState.interfaceState.replyMessageSubject - let correlationId = Int64.random(in: 0 ..< Int64.max) - message = message - .withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) - .withUpdatedCorrelationId(correlationId) - - var usedCorrelationId = false - if scheduleTime == nil, self.chatDisplayNode.shouldAnimateMessageTransition, let extractedView = videoController.extractVideoSnapshot() { - usedCorrelationId = true - self.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .videoMessage(ChatMessageTransitionNodeImpl.Source.VideoMessage(view: extractedView)), initiated: { [weak videoController, weak self] in - videoController?.hideVideoSnapshot() - guard let self else { - return - } - self.videoRecorder.set(.single(nil)) - }) - } else { - self.videoRecorder.set(.single(nil)) - } - - self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in - if let self { - self.chatDisplayNode.collapseInput() - - self.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedMediaDraftState(nil) } - }) - } - }, usedCorrelationId ? correlationId : nil) - - let messages = [message] - let transformedMessages: [EnqueueMessage] - if let silentPosting { - transformedMessages = self.transformEnqueueMessages(messages, silentPosting: silentPosting) - } else if let scheduleTime { - transformedMessages = self.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime) - } else { - transformedMessages = self.transformEnqueueMessages(messages) - } - - self.sendMessages(transformedMessages) - } - ) - controller.onResume = { [weak self] in - guard let self else { - return - } - self.resumeMediaRecorder() - } - self.videoRecorder.set(.single(controller)) - } - } - } - - func dismissMediaRecorder(_ action: ChatFinishMediaRecordingAction) { - var updatedAction = action - var isScheduledMessages = false - if case .scheduledMessages = self.presentationInterfaceState.subject { - isScheduledMessages = true - } - - if let _ = self.presentationInterfaceState.slowmodeState, !isScheduledMessages { - updatedAction = .preview - } - - if let audioRecorderValue = self.audioRecorderValue { - switch action { - case .pause: - audioRecorderValue.pause() - default: - audioRecorderValue.stop() - } - - switch updatedAction { - case .dismiss: - self.recorderDataDisposable.set(nil) - self.chatDisplayNode.updateRecordedMediaDeleted(true) - self.audioRecorder.set(.single(nil)) - case .preview, .pause: - if case .preview = updatedAction { - self.audioRecorder.set(.single(nil)) - } - self.updateChatPresentationInterfaceState(animated: true, interactive: true, { - $0.updatedInputTextPanelState { panelState in - return panelState.withUpdatedMediaRecordingState(.waitingForPreview) - } - }) - self.recorderDataDisposable.set((audioRecorderValue.takenRecordedData() - |> deliverOnMainQueue).startStrict(next: { [weak self] data in - if let strongSelf = self, let data = data { - if data.duration < 0.5 { - strongSelf.recorderFeedback?.error() - strongSelf.recorderFeedback = nil - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - $0.updatedInputTextPanelState { panelState in - return panelState.withUpdatedMediaRecordingState(nil) - } - }) - strongSelf.recorderDataDisposable.set(nil) - } else if let waveform = data.waveform { - let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max), size: Int64(data.compressedData.count)) - - strongSelf.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data.compressedData) - - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - $0.updatedInterfaceState { $0.withUpdatedMediaDraftState(.audio(ChatInterfaceMediaDraftState.Audio(resource: resource, fileSize: Int32(data.compressedData.count), duration: Int32(data.duration), waveform: AudioWaveform(bitstream: waveform, bitsPerSample: 5)))) }.updatedInputTextPanelState { panelState in - return panelState.withUpdatedMediaRecordingState(nil) - } - }) - strongSelf.recorderFeedback = nil - strongSelf.updateDownButtonVisibility() - strongSelf.recorderDataDisposable.set(nil) - } - } - })) - case let .send(viewOnce): - self.chatDisplayNode.updateRecordedMediaDeleted(false) - self.recorderDataDisposable.set((audioRecorderValue.takenRecordedData() - |> deliverOnMainQueue).startStrict(next: { [weak self] data in - if let strongSelf = self, let data = data { - if data.duration < 0.5 { - strongSelf.recorderFeedback?.error() - strongSelf.recorderFeedback = nil - strongSelf.audioRecorder.set(.single(nil)) - } else { - let randomId = Int64.random(in: Int64.min ... Int64.max) - - let resource = LocalFileMediaResource(fileId: randomId) - strongSelf.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data.compressedData) - - let waveformBuffer: Data? = data.waveform - - let correlationId = Int64.random(in: 0 ..< Int64.max) - var usedCorrelationId = false - - if strongSelf.chatDisplayNode.shouldAnimateMessageTransition, let textInputPanelNode = strongSelf.chatDisplayNode.textInputPanelNode, let micButton = textInputPanelNode.micButton { - usedCorrelationId = true - strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .audioMicInput(ChatMessageTransitionNodeImpl.Source.AudioMicInput(micButton: micButton)), initiated: { - guard let strongSelf = self else { - return - } - strongSelf.audioRecorder.set(.single(nil)) - }) - } else { - strongSelf.audioRecorder.set(.single(nil)) - } - - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.chatDisplayNode.collapseInput() - - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } - }) - } - }, usedCorrelationId ? correlationId : nil) - - var attributes: [MessageAttribute] = [] - if viewOnce { - attributes.append(AutoremoveTimeoutMessageAttribute(timeout: viewOnceTimeout, countdownBeginTime: nil)) - } - - strongSelf.sendMessages([.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)])), threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: [])]) - - strongSelf.recorderFeedback?.tap() - strongSelf.recorderFeedback = nil - strongSelf.recorderDataDisposable.set(nil) - } - } - })) - } - } else if let videoRecorderValue = self.videoRecorderValue { - if case .send = updatedAction { - self.chatDisplayNode.updateRecordedMediaDeleted(false) - videoRecorderValue.sendVideoRecording() - self.recorderDataDisposable.set(nil) - } else { - if case .dismiss = updatedAction { - self.chatDisplayNode.updateRecordedMediaDeleted(true) - self.recorderDataDisposable.set(nil) - } - - switch updatedAction { - case .preview, .pause: - if videoRecorderValue.stopVideoRecording() { - self.recorderDataDisposable.set((videoRecorderValue.takenRecordedData() - |> deliverOnMainQueue).startStrict(next: { [weak self] data in - if let strongSelf = self, let data = data { - if data.duration < 1.0 { - strongSelf.recorderFeedback?.error() - strongSelf.recorderFeedback = nil - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - $0.updatedInputTextPanelState { panelState in - return panelState.withUpdatedMediaRecordingState(nil) - } - }) - strongSelf.recorderDataDisposable.set(nil) - strongSelf.videoRecorder.set(.single(nil)) - } else { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - $0.updatedInterfaceState { - $0.withUpdatedMediaDraftState(.video( - ChatInterfaceMediaDraftState.Video( - duration: Int32(data.duration), - frames: data.frames, - framesUpdateTimestamp: data.framesUpdateTimestamp, - trimRange: data.trimRange - ) - )) - }.updatedInputTextPanelState { panelState in - return panelState.withUpdatedMediaRecordingState(nil) - } - }) - strongSelf.recorderFeedback = nil - strongSelf.updateDownButtonVisibility() - } - } - })) - } - default: - self.recorderDataDisposable.set(nil) - self.videoRecorder.set(.single(nil)) - } - } - } - } - - func stopMediaRecorder(pause: Bool = false) { - if let audioRecorderValue = self.audioRecorderValue { - if let _ = self.presentationInterfaceState.inputTextPanelState.mediaRecordingState { - self.dismissMediaRecorder(pause ? .pause : .preview) - } else { - audioRecorderValue.stop() - self.audioRecorder.set(.single(nil)) - } - } else if let _ = self.videoRecorderValue { - if let _ = self.presentationInterfaceState.inputTextPanelState.mediaRecordingState { - self.dismissMediaRecorder(pause ? .pause : .preview) - } else { - self.videoRecorder.set(.single(nil)) - } - } - } - - func resumeMediaRecorder() { - self.context.sharedContext.mediaManager.playlistControl(.playback(.pause), type: nil) - - if let audioRecorderValue = self.audioRecorderValue { - audioRecorderValue.resume() - - self.updateChatPresentationInterfaceState(animated: true, interactive: true, { - $0.updatedInputTextPanelState { panelState in - return panelState.withUpdatedMediaRecordingState(.audio(recorder: audioRecorderValue, isLocked: true)) - }.updatedInterfaceState { $0.withUpdatedMediaDraftState(nil) } - }) - } else if let videoRecorderValue = self.videoRecorderValue { - self.updateChatPresentationInterfaceState(animated: true, interactive: true, { - $0.updatedInputTextPanelState { panelState in - let recordingStatus = videoRecorderValue.recordingStatus - return panelState.withUpdatedMediaRecordingState(.video(status: .recording(InstantVideoControllerRecordingStatus(micLevel: recordingStatus.micLevel, duration: recordingStatus.duration)), isLocked: true)) - }.updatedInterfaceState { $0.withUpdatedMediaDraftState(nil) } - }) - } - } - - func lockMediaRecorder() { - if self.presentationInterfaceState.inputTextPanelState.mediaRecordingState != nil { - self.updateChatPresentationInterfaceState(animated: true, interactive: true, { - return $0.updatedInputTextPanelState { panelState in - return panelState.withUpdatedMediaRecordingState(panelState.mediaRecordingState?.withLocked(true)) - } - }) - } - - self.videoRecorderValue?.lockVideoRecording() - } - - func deleteMediaRecording() { - if let _ = self.audioRecorderValue { - self.audioRecorder.set(.single(nil)) - } else if let _ = self.videoRecorderValue { - self.videoRecorder.set(.single(nil)) - } - - self.recorderDataDisposable.set(nil) - self.chatDisplayNode.updateRecordedMediaDeleted(true) - self.updateChatPresentationInterfaceState(animated: true, interactive: true, { - $0.updatedInterfaceState { $0.withUpdatedMediaDraftState(nil) } - }) - self.updateDownButtonVisibility() - } - - func sendMediaRecording(silentPosting: Bool? = nil, scheduleTime: Int32? = nil, viewOnce: Bool = false) { - self.chatDisplayNode.updateRecordedMediaDeleted(false) - - guard let recordedMediaPreview = self.presentationInterfaceState.interfaceState.mediaDraftState else { - return - } - - switch recordedMediaPreview { - case let .audio(audio): - self.audioRecorder.set(.single(nil)) - - var isScheduledMessages = false - if case .scheduledMessages = self.presentationInterfaceState.subject { - isScheduledMessages = true - } - - if let _ = self.presentationInterfaceState.slowmodeState, !isScheduledMessages { - if let rect = self.chatDisplayNode.frameForInputActionButton() { - self.interfaceInteraction?.displaySlowmodeTooltip(self.chatDisplayNode.view, rect) - } - return - } - - let waveformBuffer = audio.waveform.makeBitstream() - - self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in - if let strongSelf = self { - strongSelf.chatDisplayNode.collapseInput() - - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedMediaDraftState(nil) } - }) - - strongSelf.updateDownButtonVisibility() - } - }, nil) - - var attributes: [MessageAttribute] = [] - if viewOnce { - attributes.append(AutoremoveTimeoutMessageAttribute(timeout: viewOnceTimeout, countdownBeginTime: nil)) - } - - let messages: [EnqueueMessage] = [.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: audio.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(audio.fileSize), attributes: [.Audio(isVoice: true, duration: Int(audio.duration), title: nil, performer: nil, waveform: waveformBuffer)])), threadId: self.chatLocation.threadId, replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] - - let transformedMessages: [EnqueueMessage] - if let silentPosting = silentPosting { - transformedMessages = self.transformEnqueueMessages(messages, silentPosting: silentPosting) - } else if let scheduleTime = scheduleTime { - transformedMessages = self.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime) - } else { - transformedMessages = self.transformEnqueueMessages(messages) - } - - guard let peerId = self.chatLocation.peerId else { - return - } - - let _ = (enqueueMessages(account: self.context.account, peerId: peerId, messages: transformedMessages) - |> deliverOnMainQueue).startStandalone(next: { [weak self] _ in - if let strongSelf = self, strongSelf.presentationInterfaceState.subject != .scheduledMessages { - strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() - } - }) - - donateSendMessageIntent(account: self.context.account, sharedContext: self.context.sharedContext, intentContext: .chat, peerIds: [peerId]) - case .video: - self.videoRecorderValue?.sendVideoRecording(silentPosting: silentPosting, scheduleTime: scheduleTime) - } - } - func updateDownButtonVisibility() { let recordingMediaMessage = self.audioRecorderValue != nil || self.videoRecorderValue != nil || self.presentationInterfaceState.interfaceState.mediaDraftState != nil @@ -14954,183 +9424,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.navigateToMessage(from: nil, to: messageLocation, scrollPosition: scrollPosition, rememberInStack: false, forceInCurrentChat: forceInCurrentChat, dropStack: dropStack, animated: animated, completion: completion, customPresentProgress: customPresentProgress) } - func openPeer(peer: EnginePeer?, navigation: ChatControllerInteractionNavigateToPeer, fromMessage: MessageReference?, fromReactionMessageId: MessageId? = nil, expandAvatar: Bool = false, peerTypes: ReplyMarkupButtonAction.PeerTypes? = nil) { - let _ = self.presentVoiceMessageDiscardAlert(action: { - if case let .peer(currentPeerId) = self.chatLocation, peer?.id == currentPeerId { - switch navigation { - case let .info(params): - var recommendedChannels = false - if let params, params.switchToRecommendedChannels { - recommendedChannels = true - } - self.navigationButtonAction(.openChatInfo(expandAvatar: expandAvatar, recommendedChannels: recommendedChannels)) - case let .chat(textInputState, _, _): - if let textInputState = textInputState { - self.updateChatPresentationInterfaceState(animated: true, interactive: true, { - return ($0.updatedInterfaceState { - return $0.withUpdatedComposeInputState(textInputState) - }).updatedInputMode({ _ in - return .text - }) - }) - } else { - self.playShakeAnimation() - } - case let .withBotStartPayload(botStart): - self.updateChatPresentationInterfaceState(animated: true, interactive: true, { - $0.updatedBotStartPayload(botStart.payload) - }) - case .withAttachBot: - self.presentAttachmentMenu(subject: .default) - default: - break - } - } else { - if let peer = peer { - do { - var chatPeerId: PeerId? - if let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramGroup { - chatPeerId = peer.id - } else if let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramChannel, case .group = peer.info, case .member = peer.participationStatus { - chatPeerId = peer.id - } - - switch navigation { - case .info, .default: - let peerSignal: Signal - if let messageId = fromMessage?.id { - peerSignal = loadedPeerFromMessage(account: self.context.account, peerId: peer.id, messageId: messageId) - } else { - peerSignal = self.context.account.postbox.loadedPeerWithId(peer.id) |> map(Optional.init) - } - self.navigationActionDisposable.set((peerSignal |> take(1) |> deliverOnMainQueue).startStrict(next: { [weak self] peer in - if let strongSelf = self, let peer = peer { - var mode: PeerInfoControllerMode = .generic - if let _ = fromMessage, let chatPeerId = chatPeerId { - mode = .group(chatPeerId) - } - if let fromReactionMessageId = fromReactionMessageId { - mode = .reaction(fromReactionMessageId) - } - if case let .info(params) = navigation, let params, params.switchToRecommendedChannels { - mode = .recommendedChannels - } - var expandAvatar = expandAvatar - if peer.smallProfileImage == nil { - expandAvatar = false - } - if let validLayout = strongSelf.validLayout, validLayout.deviceMetrics.type == .tablet { - expandAvatar = false - } - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer, mode: mode, avatarInitiallyExpanded: expandAvatar, fromChat: false, requestsContext: nil) { - strongSelf.effectiveNavigationController?.pushViewController(infoController) - } - } - })) - case let .chat(textInputState, subject, peekData): - if let textInputState = textInputState { - let _ = (ChatInterfaceState.update(engine: self.context.engine, peerId: peer.id, threadId: nil, { currentState in - return currentState.withUpdatedComposeInputState(textInputState) - }) - |> deliverOnMainQueue).startStandalone(completed: { [weak self] in - if let strongSelf = self, let navigationController = strongSelf.effectiveNavigationController { - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), subject: subject, updateTextInputState: textInputState, peekData: peekData)) - } - }) - } else { - if case let .channel(channel) = peer, channel.flags.contains(.isForum) { - self.effectiveNavigationController?.pushViewController(ChatListControllerImpl(context: self.context, location: .forum(peerId: channel.id), controlsHistoryPreload: false, enableDebugActions: false)) - } else { - self.effectiveNavigationController?.pushViewController(ChatControllerImpl(context: self.context, chatLocation: .peer(id: peer.id), subject: subject)) - } - } - case let .withBotStartPayload(botStart): - self.effectiveNavigationController?.pushViewController(ChatControllerImpl(context: self.context, chatLocation: .peer(id: peer.id), botStart: botStart)) - case let .withAttachBot(attachBotStart): - if let navigationController = self.effectiveNavigationController { - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), attachBotStart: attachBotStart)) - } - case let .withBotApp(botAppStart): - if let navigationController = self.effectiveNavigationController { - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), botAppStart: botAppStart)) - } - } - } - } else { - switch navigation { - case .info: - break - case let .chat(textInputState, _, _): - if let textInputState = textInputState { - let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, updatedPresentationData: self.updatedPresentationData, requestPeerType: peerTypes.flatMap { $0.requestPeerTypes }, selectForumThreads: true)) - controller.peerSelected = { [weak self, weak controller] peer, threadId in - let peerId = peer.id - - if let strongSelf = self, let strongController = controller { - if case let .peer(currentPeerId) = strongSelf.chatLocation, peerId == currentPeerId { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - return ($0.updatedInterfaceState { - return $0.withUpdatedComposeInputState(textInputState) - }).updatedInputMode({ _ in - return .text - }) - }) - strongController.dismiss() - } else { - let _ = (ChatInterfaceState.update(engine: strongSelf.context.engine, peerId: peerId, threadId: threadId, { currentState in - return currentState.withUpdatedComposeInputState(textInputState) - }) - |> deliverOnMainQueue).startStandalone(completed: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) - - if let navigationController = strongSelf.effectiveNavigationController { - let chatController: Signal - if let threadId { - chatController = chatControllerForForumThreadImpl(context: strongSelf.context, peerId: peerId, threadId: threadId) - } else { - chatController = .single(ChatControllerImpl(context: strongSelf.context, chatLocation: .peer(id: peerId))) - } - - let _ = (chatController - |> deliverOnMainQueue).start(next: { [weak self, weak navigationController] chatController in - guard let strongSelf = self, let navigationController else { - return - } - var viewControllers = navigationController.viewControllers - let lastController = viewControllers.last as! ViewController - if threadId != nil { - viewControllers.remove(at: viewControllers.count - 2) - lastController.navigationPresentation = .modal - } - viewControllers.insert(chatController, at: viewControllers.count - 1) - navigationController.setViewControllers(viewControllers, animated: false) - - strongSelf.controllerNavigationDisposable.set((chatController.ready.get() - |> filter { $0 } - |> take(1) - |> deliverOnMainQueue).startStrict(next: { [weak lastController] _ in - lastController?.dismiss() - })) - }) - } - }) - } - } - } - self.chatDisplayNode.dismissInput() - self.effectiveNavigationController?.pushViewController(controller) - } - default: - break - } - } - } - }) - } - func openStories(peerId: EnginePeer.Id, avatarHeaderNode: ChatMessageAvatarHeaderNodeImpl?, avatarNode: AvatarNode?) { if let avatarNode = avatarHeaderNode?.avatarNode ?? avatarNode { StoryContainerScreen.openPeerStories(context: self.context, peerId: peerId, parentController: self, avatarNode: avatarNode) @@ -15270,144 +9563,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - func unblockPeer() { - guard case let .peer(peerId) = self.chatLocation else { - return - } - let unblockingPeer = self.unblockingPeer - unblockingPeer.set(true) - - var restartBot = false - if let user = self.presentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil { - restartBot = true - } - self.editMessageDisposable.set((self.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peerId, isBlocked: false) - |> afterDisposed({ [weak self] in - Queue.mainQueue().async { - unblockingPeer.set(false) - if let strongSelf = self, restartBot { - strongSelf.startBot(strongSelf.presentationInterfaceState.botStartPayload) - } - } - })).startStrict()) - } - - func reportPeer() { - guard let renderedPeer = self.presentationInterfaceState.renderedPeer, let peer = renderedPeer.chatMainPeer, let chatPeer = renderedPeer.peer else { - return - } - self.chatDisplayNode.dismissInput() - - if let peer = peer as? TelegramChannel, let username = peer.addressName, !username.isEmpty { - let actionSheet = ActionSheetController(presentationData: self.presentationData) - - var items: [ActionSheetItem] = [] - items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ReportSpamAndLeave, color: .destructive, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.deleteChat(reportChatSpam: true) - } - })) - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - - self.present(actionSheet, in: .window(.root)) - } else if let _ = peer as? TelegramUser { - let presentationData = self.presentationData - let controller = ActionSheetController(presentationData: presentationData) - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() - } - var reportSpam = true - var deleteChat = true - var items: [ActionSheetItem] = [] - if !peer.isDeleted { - items.append(ActionSheetTextItem(title: presentationData.strings.UserInfo_BlockConfirmationTitle(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 - }) - }), - ActionSheetCheckboxItem(title: presentationData.strings.ReportSpam_DeleteThisChat, label: "", value: deleteChat, action: { [weak controller] checkValue in - deleteChat = checkValue - controller?.updateItem(groupIndex: 0, itemIndex: 2, { item in - if let item = item as? ActionSheetCheckboxItem { - return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action) - } - return item - }) - }), - ActionSheetButtonItem(title: presentationData.strings.UserInfo_BlockActionTitle(EnginePeer(peer).compactDisplayTitle).string, color: .destructive, action: { [weak self] in - dismissAction() - guard let strongSelf = self else { - return - } - let _ = strongSelf.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peer.id, isBlocked: true).startStandalone() - if let _ = chatPeer as? TelegramSecretChat { - let _ = strongSelf.context.engine.peers.terminateSecretChat(peerId: chatPeer.id, requestRemoteHistoryRemoval: true).startStandalone() - } - if deleteChat { - let _ = strongSelf.context.engine.peers.removePeerChat(peerId: chatPeer.id, reportChatSpam: reportSpam).startStandalone() - strongSelf.effectiveNavigationController?.filterController(strongSelf, animated: true) - } else if reportSpam { - let _ = strongSelf.context.engine.peers.reportPeer(peerId: peer.id, reason: .spam, message: "").startStandalone() - } - }) - ] as [ActionSheetItem]) - - controller.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) - ]) - self.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - } else { - let title: String - var infoString: String? - if let _ = peer as? TelegramGroup { - title = self.presentationData.strings.Conversation_ReportSpamAndLeave - infoString = self.presentationData.strings.Conversation_ReportSpamGroupConfirmation - } else if let channel = peer as? TelegramChannel { - title = self.presentationData.strings.Conversation_ReportSpamAndLeave - if case .group = channel.info { - infoString = self.presentationData.strings.Conversation_ReportSpamGroupConfirmation - } else { - infoString = self.presentationData.strings.Conversation_ReportSpamChannelConfirmation - } - } else { - title = self.presentationData.strings.Conversation_ReportSpam - infoString = self.presentationData.strings.Conversation_ReportSpamConfirmation - } - let actionSheet = ActionSheetController(presentationData: self.presentationData) - - var items: [ActionSheetItem] = [] - if let infoString = infoString { - items.append(ActionSheetTextItem(title: infoString)) - } - items.append(ActionSheetButtonItem(title: title, color: .destructive, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.deleteChat(reportChatSpam: true) - } - })) - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - - self.present(actionSheet, in: .window(.root)) - } - } - func shareAccountContact() { let _ = (self.context.account.postbox.loadedPeerWithId(self.context.account.peerId) |> deliverOnMainQueue).startStandalone(next: { [weak self] accountPeer in @@ -16481,241 +10636,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - public func presentThemeSelection() { - guard self.themeScreen == nil else { - return - } - let context = self.context - let peerId = self.chatLocation.peerId - - self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in - var updated = state - updated = updated.updatedInputMode({ _ in - return .none - }) - updated = updated.updatedShowCommands(false) - return updated - }) - - let animatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false) - |> map { animatedEmoji -> [String: [StickerPackItem]] in - var animatedEmojiStickers: [String: [StickerPackItem]] = [:] - switch animatedEmoji { - case let .result(_, items, _): - for item in items { - if let emoji = item.getStringRepresentationsOfIndexKeys().first { - animatedEmojiStickers[emoji.basicEmoji.0] = [item] - let strippedEmoji = emoji.basicEmoji.0.strippedEmoji - if animatedEmojiStickers[strippedEmoji] == nil { - animatedEmojiStickers[strippedEmoji] = [item] - } - } - } - default: - break - } - return animatedEmojiStickers - } - - let _ = (combineLatest(queue: Queue.mainQueue(), self.chatThemeEmoticonPromise.get(), animatedEmojiStickers) - |> take(1)).startStandalone(next: { [weak self] themeEmoticon, animatedEmojiStickers in - guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { - return - } - - var canResetWallpaper = false - if let cachedUserData = strongSelf.peerView?.cachedData as? CachedUserData { - canResetWallpaper = cachedUserData.wallpaper != nil - } - - let controller = ChatThemeScreen( - context: context, - updatedPresentationData: strongSelf.updatedPresentationData, - animatedEmojiStickers: animatedEmojiStickers, - initiallySelectedEmoticon: themeEmoticon, - peerName: strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer.flatMap(EnginePeer.init)?.compactDisplayTitle ?? "", - canResetWallpaper: canResetWallpaper, - previewTheme: { [weak self] emoticon, dark in - if let strongSelf = self { - strongSelf.presentCrossfadeSnapshot() - strongSelf.themeEmoticonAndDarkAppearancePreviewPromise.set(.single((emoticon, dark))) - } - }, - changeWallpaper: { [weak self] in - guard let strongSelf = self, let peerId else { - return - } - if let themeController = strongSelf.themeScreen { - strongSelf.themeScreen = nil - themeController.dimTapped() - } - let dismissControllers = { [weak self] in - if let self, let navigationController = self.navigationController as? NavigationController { - let controllers = navigationController.viewControllers.filter({ controller in - if controller is WallpaperGalleryController || controller is AttachmentController { - return false - } - return true - }) - navigationController.setViewControllers(controllers, animated: true) - } - } - var openWallpaperPickerImpl: ((Bool) -> Void)? - let openWallpaperPicker = { [weak self] animateAppearance in - guard let strongSelf = self else { - return - } - let controller = wallpaperMediaPickerController( - context: strongSelf.context, - updatedPresentationData: strongSelf.updatedPresentationData, - peer: EnginePeer(peer), - animateAppearance: animateAppearance, - completion: { [weak self] _, result in - guard let strongSelf = self, let asset = result as? PHAsset else { - return - } - let controller = WallpaperGalleryController(context: strongSelf.context, source: .asset(asset), mode: .peer(EnginePeer(peer), false)) - controller.navigationPresentation = .modal - controller.apply = { [weak self] wallpaper, options, editedImage, cropRect, brightness, forBoth in - if let strongSelf = self { - uploadCustomPeerWallpaper(context: strongSelf.context, wallpaper: wallpaper, mode: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness, peerId: peerId, forBoth: forBoth, completion: { - Queue.mainQueue().after(0.3, { - dismissControllers() - }) - }) - } - } - strongSelf.push(controller) - }, - openColors: { [weak self] in - guard let strongSelf = self else { - return - } - let controller = standaloneColorPickerController(context: strongSelf.context, peer: EnginePeer(peer), push: { [weak self] controller in - if let strongSelf = self { - strongSelf.push(controller) - } - }, openGallery: { - openWallpaperPickerImpl?(false) - }) - controller.navigationPresentation = .flatModal - strongSelf.push(controller) - } - ) - controller.navigationPresentation = .flatModal - strongSelf.push(controller) - } - openWallpaperPickerImpl = openWallpaperPicker - openWallpaperPicker(true) - }, - resetWallpaper: { [weak self] in - guard let strongSelf = self, let peerId else { - return - } - let _ = strongSelf.context.engine.themes.setChatWallpaper(peerId: peerId, wallpaper: nil, forBoth: false).startStandalone() - }, - completion: { [weak self] emoticon in - guard let strongSelf = self, let peerId else { - return - } - if canResetWallpaper && emoticon != nil { - let _ = context.engine.themes.setChatWallpaper(peerId: peerId, wallpaper: nil, forBoth: false).startStandalone() - } - strongSelf.themeEmoticonAndDarkAppearancePreviewPromise.set(.single((emoticon ?? "", nil))) - let _ = context.engine.themes.setChatTheme(peerId: peerId, emoticon: emoticon).startStandalone(completed: { [weak self] in - if let strongSelf = self { - strongSelf.themeEmoticonAndDarkAppearancePreviewPromise.set(.single((nil, nil))) - } - }) - } - ) - controller.navigationPresentation = .flatModal - controller.passthroughHitTestImpl = { [weak self] _ in - if let strongSelf = self { - return strongSelf.chatDisplayNode.historyNode.view - } else { - return nil - } - } - controller.dismissed = { [weak self] in - if let strongSelf = self { - strongSelf.chatDisplayNode.historyNode.tapped = nil - } - } - strongSelf.chatDisplayNode.historyNode.tapped = { [weak controller] in - controller?.dimTapped() - } - strongSelf.push(controller) - strongSelf.themeScreen = controller - }) - } - - func presentEmojiList(references: [StickerPackReference]) { - guard let packReference = references.first else { - return - } - self.chatDisplayNode.dismissTextInput() - - let presentationData = self.presentationData - let controller = StickerPackScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, mainStickerPack: packReference, stickerPacks: Array(references), parentNavigationController: self.effectiveNavigationController, sendEmoji: canSendMessagesToChat(self.presentationInterfaceState) ? { [weak self] text, attribute in - if let strongSelf = self { - strongSelf.controllerInteraction?.sendEmoji(text, attribute, false) - } - } : nil, actionPerformed: { [weak self] actions in - guard let strongSelf = self else { - return - } - let context = strongSelf.context - if actions.count > 1, let first = actions.first { - if case .add = first.2 { - strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.EmojiPackActionInfo_AddedTitle, text: presentationData.strings.EmojiPackActionInfo_MultipleAddedText(Int32(actions.count)), undo: false, info: first.0, topItem: first.1.first, context: context), elevatedLayout: true, animateInAsReplacement: false, action: { _ in - return true - })) - } else if actions.allSatisfy({ - if case .remove = $0.2 { - return true - } else { - return false - } - }) { - let isEmoji = actions[0].0.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks - strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: isEmoji ? presentationData.strings.EmojiPackActionInfo_RemovedTitle : presentationData.strings.StickerPackActionInfo_RemovedTitle, text: isEmoji ? presentationData.strings.EmojiPackActionInfo_MultipleRemovedText(Int32(actions.count)) : presentationData.strings.StickerPackActionInfo_MultipleRemovedText(Int32(actions.count)), undo: true, info: actions[0].0, topItem: actions[0].1.first, context: context), elevatedLayout: true, animateInAsReplacement: false, action: { action in - if case .undo = action { - var itemsAndIndices: [(StickerPackCollectionInfo, [StickerPackItem], Int)] = actions.compactMap { action -> (StickerPackCollectionInfo, [StickerPackItem], Int)? in - if case let .remove(index) = action.2 { - return (action.0, action.1, index) - } else { - return nil - } - } - itemsAndIndices.sort(by: { $0.2 < $1.2 }) - for (info, items, index) in itemsAndIndices.reversed() { - let _ = context.engine.stickers.addStickerPackInteractively(info: info, items: items, positionInList: index).startStandalone() - } - } - return true - })) - } - } else if let (info, items, action) = actions.first { - let isEmoji = info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks - switch action { - case .add: - strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: isEmoji ? presentationData.strings.EmojiPackActionInfo_AddedTitle : presentationData.strings.StickerPackActionInfo_AddedTitle, text: isEmoji ? presentationData.strings.EmojiPackActionInfo_AddedText(info.title).string : presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: items.first, context: context), elevatedLayout: true, animateInAsReplacement: false, action: { _ in - return true - })) - case let .remove(positionInList): - strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: isEmoji ? presentationData.strings.EmojiPackActionInfo_RemovedTitle : presentationData.strings.StickerPackActionInfo_RemovedTitle, text: isEmoji ? presentationData.strings.EmojiPackActionInfo_RemovedText(info.title).string : presentationData.strings.StickerPackActionInfo_RemovedText(info.title).string, undo: true, info: info, topItem: items.first, context: context), elevatedLayout: true, animateInAsReplacement: false, action: { action in - if case .undo = action { - let _ = context.engine.stickers.addStickerPackInteractively(info: info, items: items, positionInList: positionInList).startStandalone() - } - return true - })) - } - } - }) - self.present(controller, in: .window(.root)) - } - public func hintPlayNextOutgoingGift() { self.controllerInteraction?.playNextOutgoingGift = true } @@ -16869,129 +10789,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - func openViewOnceMediaMessage(_ message: Message) { - if self.screenCaptureManager?.isRecordingActive == true { - let controller = textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, text: self.presentationData.strings.Chat_PlayOnceMesasge_DisableScreenCapture, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { - })]) - self.present(controller, in: .window(.root)) - return - } - - let isIncoming = message.effectivelyIncoming(self.context.account.peerId) - - var presentImpl: ((ViewController) -> Void)? - let configuration = ContextController.Configuration( - sources: [ - ContextController.Source( - id: 0, - title: "", - source: .extracted(ChatViewOnceMessageContextExtractedContentSource( - context: self.context, - presentationData: self.presentationData, - chatNode: self.chatDisplayNode, - backgroundNode: self.chatBackgroundNode, - engine: self.context.engine, - message: message, - present: { c in - presentImpl?(c) - } - )), - items: .single(ContextController.Items(content: .list([]))), - closeActionTitle: isIncoming ? self.presentationData.strings.Chat_PlayOnceMesasgeCloseAndDelete : self.presentationData.strings.Chat_PlayOnceMesasgeClose, - closeAction: { [weak self] in - if let self { - self.context.sharedContext.mediaManager.setPlaylist(nil, type: .voice, control: .playback(.pause)) - } - } - ) - ], initialId: 0 - ) - - let contextController = ContextController(presentationData: self.presentationData, configuration: configuration) - contextController.getOverlayViews = { [weak self] in - guard let self else { - return [] - } - return [self.chatDisplayNode.navigateButtons.view] - } - self.currentContextController = contextController - self.presentInGlobalOverlay(contextController) - - presentImpl = { [weak contextController] c in - contextController?.present(c, in: .current) - } - - let _ = self.context.sharedContext.openChatMessage(OpenChatMessageParams(context: self.context, chatLocation: nil, chatFilterTag: nil, chatLocationContextHolder: nil, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: nil, dismissInput: { }, present: { _, _ in }, transitionNode: { _, _, _ in return nil }, addToTransitionSurface: { _ in }, openUrl: { _ in }, openPeer: { _, _ in }, callPeer: { _, _ in }, enqueueMessage: { _ in }, sendSticker: nil, sendEmoji: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in }, playlistLocation: .singleMessage(message.id))) - } - - func openStorySharing(messages: [Message]) { - let context = self.context - let subject: Signal = .single(.message(messages.map { $0.id })) - - let externalState = MediaEditorTransitionOutExternalState( - storyTarget: nil, - isForcedTarget: false, - isPeerArchived: false, - transitionOut: nil - ) - - let controller = MediaEditorScreen( - context: context, - mode: .storyEditor, - subject: subject, - transitionIn: nil, - transitionOut: { _, _ in - return nil - }, - completion: { [weak self] result, commit in - guard let self else { - return - } - let targetPeerId: EnginePeer.Id - let target: Stories.PendingTarget - if let sendAsPeerId = result.options.sendAsPeerId { - target = .peer(sendAsPeerId) - targetPeerId = sendAsPeerId - } else { - target = .myStories - targetPeerId = self.context.account.peerId - } - externalState.storyTarget = target - - if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { - rootController.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) - } - - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: targetPeerId)) - |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let self, let peer else { - return - } - let text: String - if case .channel = peer { - text = self.presentationData.strings.Story_MessageReposted_Channel(peer.compactDisplayTitle).string - } else { - text = self.presentationData.strings.Story_MessageReposted_Personal - } - Queue.mainQueue().after(0.25) { - self.present(UndoOverlayController( - presentationData: self.presentationData, - content: .forward(savedMessages: false, text: text), - elevatedLayout: false, - action: { _ in return false } - ), in: .current) - - Queue.mainQueue().after(0.1) { - self.chatDisplayNode.hapticFeedback.success() - } - } - }) - - } - ) - self.push(controller) - } - public func transferScrollingVelocity(_ velocity: CGFloat) { self.chatDisplayNode.historyNode.transferVelocity(velocity) } @@ -17007,92 +10804,3 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } } - -final class ChatContextControllerContentSourceImpl: ContextControllerContentSource { - let controller: ViewController - weak var sourceNode: ASDisplayNode? - weak var sourceView: UIView? - let sourceRect: CGRect? - - let navigationController: NavigationController? = nil - - let passthroughTouches: Bool - - init(controller: ViewController, sourceNode: ASDisplayNode?, sourceRect: CGRect? = nil, passthroughTouches: Bool) { - self.controller = controller - self.sourceNode = sourceNode - self.sourceRect = sourceRect - self.passthroughTouches = passthroughTouches - } - - init(controller: ViewController, sourceView: UIView?, sourceRect: CGRect? = nil, passthroughTouches: Bool) { - self.controller = controller - self.sourceView = sourceView - self.sourceRect = sourceRect - self.passthroughTouches = passthroughTouches - } - - func transitionInfo() -> ContextControllerTakeControllerInfo? { - let sourceView = self.sourceView - let sourceNode = self.sourceNode - let sourceRect = self.sourceRect - return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in - if let sourceView = sourceView { - return (sourceView, sourceRect ?? sourceView.bounds) - } else if let sourceNode = sourceNode { - return (sourceNode.view, sourceRect ?? sourceNode.bounds) - } else { - return nil - } - }) - } - - func animatedIn() { - } -} - -final class ChatControllerContextReferenceContentSource: ContextReferenceContentSource { - let controller: ViewController - let sourceView: UIView - let insets: UIEdgeInsets - let contentInsets: UIEdgeInsets - - init(controller: ViewController, sourceView: UIView, insets: UIEdgeInsets, contentInsets: UIEdgeInsets = UIEdgeInsets()) { - self.controller = controller - self.sourceView = sourceView - self.insets = insets - self.contentInsets = contentInsets - } - - func transitionInfo() -> ContextControllerReferenceViewInfo? { - return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds.inset(by: self.insets), insets: self.contentInsets) - } -} - -func peerMessageSelectedReactions(context: AccountContext, message: Message) -> Signal<(reactions: Set, files: Set), NoError> { - return context.engine.stickers.availableReactions() - |> take(1) - |> map { availableReactions -> (reactions: Set, files: Set) in - var result = Set() - var reactions = Set() - - if let effectiveReactions = message.effectiveReactions(isTags: message.areReactionsTags(accountPeerId: context.account.peerId)) { - for reaction in effectiveReactions { - if !reaction.isSelected { - continue - } - reactions.insert(reaction.value) - switch reaction.value { - case .builtin: - if let availableReaction = availableReactions?.reactions.first(where: { $0.value == reaction.value }) { - result.insert(availableReaction.selectAnimation.fileId) - } - case let .custom(fileId): - result.insert(MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)) - } - } - } - - return (reactions, result) - } -} diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index fbfdddad2d..64e7814da4 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -663,14 +663,14 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto var isSelectionGestureEnabled = true private var overscrollView: ComponentHostView? - var nextChannelToRead: (peer: EnginePeer, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation)? + var nextChannelToRead: (peer: EnginePeer, threadData: (id: Int64, data: MessageHistoryThreadData)?, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation)? var offerNextChannelToRead: Bool = false var nextChannelToReadDisplayName: Bool = false private var currentOverscrollExpandProgress: CGFloat = 0.0 private var freezeOverscrollControl: Bool = false private var freezeOverscrollControlProgress: Bool = false private var feedback: HapticFeedback? - var openNextChannelToRead: ((EnginePeer, TelegramEngine.NextUnreadChannelLocation) -> Void)? + var openNextChannelToRead: ((EnginePeer, (id: Int64, data: MessageHistoryThreadData)?, TelegramEngine.NextUnreadChannelLocation) -> Void)? private var contentInsetAnimator: DisplayLinkAnimator? let adMessagesContext: AdMessagesHistoryContext? @@ -1019,7 +1019,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto if strongSelf.offerNextChannelToRead, strongSelf.currentOverscrollExpandProgress >= 0.99 { if let nextChannelToRead = strongSelf.nextChannelToRead { strongSelf.freezeOverscrollControl = true - strongSelf.openNextChannelToRead?(nextChannelToRead.peer, nextChannelToRead.location) + strongSelf.openNextChannelToRead?(nextChannelToRead.peer, nextChannelToRead.threadData, nextChannelToRead.location) } else { strongSelf.freezeOverscrollControlProgress = true strongSelf.scroller.contentInset = UIEdgeInsets(top: 94.0 + 12.0, left: 0.0, bottom: 0.0, right: 0.0) @@ -2243,8 +2243,12 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto switch nextChannelToRead.location { case .same: if let controllerNode = self.controllerInteraction.chatControllerNode() as? ChatControllerNode, let chatController = controllerNode.interfaceInteraction?.chatController() as? ChatControllerImpl, chatController.customChatNavigationStack != nil { + //TODO:localize swipeText = ("Pull up to go to the next channel", []) releaseText = ("Release to go to the next channel", []) + } else if nextChannelToRead.threadData != nil { + swipeText = ("Pull up to go to the next topic", []) + releaseText = ("Release to go to the next topic", []) } else { swipeText = (self.currentPresentationData.strings.Chat_NextChannelSameLocationSwipeProgress, []) releaseText = (self.currentPresentationData.strings.Chat_NextChannelSameLocationSwipeAction, []) @@ -2288,6 +2292,12 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto backgroundColor: selectDateFillStaticColor(theme: self.currentPresentationData.theme.theme, wallpaper: self.currentPresentationData.theme.wallpaper), foregroundColor: bubbleVariableColor(variableColor: self.currentPresentationData.theme.theme.chat.serviceMessage.dateTextColor, wallpaper: self.currentPresentationData.theme.wallpaper), peer: self.nextChannelToRead?.peer, + threadData: (self.nextChannelToRead?.threadData).flatMap { threadData in + return ChatOverscrollThreadData( + id: threadData.id, + data: threadData.data + ) + }, unreadCount: self.nextChannelToRead?.unreadCount ?? 0, location: self.nextChannelToRead?.location ?? .same, context: self.context, diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 8315e7e9a6..e09cb2b915 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2056,6 +2056,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSource = .messageTags case .folderTags: mappedSource = .folderTags + case .animatedEmoji: + mappedSource = .animatedEmoji } let controller = PremiumIntroScreen(context: context, source: mappedSource, modal: modal, forceDark: forceDark) controller.wasDismissed = dismissed