diff --git a/Telegram/NotificationService/Sources/NotificationService.swift b/Telegram/NotificationService/Sources/NotificationService.swift index 2254e5331ff..e8f1ec98414 100644 --- a/Telegram/NotificationService/Sources/NotificationService.swift +++ b/Telegram/NotificationService/Sources/NotificationService.swift @@ -860,6 +860,8 @@ private final class NotificationServiceHandler { } strongSelf.stateManager = stateManager + let accountPeerId = stateManager.accountPeerId + let settings = stateManager.postbox.transaction { transaction -> NotificationSoundList? in return _internal_cachedNotificationSoundList(transaction: transaction) } @@ -981,7 +983,7 @@ private final class NotificationServiceHandler { enum Action { case logout case poll(peerId: PeerId, content: NotificationContent, messageId: MessageId?) - case pollStories(peerId: PeerId, content: NotificationContent, storyId: Int32) + case pollStories(peerId: PeerId, content: NotificationContent, storyId: Int32, isReaction: Bool) case deleteMessage([MessageId]) case readReactions([MessageId]) case readMessage(MessageId) @@ -1049,7 +1051,7 @@ private final class NotificationServiceHandler { break } } else { - if let aps = payloadJson["aps"] as? [String: Any], let peerId = peerId { + if let aps = payloadJson["aps"] as? [String: Any], var peerId = peerId { var content: NotificationContent = NotificationContent(isLockedMessage: isLockedMessage) if let alert = aps["alert"] as? [String: Any] { if let topicTitleValue = payloadJson["topic_title"] as? String { @@ -1078,8 +1080,35 @@ private final class NotificationServiceHandler { messageIdValue = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: messageId) } + + var isReaction = false + if let category = aps["category"] as? String { + if peerId.isGroupOrChannel && ["r", "m"].contains(category) { + content.category = "g\(category)" + } else { + content.category = category + } + + if aps["r"] != nil || aps["react_emoji"] != nil { + isReaction = true + content.category = "t" + } else if payloadJson["r"] != nil || payloadJson["react_emoji"] != nil { + isReaction = true + content.category = "t" + } + + if category == "str" { + isReaction = true + } + + let _ = messageId + } + if let storyId = storyId { interactionAuthorId = peerId + if isReaction { + peerId = accountPeerId + } content.userInfo["story_id"] = "\(storyId)" } @@ -1147,29 +1176,14 @@ private final class NotificationServiceHandler { content.sound = sound } - if let category = aps["category"] as? String { - if peerId.isGroupOrChannel && ["r", "m"].contains(category) { - content.category = "g\(category)" - } else { - content.category = category - } - - if aps["r"] != nil || aps["react_emoji"] != nil { - content.category = "t" - } else if payloadJson["r"] != nil || payloadJson["react_emoji"] != nil { - content.category = "t" - } - - let _ = messageId - } - if let storyId { - if content.category == "t" { + if isReaction { content.category = "str" } else { content.category = "st" } - action = .pollStories(peerId: peerId, content: content, storyId: storyId) + + action = .pollStories(peerId: peerId, content: content, storyId: storyId, isReaction: isReaction) } else { action = .poll(peerId: peerId, content: content, messageId: messageIdValue) } @@ -1648,8 +1662,8 @@ private final class NotificationServiceHandler { } else { completed() } - case let .pollStories(peerId, initialContent, storyId): - Logger.shared.log("NotificationService \(episode)", "Will poll stories for \(peerId)") + case let .pollStories(peerId, initialContent, storyId, isReaction): + Logger.shared.log("NotificationService \(episode)", "Will poll stories for \(peerId) isReaction: \(isReaction)") if let stateManager = strongSelf.stateManager { let pollCompletion: (NotificationContent) -> Void = { content in let content = content @@ -1677,8 +1691,12 @@ private final class NotificationServiceHandler { let firstUnseenItem = transaction.getStoryItems(peerId: peerId).first(where: { entry in return entry.id > state.maxReadId }) - guard let firstUnseenItem, firstUnseenItem.id == storyId else { + if isReaction { return nil + } else { + guard let firstUnseenItem, firstUnseenItem.id == storyId else { + return nil + } } guard let peer = transaction.getPeer(peerId).flatMap(PeerReference.init) else { return nil @@ -1815,8 +1833,13 @@ private final class NotificationServiceHandler { } } - let wasDisplayed = stateManager.postbox.transaction { transaction -> Bool in - return _internal_getStoryNotificationWasDisplayed(transaction: transaction, id: StoryId(peerId: peerId, id: storyId)) + let wasDisplayed: Signal + if isReaction { + wasDisplayed = .single(false) + } else { + wasDisplayed = stateManager.postbox.transaction { transaction -> Bool in + return _internal_getStoryNotificationWasDisplayed(transaction: transaction, id: StoryId(peerId: peerId, id: storyId)) + } } Logger.shared.log("NotificationService \(episode)", "Will fetch media") diff --git a/Telegram/Telegram-iOS/Icons.xcassets/Shortcuts/AppIcon.imageset/Contents.json b/Telegram/Telegram-iOS/Icons.xcassets/Shortcuts/AppIcon.imageset/Contents.json new file mode 100644 index 00000000000..f090bedc357 --- /dev/null +++ b/Telegram/Telegram-iOS/Icons.xcassets/Shortcuts/AppIcon.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "tlogo_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Telegram/Telegram-iOS/Icons.xcassets/Shortcuts/AppIcon.imageset/tlogo_24.pdf b/Telegram/Telegram-iOS/Icons.xcassets/Shortcuts/AppIcon.imageset/tlogo_24.pdf new file mode 100644 index 00000000000..3293521e27f Binary files /dev/null and b/Telegram/Telegram-iOS/Icons.xcassets/Shortcuts/AppIcon.imageset/tlogo_24.pdf differ diff --git a/Telegram/Telegram-iOS/Icons.xcassets/Shortcuts/Camera.imageset/Contents.json b/Telegram/Telegram-iOS/Icons.xcassets/Shortcuts/Camera.imageset/Contents.json deleted file mode 100644 index 868e9f50485..00000000000 --- a/Telegram/Telegram-iOS/Icons.xcassets/Shortcuts/Camera.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "ic_camera.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Telegram/Telegram-iOS/Icons.xcassets/Shortcuts/Camera.imageset/ic_camera.pdf b/Telegram/Telegram-iOS/Icons.xcassets/Shortcuts/Camera.imageset/ic_camera.pdf deleted file mode 100644 index a5e2d3c939b..00000000000 Binary files a/Telegram/Telegram-iOS/Icons.xcassets/Shortcuts/Camera.imageset/ic_camera.pdf and /dev/null differ diff --git a/Telegram/Telegram-iOS/Icons.xcassets/Shortcuts/Contents.json b/Telegram/Telegram-iOS/Icons.xcassets/Shortcuts/Contents.json index 38f0c81fc22..6e965652df6 100644 --- a/Telegram/Telegram-iOS/Icons.xcassets/Shortcuts/Contents.json +++ b/Telegram/Telegram-iOS/Icons.xcassets/Shortcuts/Contents.json @@ -1,9 +1,9 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 }, "properties" : { "provides-namespace" : true } -} \ No newline at end of file +} diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 86c2377df09..8499e21f294 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -11896,6 +11896,7 @@ Sorry for the inconvenience."; "BusinessLink.ErrorExpired" = "Link Expired"; "WebApp.AlertBiometryAccessText" = "Do you want to allow %@ to use Face ID?"; +"WebApp.AlertBiometryAccessTouchIDText" = "Do you want to allow %@ to use Touch ID?"; "StoryList.SubtitleArchived_1" = "1 archived post"; "StoryList.SubtitleArchived_any" = "%d archived posts"; @@ -12050,3 +12051,134 @@ Sorry for the inconvenience."; "MediaEditor.NewStickerPack.Title" = "New Sticker Set"; "MediaEditor.NewStickerPack.Text" = "Choose a name for your sticker set."; +"Premium.Gift.ContactSelection.SendMessage" = "Send Message"; +"Premium.Gift.ContactSelection.OpenProfile" = "Open Profile"; + +"Login.EnterWordTitle" = "Enter Word"; +"Login.EnterWordText" = "We've sent you an SMS with a secret word to your phone **%@**."; +"Login.EnterWordBeginningText" = "We've sent you an SMS with a secret word that starts with **\"%1$@\"** to your phone **%2$@**."; +"Login.EnterWordPlaceholder" = "Enter Word from SMS"; + +"Login.EnterPhraseTitle" = "Enter Phrase"; +"Login.EnterPhraseText" = "We've sent you an SMS with a secret phrase to your phone **%@**."; +"Login.EnterPhraseBeginningText" = "We've sent you an SMS with a secret phrase that starts with **\"%1$@\"** to your phone **%2$@**."; +"Login.EnterPhrasePlaceholder" = "Enter Phrase from SMS"; + +"Login.EnterPhraseHint" = "You can copy and paste the phrase here."; +"Login.BackToWord" = "Back to entering the word"; +"Login.BackToPhrase" = "Back to entering the phrase"; +"Login.BackToCode" = "Back to entering the code"; + +"Login.WrongPhraseError" = "Incorrect, please try again."; +"Login.Paste" = "Paste"; + +"Login.ReturnToWord" = "Return to entering the word"; +"Login.ReturnToPhrase" = "Return to entering the phrase"; +"Login.ReturnToCode" = "Return to entering the code"; + +"Map.LiveLocationPrivateNewDescription" = "Choose for how long **%@** will see your accurate location, including when the app is closed."; +"Map.LiveLocationGroupNewDescription" = "Choose for how long people in this chat will see your accurate location, including when the app is closed."; + +"Map.LiveLocationExtendDescription" = "For how long do you want to extend sharing your location?"; + +"Map.LiveLocationForMinutes_1" = "For %@ minute"; +"Map.LiveLocationForMinutes_any" = "For %@ minutes"; + +"Map.LiveLocationForHours_1" = "For %@ hour"; +"Map.LiveLocationForHours_any" = "For %@ hours"; + +"Map.LiveLocationIndefinite" = "Until I turn it off"; + +"Map.TapToAddTime" = "tap to add time"; +"Map.SharingLocation" = "Sharing Location..."; + +"Channel.AdminLog.Settings" = "Settings"; + +"ChannelProfile.AboutActionCopy" = "Copy"; +"ChannelProfile.ToastAboutCopied" = "Copied to clipboard."; + +"Conversation.ContextMenuBanFull" = "Ban"; + +"Channel.AdminLog.MessageManyDeleted" = "%1$@ deleted %2$@ from %3$@"; +"Channel.AdminLog.MessageManyDeletedMore" = "%1$@ deleted %2$@ from %3$@ %4$@"; +"Channel.AdminLog.MessageManyDeleted.Messages_1" = "%@ message"; +"Channel.AdminLog.MessageManyDeleted.Messages_any" = "%@ messages"; +"Channel.AdminLog.MessageManyDeleted.ShowAll" = "show all"; +"Channel.AdminLog.MessageManyDeleted.HideAll" = "hide all"; + +"Shortcut.AppIcon" = "Edit App Icon"; + +"ReportPeer.BanAndReport" = "Ban and Report"; + +"ReportPeer.ReportReaction.Text" = "Are you sure you want to report reactions from this user?"; +"ReportPeer.ReportReaction.BanAndReport" = "Ban and Report"; +"ReportPeer.ReportReaction.Report" = "Report Reaction"; + +"StoryFeed.ViewAnonymously" = "View Anonymously"; + +"Channel.AdminLogFilter.EventsAdminRights" = "New Admin Rights"; +"Channel.AdminLogFilter.EventsExceptions" = "New Exceptions"; +"Channel.AdminLogFilter.EventsLeavingGroup" = "Members Left the Group"; +"Channel.AdminLogFilter.EventsLeavingChannel" = "Subscribers Removed"; + +"Channel.AdminLogFilter.RecentActionsTitle" = "Recent Actions"; +"Channel.AdminLogFilter.FilterActionsTypeTitle" = "FILTER ACTIONS BY TYPE"; +"Channel.AdminLogFilter.FilterActionsAdminsTitle" = "FILTER ACTIONS BY ADMINS"; +"Channel.AdminLogFilter.ShowAllAdminsActions" = "Show Actions by All Admins"; +"Channel.AdminLogFilter.ApplyFilter" = "Apply Filter"; + +"Channel.AdminLogFilter.Section.MembersGroup" = "Members and Admins"; +"Channel.AdminLogFilter.Section.MembersChannel" = "Subscribers and Admins"; +"Channel.AdminLogFilter.Section.SettingsGroup" = "Group Settings"; +"Channel.AdminLogFilter.Section.SettingsChannel" = "Channel Settings"; +"Channel.AdminLogFilter.Section.Messages" = "Messages"; + +"Premium.Gift.ContactSelection.AddBirthday" = "Add Your Birthday"; + +"Story.StealthMode.EnableAndOpenAction" = "Enable and Open the Story"; + +"Channel.AdminLog.ShowMoreMessages_1" = "Show %@ More Message"; +"Channel.AdminLog.ShowMoreMessages_any" = "Show %@ More Messages"; + +"CreatePoll.OptionCountFooterFormat_1" = "You can add {count} more option."; +"CreatePoll.OptionCountFooterFormat_any" = "You can add {count} more options."; + +"Chat.AdminActionSheet.DeleteTitle_1" = "Delete 1 Message"; +"Chat.AdminActionSheet.DeleteTitle_any" = "Delete %d Messages"; +"Chat.AdminActionSheet.ReportSpam" = "Report Spam"; +"Chat.AdminActionSheet.DeleteAllSingle" = "Delete All from %@"; +"Chat.AdminActionSheet.DeleteAllMultiple" = "Delete All from Users"; +"Chat.AdminActionSheet.BanSingle" = "Ban %@"; +"Chat.AdminActionSheet.BanMultiple" = "Ban Users"; +"Chat.AdminActionSheet.RestrictSingle" = "Ban %@"; +"Chat.AdminActionSheet.RestrictMultiple" = "Ban Users"; +"Chat.AdminActionSheet.RestrictSectionHeader" = "ADDITIONAL ACTIONS"; +"Chat.AdminActionSheet.BanFooterSingle" = "Fully ban this user"; +"Chat.AdminActionSheet.RestrictFooterSingle" = "Partially restrict this user"; +"Chat.AdminActionSheet.BanFooterMultiple" = "Fully ban users"; +"Chat.AdminActionSheet.RestrictFooterMultiple" = "Partially restrict users"; +"Chat.AdminActionSheet.PermissionsSectionHeader" = "WHAT CAN THIS USER DO?"; +"Chat.AdminActionSheet.ActionButton" = "Proceed"; + +"Chat.AdminAction.ToastMessagesDeletedTitleSingle" = "Message Deleted"; +"Chat.AdminAction.ToastMessagesDeletedTitleMultiple" = "Messages Deleted"; +"Chat.AdminAction.ToastMessagesDeletedTextSingle" = "Message Deleted."; +"Chat.AdminAction.ToastMessagesDeletedTextMultiple" = "Messages Deleted."; +"Chat.AdminAction.ToastReportedSpamText_1" = "**1** user reported for spam."; +"Chat.AdminAction.ToastReportedSpamText_any" = "**%d** users reported for spam."; +"Chat.AdminAction.ToastBannedText_1" = "**1** user banned."; +"Chat.AdminAction.ToastBannedText_any" = "**%d** users banned."; +"Chat.AdminAction.ToastRestrictedText_1" = "**1** user restricted."; +"Chat.AdminAction.ToastRestrictedText_any" = "**%d** users restricted."; + +"Chat.MessageForwardInfo.StoryHeader" = "Forwarded story from"; +"Chat.MessageForwardInfo.ExpiredStoryHeader" = "Expired story from"; +"Chat.MessageForwardInfo.UnavailableStoryHeader" = "Expired story from"; +"Chat.MessageForwardInfo.MessageHeader" = "Forwarded from"; + +"Chat.NavigationNoTopics" = "You have no unread topics"; + +"Chat.NextSuggestedChannelSwipeProgress" = "Swipe up to go to the next channel"; +"Chat.NextSuggestedChannelSwipeAction" = "Release to go to the next channel"; +"Chat.NextUnreadTopicSwipeProgress" = "Swipe up to go to the next topic"; +"Chat.NextUnreadTopicSwipeAction" = "Release to go to the next topic"; diff --git a/build-system/Make/Make.py b/build-system/Make/Make.py index 83532af761c..e7d3924d194 100644 --- a/build-system/Make/Make.py +++ b/build-system/Make/Make.py @@ -609,7 +609,7 @@ def build(bazel, arguments): if arguments.outputBuildArtifactsPath is not None: artifacts_path = os.path.abspath(arguments.outputBuildArtifactsPath) if os.path.exists(artifacts_path + '/Telegram.ipa'): - os.remove(path) + os.remove(artifacts_path + '/Telegram.ipa') if os.path.exists(artifacts_path + '/DSYMs'): shutil.rmtree(artifacts_path + '/DSYMs') os.makedirs(artifacts_path, exist_ok=True) @@ -625,7 +625,7 @@ def build(bazel, arguments): sys.exit(1) shutil.copyfile(ipa_paths[0], artifacts_path + '/Telegram.ipa') - dsym_paths = glob.glob('bazel-bin/Telegram/**/*.dSYM') + dsym_paths = glob.glob('bazel-bin/Telegram/*.dSYM') for dsym_path in dsym_paths: file_name = os.path.basename(dsym_path) shutil.copytree(dsym_path, artifacts_path + '/DSYMs/{}'.format(file_name)) diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 1c94497d1cb..bec69985fcb 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -159,14 +159,16 @@ public struct ChatAvailableMessageActionOptions: OptionSet { public struct ChatAvailableMessageActions { public var options: ChatAvailableMessageActionOptions public var banAuthor: Peer? + public var banAuthors: [Peer] public var disableDelete: Bool public var isCopyProtected: Bool public var setTag: Bool public var editTags: Set - public init(options: ChatAvailableMessageActionOptions, banAuthor: Peer?, disableDelete: Bool, isCopyProtected: Bool, setTag: Bool, editTags: Set) { + public init(options: ChatAvailableMessageActionOptions, banAuthor: Peer?, banAuthors: [Peer], disableDelete: Bool, isCopyProtected: Bool, setTag: Bool, editTags: Set) { self.options = options self.banAuthor = banAuthor + self.banAuthors = banAuthors self.disableDelete = disableDelete self.isCopyProtected = isCopyProtected self.setTag = setTag @@ -637,6 +639,7 @@ public enum ContactListAction: Equatable { case generic case voiceCall case videoCall + case more } public enum ContactListPeer: Equatable { @@ -1015,7 +1018,7 @@ public protocol SharedAccountContext: AnyObject { func makeChatQrCodeScreen(context: AccountContext, peer: Peer, threadId: Int64?, temporary: Bool) -> ViewController func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController - func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, action: @escaping () -> Void) -> ViewController + func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, forceDark: Bool, action: @escaping () -> Void, dismissed: (() -> Void)?) -> ViewController func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, forceDark: Bool, cancel: @escaping () -> Void, action: @escaping () -> Bool) -> ViewController func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (() -> Void)?) -> ViewController func makePremiumPrivacyControllerController(context: AccountContext, subject: PremiumPrivacySubject, peerId: EnginePeer.Id) -> ViewController diff --git a/submodules/AccountContext/Sources/ContactMultiselectionController.swift b/submodules/AccountContext/Sources/ContactMultiselectionController.swift index 4d00d76eb1e..8bbc7dd68df 100644 --- a/submodules/AccountContext/Sources/ContactMultiselectionController.swift +++ b/submodules/AccountContext/Sources/ContactMultiselectionController.swift @@ -93,7 +93,7 @@ public final class ContactMultiselectionControllerParams { public let context: AccountContext public let updatedPresentationData: (initial: PresentationData, signal: Signal)? public let mode: ContactMultiselectionControllerMode - public let options: [ContactListAdditionalOption] + public let options: Signal<[ContactListAdditionalOption], NoError> public let filters: [ContactListFilter] public let onlyWriteable: Bool public let isGroupInvitation: Bool @@ -102,8 +102,10 @@ public final class ContactMultiselectionControllerParams { public let alwaysEnabled: Bool public let limit: Int32? public let reachedLimit: ((Int32) -> Void)? - - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, mode: ContactMultiselectionControllerMode, options: [ContactListAdditionalOption], filters: [ContactListFilter] = [.excludeSelf], onlyWriteable: Bool = false, isGroupInvitation: Bool = false, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, attemptDisabledItemSelection: ((EnginePeer, ChatListDisabledPeerReason) -> Void)? = nil, alwaysEnabled: Bool = false, limit: Int32? = nil, reachedLimit: ((Int32) -> Void)? = nil) { + public let openProfile: ((EnginePeer) -> Void)? + public let sendMessage: ((EnginePeer) -> Void)? + + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, mode: ContactMultiselectionControllerMode, options: Signal<[ContactListAdditionalOption], NoError> = .single([]), filters: [ContactListFilter] = [.excludeSelf], onlyWriteable: Bool = false, isGroupInvitation: Bool = false, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, attemptDisabledItemSelection: ((EnginePeer, ChatListDisabledPeerReason) -> Void)? = nil, alwaysEnabled: Bool = false, limit: Int32? = nil, reachedLimit: ((Int32) -> Void)? = nil, openProfile: ((EnginePeer) -> Void)? = nil, sendMessage: ((EnginePeer) -> Void)? = nil) { self.context = context self.updatedPresentationData = updatedPresentationData self.mode = mode @@ -116,6 +118,8 @@ public final class ContactMultiselectionControllerParams { self.alwaysEnabled = alwaysEnabled self.limit = limit self.reachedLimit = reachedLimit + self.openProfile = openProfile + self.sendMessage = sendMessage } } diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index 02dfa7dec28..2373353de53 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 { @@ -72,6 +73,7 @@ public enum PremiumDemoSubject { case lastSeen case messagePrivacy case folderTags + case business case businessLocation case businessHours diff --git a/submodules/AttachmentUI/Sources/AttachmentContainer.swift b/submodules/AttachmentUI/Sources/AttachmentContainer.swift index 9f2c4725121..ace93ca1d42 100644 --- a/submodules/AttachmentUI/Sources/AttachmentContainer.swift +++ b/submodules/AttachmentUI/Sources/AttachmentContainer.swift @@ -68,6 +68,7 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate { var isPanningUpdated: (Bool) -> Void = { _ in } var isExpandedUpdated: (Bool) -> Void = { _ in } + var isPanGestureEnabled: (() -> Bool)? var onExpandAnimationCompleted: () -> Void = {} override init() { @@ -132,6 +133,12 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate { if case .regular = layout.metrics.widthClass { return false } + + if let isPanGestureEnabled = self.isPanGestureEnabled { + if !isPanGestureEnabled() { + return false + } + } } return true } diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index f1ec3c9d1f7..a343b776aba 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -89,10 +89,13 @@ public enum AttachmentButtonType: Equatable { public protocol AttachmentContainable: ViewController { var requestAttachmentMenuExpansion: () -> Void { get set } var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void { get set } + var parentController: () -> ViewController? { get set } var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void { get set } + var updateTabBarVisibility: (Bool, ContainedViewLayoutTransition) -> Void { get set } var cancelPanGesture: () -> Void { get set } var isContainerPanning: () -> Bool { get set } var isContainerExpanded: () -> Bool { get set } + var isPanGestureEnabled: (() -> Bool)? { get } var mediaPickerContext: AttachmentMediaPickerContext? { get } func isContainerPanningUpdated(_ panning: Bool) @@ -124,6 +127,10 @@ public extension AttachmentContainable { func shouldDismissImmediately() -> Bool { return true } + + var isPanGestureEnabled: (() -> Bool)? { + return nil + } } public enum AttachmentMediaPickerSendMode { @@ -351,6 +358,17 @@ public class AttachmentController: ViewController { } } + self.container.isPanGestureEnabled = { [weak self] in + guard let self, let currentController = self.currentControllers.last else { + return true + } + if let isPanGestureEnabled = currentController.isPanGestureEnabled { + return isPanGestureEnabled() + } else { + return true + } + } + self.container.shouldCancelPanGesture = { [weak self] in if let strongSelf = self, let currentController = strongSelf.currentControllers.last { if !currentController.shouldDismissImmediately() { @@ -543,11 +561,23 @@ public class AttachmentController: ViewController { } } } + controller.parentController = { [weak self] in + guard let self else { + return nil + } + return self.controller + } controller.updateTabBarAlpha = { [weak self, weak controller] alpha, transition in if let strongSelf = self, strongSelf.currentControllers.contains(where: { $0 === controller }) { strongSelf.panel.updateBackgroundAlpha(alpha, transition: transition) } } + controller.updateTabBarVisibility = { [weak self, weak controller] isVisible, transition in + if let strongSelf = self, strongSelf.currentControllers.contains(where: { $0 === controller }) { + strongSelf.updateIsPanelVisible(isVisible, transition: transition) + } + } + controller.cancelPanGesture = { [weak self] in if let strongSelf = self { strongSelf.container.cancelPanGesture() @@ -731,6 +761,18 @@ public class AttachmentController: ViewController { private var hasButton = false + private var isPanelVisible: Bool = true + + private func updateIsPanelVisible(_ isVisible: Bool, transition: ContainedViewLayoutTransition) { + if self.isPanelVisible == isVisible { + return + } + self.isPanelVisible = isVisible + if let layout = self.validLayout { + self.containerLayoutUpdated(layout, transition: transition) + } + } + func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { self.validLayout = layout @@ -820,6 +862,9 @@ public class AttachmentController: ViewController { if let controller = self.controller, controller.buttons.count > 1 || controller.hasTextInput { hasPanel = true } + if !self.isPanelVisible { + hasPanel = false + } let isEffecitvelyCollapsedUpdated = (self.selectionCount > 0) != (self.panel.isSelecting) var panelHeight = self.panel.update(layout: containerLayout, buttons: self.controller?.buttons ?? [], isSelecting: self.selectionCount > 0, elevateProgress: !hasPanel && !hasButton, transition: transition) diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift index bb36ad0c57b..7a93513bec0 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift @@ -23,10 +23,11 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { var reset: (() -> Void)? public var requestNextOption: (() -> Void)? + public var requestPreviousOption: (() -> Void)? var resetEmail: (() -> Void)? var retryResetEmail: (() -> Void)? - var data: (String, String?, SentAuthorizationCodeType, AuthorizationCodeNextType?, Int32?)? + var data: (String, String?, SentAuthorizationCodeType, AuthorizationCodeNextType?, Int32?, SentAuthorizationCodeType?, Bool)? var termsOfService: (UnauthorizedAccountTermsOfService, Bool)? private let hapticFeedback = HapticFeedback() @@ -40,6 +41,10 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { } } + var isPrevious: Bool { + return self.data?.6 ?? false + } + public init(presentationData: PresentationData, back: @escaping () -> Void) { self.strings = presentationData.strings self.theme = presentationData.theme @@ -60,7 +65,7 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { let proceed: String let stop: String - if let (_, _, type, _, _) = self?.data, case .email = type { + if let (_, _, type, _, _, _, _) = self?.data, case .email = type { text = presentationData.strings.Login_CancelEmailVerification proceed = presentationData.strings.Login_CancelEmailVerificationContinue stop = presentationData.strings.Login_CancelEmailVerificationStop @@ -107,6 +112,10 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { self?.requestNextOption?() } + self.controllerNode.requestPreviousOption = { [weak self] in + self?.requestPreviousOption?() + } + self.controllerNode.updateNextEnabled = { [weak self] value in self?.navigationItem.rightBarButtonItem?.isEnabled = value } @@ -123,12 +132,12 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { self?.present(c, in: .window(.root), with: a) } - if let (number, email, codeType, nextType, timeout) = self.data { + if let (number, email, codeType, nextType, timeout, previousCodeType, isPrevious) = self.data { var appleSignInAllowed = false if case let .email(_, _, _, _, appleSignInAllowedValue, _) = codeType { appleSignInAllowed = appleSignInAllowedValue } - self.controllerNode.updateData(number: number, email: email, codeType: codeType, nextType: nextType, timeout: timeout, appleSignInAllowed: appleSignInAllowed) + self.controllerNode.updateData(number: number, email: email, codeType: codeType, nextType: nextType, timeout: timeout, appleSignInAllowed: appleSignInAllowed, previousCodeType: previousCodeType, isPrevious: isPrevious) } } @@ -136,9 +145,9 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { super.viewDidAppear(animated) if let navigationController = self.navigationController as? NavigationController, let layout = self.validLayout { - addTemporaryKeyboardSnapshotView(navigationController: navigationController, parentView: self.view, layout: layout) + addTemporaryKeyboardSnapshotView(navigationController: navigationController, layout: layout) } - + self.controllerNode.activateInput() // MARK: Nicegram AppReviewLogin @@ -156,11 +165,19 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { self.controllerNode.animateSuccess() } + public func selectIncorrectPart() { + self.controllerNode.selectIncorrectPart() + } + public func animateError(text: String) { self.hapticFeedback.error() self.controllerNode.animateError(text: text) } + func updateAppIsActive(_ isActive: Bool) { + self.controllerNode.updatePasteVisibility() + } + func updateNavigationItems() { guard let layout = self.validLayout, layout.size.width < 360.0 else { return @@ -174,10 +191,10 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { } } - public func updateData(number: String, email: String?, codeType: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, timeout: Int32?, termsOfService: (UnauthorizedAccountTermsOfService, Bool)?) { + public func updateData(number: String, email: String?, codeType: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, timeout: Int32?, termsOfService: (UnauthorizedAccountTermsOfService, Bool)?, previousCodeType: SentAuthorizationCodeType?, isPrevious: Bool) { self.termsOfService = termsOfService - if self.data?.0 != number || self.data?.1 != email || self.data?.2 != codeType || self.data?.3 != nextType || self.data?.4 != timeout { - self.data = (number, email, codeType, nextType, timeout) + if self.data?.0 != number || self.data?.1 != email || self.data?.2 != codeType || self.data?.3 != nextType || self.data?.4 != timeout || self.data?.5 != previousCodeType || self.data?.6 != isPrevious { + self.data = (number, email, codeType, nextType, timeout, previousCodeType, isPrevious) var appleSignInAllowed = false if case let .email(_, _, _, _, appleSignInAllowedValue, _) = codeType { @@ -185,7 +202,7 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { } if self.isNodeLoaded { - self.controllerNode.updateData(number: number, email: email, codeType: codeType, nextType: nextType, timeout: timeout, appleSignInAllowed: appleSignInAllowed) + self.controllerNode.updateData(number: number, email: email, codeType: codeType, nextType: nextType, timeout: timeout, appleSignInAllowed: appleSignInAllowed, previousCodeType: previousCodeType, isPrevious: isPrevious) self.requestLayout(transition: .immediate) } } @@ -199,13 +216,17 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { if !hadLayout { self.updateNavigationItems() + + if let navigationController = self.navigationController as? NavigationController { + addTemporaryKeyboardSnapshotView(navigationController: navigationController, layout: layout, local: true) + } } self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } @objc private func nextPressed() { - guard let (_, _, type, _, _) = self.data else { + guard let (_, _, type, _, _, _, _) = self.data else { return } @@ -226,7 +247,7 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { minimalCodeLength = Int(length) case let .firebase(_, length): minimalCodeLength = Int(length) - case .flashCall, .emailSetupRequired: + case .flashCall, .emailSetupRequired, .word, .phrase: break } @@ -247,18 +268,20 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { } } -func addTemporaryKeyboardSnapshotView(navigationController: NavigationController, parentView: UIView, layout: ContainerViewLayout) { +func addTemporaryKeyboardSnapshotView(navigationController: NavigationController, layout: ContainerViewLayout, local: Bool = false) { if case .compact = layout.metrics.widthClass, let statusBarHost = navigationController.statusBarHost { if let keyboardView = statusBarHost.keyboardView { + keyboardView.layer.removeAllAnimations() if let snapshotView = keyboardView.snapshotView(afterScreenUpdates: false) { - keyboardView.layer.removeAllAnimations() UIView.performWithoutAnimation { snapshotView.frame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - snapshotView.frame.size.height), size: snapshotView.frame.size) - if let keyboardWindow = statusBarHost.keyboardWindow { + if local { + navigationController.view.addSubview(snapshotView) + } else if let keyboardWindow = statusBarHost.keyboardWindow { keyboardWindow.addSubview(snapshotView) } - Queue.mainQueue().after(0.5, { + Queue.mainQueue().after(local ? 0.8 : 0.7, { snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in snapshotView?.removeFromSuperview() }) diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift index 5aa943345c2..1904e9fb5fc 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift @@ -38,6 +38,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF private let currentOptionInfoActivateAreaNode: AccessibilityAreaNode private let nextOptionTitleNode: ImmediateTextNode private let nextOptionButtonNode: HighlightableButtonNode + private let nextOptionArrowNode: ASImageNode private let resetTextNode: ImmediateTextNode private let resetNode: HighlightableButtonNode @@ -46,9 +47,17 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF private var signInWithAppleButton: UIControl? private let proceedNode: SolidRoundedButtonNode + private let textField: TextFieldNode + private let textSeparatorNode: ASDisplayNode + private let pasteButton: HighlightableButtonNode + private let codeInputView: CodeInputView private let errorTextNode: ImmediateTextNode + private let hintButtonNode: HighlightTrackingButtonNode + private let hintTextNode: ImmediateTextNode + private let hintArrowNode: ASImageNode + private var codeType: SentAuthorizationCodeType? private let countdownDisposable = MetaDisposable() @@ -58,6 +67,9 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF private var appleSignInAllowed = false + private var previousCodeType: SentAuthorizationCodeType? + private var isPrevious = false + var phoneNumber: String = "" { didSet { if self.phoneNumber != oldValue { @@ -71,7 +83,12 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF var email: String? var currentCode: String { - return self.codeInputView.text + switch self.codeType { + case .word, .phrase: + return self.textField.textField.text ?? "" + default: + return self.codeInputView.text + } } var loginWithCode: ((String) -> Void)? @@ -81,6 +98,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF var requestNextOption: (() -> Void)? var requestAnotherOption: (() -> Void)? + var requestPreviousOption: (() -> Void)? var updateNextEnabled: ((Bool) -> Void)? var reset: (() -> Void)? var retryReset: (() -> Void)? @@ -88,6 +106,19 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF var inProgress: Bool = false { didSet { self.codeInputView.alpha = self.inProgress ? 0.6 : 1.0 + + switch self.codeType { + case .word, .phrase: + if self.inProgress != oldValue { + if self.inProgress { + self.proceedNode.transitionToProgress() + } else { + self.proceedNode.transitionFromProgress() + } + } + default: + break + } } } @@ -114,6 +145,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.titleIconNode.displaysAsynchronously = false self.currentOptionNode = ImmediateTextNodeWithEntities() + self.currentOptionNode.balancedTextLayout = true self.currentOptionNode.isUserInteractionEnabled = false self.currentOptionNode.displaysAsynchronously = false self.currentOptionNode.lineSpacing = 0.1 @@ -139,7 +171,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.nextOptionButtonNode = HighlightableButtonNode() self.nextOptionButtonNode.displaysAsynchronously = false - let (nextOptionText, nextOptionActive) = authorizationNextOptionText(currentType: .sms(length: 5), nextType: .call, timeout: 60, strings: self.strings, primaryColor: self.theme.list.itemPrimaryTextColor, accentColor: self.theme.list.itemAccentColor) + let (nextOptionText, nextOptionActive) = authorizationNextOptionText(currentType: .sms(length: 5), nextType: .call, timeout: 60, strings: self.strings, primaryColor: self.theme.list.itemSecondaryTextColor, accentColor: self.theme.list.itemAccentColor) self.nextOptionTitleNode.attributedText = nextOptionText self.nextOptionButtonNode.isUserInteractionEnabled = nextOptionActive self.nextOptionButtonNode.accessibilityLabel = nextOptionText.string @@ -150,6 +182,10 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF } self.nextOptionButtonNode.addSubnode(self.nextOptionTitleNode) + self.nextOptionArrowNode = ASImageNode() + self.nextOptionArrowNode.displaysAsynchronously = false + self.nextOptionArrowNode.isUserInteractionEnabled = false + self.codeInputView = CodeInputView() self.codeInputView.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance self.codeInputView.textField.returnKeyType = .done @@ -163,10 +199,47 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.codeInputView.textField.keyboardType = .numberPad } + self.textSeparatorNode = ASDisplayNode() + self.textSeparatorNode.isLayerBacked = true + self.textSeparatorNode.backgroundColor = self.theme.list.itemPlainSeparatorColor + + self.textField = TextFieldNode() + self.textField.textField.font = Font.regular(20.0) + self.textField.textField.textColor = self.theme.list.itemPrimaryTextColor + self.textField.textField.textAlignment = .natural + self.textField.textField.autocorrectionType = .yes + self.textField.textField.autocorrectionType = .no + self.textField.textField.spellCheckingType = .yes + self.textField.textField.spellCheckingType = .no + self.textField.textField.autocapitalizationType = .none + self.textField.textField.keyboardType = .default + if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { + self.textField.textField.textContentType = UITextContentType(rawValue: "") + } + self.textField.textField.returnKeyType = .default + self.textField.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance + self.textField.textField.disableAutomaticKeyboardHandling = [.forward, .backward] + self.textField.textField.tintColor = self.theme.list.itemAccentColor + + self.pasteButton = HighlightableButtonNode() + self.errorTextNode = ImmediateTextNode() self.errorTextNode.alpha = 0.0 self.errorTextNode.displaysAsynchronously = false self.errorTextNode.textAlignment = .center + self.errorTextNode.isUserInteractionEnabled = false + + self.hintButtonNode = HighlightableButtonNode() + self.hintButtonNode.alpha = 0.0 + self.hintButtonNode.isUserInteractionEnabled = false + + self.hintTextNode = ImmediateTextNode() + self.hintTextNode.displaysAsynchronously = false + self.hintTextNode.textAlignment = .center + self.hintTextNode.isUserInteractionEnabled = false + + self.hintArrowNode = ASImageNode() + self.hintArrowNode.displaysAsynchronously = false self.resetNode = HighlightableButtonNode() self.resetNode.displaysAsynchronously = false @@ -187,11 +260,12 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.signInWithAppleButton?.isHidden = true (self.signInWithAppleButton as? ASAuthorizationAppleIDButton)?.cornerRadius = 11 } - self.proceedNode = SolidRoundedButtonNode(title: self.strings.Login_OpenFragment, theme: SolidRoundedButtonTheme(backgroundColor: UIColor(rgb: 0x37475a), foregroundColor: .white), height: 50.0, cornerRadius: 11.0, gloss: false) + self.proceedNode = SolidRoundedButtonNode(title: self.strings.Login_Continue, theme: SolidRoundedButtonTheme(theme: self.theme), height: 50.0, cornerRadius: 11.0, gloss: false) self.proceedNode.progressType = .embedded self.proceedNode.isHidden = true self.proceedNode.iconSpacing = 4.0 self.proceedNode.animationSize = CGSize(width: 36.0, height: 36.0) + self.proceedNode.isEnabled = false super.init() @@ -201,7 +275,12 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.backgroundColor = self.theme.list.plainBackgroundColor + self.textField.textField.delegate = self + self.addSubnode(self.codeInputView) + self.addSubnode(self.textSeparatorNode) + self.addSubnode(self.textField) + self.addSubnode(self.pasteButton) self.addSubnode(self.titleNode) self.addSubnode(self.titleActivateAreaNode) self.addSubnode(self.titleIconNode) @@ -212,20 +291,26 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.addSubnode(self.ngAuthCodeNode) // self.addSubnode(self.nextOptionButtonNode) + self.nextOptionButtonNode.addSubnode(self.nextOptionArrowNode) self.addSubnode(self.animationNode) self.addSubnode(self.resetNode) self.addSubnode(self.resetTextNode) self.addSubnode(self.dividerNode) self.addSubnode(self.errorTextNode) + self.addSubnode(self.hintButtonNode) + self.hintButtonNode.addSubnode(self.hintTextNode) + self.hintButtonNode.addSubnode(self.hintArrowNode) self.addSubnode(self.proceedNode) self.codeInputView.updated = { [weak self] in guard let strongSelf = self else { return } - strongSelf.textChanged(text: strongSelf.codeInputView.text) + strongSelf.codeChanged(text: strongSelf.codeInputView.text) } + self.textField.textField.addTarget(self, action: #selector(self.textDidChange), for: .editingChanged) + self.codeInputView.longPressed = { [weak self] in guard let strongSelf = self else { return @@ -260,6 +345,18 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF } self.signInWithAppleButton?.addTarget(self, action: #selector(self.signInWithApplePressed), for: .touchUpInside) self.resetNode.addTarget(self, action: #selector(self.resetPressed), forControlEvents: .touchUpInside) + + self.pasteButton.setTitle(strings.Login_Paste, with: Font.medium(13.0), with: theme.list.itemAccentColor, for: .normal) + self.pasteButton.setBackgroundImage(generateStretchableFilledCircleImage(radius: 12.0, color: theme.list.itemAccentColor.withAlphaComponent(0.1)), for: .normal) + self.pasteButton.addTarget(self, action: #selector(self.pastePressed), forControlEvents: .touchUpInside) + + self.hintButtonNode.addTarget(self, action: #selector(self.previousOptionNodePressed), forControlEvents: .touchUpInside) + + self.hintArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: theme.list.itemAccentColor) + self.hintArrowNode.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + self.nextOptionArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: theme.list.itemAccentColor) + + self.updatePasteVisibility() } deinit { @@ -273,10 +370,24 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.view.addSubview(signInWithAppleButton) } } + + @objc private func pastePressed() { + if let text = UIPasteboard.general.string, !text.isEmpty { + if checkValidity(text: text) { + self.textField.textField.text = text + self.updatePasteVisibility() + } + } + } + func updatePasteVisibility() { + let text = self.textField.textField.text ?? "" + self.pasteButton.isHidden = !text.isEmpty + } + func updateCode(_ code: String) { self.codeInputView.text = code - self.textChanged(text: code) + self.codeChanged(text: code) if let codeLength = self.requiredCodeLength, code.count == Int(codeLength) { self.loginWithCode?(code) @@ -308,10 +419,12 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.codeInputView.text = "" } - func updateData(number: String, email: String?, codeType: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, timeout: Int32?, appleSignInAllowed: Bool) { + func updateData(number: String, email: String?, codeType: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, timeout: Int32?, appleSignInAllowed: Bool, previousCodeType: SentAuthorizationCodeType?, isPrevious: Bool) { self.codeType = codeType - self.phoneNumber = number + self.phoneNumber = number.replacingOccurrences(of: " ", with: "\u{00A0}").replacingOccurrences(of: "-", with: "\u{2011}") self.email = email + self.previousCodeType = previousCodeType + self.isPrevious = isPrevious var appleSignInAllowed = appleSignInAllowed if #available(iOS 13.0, *) { @@ -334,6 +447,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.currentOptionInfoActivateAreaNode.removeFromSupernode() } } + if let timeout = timeout { #if DEBUG let timeout = min(timeout, 5) @@ -343,7 +457,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF if let strongSelf = self { if let currentTimeoutTime = strongSelf.currentTimeoutTime, currentTimeoutTime > 0 { strongSelf.currentTimeoutTime = currentTimeoutTime - 1 - let (nextOptionText, nextOptionActive) = authorizationNextOptionText(currentType: codeType, nextType: nextType, timeout: strongSelf.currentTimeoutTime, strings: strongSelf.strings, primaryColor: strongSelf.theme.list.itemPrimaryTextColor, accentColor: strongSelf.theme.list.itemAccentColor) + let (nextOptionText, nextOptionActive) = authorizationNextOptionText(currentType: codeType, nextType: nextType, previousCodeType: isPrevious ? previousCodeType : nil, timeout: strongSelf.currentTimeoutTime, strings: strongSelf.strings, primaryColor: strongSelf.theme.list.itemSecondaryTextColor, accentColor: strongSelf.theme.list.itemAccentColor) strongSelf.nextOptionTitleNode.attributedText = nextOptionText strongSelf.nextOptionButtonNode.isUserInteractionEnabled = nextOptionActive strongSelf.nextOptionButtonNode.accessibilityLabel = nextOptionText.string @@ -383,7 +497,8 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.currentTimeoutTime = nil self.countdownDisposable.set(nil) } - let (nextOptionText, nextOptionActive) = authorizationNextOptionText(currentType: codeType, nextType: nextType, timeout: self.currentTimeoutTime, strings: self.strings, primaryColor: self.theme.list.itemPrimaryTextColor, accentColor: self.theme.list.itemAccentColor) + + let (nextOptionText, nextOptionActive) = authorizationNextOptionText(currentType: codeType, nextType: nextType, previousCodeType: isPrevious ? previousCodeType : nil, timeout: self.currentTimeoutTime, strings: self.strings, primaryColor: self.theme.list.itemSecondaryTextColor, accentColor: self.theme.list.itemAccentColor) self.nextOptionTitleNode.attributedText = nextOptionText self.nextOptionButtonNode.isUserInteractionEnabled = nextOptionActive self.nextOptionButtonNode.accessibilityLabel = nextOptionText.string @@ -392,6 +507,9 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF } else { self.nextOptionButtonNode.accessibilityTraits = [.button, .notEnabled] } + + self.nextOptionArrowNode.isHidden = previousCodeType == nil || !isPrevious + if let layoutArguments = self.layoutArguments { self.containerLayoutUpdated(layoutArguments.0, navigationBarHeight: layoutArguments.1, transition: .immediate) } @@ -416,6 +534,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF var animationName = "IntroMessage" var animationPlaybackMode: AnimatedStickerPlaybackMode = .once + var textFieldPlaceholder = "" if let codeType = self.codeType { switch codeType { case .missedCall: @@ -427,9 +546,20 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.titleNode.attributedText = NSAttributedString(string: self.strings.Login_EnterCodeSMSTitle, font: Font.semibold(28.0), textColor: self.theme.list.itemPrimaryTextColor) case .fragment: self.titleNode.attributedText = NSAttributedString(string: self.strings.Login_EnterCodeFragmentTitle, font: Font.semibold(28.0), textColor: self.theme.list.itemPrimaryTextColor) + + self.proceedNode.title = self.strings.Login_OpenFragment + self.proceedNode.updateTheme(SolidRoundedButtonTheme(backgroundColor: UIColor(rgb: 0x37475a), foregroundColor: .white)) + self.proceedNode.isEnabled = true + animationName = "IntroFragment" animationPlaybackMode = .count(3) self.proceedNode.animation = "anim_fragment" + case .word: + self.titleNode.attributedText = NSAttributedString(string: self.strings.Login_EnterWordTitle, font: Font.semibold(28.0), textColor: self.theme.list.itemPrimaryTextColor) + textFieldPlaceholder = self.strings.Login_EnterWordPlaceholder + case .phrase: + self.titleNode.attributedText = NSAttributedString(string: self.strings.Login_EnterPhraseTitle, font: Font.semibold(28.0), textColor: self.theme.list.itemPrimaryTextColor) + textFieldPlaceholder = self.strings.Login_EnterPhrasePlaceholder default: self.titleNode.attributedText = NSAttributedString(string: self.strings.Login_EnterCodeTelegramTitle, font: Font.semibold(28.0), textColor: self.theme.list.itemPrimaryTextColor) } @@ -437,14 +567,17 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.titleNode.attributedText = NSAttributedString(string: self.strings.Login_EnterCodeTelegramTitle, font: Font.semibold(40.0), textColor: self.theme.list.itemPrimaryTextColor) } + self.textField.textField.attributedPlaceholder = NSAttributedString(string: textFieldPlaceholder, font: Font.regular(20.0), textColor: self.theme.list.itemPlaceholderTextColor) + self.titleActivateAreaNode.accessibilityLabel = self.titleNode.attributedText?.string ?? "" if let inputHeight = layout.inputHeight { - if let codeType = self.codeType, case .email = codeType { - insets.bottom = max(inputHeight, insets.bottom) - } else if let codeType = self.codeType, case .fragment = codeType { + switch self.codeType { + case .email, .fragment: insets.bottom = max(inputHeight, insets.bottom) - } else { + case .word, .phrase: + insets.bottom = max(inputHeight, layout.standardKeyboardHeight) + default: insets.bottom = max(inputHeight, layout.standardInputHeight) } } @@ -490,6 +623,8 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF codeLength = Int(length) case .emailSetupRequired: codeLength = 6 + case .word, .phrase: + codeLength = 0 case .none: codeLength = 6 } @@ -513,8 +648,11 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF if layout.size.width > 320.0 { items.append(AuthorizationLayoutItem(node: self.animationNode, size: animationSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 10.0, maxValue: 10.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) self.animationNode.updateLayout(size: animationSize) + self.animationNode.isHidden = false + self.animationNode.visibility = true } else { insets.top = navigationBarHeight + self.animationNode.isHidden = true } var additionalBottomInset: CGFloat = 20.0 @@ -570,7 +708,19 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF } } - items.append(AuthorizationLayoutItem(node: self.codeInputView, size: codeFieldSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 30.0, maxValue: 30.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: canReset || pendingDate != nil ? 0.0 : 104.0, maxValue: canReset ? 0.0 : 104.0))) + switch codeType { + case .word, .phrase: + self.codeInputView.isHidden = true + self.textField.isHidden = false + self.textSeparatorNode.isHidden = false + items.append(AuthorizationLayoutItem(node: self.textField, size: CGSize(width: maximumWidth - 88.0, height: 44.0), spacingBefore: AuthorizationLayoutItemSpacing(weight: 18.0, maxValue: 30.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + items.append(AuthorizationLayoutItem(node: self.textSeparatorNode, size: CGSize(width: maximumWidth - 48.0, height: UIScreenPixel), spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + default: + self.codeInputView.isHidden = false + self.textField.isHidden = true + self.textSeparatorNode.isHidden = true + items.append(AuthorizationLayoutItem(node: self.codeInputView, size: codeFieldSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 30.0, maxValue: 30.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: canReset || pendingDate != nil ? 0.0 : 104.0, maxValue: canReset ? 0.0 : 104.0))) + } if canReset { self.resetNode.setAttributedTitle(NSAttributedString(string: self.strings.Login_Email_CantAccess, font: Font.regular(17.0), textColor: self.theme.list.itemAccentColor, paragraphAlignment: .center), for: []) @@ -634,12 +784,27 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF } else { self.signInWithAppleButton?.isHidden = true self.dividerNode.isHidden = true - self.proceedNode.isHidden = true - if case .email = codeType { + switch codeType { + case .word, .phrase: + additionalBottomInset = 100.0 + + self.nextOptionButtonNode.isHidden = false + items.append(AuthorizationLayoutItem(node: self.nextOptionButtonNode, size: nextOptionSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 50.0, maxValue: 120.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + if layout.size.width > 320.0 { + self.proceedNode.isHidden = false + } else { + self.proceedNode.isHidden = true + } + + let buttonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - proceedSize.width) / 2.0), y: layout.size.height - insets.bottom - proceedSize.height - inset), size: proceedSize) + transition.updateFrame(node: self.proceedNode, frame: buttonFrame) + case .email: self.nextOptionButtonNode.isHidden = true - } else { + self.proceedNode.isHidden = true + default: self.nextOptionButtonNode.isHidden = false + self.proceedNode.isHidden = true items.append(AuthorizationLayoutItem(node: self.nextOptionButtonNode, size: nextOptionSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 50.0, maxValue: 120.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) } } @@ -656,39 +821,170 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF let _ = layoutAuthorizationItems(bounds: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - insets.bottom - additionalBottomInset)), items: items, transition: transition, failIfDoesNotFit: false) + if let codeType = self.codeType { + let yOffset: CGFloat = layout.size.width > 320.0 ? 18.0 : 5.0 + if let previousCodeType = self.previousCodeType, !self.isPrevious { + self.hintButtonNode.alpha = 1.0 + self.hintButtonNode.isUserInteractionEnabled = true + + let actionTitle: String + switch previousCodeType { + case .word: + actionTitle = self.strings.Login_BackToWord + case .phrase: + actionTitle = self.strings.Login_BackToPhrase + default: + actionTitle = self.strings.Login_BackToCode + } + + self.hintTextNode.attributedText = NSAttributedString(string: actionTitle, font: Font.regular(13.0), textColor: self.theme.list.itemAccentColor, paragraphAlignment: .center) + + let originY: CGFloat + if !self.codeInputView.isHidden { + originY = self.codeInputView.frame.maxY + 6.0 + } else { + originY = self.textField.frame.maxY + } + + let hintTextSize = self.hintTextNode.updateLayout(CGSize(width: layout.size.width - 48.0, height: .greatestFiniteMagnitude)) + transition.updateFrame(node: self.hintButtonNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - hintTextSize.width) / 2.0), y: originY + yOffset), size: hintTextSize)) + self.hintTextNode.frame = CGRect(origin: .zero, size: hintTextSize) + + if let icon = self.hintArrowNode.image { + self.hintArrowNode.frame = CGRect(origin: CGPoint(x: self.hintTextNode.frame.minX - icon.size.width - 5.0, y: self.hintTextNode.frame.midY - icon.size.height / 2.0), size: icon.size) + } + self.hintArrowNode.isHidden = false + } else if case .phrase = codeType { + if self.errorTextNode.alpha.isZero { + self.hintButtonNode.alpha = 1.0 + } + self.hintButtonNode.isUserInteractionEnabled = false + + self.hintTextNode.attributedText = NSAttributedString(string: self.strings.Login_EnterPhraseHint, font: Font.regular(13.0), textColor: self.theme.list.itemSecondaryTextColor, paragraphAlignment: .center) + + let hintTextSize = self.hintTextNode.updateLayout(CGSize(width: layout.size.width - 48.0, height: .greatestFiniteMagnitude)) + transition.updateFrame(node: self.hintButtonNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - hintTextSize.width) / 2.0), y: self.textField.frame.maxY + yOffset), size: hintTextSize)) + self.hintTextNode.frame = CGRect(origin: .zero, size: hintTextSize) + + let pasteSize = self.pasteButton.measure(layout.size) + let pasteButtonSize = CGSize(width: pasteSize.width + 16.0, height: 24.0) + transition.updateFrame(node: self.pasteButton, frame: CGRect(origin: CGPoint(x: layout.size.width - 40.0 - pasteButtonSize.width, y: self.textField.frame.midY - pasteButtonSize.height / 2.0), size: pasteButtonSize)) + self.hintArrowNode.isHidden = true + } else if case .word = codeType { + self.hintButtonNode.alpha = 0.0 + self.hintButtonNode.isUserInteractionEnabled = false + self.hintArrowNode.isHidden = true + + let pasteSize = self.pasteButton.measure(layout.size) + let pasteButtonSize = CGSize(width: pasteSize.width + 16.0, height: 24.0) + transition.updateFrame(node: self.pasteButton, frame: CGRect(origin: CGPoint(x: layout.size.width - 40.0 - pasteButtonSize.width, y: self.textField.frame.midY - pasteButtonSize.height / 2.0), size: pasteButtonSize)) + } else { + self.hintButtonNode.alpha = 0.0 + self.hintButtonNode.isUserInteractionEnabled = false + self.hintArrowNode.isHidden = true + } + } else { + self.hintButtonNode.alpha = 0.0 + self.hintButtonNode.isUserInteractionEnabled = false + self.hintArrowNode.isHidden = true + } + self.nextOptionTitleNode.frame = self.nextOptionButtonNode.bounds + if let icon = self.nextOptionArrowNode.image { + self.nextOptionArrowNode.frame = CGRect(origin: CGPoint(x: self.nextOptionTitleNode.frame.maxX + 7.0, y: self.nextOptionTitleNode.frame.midY - icon.size.height / 2.0), size: icon.size) + } + self.titleActivateAreaNode.frame = self.titleNode.frame self.currentOptionActivateAreaNode.frame = self.currentOptionNode.frame self.currentOptionInfoActivateAreaNode.frame = self.currentOptionInfoNode.frame } func activateInput() { - let _ = self.codeInputView.becomeFirstResponder() + switch self.codeType { + case .word, .phrase: + self.textField.textField.becomeFirstResponder() + default: + let _ = self.codeInputView.becomeFirstResponder() + } } func animateError() { - self.codeInputView.layer.addShakeAnimation() + switch self.codeType { + case .word, .phrase: + self.textField.layer.addShakeAnimation() + default: + self.codeInputView.layer.addShakeAnimation() + } + } + + func selectIncorrectPart() { + switch self.codeType { + case .word: + self.textField.textField.selectAll(nil) + case let .phrase(startsWith): + if let startsWith, let fromPosition = self.textField.textField.position(from: self.textField.textField.beginningOfDocument, offset: startsWith.count + 1) { + self.textField.textField.selectedTextRange = self.textField.textField.textRange(from: fromPosition, to: self.textField.textField.endOfDocument) + } else { + self.textField.textField.selectAll(nil) + } + default: + break + } } func animateError(text: String) { - self.codeInputView.animateError() - self.codeInputView.layer.addShakeAnimation(amplitude: -30.0, duration: 0.5, count: 6, decay: true) + let errorOriginY: CGFloat + let errorOriginOffset: CGFloat + switch self.codeType { + case .word, .phrase: + self.textField.layer.addShakeAnimation() + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.15, curve: .easeInOut) + transition.updateBackgroundColor(node: self.textSeparatorNode, color: self.theme.list.itemDestructiveColor) + errorOriginY = self.textField.frame.maxY + errorOriginOffset = 5.0 + default: + self.codeInputView.animateError() + self.codeInputView.layer.addShakeAnimation(amplitude: -30.0, duration: 0.5, count: 6, decay: true) + errorOriginY = self.codeInputView.frame.maxY + errorOriginOffset = 11.0 + } - self.errorTextNode.attributedText = NSAttributedString(string: text, font: Font.regular(17.0), textColor: self.theme.list.itemDestructiveColor, paragraphAlignment: .center) + self.errorTextNode.attributedText = NSAttributedString(string: text, font: Font.regular(13.0), textColor: self.theme.list.itemDestructiveColor, paragraphAlignment: .center) if let (layout, _) = self.layoutArguments { let errorTextSize = self.errorTextNode.updateLayout(CGSize(width: layout.size.width - 48.0, height: .greatestFiniteMagnitude)) - let yOffset: CGFloat = layout.size.width > 320.0 ? 28.0 : 15.0 - self.errorTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - errorTextSize.width) / 2.0), y: self.codeInputView.frame.maxY + yOffset), size: errorTextSize) + let yOffset: CGFloat = layout.size.width > 320.0 ? errorOriginOffset + 13.0 : errorOriginOffset + self.errorTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - errorTextSize.width) / 2.0), y: errorOriginY + yOffset), size: errorTextSize) } self.errorTextNode.alpha = 1.0 self.errorTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - self.errorTextNode.layer.addShakeAnimation(amplitude: -8.0, duration: 0.5, count: 6, decay: true) - Queue.mainQueue().after(0.85) { + let previousHintAlpha = self.hintButtonNode.alpha + self.hintButtonNode.alpha = 0.0 + self.hintButtonNode.layer.animateAlpha(from: previousHintAlpha, to: 0.0, duration: 0.1) + + let previousResetAlpha = self.resetNode.alpha + if !self.resetNode.isHidden { + self.resetNode.alpha = 0.0 + self.resetNode.layer.animateAlpha(from: previousResetAlpha, to: 0.0, duration: 0.1) + } + + Queue.mainQueue().after(1.6) { self.errorTextNode.alpha = 0.0 self.errorTextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15) + + self.hintButtonNode.alpha = previousHintAlpha + self.hintButtonNode.layer.animateAlpha(from: 0.0, to: previousHintAlpha, duration: 0.1) + + if !self.resetNode.isHidden { + self.resetNode.alpha = previousResetAlpha + self.resetNode.layer.animateAlpha(from: 0.0, to: previousResetAlpha, duration: 0.1) + } + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.15, curve: .easeInOut) + transition.updateBackgroundColor(node: self.textSeparatorNode, color: self.theme.list.itemPlainSeparatorColor) } } @@ -698,12 +994,16 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF let values: [NSNumber] = [1.0, 1.1, 1.0] self.codeInputView.layer.animateKeyframes(values: values, duration: 0.4, keyPath: "transform.scale") } - - @objc func codeFieldTextChanged(_ textField: UITextField) { - self.textChanged(text: textField.text ?? "") - } + + @objc private func textDidChange() { + let text = self.textField.textField.text ?? "" + self.proceedNode.isEnabled = !text.isEmpty + self.updateNextEnabled?(!text.isEmpty) - private func textChanged(text: String) { + self.updatePasteVisibility() + } + + private func codeChanged(text: String) { self.updateNextEnabled?(!text.isEmpty) if let codeType = self.codeType { var codeLength: Int32? @@ -730,25 +1030,46 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF } } } - + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { if self.inProgress { return false } - var result = "" - for c in string { - if c.unicodeScalars.count == 1 { - let scalar = c.unicodeScalars.first! - if scalar >= "0" && scalar <= "9" { - result.append(c) + + var updated = textField.text ?? "" + updated.replaceSubrange(updated.index(updated.startIndex, offsetBy: range.lowerBound) ..< updated.index(updated.startIndex, offsetBy: range.upperBound), with: string) + + if updated.isEmpty { + return true + } else { + return checkValidity(text: updated) && !updated.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + } + } + + func checkValidity(text: String) -> Bool { + if let codeType = self.codeType { + switch codeType { + case let .word(startsWith): + if let startsWith, startsWith.count == 1, !text.isEmpty && !text.hasPrefix(startsWith) { + if self.errorTextNode.alpha.isZero { + self.animateError(text: self.strings.Login_WrongPhraseError) + } + return false } + case let .phrase(startsWith): + if let startsWith, !text.isEmpty { + let firstWord = text.components(separatedBy: " ").first ?? "" + if !firstWord.isEmpty && !startsWith.hasPrefix(firstWord) { + if self.errorTextNode.alpha.isZero, text.count < 4 { + self.animateError(text: self.strings.Login_WrongPhraseError) + } + return false + } + } + default: + break } } - if result != string { - textField.text = result - self.codeFieldTextChanged(textField) - return false - } return true } @@ -756,9 +1077,20 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.requestAnotherOption?() } + @objc func previousOptionNodePressed() { + self.requestPreviousOption?() + } + @objc func proceedPressed() { - if case let .fragment(url, _) = self.codeType { + switch self.codeType { + case let .fragment(url, _): self.openFragment?(url) + case .word, .phrase: + if let text = self.textField.textField.text, !text.isEmpty { + self.loginWithCode?(text) + } + default: + break } } diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift index 70fcae7737e..8f7278fd4da 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift @@ -45,6 +45,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth private var stateDisposable: Disposable? private let actionDisposable = MetaDisposable() + private var applicationStateDisposable: Disposable? private var didPlayPresentationAnimation = false @@ -54,6 +55,10 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth } private var didSetReady = false + fileprivate var engine: TelegramEngineUnauthorized { + return TelegramEngineUnauthorized(account: self.account) + } + public init(sharedContext: SharedAccountContext, account: UnauthorizedAccount, otherAccountPhoneNumbers: ((String, AccountRecordId, Bool)?, [(String, AccountRecordId, Bool)]), presentationData: PresentationData, openUrl: @escaping (String) -> Void, apiId: Int32, apiHash: String, authorizationCompleted: @escaping () -> Void) { self.sharedContext = sharedContext self.account = account @@ -74,7 +79,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth super.init(mode: .single, theme: NavigationControllerTheme(statusBar: navigationStatusBar, navigationBar: AuthorizationSequenceController.navigationBarTheme(presentationData.theme), emptyAreaColor: .black), isFlat: true) - self.stateDisposable = (TelegramEngineUnauthorized(account: self.account).auth.state() + self.stateDisposable = (self.engine.auth.state() |> map { state -> InnerState in if case .authorized = state { return .authorized @@ -88,6 +93,18 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth |> deliverOnMainQueue).startStrict(next: { [weak self] state in self?.updateState(state: state) }).strict() + + self.applicationStateDisposable = (self.sharedContext.applicationBindings.applicationIsActive + |> deliverOnMainQueue).start(next: { [weak self] isActive in + guard let self else { + return + } + for viewController in self.viewControllers { + if let codeController = viewController as? AuthorizationSequenceCodeEntryController { + codeController.updateAppIsActive(isActive) + } + } + }) } required public init(coder aDecoder: NSCoder) { @@ -97,6 +114,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth deinit { self.stateDisposable?.dispose() self.actionDisposable.dispose() + self.applicationStateDisposable?.dispose() } override public func loadView() { @@ -127,7 +145,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth let countryCode = AuthorizationSequenceController.defaultCountryCode() - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: isTestingEnvironment, masterDatacenterId: masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).startStandalone() + let _ = strongSelf.engine.auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: isTestingEnvironment, masterDatacenterId: masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).startStandalone() } } } @@ -157,7 +175,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth transaction.removeAuth() }).startStandalone() } else { - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .empty)).startStandalone() + let _ = strongSelf.engine.auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .empty)).startStandalone() } }) if let splashController = splashController { @@ -174,13 +192,15 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth return } controller?.inProgress = true + + let disableAuthTokens = self.sharedContext.immediateExperimentalUISettings.disableReloginTokens let authorizationPushConfiguration = self.sharedContext.authorizationPushConfiguration |> take(1) |> timeout(2.0, queue: .mainQueue(), alternate: .single(nil)) let _ = (authorizationPushConfiguration |> deliverOnMainQueue).startStandalone(next: { [weak self] authorizationPushConfiguration in if let strongSelf = self { - strongSelf.actionDisposable.set((sendAuthorizationCode(accountManager: strongSelf.sharedContext.accountManager, account: strongSelf.account, phoneNumber: number, apiId: strongSelf.apiId, apiHash: strongSelf.apiHash, pushNotificationConfiguration: authorizationPushConfiguration, firebaseSecretStream: strongSelf.sharedContext.firebaseSecretStream, syncContacts: syncContacts, forcedPasswordSetupNotice: { value in + strongSelf.actionDisposable.set((sendAuthorizationCode(accountManager: strongSelf.sharedContext.accountManager, account: strongSelf.account, phoneNumber: number, apiId: strongSelf.apiId, apiHash: strongSelf.apiHash, pushNotificationConfiguration: authorizationPushConfiguration, firebaseSecretStream: strongSelf.sharedContext.firebaseSecretStream, syncContacts: syncContacts, disableAuthTokens: disableAuthTokens, forcedPasswordSetupNotice: { value in guard let entry = CodableEntry(ApplicationSpecificCounterNotice(value: value)) else { return nil } @@ -292,16 +312,13 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth return controller } - private func codeEntryController(number: String, phoneCodeHash: String, email: String?, type: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, timeout: Int32?, termsOfService: (UnauthorizedAccountTermsOfService, Bool)?) -> AuthorizationSequenceCodeEntryController { + private func codeEntryController(number: String, phoneCodeHash: String, email: String?, type: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, timeout: Int32?, previousCodeType: SentAuthorizationCodeType?, isPrevious: Bool, termsOfService: (UnauthorizedAccountTermsOfService, Bool)?) -> AuthorizationSequenceCodeEntryController { var currentController: AuthorizationSequenceCodeEntryController? for c in self.viewControllers { if let c = c as? AuthorizationSequenceCodeEntryController { if c.data?.2 == type { currentController = c } -// else if case let .email(_, _, _, newPendingDate, _, _) = type, let previousType = c.data?.2, case let .email(_, _, _, previousPendingDate, _, _) = previousType, newPendingDate != nil && previousPendingDate == nil { -// currentController = c -// } break } } @@ -315,7 +332,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth } let countryCode = AuthorizationSequenceController.defaultCountryCode() - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).startStandalone() + let _ = strongSelf.engine.auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).startStandalone() }) controller.retryResetEmail = { [weak self] in if let self { @@ -381,7 +398,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth case .codeExpired: text = self.presentationData.strings.Login_CodeExpired let account = self.account - let _ = TelegramEngineUnauthorized(account: self.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).startStandalone() + let _ = self.engine.auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).startStandalone() } controller.presentInGlobalOverlay(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})])) @@ -430,7 +447,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth case .codeExpired: text = strongSelf.presentationData.strings.Login_CodeExpired let account = strongSelf.account - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).startStandalone() + let _ = strongSelf.engine.auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).startStandalone() case .timeout: text = strongSelf.presentationData.strings.Login_NetworkError case .invalidEmailToken: @@ -459,7 +476,6 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth guard let strongSelf = self else { return } - controller?.inProgress = false switch result { case let .signUp(data): if let (termsOfService, explicit) = termsOfService, explicit { @@ -490,7 +506,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth return } let account = strongSelf.account - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).startStandalone() + let _ = strongSelf.engine.auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).startStandalone() })]), on: .root, blockInteraction: false, completion: {}) }) ], actionLayout: .vertical, dismissOnOutsideTap: true) @@ -522,7 +538,15 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth controller.inProgress = false if case .invalidCode = error { - controller.animateError(text: strongSelf.presentationData.strings.Login_WrongCodeError) + let text: String + switch type { + case .word, .phrase: + text = strongSelf.presentationData.strings.Login_WrongPhraseError + controller.selectIncorrectPart() + default: + text = strongSelf.presentationData.strings.Login_WrongCodeError + } + controller.animateError(text: text) } else { var resetCode = false let text: String @@ -538,7 +562,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth case .codeExpired: text = strongSelf.presentationData.strings.Login_CodeExpired let account = strongSelf.account - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).startStandalone() + let _ = strongSelf.engine.auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).startStandalone() case .invalidEmailToken: text = strongSelf.presentationData.strings.Login_InvalidEmailTokenError case .invalidEmailAddress: @@ -560,9 +584,18 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth } controller.requestNextOption = { [weak self, weak controller] in if let strongSelf = self { + if previousCodeType != nil && isPrevious { + strongSelf.actionDisposable.set(togglePreviousCodeEntry(account: strongSelf.account).start()) + return + } + if nextType == nil { if let controller { - AuthorizationSequenceController.presentDidNotGetCodeUI(controller: controller, presentationData: strongSelf.presentationData, number: number) + let carrier = CTCarrier() + let mnc = carrier.mobileNetworkCode ?? "none" + let _ = strongSelf.engine.auth.reportMissingCode(phoneNumber: number, phoneCodeHash: phoneCodeHash, mnc: mnc).start() + + AuthorizationSequenceController.presentDidNotGetCodeUI(controller: controller, presentationData: strongSelf.presentationData, phoneNumber: number, mnc: mnc) } } else { controller?.inProgress = true @@ -604,11 +637,17 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth } } } + controller.requestPreviousOption = { [weak self] in + guard let self else { + return + } + self.actionDisposable.set(togglePreviousCodeEntry(account: self.account).start()) + } controller.reset = { [weak self] in - if let strongSelf = self { - let account = strongSelf.account - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).startStandalone() + guard let self else { + return } + let _ = self.engine.auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: self.account.testingEnvironment, masterDatacenterId: self.account.masterDatacenterId, contents: .empty)).startStandalone() } controller.signInWithApple = { [weak self] in guard let strongSelf = self else { @@ -633,7 +672,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth strongSelf.sharedContext.applicationBindings.openUrl(url) } } - controller.updateData(number: formatPhoneNumber(number), email: email, codeType: type, nextType: nextType, timeout: timeout, termsOfService: termsOfService) + controller.updateData(number: formatPhoneNumber(number), email: email, codeType: type, nextType: nextType, timeout: timeout, termsOfService: termsOfService, previousCodeType: previousCodeType, isPrevious: isPrevious) return controller } @@ -659,7 +698,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth } let countryCode = AuthorizationSequenceController.defaultCountryCode() - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).startStandalone() + let _ = strongSelf.engine.auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).startStandalone() }) } controller.proceedWithEmail = { [weak self, weak controller] email in @@ -783,7 +822,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth case .codeExpired: text = strongSelf.presentationData.strings.Login_CodeExpired let account = strongSelf.account - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).startStandalone() + let _ = strongSelf.engine.auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).startStandalone() case .invalidEmailToken: text = strongSelf.presentationData.strings.Login_InvalidEmailTokenError case .invalidEmailAddress: @@ -832,7 +871,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth } let countryCode = AuthorizationSequenceController.defaultCountryCode() - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).startStandalone() + let _ = strongSelf.engine.auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).startStandalone() }) controller.loginWithPassword = { [weak self, weak controller] password in if let strongSelf = self { @@ -864,19 +903,19 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth controller.forgot = { [weak self, weak controller] in if let strongSelf = self, let strongController = controller { strongController.inProgress = true - strongSelf.actionDisposable.set((TelegramEngineUnauthorized(account: strongSelf.account).auth.requestTwoStepVerificationPasswordRecoveryCode() + strongSelf.actionDisposable.set((strongSelf.engine.auth.requestTwoStepVerificationPasswordRecoveryCode() |> deliverOnMainQueue).startStrict(next: { pattern in if let strongSelf = self, let strongController = controller { strongController.inProgress = false - let _ = (TelegramEngineUnauthorized(account: strongSelf.account).auth.state() + let _ = (strongSelf.engine.auth.state() |> take(1) |> deliverOnMainQueue).startStandalone(next: { state in guard let strongSelf = self else { return } if case let .unauthorized(state) = state, case let .passwordEntry(hint, number, code, _, syncContacts) = state.contents { - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .passwordRecovery(hint: hint, number: number, code: code, emailPattern: pattern, syncContacts: syncContacts))).startStandalone() + let _ = strongSelf.engine.auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .passwordRecovery(hint: hint, number: number, code: code, emailPattern: pattern, syncContacts: syncContacts))).startStandalone() } }) } @@ -937,7 +976,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth if let currentController = currentController { controller = currentController } else { - controller = TwoFactorDataInputScreen(sharedContext: self.sharedContext, engine: .unauthorized(TelegramEngineUnauthorized(account: self.account)), mode: .passwordRecoveryEmail(emailPattern: emailPattern, mode: .notAuthorized(syncContacts: syncContacts), doneText: self.presentationData.strings.TwoFactorSetup_Done_Action), stateUpdated: { _ in + controller = TwoFactorDataInputScreen(sharedContext: self.sharedContext, engine: .unauthorized(self.engine), mode: .passwordRecoveryEmail(emailPattern: emailPattern, mode: .notAuthorized(syncContacts: syncContacts), doneText: self.presentationData.strings.TwoFactorSetup_Done_Action), stateUpdated: { _ in }, presentation: .default) } controller.passwordRecoveryFailed = { [weak self] in @@ -945,14 +984,14 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth return } - let _ = (TelegramEngineUnauthorized(account: strongSelf.account).auth.state() + let _ = (strongSelf.engine.auth.state() |> take(1) |> deliverOnMainQueue).startStandalone(next: { state in guard let strongSelf = self else { return } if case let .unauthorized(state) = state, case let .passwordRecovery(hint, number, code, _, syncContacts) = state.contents { - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .passwordEntry(hint: hint, number: number, code: code, suggestReset: true, syncContacts: syncContacts))).startStandalone() + let _ = strongSelf.engine.auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .passwordEntry(hint: hint, number: number, code: code, suggestReset: true, syncContacts: syncContacts))).startStandalone() } }) } @@ -977,7 +1016,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth } let countryCode = AuthorizationSequenceController.defaultCountryCode() - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).startStandalone() + let _ = strongSelf.engine.auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).startStandalone() }) controller.reset = { [weak self, weak controller] in if let strongSelf = self, let strongController = controller { @@ -1011,7 +1050,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth controller.logout = { [weak self] in if let strongSelf = self { let account = strongSelf.account - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).startStandalone() + let _ = strongSelf.engine.auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).startStandalone() } } } @@ -1037,7 +1076,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth } let countryCode = AuthorizationSequenceController.defaultCountryCode() - let _ = TelegramEngineUnauthorized(account: strongSelf.account).auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).startStandalone() + let _ = strongSelf.engine.auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: strongSelf.account.testingEnvironment, masterDatacenterId: strongSelf.account.masterDatacenterId, contents: .phoneEntry(countryCode: countryCode, number: ""))).startStandalone() }, displayCancel: displayCancel) controller.openUrl = { [weak self] url in guard let self else { @@ -1056,7 +1095,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth let avatarVideo: Signal? if let avatarAsset = avatarAsset as? AVAsset { - let account = strongSelf.account + let engine = strongSelf.engine avatarVideo = Signal { subscriber in let entityRenderer: LegacyPaintEntityRenderer? = avatarAdjustments.flatMap { adjustments in if let paintingData = adjustments.paintingData, paintingData.hasAnimation { @@ -1075,7 +1114,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth if stat(result.fileURL.path, &value) == 0 { if let data = try? Data(contentsOf: result.fileURL) { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + engine.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) subscriber.putNext(resource) EngineTempBox.shared.dispose(tempFile) @@ -1096,7 +1135,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth } |> mapToSignal { resource -> Signal in if let resource = resource { - return TelegramEngineUnauthorized(account: account).auth.uploadedPeerVideo(resource: resource) |> map(Optional.init) + return engine.auth.uploadedPeerVideo(resource: resource) |> map(Optional.init) } else { return .single(nil) } @@ -1180,12 +1219,14 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth controllers.append(self.phoneEntryController(countryCode: countryCode, number: number, splashController: previousSplashController)) self.setViewControllers(controllers, animated: !self.viewControllers.isEmpty && (previousSplashController == nil || self.viewControllers.count > 2)) - case let .confirmationCodeEntry(number, type, phoneCodeHash, timeout, nextType, _): + case let .confirmationCodeEntry(number, type, phoneCodeHash, timeout, nextType, _, previousCodeEntry, usePrevious): var controllers: [ViewController] = [] if !self.otherAccountPhoneNumbers.1.isEmpty { controllers.append(self.splashController()) } controllers.append(self.phoneEntryController(countryCode: AuthorizationSequenceController.defaultCountryCode(), number: "", splashController: nil)) + + var isGoingBack = false if case let .emailSetupRequired(appleSignInAllowed) = type { self.appleSignInAllowed = appleSignInAllowed controllers.append(self.emailSetupController(number: number, appleSignInAllowed: appleSignInAllowed)) @@ -1193,9 +1234,29 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth if let _ = self.currentEmail { controllers.append(self.emailSetupController(number: number, appleSignInAllowed: self.appleSignInAllowed)) } - controllers.append(self.codeEntryController(number: number, phoneCodeHash: phoneCodeHash, email: self.currentEmail, type: type, nextType: nextType, timeout: timeout, termsOfService: nil)) + + if let previousCodeEntry, case let .confirmationCodeEntry(number, previousType, phoneCodeHash, timeout, nextType, _, _, _) = previousCodeEntry, usePrevious { + controllers.append(self.codeEntryController(number: number, phoneCodeHash: phoneCodeHash, email: self.currentEmail, type: previousType, nextType: nextType, timeout: timeout, previousCodeType: type, isPrevious: true, termsOfService: nil)) + isGoingBack = true + } else { + var previousCodeType: SentAuthorizationCodeType? + if let previousCodeEntry, case let .confirmationCodeEntry(_, type, _, _, _, _, _, _) = previousCodeEntry { + previousCodeType = type + } + controllers.append(self.codeEntryController(number: number, phoneCodeHash: phoneCodeHash, email: self.currentEmail, type: type, nextType: nextType, timeout: timeout, previousCodeType: previousCodeType, isPrevious: false, termsOfService: nil)) + } + } + + if isGoingBack, let currentLastController = self.viewControllers.last as? AuthorizationSequenceCodeEntryController, !currentLastController.isPrevious { + var tempControllers = controllers + tempControllers.append(currentLastController) + self.setViewControllers(tempControllers, animated: false) + Queue.mainQueue().justDispatch { + self.setViewControllers(controllers, animated: true) + } + } else { + self.setViewControllers(controllers, animated: !self.viewControllers.isEmpty) } - self.setViewControllers(controllers, animated: !self.viewControllers.isEmpty) case let .passwordEntry(hint, _, _, suggestReset, syncContacts): var controllers: [ViewController] = [] if !self.otherAccountPhoneNumbers.1.isEmpty { @@ -1329,9 +1390,14 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth return countryCode } - public static func presentDidNotGetCodeUI(controller: ViewController, presentationData: PresentationData, number: String) { + public static func presentDidNotGetCodeUI( + controller: ViewController, + presentationData: PresentationData, + phoneNumber: String, + mnc: String + ) { if MFMailComposeViewController.canSendMail() { - let formattedNumber = formatPhoneNumber(number) + let formattedNumber = formatPhoneNumber(phoneNumber) var emailBody = "" emailBody.append(presentationData.strings.Login_EmailCodeBody(formattedNumber).string) @@ -1340,8 +1406,6 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown" let systemVersion = UIDevice.current.systemVersion let locale = Locale.current.identifier - let carrier = CTCarrier() - let mnc = carrier.mobileNetworkCode ?? "none" emailBody.append("Telegram: \(appVersion)\n") emailBody.append("OS: \(systemVersion)\n") emailBody.append("Locale: \(locale)\n") diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePasswordEntryController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePasswordEntryController.swift index c9fc4694fda..22944e3c744 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePasswordEntryController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePasswordEntryController.swift @@ -95,7 +95,7 @@ final class AuthorizationSequencePasswordEntryController: ViewController { super.viewDidAppear(animated) if let navigationController = self.navigationController as? NavigationController, let layout = self.validLayout { - addTemporaryKeyboardSnapshotView(navigationController: navigationController, parentView: self.view, layout: layout) + addTemporaryKeyboardSnapshotView(navigationController: navigationController, layout: layout) } self.controllerNode.activateInput() @@ -139,6 +139,10 @@ final class AuthorizationSequencePasswordEntryController: ViewController { if !hadLayout { self.updateNavigationItems() + + if let navigationController = self.navigationController as? NavigationController { + addTemporaryKeyboardSnapshotView(navigationController: navigationController, layout: layout, local: true) + } } self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift index 9b2675a8ce6..e24f36c707f 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift @@ -719,7 +719,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { tokenString = tokenString.replacingOccurrences(of: "/", with: "_") let urlString = "tg://login?token=\(tokenString)" let _ = (qrCode(string: urlString, color: .black, backgroundColor: .white, icon: .none) - |> deliverOnMainQueue).startStrict(next: { _, generate in + |> deliverOnMainQueue).startStandalone(next: { _, generate in guard let strongSelf = self else { return } diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceSignUpController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceSignUpController.swift index fda1ef98551..8c8fdd8df92 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceSignUpController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceSignUpController.swift @@ -216,7 +216,7 @@ final class AuthorizationSequenceSignUpController: ViewController { super.viewDidAppear(animated) if let navigationController = self.navigationController as? NavigationController, let layout = self.validLayout { - addTemporaryKeyboardSnapshotView(navigationController: navigationController, parentView: self.view, layout: layout) + addTemporaryKeyboardSnapshotView(navigationController: navigationController, layout: layout) } self.controllerNode.activateInput() @@ -242,6 +242,10 @@ final class AuthorizationSequenceSignUpController: ViewController { if !hadLayout { self.updateNavigationItems() + + if let navigationController = self.navigationController as? NavigationController { + addTemporaryKeyboardSnapshotView(navigationController: navigationController, layout: layout, local: true) + } } self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) diff --git a/submodules/AuthorizationUtils/Sources/AuthorizationOptionText.swift b/submodules/AuthorizationUtils/Sources/AuthorizationOptionText.swift index fe2248742a1..cc8c61d54a6 100644 --- a/submodules/AuthorizationUtils/Sources/AuthorizationOptionText.swift +++ b/submodules/AuthorizationUtils/Sources/AuthorizationOptionText.swift @@ -41,39 +41,64 @@ public func authorizationCurrentOptionText(_ type: SentAuthorizationCodeType, ph return parseMarkdownIntoAttributedString(strings.Login_EnterCodeFragmentText(phoneNumber).string, attributes: attributes, textAlignment: .center) case .firebase: return parseMarkdownIntoAttributedString(strings.Login_EnterCodeSMSText(phoneNumber).string, attributes: attributes, textAlignment: .center) + case let .word(startsWith): + if let startsWith { + return parseMarkdownIntoAttributedString(strings.Login_EnterWordBeginningText(startsWith, phoneNumber).string, attributes: attributes, textAlignment: .center) + } else { + return parseMarkdownIntoAttributedString(strings.Login_EnterWordText(phoneNumber).string, attributes: attributes, textAlignment: .center) + } + case let .phrase(startsWith): + if let startsWith { + return parseMarkdownIntoAttributedString(strings.Login_EnterPhraseBeginningText(startsWith, phoneNumber).string, attributes: attributes, textAlignment: .center) + } else { + return parseMarkdownIntoAttributedString(strings.Login_EnterPhraseText(phoneNumber).string, attributes: attributes, textAlignment: .center) + } } } -public func authorizationNextOptionText(currentType: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, timeout: Int32?, strings: PresentationStrings, primaryColor: UIColor, accentColor: UIColor) -> (NSAttributedString, Bool) { +public func authorizationNextOptionText(currentType: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, previousCodeType: SentAuthorizationCodeType? = nil, timeout: Int32?, strings: PresentationStrings, primaryColor: UIColor, accentColor: UIColor) -> (NSAttributedString, Bool) { + let font = Font.regular(16.0) + + if let previousCodeType { + switch previousCodeType { + case .word: + return (NSAttributedString(string: strings.Login_ReturnToWord, font: font, textColor: accentColor, paragraphAlignment: .center), true) + case .phrase: + return (NSAttributedString(string: strings.Login_ReturnToPhrase, font: font, textColor: accentColor, paragraphAlignment: .center), true) + default: + return (NSAttributedString(string: strings.Login_ReturnToCode, font: font, textColor: accentColor, paragraphAlignment: .center), true) + } + } + if let nextType = nextType, let timeout = timeout, timeout > 0 { let minutes = timeout / 60 let seconds = timeout % 60 switch nextType { case .sms: if timeout <= 0 { - return (NSAttributedString(string: strings.Login_CodeSentSms, font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center), false) + return (NSAttributedString(string: strings.Login_CodeSentSms, font: font, textColor: primaryColor, paragraphAlignment: .center), false) } else { let timeString = NSString(format: "%d:%.02d", Int(minutes), Int(seconds)) - return (NSAttributedString(string: strings.Login_WillSendSms(timeString as String).string, font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center), false) + return (NSAttributedString(string: strings.Login_WillSendSms(timeString as String).string, font: font, textColor: primaryColor, paragraphAlignment: .center), false) } case .call: if timeout <= 0 { - return (NSAttributedString(string: strings.Login_CodeSentCall, font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center), false) + return (NSAttributedString(string: strings.Login_CodeSentCall, font: font, textColor: primaryColor, paragraphAlignment: .center), false) } else { - return (NSAttributedString(string: String(format: strings.ChangePhoneNumberCode_CallTimer(String(format: "%d:%.2d", minutes, seconds)).string, minutes, seconds), font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center), false) + return (NSAttributedString(string: String(format: strings.ChangePhoneNumberCode_CallTimer(String(format: "%d:%.2d", minutes, seconds)).string, minutes, seconds), font: font, textColor: primaryColor, paragraphAlignment: .center), false) } case .flashCall, .missedCall: if timeout <= 0 { - return (NSAttributedString(string: strings.ChangePhoneNumberCode_Called, font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center), false) + return (NSAttributedString(string: strings.ChangePhoneNumberCode_Called, font: font, textColor: primaryColor, paragraphAlignment: .center), false) } else { - return (NSAttributedString(string: String(format: strings.ChangePhoneNumberCode_CallTimer(String(format: "%d:%.2d", minutes, seconds)).string, minutes, seconds), font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center), false) + return (NSAttributedString(string: String(format: strings.ChangePhoneNumberCode_CallTimer(String(format: "%d:%.2d", minutes, seconds)).string, minutes, seconds), font: font, textColor: primaryColor, paragraphAlignment: .center), false) } case .fragment: if timeout <= 0 { - return (NSAttributedString(string: strings.Login_CodeSentSms, font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center), false) + return (NSAttributedString(string: strings.Login_CodeSentSms, font: font, textColor: primaryColor, paragraphAlignment: .center), false) } else { let timeString = NSString(format: "%d:%.02d", Int(minutes), Int(seconds)) - return (NSAttributedString(string: strings.Login_WillSendSms(timeString as String).string, font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center), false) + return (NSAttributedString(string: strings.Login_WillSendSms(timeString as String).string, font: font, textColor: primaryColor, paragraphAlignment: .center), false) } } } else { @@ -81,28 +106,28 @@ public func authorizationNextOptionText(currentType: SentAuthorizationCodeType, case .otherSession: switch nextType { case .sms: - return (NSAttributedString(string: strings.Login_SendCodeViaSms, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true) + return (NSAttributedString(string: strings.Login_SendCodeViaSms, font: font, textColor: accentColor, paragraphAlignment: .center), true) case .call: - return (NSAttributedString(string: strings.Login_SendCodeViaCall, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true) + return (NSAttributedString(string: strings.Login_SendCodeViaCall, font: font, textColor: accentColor, paragraphAlignment: .center), true) case .flashCall, .missedCall: - return (NSAttributedString(string: strings.Login_SendCodeViaFlashCall, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true) + return (NSAttributedString(string: strings.Login_SendCodeViaFlashCall, font: font, textColor: accentColor, paragraphAlignment: .center), true) case .fragment: - return (NSAttributedString(string: strings.Login_GetCodeViaFragment, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true) + return (NSAttributedString(string: strings.Login_GetCodeViaFragment, font: font, textColor: accentColor, paragraphAlignment: .center), true) case .none: - return (NSAttributedString(string: strings.Login_HaveNotReceivedCodeInternal, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true) + return (NSAttributedString(string: strings.Login_HaveNotReceivedCodeInternal, font: font, textColor: accentColor, paragraphAlignment: .center), true) } default: switch nextType { case .sms: - return (NSAttributedString(string: strings.Login_SendCodeViaSms, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true) + return (NSAttributedString(string: strings.Login_SendCodeViaSms, font: font, textColor: accentColor, paragraphAlignment: .center), true) case .call: - return (NSAttributedString(string: strings.Login_SendCodeViaCall, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true) + return (NSAttributedString(string: strings.Login_SendCodeViaCall, font: font, textColor: accentColor, paragraphAlignment: .center), true) case .flashCall, .missedCall: - return (NSAttributedString(string: strings.Login_SendCodeViaFlashCall, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true) + return (NSAttributedString(string: strings.Login_SendCodeViaFlashCall, font: font, textColor: accentColor, paragraphAlignment: .center), true) case .fragment: - return (NSAttributedString(string: strings.Login_GetCodeViaFragment, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true) + return (NSAttributedString(string: strings.Login_GetCodeViaFragment, font: font, textColor: accentColor, paragraphAlignment: .center), true) case .none: - return (NSAttributedString(string: strings.Login_HaveNotReceivedCodeInternal, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true) + return (NSAttributedString(string: strings.Login_HaveNotReceivedCodeInternal, font: font, textColor: accentColor, paragraphAlignment: .center), true) } } } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift index afc99797053..3104afe9211 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift @@ -331,7 +331,7 @@ class BotCheckoutHeaderItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutPriceItem.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutPriceItem.swift index a3050cf7e5b..745aa4162f6 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutPriceItem.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutPriceItem.swift @@ -256,7 +256,7 @@ class BotCheckoutPriceItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutTipItem.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutTipItem.swift index 6fb0c828226..7989f74465b 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutTipItem.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutTipItem.swift @@ -772,7 +772,7 @@ class BotCheckoutTipItemNode: ListViewItemNode, UITextFieldDelegate { @objc public func textFieldDidEndEditing(_ textField: UITextField) { } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/CallListUI/Sources/CallListCallItem.swift b/submodules/CallListUI/Sources/CallListCallItem.swift index b2113a287cc..dd8bd7bea06 100644 --- a/submodules/CallListUI/Sources/CallListCallItem.swift +++ b/submodules/CallListUI/Sources/CallListCallItem.swift @@ -708,7 +708,7 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { accessoryItemNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -29.0), size: CGSize(width: bounds.size.width, height: 29.0)) } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } diff --git a/submodules/CallListUI/Sources/CallListGroupCallItem.swift b/submodules/CallListUI/Sources/CallListGroupCallItem.swift index be34a755dc6..592cd360db9 100644 --- a/submodules/CallListUI/Sources/CallListGroupCallItem.swift +++ b/submodules/CallListUI/Sources/CallListGroupCallItem.swift @@ -457,7 +457,7 @@ class CallListGroupCallItemNode: ItemListRevealOptionsItemNode { accessoryItemNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -29.0), size: CGSize(width: bounds.size.width, height: 29.0)) } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } diff --git a/submodules/ChatListFilterSettingsHeaderItem/Sources/ChatListFilterSettingsHeaderItem.swift b/submodules/ChatListFilterSettingsHeaderItem/Sources/ChatListFilterSettingsHeaderItem.swift index 45c7cc2ccf7..ebe6c50e290 100644 --- a/submodules/ChatListFilterSettingsHeaderItem/Sources/ChatListFilterSettingsHeaderItem.swift +++ b/submodules/ChatListFilterSettingsHeaderItem/Sources/ChatListFilterSettingsHeaderItem.swift @@ -173,7 +173,7 @@ class ChatListFilterSettingsHeaderItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index 57661435f95..12db43b4bcd 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -120,6 +120,7 @@ swift_library( "//submodules/TelegramUI/Components/Settings/PeerNameColorItem", "//submodules/TelegramUI/Components/Settings/BirthdayPickerScreen", "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatListAdditionalCategoryItem.swift b/submodules/ChatListUI/Sources/ChatListAdditionalCategoryItem.swift index 546bdad6707..a1614b2889a 100644 --- a/submodules/ChatListUI/Sources/ChatListAdditionalCategoryItem.swift +++ b/submodules/ChatListUI/Sources/ChatListAdditionalCategoryItem.swift @@ -361,7 +361,7 @@ public class ChatListAdditionalCategoryItemNode: ItemListRevealOptionsItemNode { accessoryItemNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -29.0), size: CGSize(width: bounds.size.width, height: 29.0)) } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 6bc589bebbb..e1b9638e1c2 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -228,6 +228,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private var sharedOpenStoryProgressDisposable = MetaDisposable() + var currentTooltipUpdateTimer: Foundation.Timer? + private var fullScreenEffectView: RippleEffectView? public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) { @@ -3166,6 +3168,30 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } }))) + items.append(.action(ContextMenuActionItem(text: presentationData.strings.StoryFeed_ViewAnonymously, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: self.context.isPremium ? "Chat/Context Menu/Eye" : "Chat/Context Menu/EyeLocked"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + if self.context.isPremium { + self.requestStealthMode(openStory: { [weak self] presentTooltip in + guard let self else { + return + } + self.openStories(peerId: peer.id, completion: { storyController in + presentTooltip(storyController) + }) + }) + } else { + self.presentStealthModeUpgrade(action: { [weak self] in + self?.presentUpgradeStoriesScreen() + }) + } + }))) + let hideText: String if self.location == .chatList(groupId: .archive) { hideText = self.presentationData.strings.StoryFeed_ContextUnarchive @@ -4096,6 +4122,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } public func openStories(peerId: EnginePeer.Id) { + self.openStories(peerId: peerId, completion: { _ in }) + } + + public func openStories(peerId: EnginePeer.Id, completion: @escaping (StoryContainerScreen) -> Void = { _ in }) { if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View { if navigationBarView.storiesUnlocked { self.shouldFixStorySubscriptionOrder = true @@ -4198,7 +4228,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.sharedOpenStoryProgressDisposable.set(nil) componentView.storyPeerListView()?.setLoadingItem(peerId: peerId, signal: signal) } - } + }, + completion: completion ) return diff --git a/submodules/ChatListUI/Sources/ChatListControllerStoryStealthMode.swift b/submodules/ChatListUI/Sources/ChatListControllerStoryStealthMode.swift new file mode 100644 index 00000000000..e50b7668d65 --- /dev/null +++ b/submodules/ChatListUI/Sources/ChatListControllerStoryStealthMode.swift @@ -0,0 +1,162 @@ +import Foundation +import Display +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import AccountContext +import StoryContainerScreen +import StoryStealthModeSheetScreen +import UndoUI + +extension ChatListControllerImpl { + func requestStealthMode(openStory: @escaping (@escaping (StoryContainerScreen) -> Void) -> Void) { + let context = self.context + + let _ = (context.engine.data.get( + TelegramEngine.EngineData.Item.Configuration.StoryConfigurationState(), + TelegramEngine.EngineData.Item.Configuration.App() + ) + |> deliverOnMainQueue).start(next: { [weak self] config, appConfig in + guard let self else { + return + } + + let timestamp = Int32(Date().timeIntervalSince1970) + if let activeUntilTimestamp = config.stealthModeState.actualizedNow().activeUntilTimestamp, activeUntilTimestamp > timestamp { + let remainingActiveSeconds = activeUntilTimestamp - timestamp + + let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) + let text = presentationData.strings.Story_ToastStealthModeActiveText(timeIntervalString(strings: presentationData.strings, value: remainingActiveSeconds)).string + let tooltipScreen = UndoOverlayController( + presentationData: presentationData, + content: .actionSucceeded(title: presentationData.strings.Story_ToastStealthModeActiveTitle, text: text, cancel: "", destructive: false), + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in + return false + } + ) + tooltipScreen.tag = "no_auto_dismiss" + weak var tooltipScreenValue: UndoOverlayController? = tooltipScreen + self.currentTooltipUpdateTimer?.invalidate() + self.currentTooltipUpdateTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] _ in + guard let self else { + return + } + guard let tooltipScreenValue else { + self.currentTooltipUpdateTimer?.invalidate() + self.currentTooltipUpdateTimer = nil + return + } + + let timestamp = Int32(Date().timeIntervalSince1970) + let remainingActiveSeconds = max(1, activeUntilTimestamp - timestamp) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) + let text = presentationData.strings.Story_ToastStealthModeActiveText(timeIntervalString(strings: presentationData.strings, value: remainingActiveSeconds)).string + tooltipScreenValue.content = .actionSucceeded(title: presentationData.strings.Story_ToastStealthModeActiveTitle, text: text, cancel: "", destructive: false) + }) + + openStory({ storyController in + storyController.presentExternalTooltip(tooltipScreen) + }) + + return + } + + let pastPeriod: Int32 + let futurePeriod: Int32 + if let data = appConfig.data, let futurePeriodF = data["stories_stealth_future_period"] as? Double, let pastPeriodF = data["stories_stealth_past_period"] as? Double { + futurePeriod = Int32(futurePeriodF) + pastPeriod = Int32(pastPeriodF) + } else { + pastPeriod = 5 * 60 + futurePeriod = 25 * 60 + } + + let sheet = StoryStealthModeSheetScreen( + context: context, + mode: .control(external: true, cooldownUntilTimestamp: config.stealthModeState.actualizedNow().cooldownUntilTimestamp), + forceDark: false, + backwardDuration: pastPeriod, + forwardDuration: futurePeriod, + buttonAction: { + let _ = (context.engine.messages.enableStoryStealthMode() + |> deliverOnMainQueue).start(completed: { + let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) + let text = presentationData.strings.Story_ToastStealthModeActivatedText(timeIntervalString(strings: presentationData.strings, value: pastPeriod), timeIntervalString(strings: presentationData.strings, value: futurePeriod)).string + let tooltipScreen = UndoOverlayController( + presentationData: presentationData, + content: .actionSucceeded(title: presentationData.strings.Story_ToastStealthModeActivatedTitle, text: text, cancel: "", destructive: false), + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in + return false + } + ) + + openStory({ storyController in + storyController.presentExternalTooltip(tooltipScreen) + }) + + HapticFeedback().success() + }) + } + ) + self.push(sheet) + }) + } + + func presentStealthModeUpgrade(action: @escaping () -> Void) { + let context = self.context + let _ = (context.engine.data.get( + TelegramEngine.EngineData.Item.Configuration.StoryConfigurationState(), + TelegramEngine.EngineData.Item.Configuration.App() + ) + |> deliverOnMainQueue).start(next: { [weak self] config, appConfig in + guard let self else { + return + } + + let pastPeriod: Int32 + let futurePeriod: Int32 + if let data = appConfig.data, let futurePeriodF = data["stories_stealth_future_period"] as? Double, let pastPeriodF = data["stories_stealth_past_period"] as? Double { + futurePeriod = Int32(futurePeriodF) + pastPeriod = Int32(pastPeriodF) + } else { + pastPeriod = 5 * 60 + futurePeriod = 25 * 60 + } + + let sheet = StoryStealthModeSheetScreen( + context: context, + mode: .upgrade, + forceDark: false, + backwardDuration: pastPeriod, + forwardDuration: futurePeriod, + buttonAction: { + action() + } + ) + self.push(sheet) + }) + } + + func presentUpgradeStoriesScreen() { + let context = self.context + var replaceImpl: ((ViewController) -> Void)? + let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .stories, forceDark: false, action: { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .storiesStealthMode, forceDark: false, dismissed: nil) + replaceImpl?(controller) + }, dismissed: nil) + replaceImpl = { [weak self, weak controller] c in + controller?.dismiss(animated: true, completion: { + guard let self else { + return + } + self.push(c) + }) + } + self.push(controller) + } +} diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetCategoryItem.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetCategoryItem.swift index bd6f470bbba..30689e67294 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetCategoryItem.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetCategoryItem.swift @@ -407,7 +407,7 @@ class ChatListFilterPresetCategoryItemNode: ItemListRevealOptionsItemNode, ItemL } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index 662f8cfe9cc..b45661aca5b 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -760,7 +760,7 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f selectedChats: Set(filterData.includePeers.peers), additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories), chatListFilters: allFilters - )), options: [], filters: [], alwaysEnabled: true, limit: isPremium ? premiumLimit : limit, reachedLimit: { count in + )), filters: [], alwaysEnabled: true, limit: isPremium ? premiumLimit : limit, reachedLimit: { count in if count >= premiumLimit { let limitController = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: min(premiumLimit, count), action: { return true @@ -913,7 +913,7 @@ private func internalChatListFilterExcludeChatsController(context: AccountContex selectedChats: Set(filterData.excludePeers), additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories), chatListFilters: allFilters - )), options: [], filters: [], alwaysEnabled: true, limit: 100)) + )), filters: [], alwaysEnabled: true, limit: 100)) controller.navigationPresentation = .modal let _ = (controller.result |> take(1) @@ -1579,10 +1579,10 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi }, openTagColorPremium: { var replaceImpl: ((ViewController) -> Void)? - let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .folderTags, action: { + let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .folderTags, forceDark: false, action: { let controller = context.sharedContext.makePremiumIntroController(context: context, source: .folderTags, forceDark: false, dismissed: nil) replaceImpl?(controller) - }) + }, dismissed: nil) replaceImpl = { [weak controller] c in controller?.replace(with: c) } diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index c398146a213..a6e9fe1af95 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -559,10 +559,10 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch context.engine.peers.updateChatListFiltersDisplayTags(isEnabled: value) }, updateDisplayTagsLocked: { var replaceImpl: ((ViewController) -> Void)? - let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .folderTags, action: { + let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .folderTags, forceDark: false, action: { let controller = context.sharedContext.makePremiumIntroController(context: context, source: .folderTags, forceDark: false, dismissed: nil) replaceImpl?(controller) - }) + }, dismissed: nil) replaceImpl = { [weak controller] c in controller?.replace(with: c) } diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift index 4af495c8a96..91f2b2acfae 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift @@ -512,7 +512,7 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListSuggestedItem.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListSuggestedItem.swift index f66c74bed86..1dc40b2ea8e 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListSuggestedItem.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListSuggestedItem.swift @@ -368,7 +368,7 @@ public class ChatListFilterPresetListSuggestedItemNode: ListViewItemNode, ItemLi } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift b/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift index f65374eda0d..5254100773c 100644 --- a/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift +++ b/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift @@ -159,7 +159,7 @@ class ChatListRecentPeersListItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 71019ef7152..0ae0d262aab 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -4513,7 +4513,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { item.interaction.openForumThread(index.messageIndex.id.peerId, topicItem.id) } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } diff --git a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift index bd08ffac407..a1c256698e4 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift @@ -292,7 +292,19 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: messageText = text } case let poll as TelegramMediaPoll: - messageText = "📊 \(poll.text)" + let pollPrefix = "📊 " + let entityOffset = (pollPrefix as NSString).length + messageText = "\(pollPrefix)\(poll.text)" + for entity in poll.textEntities { + if case let .CustomEmoji(_, fileId) = entity.type { + if customEmojiRanges == nil { + customEmojiRanges = [] + } + let range = NSRange(location: entityOffset + entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound) + let attribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: message.associatedMedia[EngineMedia.Id(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile) + customEmojiRanges?.append((range, attribute)) + } + } case let dice as TelegramMediaDice: messageText = dice.emoji case let story as TelegramMediaStory: diff --git a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift index 760b9edd7bb..d1075fb80f0 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift @@ -458,8 +458,8 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { self.item?.action(.buttonChoice(isPositive: false)) } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - super.animateInsertion(currentTimestamp, duration: duration, short: short) + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + super.animateInsertion(currentTimestamp, duration: duration, options: options) //self.transitionOffset = self.bounds.size.height //self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp) diff --git a/submodules/ComposePollUI/BUILD b/submodules/ComposePollUI/BUILD index 4077c4e2cc9..209d04ead71 100644 --- a/submodules/ComposePollUI/BUILD +++ b/submodules/ComposePollUI/BUILD @@ -10,18 +10,37 @@ swift_library( "-warnings-as-errors", ], deps = [ - "//submodules/AsyncDisplayKit:AsyncDisplayKit", - "//submodules/Display:Display", - "//submodules/TelegramCore:TelegramCore", - "//submodules/TelegramPresentationData:TelegramPresentationData", - "//submodules/ItemListUI:ItemListUI", - "//submodules/AccountContext:AccountContext", - "//submodules/AlertUI:AlertUI", - "//submodules/PresentationDataUtils:PresentationDataUtils", - "//submodules/TextFormat:TextFormat", - "//submodules/ObjCRuntimeUtils:ObjCRuntimeUtils", - "//submodules/AttachmentUI:AttachmentUI", - "//submodules/TextInputMenu:TextInputMenu", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/ItemListUI", + "//submodules/AccountContext", + "//submodules/AlertUI", + "//submodules/PresentationDataUtils", + "//submodules/TextFormat", + "//submodules/ObjCRuntimeUtils", + "//submodules/AttachmentUI", + "//submodules/TextInputMenu", + "//submodules/ComponentFlow", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/AppBundle", + "//submodules/TelegramUI/Components/EntityKeyboard", + "//submodules/UndoUI", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/AnimatedTextComponent", + "//submodules/TelegramUI/Components/PeerAllowedReactionsScreen", + "//submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/TextFieldComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/ChatPresentationInterfaceState", + "//submodules/TelegramUI/Components/EmojiSuggestionsComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/ComposePollUI/Sources/ComposePollScreen.swift b/submodules/ComposePollUI/Sources/ComposePollScreen.swift new file mode 100644 index 00000000000..6bfda6e7141 --- /dev/null +++ b/submodules/ComposePollUI/Sources/ComposePollScreen.swift @@ -0,0 +1,1606 @@ +import Foundation +import UIKit +import Display +import AccountContext +import TelegramCore +import Postbox +import SwiftSignalKit +import TelegramPresentationData +import ComponentFlow +import ComponentDisplayAdapters +import AppBundle +import ViewControllerComponent +import EntityKeyboard +import MultilineTextComponent +import UndoUI +import BundleIconComponent +import AnimatedTextComponent +import AudioToolbox +import ListSectionComponent +import PeerAllowedReactionsScreen +import AttachmentUI +import ListMultilineTextFieldItemComponent +import ListActionItemComponent +import ChatEntityKeyboardInputNode +import ChatPresentationInterfaceState +import EmojiSuggestionsComponent +import TextFormat +import TextFieldComponent + +final class ComposePollScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let peer: EnginePeer + let isQuiz: Bool? + let initialData: ComposePollScreen.InitialData + let completion: (ComposedPoll) -> Void + + init( + context: AccountContext, + peer: EnginePeer, + isQuiz: Bool?, + initialData: ComposePollScreen.InitialData, + completion: @escaping (ComposedPoll) -> Void + ) { + self.context = context + self.peer = peer + self.isQuiz = isQuiz + self.initialData = initialData + self.completion = completion + } + + static func ==(lhs: ComposePollScreenComponent, rhs: ComposePollScreenComponent) -> Bool { + return true + } + + private final class PollOption { + let id: Int + let textInputState = TextFieldComponent.ExternalState() + let textFieldTag = NSObject() + var resetText: String? + + init(id: Int) { + self.id = id + } + } + + final class View: UIView, UIScrollViewDelegate { + private let scrollView: UIScrollView + private var reactionInput: ComponentView? + private let pollTextSection = ComponentView() + private let quizAnswerSection = ComponentView() + + private let pollOptionsSectionHeader = ComponentView() + private let pollOptionsSectionFooterContainer = UIView() + private var pollOptionsSectionFooter = ComponentView() + private var pollOptionsSectionContainer: ListSectionContentView + + private let pollSettingsSection = ComponentView() + private let actionButton = ComponentView() + + private var reactionSelectionControl: ComponentView? + + private var isUpdating: Bool = false + private var ignoreScrolling: Bool = false + private var previousHadInputHeight: Bool = false + + private var component: ComposePollScreenComponent? + private(set) weak var state: EmptyComponentState? + private var environment: EnvironmentType? + + private let pollTextInputState = TextFieldComponent.ExternalState() + private let pollTextFieldTag = NSObject() + private var resetPollText: String? + + private var quizAnswerTextInputState = TextFieldComponent.ExternalState() + private let quizAnswerTextInputTag = NSObject() + private var resetQuizAnswerText: String? + + private var nextPollOptionId: Int = 0 + private var pollOptions: [PollOption] = [] + private var currentPollOptionsLimitReached: Bool = false + + private var isAnonymous: Bool = true + private var isMultiAnswer: Bool = false + private var isQuiz: Bool = false + private var selectedQuizOptionId: Int? + + private var currentInputMode: ListComposePollOptionComponent.InputMode = .keyboard + + private var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData? + private var inputMediaNodeDataDisposable: Disposable? + private var inputMediaNodeStateContext = ChatEntityKeyboardInputNode.StateContext() + private var inputMediaInteraction: ChatEntityKeyboardInputNode.Interaction? + private var inputMediaNode: ChatEntityKeyboardInputNode? + private var inputMediaNodeBackground = SimpleLayer() + private var inputMediaNodeTargetTag: AnyObject? + + private let inputMediaNodeDataPromise = Promise() + + private var currentEmojiSuggestionView: ComponentHostView? + + private var currentEditingTag: AnyObject? + + override init(frame: CGRect) { + self.scrollView = UIScrollView() + self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.scrollsToTop = false + self.scrollView.delaysContentTouches = false + self.scrollView.canCancelContentTouches = true + self.scrollView.contentInsetAdjustmentBehavior = .never + self.scrollView.alwaysBounceVertical = true + + self.pollOptionsSectionContainer = ListSectionContentView(frame: CGRect()) + + super.init(frame: frame) + + self.scrollView.delegate = self + self.addSubview(self.scrollView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.inputMediaNodeDataDisposable?.dispose() + } + + func scrollToTop() { + self.scrollView.setContentOffset(CGPoint(), animated: true) + } + + func validatedInput() -> ComposedPoll? { + if self.pollTextInputState.text.length == 0 { + return nil + } + + let mappedKind: TelegramMediaPollKind + if self.isQuiz { + mappedKind = .quiz + } else { + mappedKind = .poll(multipleAnswers: self.isMultiAnswer) + } + + var mappedOptions: [TelegramMediaPollOption] = [] + var selectedQuizOption: Data? + for pollOption in self.pollOptions { + if pollOption.textInputState.text.length == 0 { + continue + } + let optionData = "\(mappedOptions.count)".data(using: .utf8)! + if self.selectedQuizOptionId == pollOption.id { + selectedQuizOption = optionData + } + var entities: [MessageTextEntity] = [] + for entity in generateChatInputTextEntities(pollOption.textInputState.text) { + switch entity.type { + case .CustomEmoji: + entities.append(entity) + default: + break + } + } + + mappedOptions.append(TelegramMediaPollOption( + text: pollOption.textInputState.text.string, + entities: entities, + opaqueIdentifier: optionData + )) + } + + if mappedOptions.count < 2 { + return nil + } + + var mappedCorrectAnswers: [Data]? + if self.isQuiz { + if let selectedQuizOption { + mappedCorrectAnswers = [selectedQuizOption] + } else { + return nil + } + } + + var mappedSolution: (String, [MessageTextEntity])? + if self.isQuiz && self.quizAnswerTextInputState.text.length != 0 { + var solutionTextEntities: [MessageTextEntity] = [] + for entity in generateChatInputTextEntities(self.quizAnswerTextInputState.text) { + switch entity.type { + case .CustomEmoji: + solutionTextEntities.append(entity) + default: + break + } + } + + mappedSolution = (self.quizAnswerTextInputState.text.string, solutionTextEntities) + } + + var textEntities: [MessageTextEntity] = [] + for entity in generateChatInputTextEntities(self.pollTextInputState.text) { + switch entity.type { + case .CustomEmoji: + textEntities.append(entity) + default: + break + } + } + + let usedCustomEmojiFiles: [Int64: TelegramMediaFile] = [:] + + return ComposedPoll( + publicity: self.isAnonymous ? .anonymous : .public, + kind: mappedKind, + text: ComposedPoll.Text(string: self.pollTextInputState.text.string, entities: textEntities), + options: mappedOptions, + correctAnswers: mappedCorrectAnswers, + results: TelegramMediaPollResults( + voters: nil, + totalVoters: nil, + recentVoters: [], + solution: mappedSolution.flatMap { mappedSolution in + return TelegramMediaPollResults.Solution(text: mappedSolution.0, entities: mappedSolution.1) + } + ), + deadlineTimeout: nil, + usedCustomEmojiFiles: usedCustomEmojiFiles + ) + } + + func attemptNavigation(complete: @escaping () -> Void) -> Bool { + guard let component = self.component else { + return true + } + + let _ = component + + return true + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + } + } + + private func updateScrolling(transition: Transition) { + let navigationAlphaDistance: CGFloat = 16.0 + let navigationAlpha: CGFloat = max(0.0, min(1.0, self.scrollView.contentOffset.y / navigationAlphaDistance)) + if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { + transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha) + transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha) + } + } + + func isPanGestureEnabled() -> Bool { + if self.inputMediaNode != nil { + return false + } + + for (_, state) in self.collectTextInputStates() { + if state.isEditing { + return false + } + } + + return true + } + + private func updateInputMediaNode( + component: ComposePollScreenComponent, + availableSize: CGSize, + bottomInset: CGFloat, + inputHeight: CGFloat, + effectiveInputHeight: CGFloat, + metrics: LayoutMetrics, + deviceMetrics: DeviceMetrics, + transition: Transition + ) -> CGFloat { + let bottomInset: CGFloat = bottomInset + 8.0 + let bottomContainerInset: CGFloat = 0.0 + let needsInputActivation: Bool = !"".isEmpty + + var height: CGFloat = 0.0 + if case .emoji = self.currentInputMode, let inputData = self.inputMediaNodeData { + if let updatedTag = self.collectTextInputStates().first(where: { $1.isEditing })?.view.currentTag { + self.inputMediaNodeTargetTag = updatedTag + } + + let inputMediaNode: ChatEntityKeyboardInputNode + var inputMediaNodeTransition = transition + var animateIn = false + if let current = self.inputMediaNode { + inputMediaNode = current + } else { + animateIn = true + inputMediaNodeTransition = inputMediaNodeTransition.withAnimation(.none) + inputMediaNode = ChatEntityKeyboardInputNode( + context: component.context, + currentInputData: inputData, + updatedInputData: self.inputMediaNodeDataPromise.get(), + defaultToEmojiTab: true, + opaqueTopPanelBackground: false, + useOpaqueTheme: true, + interaction: self.inputMediaInteraction, + chatPeerId: nil, + stateContext: self.inputMediaNodeStateContext + ) + inputMediaNode.clipsToBounds = true + + inputMediaNode.externalTopPanelContainerImpl = nil + inputMediaNode.useExternalSearchContainer = true + if inputMediaNode.view.superview == nil { + self.inputMediaNodeBackground.removeAllAnimations() + self.layer.addSublayer(self.inputMediaNodeBackground) + self.addSubview(inputMediaNode.view) + } + self.inputMediaNode = inputMediaNode + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let presentationInterfaceState = ChatPresentationInterfaceState( + chatWallpaper: .builtin(WallpaperSettings()), + theme: presentationData.theme, + strings: presentationData.strings, + dateTimeFormat: presentationData.dateTimeFormat, + nameDisplayOrder: presentationData.nameDisplayOrder, + limitsConfiguration: component.context.currentLimitsConfiguration.with { $0 }, + fontSize: presentationData.chatFontSize, + bubbleCorners: presentationData.chatBubbleCorners, + accountPeerId: component.context.account.peerId, + mode: .standard(.default), + chatLocation: .peer(id: component.context.account.peerId), + subject: nil, + peerNearbyData: nil, + greetingData: nil, + pendingUnpinnedAllMessages: false, + activeGroupCallInfo: nil, + hasActiveGroupCall: false, + importState: nil, + threadData: nil, + isGeneralThreadClosed: nil, + replyMessage: nil, + accountPeerColor: nil, + businessIntro: nil + ) + + self.inputMediaNodeBackground.backgroundColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor.cgColor + + let heightAndOverflow = inputMediaNode.updateLayout(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, bottomInset: bottomInset, standardInputHeight: deviceMetrics.standardInputHeight(inLandscape: false), inputHeight: inputHeight < 100.0 ? inputHeight - bottomContainerInset : inputHeight, maximumHeight: availableSize.height, inputPanelHeight: 0.0, transition: .immediate, interfaceState: presentationInterfaceState, layoutMetrics: metrics, deviceMetrics: deviceMetrics, isVisible: true, isExpanded: false) + let inputNodeHeight = heightAndOverflow.0 + let inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputNodeHeight), size: CGSize(width: availableSize.width, height: inputNodeHeight)) + + let inputNodeBackgroundFrame = CGRect(origin: CGPoint(x: inputNodeFrame.minX, y: inputNodeFrame.minY - 6.0), size: CGSize(width: inputNodeFrame.width, height: inputNodeFrame.height + 6.0)) + + if needsInputActivation { + let inputNodeFrame = inputNodeFrame.offsetBy(dx: 0.0, dy: inputNodeHeight) + Transition.immediate.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) + Transition.immediate.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame) + } + + if animateIn { + var targetFrame = inputNodeFrame + targetFrame.origin.y = availableSize.height + inputMediaNodeTransition.setFrame(layer: inputMediaNode.layer, frame: targetFrame) + + let inputNodeBackgroundTargetFrame = CGRect(origin: CGPoint(x: targetFrame.minX, y: targetFrame.minY - 6.0), size: CGSize(width: targetFrame.width, height: targetFrame.height + 6.0)) + + inputMediaNodeTransition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundTargetFrame) + + transition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) + transition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame) + } else { + inputMediaNodeTransition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) + inputMediaNodeTransition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame) + } + + height = heightAndOverflow.0 + } else { + self.inputMediaNodeTargetTag = nil + + if let inputMediaNode = self.inputMediaNode { + self.inputMediaNode = nil + var targetFrame = inputMediaNode.frame + targetFrame.origin.y = availableSize.height + transition.setFrame(view: inputMediaNode.view, frame: targetFrame, completion: { [weak inputMediaNode] _ in + if let inputMediaNode { + Queue.mainQueue().after(0.3) { + inputMediaNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { [weak inputMediaNode] _ in + inputMediaNode?.view.removeFromSuperview() + }) + } + } + }) + transition.setFrame(layer: self.inputMediaNodeBackground, frame: targetFrame, completion: { [weak self] _ in + Queue.mainQueue().after(0.3) { + guard let self else { + return + } + if self.currentInputMode == .keyboard { + self.inputMediaNodeBackground.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { [weak self] finished in + guard let self else { + return + } + + if finished { + self.inputMediaNodeBackground.removeFromSuperlayer() + } + self.inputMediaNodeBackground.removeAllAnimations() + }) + } + } + }) + } + } + + /*if needsInputActivation { + needsInputActivation = false + Queue.mainQueue().justDispatch { + inputPanelView.activateInput() + } + }*/ + + if let controller = self.environment?.controller() as? ComposePollScreen { + let isTabBarVisible = self.inputMediaNode == nil + DispatchQueue.main.async { [weak controller] in + controller?.updateTabBarVisibility(isTabBarVisible, transition.containedViewLayoutTransition) + } + } + + return height + } + + private func collectTextInputStates() -> [(view: ListComposePollOptionComponent.View, state: TextFieldComponent.ExternalState)] { + var textInputStates: [(view: ListComposePollOptionComponent.View, state: TextFieldComponent.ExternalState)] = [] + if let textInputView = self.pollTextSection.findTaggedView(tag: self.pollTextFieldTag) as? ListComposePollOptionComponent.View { + textInputStates.append((textInputView, self.pollTextInputState)) + } + for pollOption in self.pollOptions { + if let textInputView = findTaggedComponentViewImpl(view: self.pollOptionsSectionContainer, tag: pollOption.textFieldTag) as? ListComposePollOptionComponent.View { + textInputStates.append((textInputView, pollOption.textInputState)) + } + } + if self.isQuiz { + if let textInputView = self.quizAnswerSection.findTaggedView(tag: self.quizAnswerTextInputTag) as? ListComposePollOptionComponent.View { + textInputStates.append((textInputView, self.quizAnswerTextInputState)) + } + } + + return textInputStates + } + + func update(component: ComposePollScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + var alphaTransition = transition + if !transition.animation.isImmediate { + alphaTransition = alphaTransition.withAnimation(.curve(duration: 0.25, curve: .easeInOut)) + } + + let environment = environment[EnvironmentType.self].value + let themeUpdated = self.environment?.theme !== environment.theme + self.environment = environment + + if self.component == nil { + self.pollOptions.append(ComposePollScreenComponent.PollOption( + id: self.nextPollOptionId + )) + self.nextPollOptionId += 1 + self.pollOptions.append(ComposePollScreenComponent.PollOption( + id: self.nextPollOptionId + )) + self.nextPollOptionId += 1 + + self.inputMediaNodeDataPromise.set( + ChatEntityKeyboardInputNode.inputData( + context: component.context, + chatPeerId: nil, + areCustomEmojiEnabled: true, + hasTrending: false, + hasSearch: true, + hasStickers: false, + hasGifs: false, + hideBackground: true, + sendGif: nil + ) + ) + self.inputMediaNodeDataDisposable = (self.inputMediaNodeDataPromise.get() + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let self else { + return + } + self.inputMediaNodeData = value + }) + + self.inputMediaInteraction = ChatEntityKeyboardInputNode.Interaction( + sendSticker: { _, _, _, _, _, _, _, _, _ in + return false + }, + sendEmoji: { _, _, _ in + let _ = self + }, + sendGif: { _, _, _, _, _ in + return false + }, + sendBotContextResultAsGif: { _, _ , _, _, _, _ in + return false + }, + updateChoosingSticker: { _ in + }, + switchToTextInput: { [weak self] in + guard let self else { + return + } + self.currentInputMode = .keyboard + self.state?.updated(transition: .spring(duration: 0.4)) + }, + dismissTextInput: { + }, + insertText: { [weak self] text in + guard let self else { + return + } + + var found = false + for (textInputView, externalState) in self.collectTextInputStates() { + if externalState.isEditing { + textInputView.insertText(text: text) + found = true + break + } + } + if !found, let inputMediaNodeTargetTag = self.inputMediaNodeTargetTag { + for (textInputView, _) in self.collectTextInputStates() { + if textInputView.currentTag === inputMediaNodeTargetTag { + textInputView.insertText(text: text) + found = true + break + } + } + } + }, + backwardsDeleteText: { [weak self] in + guard let self else { + return + } + var found = false + for (textInputView, externalState) in self.collectTextInputStates() { + if externalState.isEditing { + textInputView.backwardsDeleteText() + found = true + break + } + } + if !found, let inputMediaNodeTargetTag = self.inputMediaNodeTargetTag { + for (textInputView, _) in self.collectTextInputStates() { + if textInputView.currentTag === inputMediaNodeTargetTag { + textInputView.backwardsDeleteText() + found = true + break + } + } + } + }, + openStickerEditor: { + }, + presentController: { [weak self] c, a in + guard let self else { + return + } + self.environment?.controller()?.present(c, in: .window(.root), with: a) + }, + presentGlobalOverlayController: { [weak self] c, a in + guard let self else { + return + } + self.environment?.controller()?.presentInGlobalOverlay(c, with: a) + }, + getNavigationController: { [weak self] () -> NavigationController? in + guard let self else { + return nil + } + guard let controller = self.environment?.controller() as? ComposePollScreen else { + return nil + } + + if let navigationController = controller.navigationController as? NavigationController { + return navigationController + } + if let parentController = controller.parentController() { + return parentController.navigationController as? NavigationController + } + return nil + }, + requestLayout: { [weak self] transition in + guard let self else { + return + } + if !self.isUpdating { + self.state?.updated(transition: Transition(transition)) + } + } + ) + } + + self.component = component + self.state = state + + let topInset: CGFloat = 24.0 + let bottomInset: CGFloat = 8.0 + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let sectionSpacing: CGFloat = 24.0 + + if themeUpdated { + self.backgroundColor = environment.theme.list.blocksBackgroundColor + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + var contentHeight: CGFloat = 0.0 + contentHeight += environment.navigationHeight + contentHeight += topInset + + var pollTextSectionItems: [AnyComponentWithIdentity] = [] + pollTextSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListComposePollOptionComponent( + externalState: self.pollTextInputState, + context: component.context, + theme: environment.theme, + strings: environment.strings, + resetText: self.resetPollText.flatMap { resetText in + return ListComposePollOptionComponent.ResetText(value: resetText) + }, + assumeIsEditing: self.inputMediaNodeTargetTag === self.pollTextFieldTag, + characterLimit: component.initialData.maxPollTextLength, + returnKeyAction: { [weak self] in + guard let self else { + return + } + if !self.pollOptions.isEmpty { + if let pollOptionView = self.pollOptionsSectionContainer.itemViews[self.pollOptions[0].id] { + if let pollOptionComponentView = pollOptionView.contents.view as? ListComposePollOptionComponent.View { + pollOptionComponentView.activateInput() + } + } + } + }, + backspaceKeyAction: nil, + selection: nil, + inputMode: self.currentInputMode, + toggleInputMode: { [weak self] in + guard let self else { + return + } + switch self.currentInputMode { + case .keyboard: + self.currentInputMode = .emoji + case .emoji: + self.currentInputMode = .keyboard + } + self.state?.updated(transition: .spring(duration: 0.4)) + }, + tag: self.pollTextFieldTag + )))) + self.resetPollText = nil + + let pollTextSectionSize = self.pollTextSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.CreatePoll_TextHeader, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: pollTextSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let pollTextSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: pollTextSectionSize) + if let pollTextSectionView = self.pollTextSection.view as? ListSectionComponent.View { + if pollTextSectionView.superview == nil { + self.scrollView.addSubview(pollTextSectionView) + self.pollTextSection.parentState = state + } + transition.setFrame(view: pollTextSectionView, frame: pollTextSectionFrame) + + if let itemView = pollTextSectionView.itemView(id: 0) as? ListComposePollOptionComponent.View { + itemView.updateCustomPlaceholder(value: environment.strings.CreatePoll_TextPlaceholder, size: itemView.bounds.size, transition: .immediate) + } + } + contentHeight += pollTextSectionSize.height + contentHeight += sectionSpacing + + var pollOptionsSectionItems: [AnyComponentWithIdentity] = [] + + var pollOptionsSectionReadyItems: [ListSectionContentView.ReadyItem] = [] + + let processPollOptionItem: (Int) -> Void = { i in + let pollOption = self.pollOptions[i] + + let optionId = pollOption.id + + var optionSelection: ListComposePollOptionComponent.Selection? + if self.isQuiz { + optionSelection = ListComposePollOptionComponent.Selection(isSelected: self.selectedQuizOptionId == optionId, toggle: { [weak self] in + guard let self else { + return + } + self.selectedQuizOptionId = optionId + self.state?.updated(transition: .spring(duration: 0.35)) + }) + } + + pollOptionsSectionItems.append(AnyComponentWithIdentity(id: pollOption.id, component: AnyComponent(ListComposePollOptionComponent( + externalState: pollOption.textInputState, + context: component.context, + theme: environment.theme, + strings: environment.strings, + resetText: pollOption.resetText.flatMap { resetText in + return ListComposePollOptionComponent.ResetText(value: resetText) + }, + assumeIsEditing: self.inputMediaNodeTargetTag === pollOption.textFieldTag, + characterLimit: component.initialData.maxPollOptionLength, + returnKeyAction: { [weak self] in + guard let self else { + return + } + if let index = self.pollOptions.firstIndex(where: { $0.id == optionId }) { + if index == self.pollOptions.count - 1 { + self.endEditing(true) + } else { + if let pollOptionView = self.pollOptionsSectionContainer.itemViews[self.pollOptions[index + 1].id] { + if let pollOptionComponentView = pollOptionView.contents.view as? ListComposePollOptionComponent.View { + pollOptionComponentView.activateInput() + } + } + } + } + }, + backspaceKeyAction: { [weak self] in + guard let self else { + return + } + if let index = self.pollOptions.firstIndex(where: { $0.id == optionId }) { + if index == 0 { + if let textInputView = self.pollTextSection.findTaggedView(tag: self.pollTextFieldTag) as? ListComposePollOptionComponent.View { + textInputView.activateInput() + } + } else { + if let pollOptionView = self.pollOptionsSectionContainer.itemViews[self.pollOptions[index - 1].id] { + if let pollOptionComponentView = pollOptionView.contents.view as? ListComposePollOptionComponent.View { + pollOptionComponentView.activateInput() + } + } + } + } + }, + selection: optionSelection, + inputMode: self.currentInputMode, + toggleInputMode: { [weak self] in + guard let self else { + return + } + switch self.currentInputMode { + case .keyboard: + self.currentInputMode = .emoji + case .emoji: + self.currentInputMode = .keyboard + } + self.state?.updated(transition: .spring(duration: 0.4)) + }, + tag: pollOption.textFieldTag + )))) + + let item = pollOptionsSectionItems[i] + let itemId = item.id + + let itemView: ListSectionContentView.ItemView + var itemTransition = transition + if let current = self.pollOptionsSectionContainer.itemViews[itemId] { + itemView = current + } else { + itemTransition = itemTransition.withAnimation(.none) + itemView = ListSectionContentView.ItemView() + self.pollOptionsSectionContainer.itemViews[itemId] = itemView + itemView.contents.parentState = state + } + + let itemSize = itemView.contents.update( + transition: itemTransition, + component: item.component, + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) + ) + + pollOptionsSectionReadyItems.append(ListSectionContentView.ReadyItem( + id: itemId, + itemView: itemView, + size: itemSize, + transition: itemTransition + )) + } + + for i in 0 ..< self.pollOptions.count { + processPollOptionItem(i) + } + + if self.pollOptions.count > 2 { + let lastOption = self.pollOptions[self.pollOptions.count - 1] + let secondToLastOption = self.pollOptions[self.pollOptions.count - 2] + + if !lastOption.textInputState.isEditing && lastOption.textInputState.text.length == 0 && secondToLastOption.textInputState.text.length == 0 { + self.pollOptions.removeLast() + pollOptionsSectionItems.removeLast() + pollOptionsSectionReadyItems.removeLast() + } + } + + if self.pollOptions.count < 10, let lastOption = self.pollOptions.last { + if lastOption.textInputState.text.length != 0 { + self.pollOptions.append(PollOption(id: self.nextPollOptionId)) + self.nextPollOptionId += 1 + processPollOptionItem(self.pollOptions.count - 1) + } + } + + for i in 0 ..< pollOptionsSectionReadyItems.count { + let placeholder: String + if i == pollOptionsSectionReadyItems.count - 1 { + placeholder = environment.strings.CreatePoll_AddOption + } else { + placeholder = environment.strings.CreatePoll_OptionPlaceholder + } + + if let itemView = pollOptionsSectionReadyItems[i].itemView.contents.view as? ListComposePollOptionComponent.View { + itemView.updateCustomPlaceholder(value: placeholder, size: pollOptionsSectionReadyItems[i].size, transition: pollOptionsSectionReadyItems[i].transition) + } + } + + let pollOptionsSectionUpdateResult = self.pollOptionsSectionContainer.update( + configuration: ListSectionContentView.Configuration( + theme: environment.theme, + displaySeparators: true, + extendsItemHighlightToSection: false, + background: .all + ), + width: availableSize.width - sideInset * 2.0, + leftInset: 0.0, + readyItems: pollOptionsSectionReadyItems, + transition: transition + ) + + let sectionHeaderSideInset: CGFloat = 16.0 + let pollOptionsSectionHeaderSize = self.pollOptionsSectionHeader.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.CreatePoll_OptionsHeader, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - sectionHeaderSideInset * 2.0, height: 1000.0) + ) + let pollOptionsSectionHeaderFrame = CGRect(origin: CGPoint(x: sideInset + sectionHeaderSideInset, y: contentHeight), size: pollOptionsSectionHeaderSize) + if let pollOptionsSectionHeaderView = self.pollOptionsSectionHeader.view { + if pollOptionsSectionHeaderView.superview == nil { + pollOptionsSectionHeaderView.layer.anchorPoint = CGPoint() + self.scrollView.addSubview(pollOptionsSectionHeaderView) + } + transition.setPosition(view: pollOptionsSectionHeaderView, position: pollOptionsSectionHeaderFrame.origin) + pollOptionsSectionHeaderView.bounds = CGRect(origin: CGPoint(), size: pollOptionsSectionHeaderFrame.size) + } + contentHeight += pollOptionsSectionHeaderSize.height + contentHeight += 7.0 + + let pollOptionsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: pollOptionsSectionUpdateResult.size) + if self.pollOptionsSectionContainer.superview == nil { + self.scrollView.addSubview(self.pollOptionsSectionContainer.externalContentBackgroundView) + self.scrollView.addSubview(self.pollOptionsSectionContainer) + } + transition.setFrame(view: self.pollOptionsSectionContainer, frame: pollOptionsSectionFrame) + transition.setFrame(view: self.pollOptionsSectionContainer.externalContentBackgroundView, frame: pollOptionsSectionUpdateResult.backgroundFrame.offsetBy(dx: pollOptionsSectionFrame.minX, dy: pollOptionsSectionFrame.minY)) + contentHeight += pollOptionsSectionUpdateResult.size.height + + contentHeight += 7.0 + + let pollOptionsLimitReached = self.pollOptions.count >= 10 + var animatePollOptionsFooterIn = false + var pollOptionsFooterTransition = transition + if self.currentPollOptionsLimitReached != pollOptionsLimitReached { + self.currentPollOptionsLimitReached = pollOptionsLimitReached + if let pollOptionsSectionFooterView = self.pollOptionsSectionFooter.view { + animatePollOptionsFooterIn = true + pollOptionsFooterTransition = pollOptionsFooterTransition.withAnimation(.none) + alphaTransition.setAlpha(view: pollOptionsSectionFooterView, alpha: 0.0, completion: { [weak pollOptionsSectionFooterView] _ in + pollOptionsSectionFooterView?.removeFromSuperview() + }) + self.pollOptionsSectionFooter = ComponentView() + } + } + + let pollOptionsComponent: AnyComponent + if pollOptionsLimitReached { + pollOptionsFooterTransition = pollOptionsFooterTransition.withAnimation(.none) + pollOptionsComponent = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: environment.strings.CreatePoll_AllOptionsAdded, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor)), + maximumNumberOfLines: 0 + )) + } else { + let remainingCount = 10 - self.pollOptions.count + let rawString = environment.strings.CreatePoll_OptionCountFooterFormat(Int32(remainingCount)) + + var pollOptionsFooterItems: [AnimatedTextComponent.Item] = [] + if let range = rawString.range(of: "{count}") { + if range.lowerBound != rawString.startIndex { + pollOptionsFooterItems.append(AnimatedTextComponent.Item( + id: 0, + isUnbreakable: true, + content: .text(String(rawString[rawString.startIndex ..< range.lowerBound])) + )) + } + pollOptionsFooterItems.append(AnimatedTextComponent.Item( + id: 1, + isUnbreakable: true, + content: .number(remainingCount, minDigits: 1) + )) + if range.upperBound != rawString.endIndex { + pollOptionsFooterItems.append(AnimatedTextComponent.Item( + id: 2, + isUnbreakable: true, + content: .text(String(rawString[range.upperBound ..< rawString.endIndex])) + )) + } + } + + pollOptionsComponent = AnyComponent(AnimatedTextComponent( + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + color: environment.theme.list.freeTextColor, + items: pollOptionsFooterItems + )) + } + + let pollOptionsSectionFooterSize = self.pollOptionsSectionFooter.update( + transition: pollOptionsFooterTransition, + component: pollOptionsComponent, + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - sectionHeaderSideInset * 2.0, height: 1000.0) + ) + let pollOptionsSectionFooterFrame = CGRect(origin: CGPoint(x: sideInset + sectionHeaderSideInset, y: contentHeight), size: pollOptionsSectionFooterSize) + + if self.pollOptionsSectionFooterContainer.superview == nil { + self.scrollView.addSubview(self.pollOptionsSectionFooterContainer) + } + transition.setFrame(view: self.pollOptionsSectionFooterContainer, frame: pollOptionsSectionFooterFrame) + + if let pollOptionsSectionFooterView = self.pollOptionsSectionFooter.view { + if pollOptionsSectionFooterView.superview == nil { + pollOptionsSectionFooterView.layer.anchorPoint = CGPoint() + self.pollOptionsSectionFooterContainer.addSubview(pollOptionsSectionFooterView) + } + pollOptionsFooterTransition.setPosition(view: pollOptionsSectionFooterView, position: CGPoint()) + pollOptionsSectionFooterView.bounds = CGRect(origin: CGPoint(), size: pollOptionsSectionFooterFrame.size) + if animatePollOptionsFooterIn && !transition.animation.isImmediate { + alphaTransition.animateAlpha(view: pollOptionsSectionFooterView, from: 0.0, to: 1.0) + } + } + contentHeight += pollOptionsSectionFooterSize.height + contentHeight += sectionSpacing + + var pollSettingsSectionItems: [AnyComponentWithIdentity] = [] + pollSettingsSectionItems.append(AnyComponentWithIdentity(id: "anonymous", component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.CreatePoll_Anonymous, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.isAnonymous, action: { [weak self] _ in + guard let self else { + return + } + self.isAnonymous = !self.isAnonymous + self.state?.updated(transition: .spring(duration: 0.4)) + })), + action: nil + )))) + pollSettingsSectionItems.append(AnyComponentWithIdentity(id: "multiAnswer", component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.CreatePoll_MultipleChoice, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.isMultiAnswer, action: { [weak self] _ in + guard let self else { + return + } + self.isMultiAnswer = !self.isMultiAnswer + if self.isMultiAnswer { + self.isQuiz = false + } + self.state?.updated(transition: .spring(duration: 0.4)) + })), + action: nil + )))) + pollSettingsSectionItems.append(AnyComponentWithIdentity(id: "quiz", component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.CreatePoll_Quiz, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.isQuiz, action: { [weak self] _ in + guard let self else { + return + } + self.isQuiz = !self.isQuiz + if self.isQuiz { + self.isMultiAnswer = false + } + self.state?.updated(transition: .spring(duration: 0.4)) + })), + action: nil + )))) + + let pollSettingsSectionSize = self.pollSettingsSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.CreatePoll_QuizInfo, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: pollSettingsSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let pollSettingsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: pollSettingsSectionSize) + if let pollSettingsSectionView = self.pollSettingsSection.view { + if pollSettingsSectionView.superview == nil { + self.scrollView.addSubview(pollSettingsSectionView) + self.pollSettingsSection.parentState = state + } + transition.setFrame(view: pollSettingsSectionView, frame: pollSettingsSectionFrame) + } + contentHeight += pollSettingsSectionSize.height + + var quizAnswerSectionHeight: CGFloat = 0.0 + quizAnswerSectionHeight += sectionSpacing + let quizAnswerSectionSize = self.quizAnswerSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.CreatePoll_ExplanationHeader, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.CreatePoll_ExplanationInfo, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListComposePollOptionComponent( + externalState: self.quizAnswerTextInputState, + context: component.context, + theme: environment.theme, + strings: environment.strings, + resetText: self.resetQuizAnswerText.flatMap { resetText in + return ListComposePollOptionComponent.ResetText(value: resetText) + }, + assumeIsEditing: self.inputMediaNodeTargetTag === self.quizAnswerTextInputTag, + characterLimit: component.initialData.maxPollTextLength, + returnKeyAction: { [weak self] in + guard let self else { + return + } + self.endEditing(true) + }, + backspaceKeyAction: nil, + selection: nil, + inputMode: self.currentInputMode, + toggleInputMode: { [weak self] in + guard let self else { + return + } + switch self.currentInputMode { + case .keyboard: + self.currentInputMode = .emoji + case .emoji: + self.currentInputMode = .keyboard + } + self.state?.updated(transition: .spring(duration: 0.4)) + }, + tag: self.quizAnswerTextInputTag + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + self.resetQuizAnswerText = nil + let quizAnswerSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + quizAnswerSectionHeight), size: quizAnswerSectionSize) + if let quizAnswerSectionView = self.quizAnswerSection.view as? ListSectionComponent.View { + if quizAnswerSectionView.superview == nil { + self.scrollView.addSubview(quizAnswerSectionView) + self.quizAnswerSection.parentState = state + } + transition.setFrame(view: quizAnswerSectionView, frame: quizAnswerSectionFrame) + transition.setAlpha(view: quizAnswerSectionView, alpha: self.isQuiz ? 1.0 : 0.0) + + if let itemView = quizAnswerSectionView.itemView(id: 0) as? ListComposePollOptionComponent.View { + itemView.updateCustomPlaceholder(value: environment.strings.CreatePoll_Explanation, size: itemView.bounds.size, transition: .immediate) + } + } + quizAnswerSectionHeight += quizAnswerSectionSize.height + + if self.isQuiz { + contentHeight += quizAnswerSectionHeight + } + + var inputHeight: CGFloat = 0.0 + inputHeight += self.updateInputMediaNode( + component: component, + availableSize: availableSize, + bottomInset: environment.safeInsets.bottom, + inputHeight: 0.0, + effectiveInputHeight: environment.deviceMetrics.standardInputHeight(inLandscape: false), + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + transition: transition + ) + if self.inputMediaNode == nil { + inputHeight = environment.inputHeight + } + + let textInputStates = self.collectTextInputStates() + + let previousEditingTag = self.currentEditingTag + let isEditing: Bool + if let index = textInputStates.firstIndex(where: { $0.state.isEditing }) { + isEditing = true + self.currentEditingTag = textInputStates[index].view.currentTag + } else { + isEditing = false + self.currentEditingTag = nil + } + + if let (_, suggestionTextInputState) = textInputStates.first(where: { $0.state.isEditing && $0.state.currentEmojiSuggestion != nil }), let emojiSuggestion = suggestionTextInputState.currentEmojiSuggestion, emojiSuggestion.disposable == nil { + emojiSuggestion.disposable = (EmojiSuggestionsComponent.suggestionData(context: component.context, isSavedMessages: false, query: emojiSuggestion.position.value) + |> deliverOnMainQueue).start(next: { [weak self, weak suggestionTextInputState, weak emojiSuggestion] result in + guard let self, let suggestionTextInputState, let emojiSuggestion, suggestionTextInputState.currentEmojiSuggestion === emojiSuggestion else { + return + } + + emojiSuggestion.value = result + self.state?.updated() + }) + } + + for (_, suggestionTextInputState) in textInputStates { + var hasTrackingView = suggestionTextInputState.hasTrackingView + if let currentEmojiSuggestion = suggestionTextInputState.currentEmojiSuggestion, let value = currentEmojiSuggestion.value as? [TelegramMediaFile], value.isEmpty { + hasTrackingView = false + } + if !suggestionTextInputState.isEditing { + hasTrackingView = false + } + + if !hasTrackingView { + if let currentEmojiSuggestion = suggestionTextInputState.currentEmojiSuggestion { + suggestionTextInputState.currentEmojiSuggestion = nil + currentEmojiSuggestion.disposable?.dispose() + } + + if let currentEmojiSuggestionView = self.currentEmojiSuggestionView { + self.currentEmojiSuggestionView = nil + + currentEmojiSuggestionView.alpha = 0.0 + currentEmojiSuggestionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak currentEmojiSuggestionView] _ in + currentEmojiSuggestionView?.removeFromSuperview() + }) + } + } + } + + if let (suggestionTextInputView, suggestionTextInputState) = textInputStates.first(where: { $0.state.isEditing && $0.state.currentEmojiSuggestion != nil }), let emojiSuggestion = suggestionTextInputState.currentEmojiSuggestion, let value = emojiSuggestion.value as? [TelegramMediaFile] { + let currentEmojiSuggestionView: ComponentHostView + if let current = self.currentEmojiSuggestionView { + currentEmojiSuggestionView = current + } else { + currentEmojiSuggestionView = ComponentHostView() + self.currentEmojiSuggestionView = currentEmojiSuggestionView + self.addSubview(currentEmojiSuggestionView) + + currentEmojiSuggestionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + + let globalPosition: CGPoint + if let textView = suggestionTextInputView.textFieldView { + globalPosition = textView.convert(emojiSuggestion.localPosition, to: self) + } else { + globalPosition = .zero + } + + let sideInset: CGFloat = 7.0 + + let viewSize = currentEmojiSuggestionView.update( + transition: .immediate, + component: AnyComponent(EmojiSuggestionsComponent( + context: component.context, + userLocation: .other, + theme: EmojiSuggestionsComponent.Theme(theme: environment.theme), + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + files: value, + action: { [weak self, weak suggestionTextInputView, weak suggestionTextInputState] file in + guard let self, let suggestionTextInputView, let suggestionTextInputState, let textView = suggestionTextInputView.textFieldView, let currentEmojiSuggestion = suggestionTextInputState.currentEmojiSuggestion else { + return + } + + let _ = self + + AudioServicesPlaySystemSound(0x450) + + let inputState = textView.getInputState() + let inputText = NSMutableAttributedString(attributedString: inputState.inputText) + + var text: String? + var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? + loop: for attribute in file.attributes { + switch attribute { + case let .CustomEmoji(_, _, displayText, _): + text = displayText + emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file) + break loop + default: + break + } + } + + if let emojiAttribute = emojiAttribute, let text = text { + let replacementText = NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute]) + + let range = currentEmojiSuggestion.position.range + let previousText = inputText.attributedSubstring(from: range) + inputText.replaceCharacters(in: range, with: replacementText) + + var replacedUpperBound = range.lowerBound + while true { + if inputText.attributedSubstring(from: NSRange(location: 0, length: replacedUpperBound)).string.hasSuffix(previousText.string) { + let replaceRange = NSRange(location: replacedUpperBound - previousText.length, length: previousText.length) + if replaceRange.location < 0 { + break + } + let adjacentString = inputText.attributedSubstring(from: replaceRange) + if adjacentString.string != previousText.string || adjacentString.attribute(ChatTextInputAttributes.customEmoji, at: 0, effectiveRange: nil) != nil { + break + } + inputText.replaceCharacters(in: replaceRange, with: NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: emojiAttribute.interactivelySelectedFromPackId, fileId: emojiAttribute.fileId, file: emojiAttribute.file)])) + replacedUpperBound = replaceRange.lowerBound + } else { + break + } + } + + let selectionPosition = range.lowerBound + (replacementText.string as NSString).length + textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition) + } + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + + let viewFrame = CGRect(origin: CGPoint(x: min(availableSize.width - sideInset - viewSize.width, max(sideInset, floor(globalPosition.x - viewSize.width / 2.0))), y: globalPosition.y - 4.0 - viewSize.height), size: viewSize) + currentEmojiSuggestionView.frame = viewFrame + if let componentView = currentEmojiSuggestionView.componentView as? EmojiSuggestionsComponent.View { + componentView.adjustBackground(relativePositionX: floor(globalPosition.x + 10.0)) + } + } + + let combinedBottomInset: CGFloat + combinedBottomInset = bottomInset + max(environment.safeInsets.bottom, 8.0 + inputHeight) + contentHeight += combinedBottomInset + + var recenterOnTag: AnyObject? + if let hint = transition.userData(TextFieldComponent.AnimationHint.self), let targetView = hint.view { + var matches = false + switch hint.kind { + case .textChanged: + matches = true + case let .textFocusChanged(isFocused): + if isFocused { + matches = true + } + } + + if matches { + for (textView, _) in self.collectTextInputStates() { + if targetView.isDescendant(of: textView) { + recenterOnTag = textView.currentTag + break + } + } + } + } + if recenterOnTag == nil && self.previousHadInputHeight != (inputHeight > 0.0) { + for (textView, state) in self.collectTextInputStates() { + if state.isEditing { + recenterOnTag = textView.currentTag + break + } + } + } + self.previousHadInputHeight = (inputHeight > 0.0) + + self.ignoreScrolling = true + let previousBounds = self.scrollView.bounds + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) { + self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize) + } + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: environment.safeInsets.bottom, right: 0.0) + if self.scrollView.scrollIndicatorInsets != scrollInsets { + self.scrollView.scrollIndicatorInsets = scrollInsets + } + + if let recenterOnTag { + if let targetView = self.collectTextInputStates().first(where: { $0.view.currentTag === recenterOnTag })?.view { + let caretRect = targetView.convert(targetView.bounds, to: self.scrollView) + var scrollViewBounds = self.scrollView.bounds + let minButtonDistance: CGFloat = 16.0 + if -scrollViewBounds.minY + caretRect.maxY > availableSize.height - combinedBottomInset - minButtonDistance { + scrollViewBounds.origin.y = -(availableSize.height - combinedBottomInset - minButtonDistance - caretRect.maxY) + if scrollViewBounds.origin.y < 0.0 { + scrollViewBounds.origin.y = 0.0 + } + } + if self.scrollView.bounds != scrollViewBounds { + self.scrollView.bounds = scrollViewBounds + } + } + } + if !previousBounds.isEmpty, !transition.animation.isImmediate { + let bounds = self.scrollView.bounds + if bounds.maxY != previousBounds.maxY { + let offsetY = previousBounds.maxY - bounds.maxY + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) + } + } + self.ignoreScrolling = false + + self.updateScrolling(transition: transition) + + if isEditing { + if let controller = environment.controller() as? ComposePollScreen { + DispatchQueue.main.async { [weak controller] in + controller?.requestAttachmentMenuExpansion() + } + } + } + + let isValid = self.validatedInput() != nil + if let controller = environment.controller() as? ComposePollScreen, let sendButtonItem = controller.sendButtonItem { + if sendButtonItem.isEnabled != isValid { + sendButtonItem.isEnabled = isValid + } + + let controllerTitle = self.isQuiz ? presentationData.strings.CreatePoll_QuizTitle : presentationData.strings.CreatePoll_Title + if controller.title != controllerTitle { + controller.title = controllerTitle + } + } + + if let currentEditingTag = self.currentEditingTag, previousEditingTag !== currentEditingTag, self.currentInputMode != .keyboard { + DispatchQueue.main.async { [weak self] in + guard let self else { + return + } + self.currentInputMode = .keyboard + self.state?.updated(transition: .spring(duration: 0.4)) + } + } + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public class ComposePollScreen: ViewControllerComponentContainer, AttachmentContainable { + public final class InitialData { + fileprivate let maxPollTextLength: Int + fileprivate let maxPollOptionLength: Int + + fileprivate init( + maxPollTextLength: Int, + maxPollOptionLength: Int + ) { + self.maxPollTextLength = maxPollTextLength + self.maxPollOptionLength = maxPollOptionLength + } + } + + private let context: AccountContext + private let completion: (ComposedPoll) -> Void + private var isDismissed: Bool = false + + fileprivate private(set) var sendButtonItem: UIBarButtonItem? + + public var requestAttachmentMenuExpansion: () -> Void = { + } + public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in + } + public var parentController: () -> ViewController? = { + return nil + } + public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in + } + public var updateTabBarVisibility: (Bool, ContainedViewLayoutTransition) -> Void = { _, _ in + } + public var cancelPanGesture: () -> Void = { + } + public var isContainerPanning: () -> Bool = { + return false + } + public var isContainerExpanded: () -> Bool = { + return false + } + public var mediaPickerContext: AttachmentMediaPickerContext? + + public var isPanGestureEnabled: (() -> Bool)? { + return { [weak self] in + guard let self, let componentView = self.node.hostView.componentView as? ComposePollScreenComponent.View else { + return true + } + return componentView.isPanGestureEnabled() + } + } + + public init( + context: AccountContext, + initialData: InitialData, + peer: EnginePeer, + isQuiz: Bool?, + completion: @escaping (ComposedPoll) -> Void + ) { + self.context = context + self.completion = completion + + super.init(context: context, component: ComposePollScreenComponent( + context: context, + peer: peer, + isQuiz: isQuiz, + initialData: initialData, + completion: completion + ), navigationBarAppearance: .default, theme: .default) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + self.title = isQuiz == true ? presentationData.strings.CreatePoll_QuizTitle : presentationData.strings.CreatePoll_Title + + self.navigationItem.setLeftBarButton(UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)), animated: false) + + let sendButtonItem = UIBarButtonItem(title: presentationData.strings.CreatePoll_Create, style: .done, target: self, action: #selector(self.sendPressed)) + self.sendButtonItem = sendButtonItem + self.navigationItem.setRightBarButton(sendButtonItem, animated: false) + sendButtonItem.isEnabled = false + + self.scrollToTop = { [weak self] in + guard let self, let componentView = self.node.hostView.componentView as? ComposePollScreenComponent.View else { + return + } + componentView.scrollToTop() + } + + self.attemptNavigation = { [weak self] complete in + guard let self, let componentView = self.node.hostView.componentView as? ComposePollScreenComponent.View else { + return true + } + + return componentView.attemptNavigation(complete: complete) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + public static func initialData(context: AccountContext) -> InitialData { + return InitialData( + maxPollTextLength: Int(context.userLimits.maxCaptionLength), + maxPollOptionLength: 100 + ) + } + + @objc private func cancelPressed() { + self.dismiss() + } + + @objc private func sendPressed() { + guard let componentView = self.node.hostView.componentView as? ComposePollScreenComponent.View else { + return + } + if let input = componentView.validatedInput() { + self.completion(input) + } + self.dismiss() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + } + + public func isContainerPanningUpdated(_ panning: Bool) { + } + + public func resetForReuse() { + } + + public func prepareForReuse() { + } + + public func requestDismiss(completion: @escaping () -> Void) { + completion() + } + + public func shouldDismissImmediately() -> Bool { + return true + } +} diff --git a/submodules/ComposePollUI/Sources/CreatePollController.swift b/submodules/ComposePollUI/Sources/CreatePollController.swift index c523e844ec0..5fa3f3b45a6 100644 --- a/submodules/ComposePollUI/Sources/CreatePollController.swift +++ b/submodules/ComposePollUI/Sources/CreatePollController.swift @@ -488,23 +488,35 @@ private func createPollControllerEntries(presentationData: PresentationData, pee } public final class ComposedPoll { + public struct Text { + public let string: String + public let entities: [MessageTextEntity] + + public init(string: String, entities: [MessageTextEntity]) { + self.string = string + self.entities = entities + } + } + public let publicity: TelegramMediaPollPublicity public let kind: TelegramMediaPollKind - public let text: String + public let text: Text public let options: [TelegramMediaPollOption] public let correctAnswers: [Data]? public let results: TelegramMediaPollResults public let deadlineTimeout: Int32? + public let usedCustomEmojiFiles: [Int64: TelegramMediaFile] public init( publicity: TelegramMediaPollPublicity, kind: TelegramMediaPollKind, - text: String, + text: Text, options: [TelegramMediaPollOption], correctAnswers: [Data]?, results: TelegramMediaPollResults, - deadlineTimeout: Int32? + deadlineTimeout: Int32?, + usedCustomEmojiFiles: [Int64: TelegramMediaFile] ) { self.publicity = publicity self.kind = kind @@ -513,6 +525,7 @@ public final class ComposedPoll { self.correctAnswers = correctAnswers self.results = results self.deadlineTimeout = deadlineTimeout + self.usedCustomEmojiFiles = usedCustomEmojiFiles } } @@ -550,7 +563,11 @@ private final class CreatePollContext: AttachmentMediaPickerContext { public class CreatePollControllerImpl: ItemListController, AttachmentContainable { public var requestAttachmentMenuExpansion: () -> Void = {} public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } + public var parentController: () -> ViewController? = { + return nil + } public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in } + public var updateTabBarVisibility: (Bool, ContainedViewLayoutTransition) -> Void = { _, _ in } public var cancelPanGesture: () -> Void = { } public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } @@ -598,7 +615,17 @@ public class CreatePollControllerImpl: ItemListController, AttachmentContainable } } -public func createPollController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peer: EnginePeer, isQuiz: Bool? = nil, completion: @escaping (ComposedPoll) -> Void) -> CreatePollControllerImpl { +public func createPollController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peer: EnginePeer, isQuiz: Bool? = nil, completion: @escaping (ComposedPoll) -> Void) -> ViewController { + return ComposePollScreen( + context: context, + initialData: ComposePollScreen.initialData(context: context), + peer: peer, + isQuiz: isQuiz, + completion: completion + ) +} + +private func legacyCreatePollController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peer: EnginePeer, isQuiz: Bool?, completion: @escaping (ComposedPoll) -> Void) -> ViewController { var initialState = CreatePollControllerState() if let isQuiz = isQuiz { initialState.isQuiz = isQuiz @@ -919,7 +946,7 @@ public func createPollController(context: AccountContext, updatedPresentationDat let optionText = state.options[i].item.text.trimmingCharacters(in: .whitespacesAndNewlines) if !optionText.isEmpty { let optionData = "\(i)".data(using: .utf8)! - options.append(TelegramMediaPollOption(text: optionText, opaqueIdentifier: optionData)) + options.append(TelegramMediaPollOption(text: optionText, entities: [], opaqueIdentifier: optionData)) if state.isQuiz && state.options[i].item.isSelected { correctAnswers = [optionData] } @@ -950,11 +977,12 @@ public func createPollController(context: AccountContext, updatedPresentationDat completion(ComposedPoll( publicity: publicity, kind: kind, - text: processPollText(state.text), + text: ComposedPoll.Text(string: processPollText(state.text), entities: []), options: options, correctAnswers: correctAnswers, results: TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [], solution: resolvedSolution), - deadlineTimeout: deadlineTimeout + deadlineTimeout: deadlineTimeout, + usedCustomEmojiFiles: [:] )) }) diff --git a/submodules/ComposePollUI/Sources/CreatePollOptionActionItem.swift b/submodules/ComposePollUI/Sources/CreatePollOptionActionItem.swift index aa61e46eeeb..35f7a146047 100644 --- a/submodules/ComposePollUI/Sources/CreatePollOptionActionItem.swift +++ b/submodules/ComposePollUI/Sources/CreatePollOptionActionItem.swift @@ -264,7 +264,7 @@ class CreatePollOptionActionItemNode: ListViewItemNode, ItemListItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/ComposePollUI/Sources/CreatePollOptionItem.swift b/submodules/ComposePollUI/Sources/CreatePollOptionItem.swift index 40ec5723d83..891510a8072 100644 --- a/submodules/ComposePollUI/Sources/CreatePollOptionItem.swift +++ b/submodules/ComposePollUI/Sources/CreatePollOptionItem.swift @@ -495,7 +495,7 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode, self.item?.delete(self.textNode.isFirstResponder()) } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { //self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift b/submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift index 509b9b23de9..37106a74a6c 100644 --- a/submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift +++ b/submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift @@ -384,7 +384,7 @@ public class CreatePollTextInputItemNode: ListViewItemNode, ASEditableTextNodeDe } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/ComposePollUI/Sources/ListComposePollOptionComponent.swift b/submodules/ComposePollUI/Sources/ListComposePollOptionComponent.swift new file mode 100644 index 00000000000..c209cc6de06 --- /dev/null +++ b/submodules/ComposePollUI/Sources/ListComposePollOptionComponent.swift @@ -0,0 +1,553 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramPresentationData +import CheckNode +import ListSectionComponent +import ComponentFlow +import TextFieldComponent +import AccountContext +import MultilineTextComponent +import PresentationDataUtils +import LottieComponent +import PlainButtonComponent +import SwiftSignalKit + +public final class ListComposePollOptionComponent: Component { + public final class ResetText: Equatable { + public let value: String + + public init(value: String) { + self.value = value + } + + public static func ==(lhs: ResetText, rhs: ResetText) -> Bool { + return lhs === rhs + } + } + + public final class Selection: Equatable { + public let isSelected: Bool + public let toggle: () -> Void + + public init(isSelected: Bool, toggle: @escaping () -> Void) { + self.isSelected = isSelected + self.toggle = toggle + } + + public static func ==(lhs: Selection, rhs: Selection) -> Bool { + if lhs.isSelected != rhs.isSelected { + return false + } + return true + } + } + + public enum InputMode { + case keyboard + case emoji + } + + public final class EmojiSuggestion { + public struct Position: Equatable { + public var range: NSRange + public var value: String + } + + public var localPosition: CGPoint + public var position: Position + public var disposable: Disposable? + public var value: Any? + + init(localPosition: CGPoint, position: Position) { + self.localPosition = localPosition + self.position = position + self.disposable = nil + self.value = nil + } + } + + public let externalState: TextFieldComponent.ExternalState? + public let context: AccountContext + public let theme: PresentationTheme + public let strings: PresentationStrings + public let resetText: ResetText? + public let assumeIsEditing: Bool + public let characterLimit: Int? + public let returnKeyAction: (() -> Void)? + public let backspaceKeyAction: (() -> Void)? + public let selection: Selection? + public let inputMode: InputMode? + public let toggleInputMode: (() -> Void)? + public let tag: AnyObject? + + public init( + externalState: TextFieldComponent.ExternalState?, + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + resetText: ResetText? = nil, + assumeIsEditing: Bool = false, + characterLimit: Int, + returnKeyAction: (() -> Void)?, + backspaceKeyAction: (() -> Void)?, + selection: Selection?, + inputMode: InputMode?, + toggleInputMode: (() -> Void)?, + tag: AnyObject? = nil + ) { + self.externalState = externalState + self.context = context + self.theme = theme + self.strings = strings + self.resetText = resetText + self.assumeIsEditing = assumeIsEditing + self.characterLimit = characterLimit + self.returnKeyAction = returnKeyAction + self.backspaceKeyAction = backspaceKeyAction + self.selection = selection + self.inputMode = inputMode + self.toggleInputMode = toggleInputMode + self.tag = tag + } + + public static func ==(lhs: ListComposePollOptionComponent, rhs: ListComposePollOptionComponent) -> Bool { + if lhs.externalState !== rhs.externalState { + return false + } + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.resetText != rhs.resetText { + return false + } + if lhs.assumeIsEditing != rhs.assumeIsEditing { + return false + } + if lhs.characterLimit != rhs.characterLimit { + return false + } + if lhs.selection != rhs.selection { + return false + } + if lhs.inputMode != rhs.inputMode { + return false + } + + return true + } + + private final class CheckView: HighlightTrackingButton { + private var checkLayer: CheckLayer? + private var theme: PresentationTheme? + + var action: (() -> Void)? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + + self.highligthedChanged = { [weak self] highlighted in + if let self, self.bounds.width > 0.0 { + let animateScale = true + + let topScale: CGFloat = (self.bounds.width - 8.0) / self.bounds.width + let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width + + if highlighted { + self.layer.removeAnimation(forKey: "opacity") + self.layer.removeAnimation(forKey: "transform.scale") + + if animateScale { + let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + transition.setScale(layer: self.layer, scale: topScale) + } + } else { + if animateScale { + let transition = Transition(animation: .none) + transition.setScale(layer: self.layer, scale: 1.0) + + self.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in + guard let self else { + return + } + + self.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue) + }) + } + } + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + self.action?() + } + + func update(size: CGSize, theme: PresentationTheme, isSelected: Bool, transition: Transition) { + let checkLayer: CheckLayer + if let current = self.checkLayer { + checkLayer = current + } else { + checkLayer = CheckLayer(theme: CheckNodeTheme(theme: theme, style: .plain), content: .check) + self.checkLayer = checkLayer + self.layer.addSublayer(checkLayer) + } + + if self.theme !== theme { + self.theme = theme + + checkLayer.theme = CheckNodeTheme(theme: theme, style: .plain) + } + + checkLayer.frame = CGRect(origin: CGPoint(), size: size) + checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate) + } + } + + public final class View: UIView, ListSectionComponent.ChildView, ComponentTaggedView { + private let textField = ComponentView() + + private var modeSelector: ComponentView? + + private var checkView: CheckView? + + private var customPlaceholder: ComponentView? + + private var component: ListComposePollOptionComponent? + private weak var state: EmptyComponentState? + private var isUpdating: Bool = false + + public var currentText: String { + if let textFieldView = self.textField.view as? TextFieldComponent.View { + return textFieldView.inputState.inputText.string + } else { + return "" + } + } + + public var textFieldView: TextFieldComponent.View? { + return self.textField.view as? TextFieldComponent.View + } + + public var currentTag: AnyObject? { + return self.component?.tag + } + + public var customUpdateIsHighlighted: ((Bool) -> Void)? + public private(set) var separatorInset: CGFloat = 0.0 + + public override init(frame: CGRect) { + super.init(frame: CGRect()) + } + + required public init?(coder: NSCoder) { + preconditionFailure() + } + + public func matches(tag: Any) -> Bool { + if let component = self.component, let componentTag = component.tag { + let tag = tag as AnyObject + if componentTag === tag { + return true + } + } + return false + } + + public func activateInput() { + if let textFieldView = self.textField.view as? TextFieldComponent.View { + textFieldView.activateInput() + } + } + + public func insertText(text: NSAttributedString) { + if let textFieldView = self.textField.view as? TextFieldComponent.View { + textFieldView.insertText(text) + } + } + + public func backwardsDeleteText() { + if let textFieldView = self.textField.view as? TextFieldComponent.View { + textFieldView.deleteBackward() + } + } + + func update(component: ListComposePollOptionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let previousComponent = self.component + self.component = component + self.state = state + + let verticalInset: CGFloat = 12.0 + var leftInset: CGFloat = 16.0 + var rightInset: CGFloat = 16.0 + let modeSelectorSize = CGSize(width: 32.0, height: 32.0) + + if component.selection != nil { + leftInset += 34.0 + } + + if component.inputMode != nil { + rightInset += 34.0 + } + + let textFieldSize = self.textField.update( + transition: transition, + component: AnyComponent(TextFieldComponent( + context: component.context, + theme: component.theme, + strings: component.strings, + externalState: component.externalState ?? TextFieldComponent.ExternalState(), + fontSize: 17.0, + textColor: component.theme.list.itemPrimaryTextColor, + insets: UIEdgeInsets(top: verticalInset, left: 8.0, bottom: verticalInset, right: 8.0), + hideKeyboard: component.inputMode == .emoji, + customInputView: nil, + resetText: component.resetText.flatMap { resetText in + return NSAttributedString(string: resetText.value, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor) + }, + isOneLineWhenUnfocused: false, + characterLimit: component.characterLimit, + emptyLineHandling: .notAllowed, + formatMenuAvailability: .none, + returnKeyType: .next, + lockedFormatAction: { + }, + present: { _ in + }, + paste: { _ in + }, + returnKeyAction: { [weak self] in + guard let self, let component = self.component else { + return + } + component.returnKeyAction?() + }, + backspaceKeyAction: { [weak self] in + guard let self, let component = self.component else { + return + } + component.backspaceKeyAction?() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset + 8.0 * 2.0, height: availableSize.height) + ) + + let size = CGSize(width: availableSize.width, height: textFieldSize.height - 1.0) + let textFieldFrame = CGRect(origin: CGPoint(x: leftInset - 16.0, y: 0.0), size: textFieldSize) + + if let textFieldView = self.textField.view { + if textFieldView.superview == nil { + self.addSubview(textFieldView) + self.textField.parentState = state + } + transition.setFrame(view: textFieldView, frame: textFieldFrame) + } + + if let selection = component.selection { + let checkView: CheckView + var animateIn = false + if let current = self.checkView { + checkView = current + } else { + animateIn = true + checkView = CheckView() + self.checkView = checkView + self.addSubview(checkView) + + checkView.action = { [weak self] in + guard let self, let component = self.component else { + return + } + component.selection?.toggle() + } + } + let checkSize = CGSize(width: 22.0, height: 22.0) + let checkFrame = CGRect(origin: CGPoint(x: floor((leftInset - checkSize.width) * 0.5), y: floor((size.height - checkSize.height) * 0.5)), size: checkSize) + + if animateIn { + checkView.frame = CGRect(origin: CGPoint(x: -checkSize.width, y: self.bounds.height == 0.0 ? checkFrame.minY : floor((self.bounds.height - checkSize.height) * 0.5)), size: checkFrame.size) + transition.setPosition(view: checkView, position: checkFrame.center) + transition.setBounds(view: checkView, bounds: CGRect(origin: CGPoint(), size: checkFrame.size)) + checkView.update(size: checkFrame.size, theme: component.theme, isSelected: selection.isSelected, transition: .immediate) + } else { + transition.setPosition(view: checkView, position: checkFrame.center) + transition.setBounds(view: checkView, bounds: CGRect(origin: CGPoint(), size: checkFrame.size)) + checkView.update(size: checkFrame.size, theme: component.theme, isSelected: selection.isSelected, transition: transition) + } + } else if let checkView = self.checkView { + self.checkView = nil + transition.setPosition(view: checkView, position: CGPoint(x: -checkView.bounds.width * 0.5, y: size.height * 0.5), completion: { [weak checkView] _ in + checkView?.removeFromSuperview() + }) + } + + if let inputMode = component.inputMode { + var modeSelectorTransition = transition + let modeSelector: ComponentView + if let current = self.modeSelector { + modeSelector = current + } else { + modeSelectorTransition = modeSelectorTransition.withAnimation(.none) + modeSelector = ComponentView() + self.modeSelector = modeSelector + } + let animationName: String + var playAnimation = false + if let previousComponent, let previousInputMode = previousComponent.inputMode { + if previousInputMode != inputMode { + playAnimation = true + } + } + switch inputMode { + case .keyboard: + animationName = "input_anim_keyToSmile" + case .emoji: + animationName = "input_anim_smileToKey" + } + + let _ = modeSelector.update( + transition: modeSelectorTransition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent( + name: animationName + ), + color: component.theme.chat.inputPanel.inputControlColor.blitOver(component.theme.list.itemBlocksBackgroundColor, alpha: 1.0), + size: modeSelectorSize + )), + effectAlignment: .center, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.toggleInputMode?() + }, + animateScale: false + )), + environment: {}, + containerSize: modeSelectorSize + ) + let modeSelectorFrame = CGRect(origin: CGPoint(x: size.width - 4.0 - modeSelectorSize.width, y: floor((size.height - modeSelectorSize.height) * 0.5)), size: modeSelectorSize) + if let modeSelectorView = modeSelector.view as? PlainButtonComponent.View { + let alphaTransition: Transition = .easeInOut(duration: 0.2) + + if modeSelectorView.superview == nil { + self.addSubview(modeSelectorView) + Transition.immediate.setAlpha(view: modeSelectorView, alpha: 0.0) + Transition.immediate.setScale(view: modeSelectorView, scale: 0.001) + } + + if playAnimation, let animationView = modeSelectorView.contentView as? LottieComponent.View { + animationView.playOnce() + } + + modeSelectorTransition.setPosition(view: modeSelectorView, position: modeSelectorFrame.center) + modeSelectorTransition.setBounds(view: modeSelectorView, bounds: CGRect(origin: CGPoint(), size: modeSelectorFrame.size)) + + if let externalState = component.externalState { + let displaySelector = externalState.isEditing + + alphaTransition.setAlpha(view: modeSelectorView, alpha: displaySelector ? 1.0 : 0.0) + alphaTransition.setScale(view: modeSelectorView, scale: displaySelector ? 1.0 : 0.001) + } + } + } else if let modeSelector = self.modeSelector { + self.modeSelector = nil + if let modeSelectorView = modeSelector.view { + if !transition.animation.isImmediate { + let alphaTransition: Transition = .easeInOut(duration: 0.2) + alphaTransition.setAlpha(view: modeSelectorView, alpha: 0.0, completion: { [weak modeSelectorView] _ in + modeSelectorView?.removeFromSuperview() + }) + alphaTransition.setScale(view: modeSelectorView, scale: 0.001) + } else { + modeSelectorView.removeFromSuperview() + } + } + } + + self.separatorInset = leftInset + + return size + } + + public func updateCustomPlaceholder(value: String, size: CGSize, transition: Transition) { + guard let component = self.component else { + return + } + + let verticalInset: CGFloat = 12.0 + var leftInset: CGFloat = 16.0 + let rightInset: CGFloat = 16.0 + + if component.selection != nil { + leftInset += 34.0 + } + + if !value.isEmpty { + let customPlaceholder: ComponentView + var customPlaceholderTransition = transition + if let current = self.customPlaceholder { + customPlaceholder = current + } else { + customPlaceholderTransition = customPlaceholderTransition.withAnimation(.none) + customPlaceholder = ComponentView() + self.customPlaceholder = customPlaceholder + } + + let placeholderSize = customPlaceholder.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: value.isEmpty ? " " : value, font: Font.regular(17.0), textColor: component.theme.list.itemPlaceholderTextColor)) + )), + environment: {}, + containerSize: CGSize(width: size.width - leftInset - rightInset, height: 100.0) + ) + let placeholderFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: placeholderSize) + if let placeholderView = customPlaceholder.view { + if placeholderView.superview == nil { + placeholderView.layer.anchorPoint = CGPoint() + placeholderView.isUserInteractionEnabled = false + self.insertSubview(placeholderView, at: 0) + } + transition.setPosition(view: placeholderView, position: placeholderFrame.origin) + placeholderView.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size) + + if let externalState = component.externalState { + placeholderView.isHidden = externalState.hasText + } + } + } else if let customPlaceholder = self.customPlaceholder { + self.customPlaceholder = nil + customPlaceholder.view?.removeFromSuperview() + } + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/ContactListUI/BUILD b/submodules/ContactListUI/BUILD index 1ce3cafb91b..ef6c5d49a2a 100644 --- a/submodules/ContactListUI/BUILD +++ b/submodules/ContactListUI/BUILD @@ -45,6 +45,7 @@ swift_library( "//submodules/TooltipUI", "//submodules/UndoUI", "//submodules/TelegramIntents", + "//submodules/ContextUI", ], visibility = [ "//visibility:public", diff --git a/submodules/ContactListUI/Sources/ContactAddItem.swift b/submodules/ContactListUI/Sources/ContactAddItem.swift index 8af09937ad5..22d6856ccb9 100644 --- a/submodules/ContactListUI/Sources/ContactAddItem.swift +++ b/submodules/ContactListUI/Sources/ContactAddItem.swift @@ -238,7 +238,7 @@ class ContactsAddItemNode: ListViewItemNode { accessoryItemNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -29.0), size: CGSize(width: bounds.size.width, height: 29.0)) } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } diff --git a/submodules/ContactListUI/Sources/ContactListActionItem.swift b/submodules/ContactListUI/Sources/ContactListActionItem.swift index f27118bdab0..4588b5a5bf3 100644 --- a/submodules/ContactListUI/Sources/ContactListActionItem.swift +++ b/submodules/ContactListUI/Sources/ContactListActionItem.swift @@ -317,7 +317,7 @@ class ContactListActionItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index 09cd361345b..be109ead0cd 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -23,6 +23,7 @@ import AppBundle import ContextUI import PhoneNumberFormat import LocalizedPeerData +import ContextUI private let dropDownIcon = { () -> UIImage in UIGraphicsBeginImageContextWithOptions(CGSize(width: 12.0, height: 12.0), false, 0.0) @@ -56,7 +57,7 @@ private final class ContactListNodeInteraction { fileprivate let activateSearch: () -> Void fileprivate let authorize: () -> Void fileprivate let suppressWarning: () -> Void - fileprivate let openPeer: (ContactListPeer, ContactListAction) -> Void + fileprivate let openPeer: (ContactListPeer, ContactListAction, ASDisplayNode?, ContextGesture?) -> Void fileprivate let openDisabledPeer: (EnginePeer, ChatListDisabledPeerReason) -> Void fileprivate let contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?, Bool) -> Void)? fileprivate let openStories: (EnginePeer, ASDisplayNode) -> Void @@ -65,7 +66,7 @@ private final class ContactListNodeInteraction { let itemHighlighting = ContactItemHighlighting() - init(activateSearch: @escaping () -> Void, authorize: @escaping () -> Void, suppressWarning: @escaping () -> Void, openPeer: @escaping (ContactListPeer, ContactListAction) -> Void, openDisabledPeer: @escaping (EnginePeer, ChatListDisabledPeerReason) -> Void, contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?, Bool) -> Void)?, openStories: @escaping (EnginePeer, ASDisplayNode) -> Void, deselectAll: @escaping () -> Void, toggleSelection: @escaping ([EnginePeer], Bool) -> Void) { + init(activateSearch: @escaping () -> Void, authorize: @escaping () -> Void, suppressWarning: @escaping () -> Void, openPeer: @escaping (ContactListPeer, ContactListAction, ASDisplayNode?, ContextGesture?) -> Void, openDisabledPeer: @escaping (EnginePeer, ChatListDisabledPeerReason) -> Void, contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?, Bool) -> Void)?, openStories: @escaping (EnginePeer, ASDisplayNode) -> Void, deselectAll: @escaping () -> Void, toggleSelection: @escaping ([EnginePeer], Bool) -> Void) { self.activateSearch = activateSearch self.authorize = authorize self.suppressWarning = suppressWarning @@ -96,7 +97,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { case permissionInfo(PresentationTheme, String, String, Bool) case permissionEnable(PresentationTheme, String) case option(Int, ContactListAdditionalOption, ListViewItemHeader?, PresentationTheme, PresentationStrings) - case peer(Int, ContactListPeer, EnginePeer.Presence?, ListViewItemHeader?, ContactsPeerItemSelection, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PresentationPersonNameOrder, Bool, Bool, StoryData?, Bool) + case peer(Int, ContactListPeer, EnginePeer.Presence?, ListViewItemHeader?, ContactsPeerItemSelection, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PresentationPersonNameOrder, Bool, Bool, Bool, StoryData?, Bool) var stableId: ContactListNodeEntryId { switch self { @@ -110,7 +111,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { return .permission(action: true) case let .option(index, _, _, _, _): return .option(index: index) - case let .peer(_, peer, _, _, _, _, _, _, _, _, _, _, storyData, _): + case let .peer(_, peer, _, _, _, _, _, _, _, _, _, _, _, storyData, _): switch peer { case let .peer(peer, _, _): return .peerId(peerId: peer.id.toInt64(), section: storyData != nil ? .stories : .contacts) @@ -143,7 +144,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { }) case let .option(_, option, header, _, _): return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: option.title, icon: option.icon, clearHighlightAutomatically: option.clearHighlightAutomatically, header: header, action: option.action) - case let .peer(_, peer, presence, header, selection, _, strings, dateTimeFormat, nameSortOrder, nameDisplayOrder, displayCallIcons, enabled, storyData, requiresPremiumForMessaging): + case let .peer(_, peer, presence, header, selection, _, strings, dateTimeFormat, nameSortOrder, nameDisplayOrder, displayCallIcons, hasMoreButton, enabled, storyData, requiresPremiumForMessaging): var status: ContactsPeerItemStatus let itemPeer: ContactsPeerItemPeer var isContextActionEnabled = false @@ -200,11 +201,15 @@ private enum ContactListNodeEntry: Comparable, Identifiable { } var additionalActions: [ContactsPeerItemAction] = [] - if displayCallIcons { - additionalActions = [ContactsPeerItemAction(icon: .voiceCall, action: { _ in - interaction.openPeer(peer, .voiceCall) - }), ContactsPeerItemAction(icon: .videoCall, action: { _ in - interaction.openPeer(peer, .videoCall) + if hasMoreButton { + additionalActions = [ContactsPeerItemAction(icon: .more, action: { _, sourceNode, gesture in + interaction.openPeer(peer, .more, sourceNode, gesture) + })] + } else if displayCallIcons { + additionalActions = [ContactsPeerItemAction(icon: .voiceCall, action: { _, sourceNode, gesture in + interaction.openPeer(peer, .voiceCall, sourceNode, gesture) + }), ContactsPeerItemAction(icon: .videoCall, action: { _, sourceNode, gesture in + interaction.openPeer(peer, .videoCall, sourceNode, gesture) })] } @@ -218,7 +223,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { } return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: isSearch ? .generalSearch(isSavedMessages: false) : .peer, peer: itemPeer, status: status, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: selection, selectionPosition: .left, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), additionalActions: additionalActions, index: nil, header: header, action: { _ in - interaction.openPeer(peer, .generic) + interaction.openPeer(peer, .generic, nil, nil) }, disabledAction: { _ in if case let .peer(peer, _, _) = peer { interaction.openDisabledPeer(EnginePeer(peer), requiresPremiumForMessaging ? .premiumRequired : .generic) @@ -263,9 +268,9 @@ private enum ContactListNodeEntry: Comparable, Identifiable { } else { return false } - case let .peer(lhsIndex, lhsPeer, lhsPresence, lhsHeader, lhsSelection, lhsTheme, lhsStrings, lhsTimeFormat, lhsSortOrder, lhsDisplayOrder, lhsDisplayCallIcons, lhsEnabled, lhsStoryData, lhsRequiresPremiumForMessaging): + case let .peer(lhsIndex, lhsPeer, lhsPresence, lhsHeader, lhsSelection, lhsTheme, lhsStrings, lhsTimeFormat, lhsSortOrder, lhsDisplayOrder, lhsDisplayCallIcons, lhsHasMoreButton, lhsEnabled, lhsStoryData, lhsRequiresPremiumForMessaging): switch rhs { - case let .peer(rhsIndex, rhsPeer, rhsPresence, rhsHeader, rhsSelection, rhsTheme, rhsStrings, rhsTimeFormat, rhsSortOrder, rhsDisplayOrder, rhsDisplayCallIcons, rhsEnabled, rhsStoryData, rhsRequiresPremiumForMessaging): + case let .peer(rhsIndex, rhsPeer, rhsPresence, rhsHeader, rhsSelection, rhsTheme, rhsStrings, rhsTimeFormat, rhsSortOrder, rhsDisplayOrder, rhsDisplayCallIcons, rhsHasMoreButton, rhsEnabled, rhsStoryData, rhsRequiresPremiumForMessaging): if lhsIndex != rhsIndex { return false } @@ -303,6 +308,9 @@ private enum ContactListNodeEntry: Comparable, Identifiable { if lhsDisplayCallIcons != rhsDisplayCallIcons { return false } + if lhsHasMoreButton != rhsHasMoreButton { + return false + } if lhsEnabled != rhsEnabled { return false } @@ -353,11 +361,11 @@ private enum ContactListNodeEntry: Comparable, Identifiable { case .peer: return true } - case let .peer(lhsIndex, _, _, _, _, _, _, _, _, _, _, _, lhsStoryData, _): + case let .peer(lhsIndex, _, _, _, _, _, _, _, _, _, _, _, _, lhsStoryData, _): switch rhs { case .search, .sort, .permissionInfo, .permissionEnable, .option: return false - case let .peer(rhsIndex, _, _, _, _, _, _, _, _, _, _, _, rhsStoryData, _): + case let .peer(rhsIndex, _, _, _, _, _, _, _, _, _, _, _, _, rhsStoryData, _): if (lhsStoryData == nil) != (rhsStoryData == nil) { if lhsStoryData != nil { return true @@ -551,7 +559,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis } let presence = presences[peer.id] - entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, true, nil, false)) + entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, nil, false)) index += 1 } @@ -608,7 +616,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis } let presence = presences[peer.id] - entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, true, nil, false)) + entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, true, true, nil, false)) index += 1 } @@ -657,7 +665,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis } let presence = presences[peer.id] - entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, true, nil, false)) + entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, nil, false)) index += 1 } @@ -701,7 +709,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis enabled = true } - entries.append(.peer(index, peer, presence, nil, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, enabled, nil, false)) + entries.append(.peer(index, peer, presence, nil, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, false, enabled, nil, false)) index += 1 } } @@ -748,7 +756,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis } - entries.append(.peer(index, peer, presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, enabled, nil, requiresPremiumForMessaging)) + entries.append(.peer(index, peer, presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, false, enabled, nil, requiresPremiumForMessaging)) index += 1 } return entries @@ -772,7 +780,7 @@ private func preparedContactListNodeTransition(context: AccountContext, presenta case .search: //indexSections.apend(CollectionIndexNode.searchIndex) break - case let .peer(_, _, _, header, _, _, _, _, _, _, _, _, _, _): + case let .peer(_, _, _, header, _, _, _, _, _, _, _, _, _, _, _): if let header = header as? ContactListNameIndexHeader { if !existingSections.contains(header.letter) { existingSections.insert(header.letter) @@ -1036,7 +1044,7 @@ public final class ContactListNode: ASDisplayNode { public var contentScrollingEnded: ((ListView) -> Bool)? public var activateSearch: (() -> Void)? - public var openPeer: ((ContactListPeer, ContactListAction) -> Void)? + public var openPeer: ((ContactListPeer, ContactListAction, ASDisplayNode?, ContextGesture?) -> Void)? public var openDisabledPeer: ((EnginePeer, ChatListDisabledPeerReason) -> Void)? public var deselectedAll: (() -> Void)? public var updatedSelection: (([EnginePeer], Bool) -> Void)? @@ -1137,7 +1145,7 @@ public final class ContactListNode: ASDisplayNode { authorizeImpl?() }, suppressWarning: { [weak self] in self?.suppressPermissionWarning?() - }, openPeer: { [weak self] peer, action in + }, openPeer: { [weak self] peer, action, sourceNode, gesture in if let strongSelf = self { if strongSelf.multipleSelection { var updated = false @@ -1152,10 +1160,10 @@ public final class ContactListNode: ASDisplayNode { } }) if !updated { - strongSelf.openPeer?(peer, action) + strongSelf.openPeer?(peer, action, sourceNode, gesture) } } else { - strongSelf.openPeer?(peer, action) + strongSelf.openPeer?(peer, action, sourceNode, gesture) } } }, openDisabledPeer: { [weak self] peer, reason in @@ -1225,7 +1233,7 @@ public final class ContactListNode: ASDisplayNode { strongSelf.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.PreferSynchronousDrawing, .PreferSynchronousResourceLoading], scrollToItem: ListViewScrollToItem(index: index, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: nil), directionHint: .Down), additionalScrollDistance: 0.0, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) break loop } - case let .peer(_, _, _, header, _, _, _, _, _, _, _, _, _, _): + case let .peer(_, _, _, header, _, _, _, _, _, _, _, _, _, _, _): if let header = header as? ContactListNameIndexHeader { if let scalar = UnicodeScalar(header.letter) { let title = "\(Character(scalar))" @@ -1763,25 +1771,32 @@ public final class ContactListNode: ASDisplayNode { if (authorizationStatus == .notDetermined || authorizationStatus == .denied) && peers.isEmpty { isEmpty = true } + let entries = contactListNodeEntries(accountPeer: view.1, peers: peers, presences: presences, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, peerRequiresPremiumForMessaging: view.2, authorizationStatus: authorizationStatus, warningSuppressed: warningSuppressed, displaySortOptions: displaySortOptions, displayCallIcons: displayCallIcons, storySubscriptions: storySubscriptions, topPeers: topPeers.map { $0.peer }, topPeersPresentation: displayTopPeers, interaction: interaction) let previous = previousEntries.swap(entries) let previousSelection = previousSelectionState.swap(selectionState) let previousPendingRemovalPeerIds = previousPendingRemovalPeerIds.swap(pendingRemovalPeerIds) var hadPermissionInfo = false + var previousOptionsCount = 0 if let previous = previous { for entry in previous { if case .permissionInfo = entry { hadPermissionInfo = true - break + } + if case .option = entry { + previousOptionsCount += 1 } } } var hasPermissionInfo = false + var optionsCount = 0 for entry in entries { if case .permissionInfo = entry { hasPermissionInfo = true - break + } + if case .option = entry { + optionsCount += 1 } } @@ -1792,6 +1807,8 @@ public final class ContactListNode: ASDisplayNode { animation = .insertion } else if hadPermissionInfo != hasPermissionInfo { animation = .insertion + } else if optionsCount < previousOptionsCount { + animation = .insertion } else { animation = .none } diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index c79c7a5bb55..593a022af4e 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -345,7 +345,7 @@ public class ContactsController: ViewController { self?.activateSearch() } - self.contactsNode.contactListNode.openPeer = { [weak self] peer, _ in + self.contactsNode.contactListNode.openPeer = { [weak self] peer, _, _, _ in guard let self else { return } diff --git a/submodules/ContactsPeerItem/BUILD b/submodules/ContactsPeerItem/BUILD index 66af4b82ba5..921376ec475 100644 --- a/submodules/ContactsPeerItem/BUILD +++ b/submodules/ContactsPeerItem/BUILD @@ -31,6 +31,7 @@ swift_library( "//submodules/TelegramUI/Components/MultiAnimationRenderer", "//submodules/TelegramUI/Components/EmojiStatusComponent", "//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent", + "//submodules/MoreButtonNode", ], visibility = [ "//visibility:public", diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index aee623f1483..61ed16b0af9 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -20,6 +20,7 @@ import ComponentFlow import AnimationCache import MultiAnimationRenderer import EmojiStatusComponent +import MoreButtonNode public final class ContactItemHighlighting { public var chatLocation: ChatLocation? @@ -88,13 +89,14 @@ public enum ContactsPeerItemActionIcon { case add case voiceCall case videoCall + case more } public struct ContactsPeerItemAction { public let icon: ContactsPeerItemActionIcon - public let action: ((ContactsPeerItemPeer) -> Void)? + public let action: ((ContactsPeerItemPeer, ASDisplayNode, ContextGesture?) -> Void)? - public init(icon: ContactsPeerItemActionIcon, action: @escaping (ContactsPeerItemPeer) -> Void) { + public init(icon: ContactsPeerItemActionIcon, action: @escaping (ContactsPeerItemPeer, ASDisplayNode, ContextGesture?) -> Void) { self.icon = icon self.action = action } @@ -163,6 +165,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { public let peer: ContactsPeerItemPeer let status: ContactsPeerItemStatus let badge: ContactsPeerItemBadge? + let rightLabelText: String? let requiresPremiumForMessaging: Bool let enabled: Bool let selection: ContactsPeerItemSelection @@ -200,6 +203,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { peer: ContactsPeerItemPeer, status: ContactsPeerItemStatus, badge: ContactsPeerItemBadge? = nil, + rightLabelText: String? = nil, requiresPremiumForMessaging: Bool = false, enabled: Bool, selection: ContactsPeerItemSelection, @@ -231,6 +235,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { self.peer = peer self.status = status self.badge = badge + self.rightLabelText = rightLabelText self.requiresPremiumForMessaging = requiresPremiumForMessaging self.enabled = enabled self.selection = selection @@ -417,7 +422,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { private var badgeTextNode: TextNode? private var selectionNode: CheckNode? private var actionButtonNodes: [HighlightableButtonNode]? + private var moreButtonNode: MoreButtonNode? private var arrowButtonNode: HighlightableButtonNode? + private var rightLabelTextNode: TextNode? private var avatarTapRecognizer: UITapGestureRecognizer? @@ -576,6 +583,10 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect) } + if let rightLabelTextNode = strongSelf.rightLabelTextNode { + transition.updateTransform(node: rightLabelTextNode, transform: CGAffineTransformMakeTranslation(isExtracted ? -24.0 : 0.0, 0.0)) + } + transition.updateSublayerTransformOffset(layer: strongSelf.offsetContainerNode.layer, offset: CGPoint(x: isExtracted ? 12.0 : 0.0, y: 0.0)) transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in if !isExtracted { @@ -585,6 +596,20 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { } } + override public func didLoad() { + super.didLoad() + + self.updateEnableGestures() + } + + private func updateEnableGestures() { + if let item = self.layoutParams?.0, !item.options.isEmpty { + self.view.disablesInteractiveTransitionGestureRecognizer = false + } else { + self.view.disablesInteractiveTransitionGestureRecognizer = false + } + } + public override func secondaryAction(at point: CGPoint) { guard let item = self.item, let contextAction = item.contextAction else { return @@ -662,6 +687,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { let currentSelectionNode = self.selectionNode let makeBadgeTextLayout = TextNode.asyncLayout(self.badgeTextNode) + let makeRightLabelTextLayout = TextNode.asyncLayout(self.rightLabelTextNode) let currentItem = self.layoutParams?.0 @@ -709,6 +735,13 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { } } + var rightLabelTextLayoutAndApply: (TextNodeLayout, () -> TextNode)? + if let rightLabelText = item.rightLabelText { + let rightLabelTextLayoutAndApplyValue = makeRightLabelTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: rightLabelText, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 20.0, height: 100.0))) + rightLabelTextLayoutAndApply = rightLabelTextLayoutAndApplyValue + rightInset -= 6.0 + rightLabelTextLayoutAndApplyValue.0.size.width + } + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: item.context.currentAppConfiguration.with { $0 }) var credibilityIcon: EmojiStatusComponent.Content? @@ -744,10 +777,11 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { var actionButtons: [ActionButton]? struct ActionButton { + let type: ContactsPeerItemActionIcon let image: UIImage? - let action: ((ContactsPeerItemPeer) -> Void)? + let action: ((ContactsPeerItemPeer, ASDisplayNode, ContextGesture?) -> Void)? - init(theme: PresentationTheme, icon: ContactsPeerItemActionIcon, action: ((ContactsPeerItemPeer) -> Void)?) { + init(theme: PresentationTheme, icon: ContactsPeerItemActionIcon, action: ((ContactsPeerItemPeer, ASDisplayNode, ContextGesture?) -> Void)?) { let image: UIImage? switch icon { case .none: @@ -758,7 +792,10 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { image = PresentationResourcesItemList.voiceCallIcon(theme) case .videoCall: image = PresentationResourcesItemList.videoCallIcon(theme) + case .more: + image = PresentationResourcesItemList.videoCallIcon(theme) } + self.type = icon self.image = image self.action = action } @@ -1110,7 +1147,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { transition = .immediate } - let revealOffset = strongSelf.revealOffset + let revealOffset: CGFloat = 0.0 if let _ = updatedTheme { switch item.style { @@ -1357,7 +1394,23 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { verifiedIconView.removeFromSuperview() } - if let actionButtons = actionButtons { + if let actionButtons, actionButtons.count == 1, let actionButton = actionButtons.first, case .more = actionButton.type { + let moreButtonNode: MoreButtonNode + if let current = strongSelf.moreButtonNode { + moreButtonNode = current + } else { + moreButtonNode = MoreButtonNode(theme: item.presentationData.theme) + moreButtonNode.iconNode.enqueueState(.more, animated: false) + moreButtonNode.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0) + strongSelf.offsetContainerNode.addSubnode(moreButtonNode) + strongSelf.moreButtonNode = moreButtonNode + } + moreButtonNode.action = { sourceNode, gesture in + actionButton.action?(item.peer, sourceNode, gesture) + } + let moreButtonSize = moreButtonNode.measure(CGSize(width: 100.0, height: nodeLayout.contentSize.height)) + moreButtonNode.frame = CGRect(origin: CGPoint(x: revealOffset + params.width - params.rightInset - 18.0 - moreButtonSize.width, y:floor((nodeLayout.contentSize.height - moreButtonSize.height) / 2.0)), size: moreButtonSize) + } else if let actionButtons = actionButtons { if strongSelf.actionButtonNodes == nil { var actionButtonNodes: [HighlightableButtonNode] = [] for action in actionButtons { @@ -1456,6 +1509,28 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { } } + if let (rightLabelTextLayout, rightLabelTextApply) = rightLabelTextLayoutAndApply { + let rightLabelTextNode = rightLabelTextApply() + var rightLabelTextTransition = transition + if rightLabelTextNode !== strongSelf.rightLabelTextNode { + strongSelf.rightLabelTextNode?.removeFromSupernode() + strongSelf.rightLabelTextNode = rightLabelTextNode + strongSelf.offsetContainerNode.addSubnode(rightLabelTextNode) + rightLabelTextTransition = .immediate + } + + var rightLabelTextFrame = CGRect(x: revealOffset + params.width - params.rightInset - 8.0 - rightLabelTextLayout.size.width, y: floor((nodeLayout.contentSize.height - rightLabelTextLayout.size.height) / 2.0), width: rightLabelTextLayout.size.width, height: rightLabelTextLayout.size.height) + if let arrowButtonImage = arrowButtonImage { + rightLabelTextFrame.origin.x -= arrowButtonImage.size.width + 6.0 + } + + rightLabelTextNode.bounds = CGRect(origin: CGPoint(), size: rightLabelTextFrame.size) + rightLabelTextTransition.updatePosition(node: rightLabelTextNode, position: rightLabelTextFrame.center) + } else if let rightLabelTextNode = strongSelf.rightLabelTextNode { + strongSelf.rightLabelTextNode = nil + rightLabelTextNode.removeFromSupernode() + } + if let updatedSelectionNode = updatedSelectionNode { let hadSelectionNode = strongSelf.selectionNode != nil if strongSelf.selectionNode !== updatedSelectionNode { @@ -1510,6 +1585,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { strongSelf.setRevealOptions((left: [], right: peerRevealOptions)) strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated) } + + strongSelf.updateEnableGestures() } }) } else { @@ -1524,63 +1601,15 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { guard let actionButtonNodes = self.actionButtonNodes, let index = actionButtonNodes.firstIndex(of: sender), let item = self.item, index < item.additionalActions.count else { return } - item.additionalActions[index].action?(item.peer) + item.additionalActions[index].action?(item.peer, sender, nil) } override public func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { super.updateRevealOffset(offset: offset, transition: transition) - if let item = self.item, let params = self.layoutParams?.1 { - var leftInset: CGFloat = 65.0 + params.leftInset - - switch item.selection { - case .none: - break - case .selectable: - leftInset += 28.0 - } - - var avatarFrame = self.avatarNode.frame - avatarFrame.origin.x = offset + leftInset - 50.0 - transition.updateFrame(node: self.avatarNode, frame: avatarFrame) - - var titleFrame = self.titleNode.frame - titleFrame.origin.x = leftInset + offset - transition.updateFrame(node: self.titleNode, frame: titleFrame) - - var statusFrame = self.statusNode.frame - let previousStatusFrame = statusFrame - statusFrame.origin.x = leftInset + offset - if let statusIconImage = self.statusIconNode?.image { - statusFrame.origin.x += statusIconImage.size.width + 1.0 - } - self.statusNode.frame = statusFrame - transition.animatePositionAdditive(node: self.statusNode, offset: CGPoint(x: previousStatusFrame.minX - statusFrame.minX, y: 0)) - - var nextIconX = titleFrame.maxX - if let credibilityIconView = self.credibilityIconView { - var iconFrame = credibilityIconView.frame - iconFrame.origin.x = nextIconX + 4.0 - nextIconX += 4.0 + iconFrame.width - transition.updateFrame(view: credibilityIconView, frame: iconFrame) - } - if let verifiedIconView = self.verifiedIconView { - var iconFrame = verifiedIconView.frame - iconFrame.origin.x = nextIconX + 4.0 - nextIconX += 4.0 + iconFrame.width - transition.updateFrame(view: verifiedIconView, frame: iconFrame) - } - - if let badgeBackgroundNode = self.badgeBackgroundNode, let badgeTextNode = self.badgeTextNode { - var badgeBackgroundFrame = badgeBackgroundNode.frame - badgeBackgroundFrame.origin.x = offset + params.width - params.rightInset - badgeBackgroundFrame.width - 6.0 - var badgeTextFrame = badgeTextNode.frame - badgeTextFrame.origin.x = badgeBackgroundFrame.midX - badgeTextFrame.width / 2.0 - - transition.updateFrame(node: badgeBackgroundNode, frame: badgeBackgroundFrame) - transition.updateFrame(node: badgeTextNode, frame: badgeTextFrame) - } - } + var offsetContainerBounds = self.offsetContainerNode.bounds + offsetContainerBounds.origin.x = -offset + transition.updateBounds(node: self.offsetContainerNode, bounds: offsetContainerBounds) } override public func revealOptionsInteractivelyOpened() { @@ -1640,7 +1669,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { accessoryItemNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -29.0), size: CGSize(width: bounds.size.width, height: 29.0)) } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } diff --git a/submodules/CounterContollerTitleView/BUILD b/submodules/CounterControllerTitleView/BUILD similarity index 83% rename from submodules/CounterContollerTitleView/BUILD rename to submodules/CounterControllerTitleView/BUILD index 7ad134d0098..f63f0315bbd 100644 --- a/submodules/CounterContollerTitleView/BUILD +++ b/submodules/CounterControllerTitleView/BUILD @@ -1,8 +1,8 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") swift_library( - name = "CounterContollerTitleView", - module_name = "CounterContollerTitleView", + name = "CounterControllerTitleView", + module_name = "CounterControllerTitleView", srcs = glob([ "Sources/**/*.swift", ]), diff --git a/submodules/CounterContollerTitleView/Sources/CounterContollerTitleView.swift b/submodules/CounterControllerTitleView/Sources/CounterControllerTitleView.swift similarity index 95% rename from submodules/CounterContollerTitleView/Sources/CounterContollerTitleView.swift rename to submodules/CounterControllerTitleView/Sources/CounterControllerTitleView.swift index 17c9e99ac03..42d3b93faad 100644 --- a/submodules/CounterContollerTitleView/Sources/CounterContollerTitleView.swift +++ b/submodules/CounterControllerTitleView/Sources/CounterControllerTitleView.swift @@ -4,7 +4,7 @@ import Display import AsyncDisplayKit import TelegramPresentationData -public struct CounterContollerTitle: Equatable { +public struct CounterControllerTitle: Equatable { public var title: String public var counter: String @@ -14,11 +14,11 @@ public struct CounterContollerTitle: Equatable { } } -public final class CounterContollerTitleView: UIView { +public final class CounterControllerTitleView: UIView { private let titleNode: ImmediateTextNode private let subtitleNode: ImmediateTextNode - public var title: CounterContollerTitle = CounterContollerTitle(title: "", counter: "") { + public var title: CounterControllerTitle = CounterControllerTitle(title: "", counter: "") { didSet { if self.title != oldValue { self.update() diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index 0cdadc755f3..b36d56889e1 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -112,6 +112,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case storiesJpegExperiment(Bool) case playlistPlayback(Bool) case enableQuickReactionSwitch(Bool) + case disableReloginTokens(Bool) case voiceConference case preferredVideoCodec(Int, String, String?, Bool) case disableVideoAspectScaling(Bool) @@ -142,7 +143,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.web.rawValue case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure: return DebugControllerSection.experiments.rawValue - case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases: + case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens: return DebugControllerSection.experiments.rawValue case .logTranslationRecognition, .resetTranslationStates: return DebugControllerSection.translation.rawValue @@ -255,14 +256,16 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 46 case .storiesJpegExperiment: return 47 - case .playlistPlayback: + case .disableReloginTokens: return 48 - case .enableQuickReactionSwitch: + case .playlistPlayback: return 49 - case .voiceConference: + case .enableQuickReactionSwitch: return 50 + case .voiceConference: + return 51 case let .preferredVideoCodec(index, _, _, _): - return 51 + index + return 52 + index case .disableVideoAspectScaling: return 100 case .enableNetworkFramework: @@ -1426,6 +1429,14 @@ private enum DebugControllerEntry: ItemListNodeEntry { } }) }) + case let .disableReloginTokens(value): + return ItemListSwitchItem(presentationData: presentationData, title: "Disable Relogin Tokens", value: value, sectionId: self.section, style: .blocks, updated: { value in + let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in + var settings = settings + settings.disableReloginTokens = value + return settings + }).start() + }) case let .hostInfo(_, string): return ItemListTextItem(presentationData: presentationData, text: .plain(string), sectionId: self.section) case .versionInfo: @@ -1526,10 +1537,11 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.logTranslationRecognition(experimentalSettings.logLanguageRecognition)) entries.append(.resetTranslationStates) - + if case .internal = sharedContext.applicationBindings.appBuildType { entries.append(.storiesExperiment(experimentalSettings.storiesExperiment)) entries.append(.storiesJpegExperiment(experimentalSettings.storiesJpegExperiment)) + entries.append(.disableReloginTokens(experimentalSettings.disableReloginTokens)) } entries.append(.playlistPlayback(experimentalSettings.playlistPlayback)) entries.append(.enableQuickReactionSwitch(!experimentalSettings.disableQuickReaction)) diff --git a/submodules/Display/Source/ActionSheetTextItem.swift b/submodules/Display/Source/ActionSheetTextItem.swift index b13d4f3240a..7921a5fe45a 100644 --- a/submodules/Display/Source/ActionSheetTextItem.swift +++ b/submodules/Display/Source/ActionSheetTextItem.swift @@ -4,11 +4,18 @@ import AsyncDisplayKit import Markdown public class ActionSheetTextItem: ActionSheetItem { + public enum Font { + case `default` + case large + } + public let title: String + public let font: Font public let parseMarkdown: Bool - public init(title: String, parseMarkdown: Bool = true) { + public init(title: String, font: Font = .default, parseMarkdown: Bool = true) { self.title = title + self.font = font self.parseMarkdown = parseMarkdown } @@ -30,8 +37,6 @@ public class ActionSheetTextItem: ActionSheetItem { } public class ActionSheetTextNode: ActionSheetItemNode { - private let defaultFont: UIFont - private let theme: ActionSheetControllerTheme private var item: ActionSheetTextItem? @@ -42,7 +47,6 @@ public class ActionSheetTextNode: ActionSheetItemNode { override public init(theme: ActionSheetControllerTheme) { self.theme = theme - self.defaultFont = Font.regular(floor(theme.baseFontSize * 13.0 / 17.0)) self.label = ImmediateTextNode() self.label.isUserInteractionEnabled = false @@ -66,8 +70,16 @@ public class ActionSheetTextNode: ActionSheetItemNode { func setItem(_ item: ActionSheetTextItem) { self.item = item - let defaultFont = Font.regular(floor(theme.baseFontSize * 13.0 / 17.0)) - let boldFont = Font.semibold(floor(theme.baseFontSize * 13.0 / 17.0)) + let fontSize: CGFloat + switch item.font { + case .default: + fontSize = 13.0 + case .large: + fontSize = 15.0 + } + + let defaultFont = Font.regular(floor(self.theme.baseFontSize * fontSize / 17.0)) + let boldFont = Font.semibold(floor(self.theme.baseFontSize * fontSize / 17.0)) if item.parseMarkdown { let body = MarkdownAttributeSet(font: defaultFont, textColor: self.theme.secondaryTextColor) diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index facaad06859..a378c94a9f7 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -2154,7 +2154,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel let beginReplay = { [weak self] in if let strongSelf = self { - strongSelf.replayOperations(animated: animated, animateAlpha: options.contains(.AnimateAlpha), animateCrossfade: options.contains(.AnimateCrossfade), animateFullTransition: options.contains(.AnimateFullTransition), synchronous: options.contains(.Synchronous), synchronousLoads: options.contains(.PreferSynchronousResourceLoading), animateTopItemVerticalOrigin: options.contains(.AnimateTopItemPosition), operations: updatedOperations, requestItemInsertionAnimationsIndices: options.contains(.RequestItemInsertionAnimations) ? insertedIndexSet : Set(), scrollToItem: scrollToItem, additionalScrollDistance: additionalScrollDistance, updateSizeAndInsets: updateSizeAndInsets, stationaryItemIndex: stationaryItemIndex, updateOpaqueState: updateOpaqueState, completion: { + strongSelf.replayOperations(animated: animated, animateAlpha: options.contains(.AnimateAlpha), animateCrossfade: options.contains(.AnimateCrossfade), animateFullTransition: options.contains(.AnimateFullTransition), synchronous: options.contains(.Synchronous), synchronousLoads: options.contains(.PreferSynchronousResourceLoading), animateTopItemVerticalOrigin: options.contains(.AnimateTopItemPosition), operations: updatedOperations, requestItemInsertionAnimationsIndices: options.contains(.RequestItemInsertionAnimations) ? insertedIndexSet : Set(), scrollToItem: scrollToItem, additionalScrollDistance: additionalScrollDistance, updateSizeAndInsets: updateSizeAndInsets, stationaryItemIndex: stationaryItemIndex, updateOpaqueState: updateOpaqueState, forceInvertOffsetDirection: options.contains(.InvertOffsetDirection), completion: { if options.contains(.PreferSynchronousDrawing) { self?.recursivelyEnsureDisplaySynchronously(true) } @@ -2377,7 +2377,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel } } - private func insertNodeAtIndex(animated: Bool, animateAlpha: Bool, animateFullTransition: Bool, forceAnimateInsertion: Bool, previousFrame: CGRect?, nodeIndex: Int, offsetDirection: ListViewInsertionOffsetDirection, node: ListViewItemNode, layout: ListViewItemNodeLayout, apply: () -> (Signal?, (ListViewItemApply) -> Void), timestamp: Double, listInsets: UIEdgeInsets, visibleBounds: CGRect) { + private func insertNodeAtIndex(animated: Bool, animateAlpha: Bool, animateFullTransition: Bool, forceAnimateInsertion: Bool, previousFrame: CGRect?, nodeIndex: Int, offsetDirection: ListViewInsertionOffsetDirection, node: ListViewItemNode, layout: ListViewItemNodeLayout, apply: () -> (Signal?, (ListViewItemApply) -> Void), timestamp: Double, listInsets: UIEdgeInsets, visibleBounds: CGRect, forceInvertOffsetDirection: Bool) { let insertionOrigin = self.referencePointForInsertionAtIndex(nodeIndex) let nodeOrigin: CGPoint @@ -2400,7 +2400,11 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel if let accessoryItemNode = node.accessoryItemNode { node.layoutAccessoryItemNode(accessoryItemNode, leftInset: listInsets.left, rightInset: listInsets.right) } - apply().1(ListViewItemApply(isOnScreen: visibleBounds.intersects(nodeFrame), timestamp: timestamp)) + + let applyContext = ListViewItemApply(isOnScreen: visibleBounds.intersects(nodeFrame), timestamp: timestamp) + apply().1(applyContext) + let invertOffsetDirection = forceInvertOffsetDirection + self.itemNodes.insert(node, at: nodeIndex) var offsetHeight = node.apparentHeight @@ -2426,7 +2430,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel takenAnimation = true if abs(layout.size.height - previousApparentHeight) > CGFloat.ulpOfOne { - node.addApparentHeightAnimation(layout.size.height, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp, update: { [weak node] progress, currentValue in + node.addApparentHeightAnimation(layout.size.height, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp, invertOffsetDirection: invertOffsetDirection, update: { [weak node] progress, currentValue in if let node = node { node.animateFrameTransition(progress, currentValue) } @@ -2453,7 +2457,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel node.addHeightAnimation(0.0, duration: duration * UIView.animationDurationFactor(), beginAt: timestamp) } if node.animationForKey("apparentHeight") == nil || !(node is ListViewTempItemNode) { - node.addApparentHeightAnimation(0.0, duration: duration * UIView.animationDurationFactor(), beginAt: timestamp, update: { [weak node] progress, currentValue in + node.addApparentHeightAnimation(0.0, duration: duration * UIView.animationDurationFactor(), beginAt: timestamp, invertOffsetDirection: invertOffsetDirection, update: { [weak node] progress, currentValue in if let node = node { node.animateFrameTransition(progress, currentValue) } @@ -2484,7 +2488,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel } else { if !nodeFrame.size.height.isEqual(to: node.apparentHeight) { let addAnimation = previousFrame?.height != nodeFrame.size.height - node.addApparentHeightAnimation(nodeFrame.size.height, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp, update: { [weak node] progress, currentValue in + node.addApparentHeightAnimation(nodeFrame.size.height, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp, invertOffsetDirection: invertOffsetDirection, update: { [weak node] progress, currentValue in if let node = node, addAnimation { node.animateFrameTransition(progress, currentValue) } @@ -2517,13 +2521,13 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel node.addTransitionOffsetAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) } } - node.animateInsertion(timestamp, duration: insertionAnimationDuration * UIView.animationDurationFactor(), short: false) + node.animateInsertion(timestamp, duration: insertionAnimationDuration * UIView.animationDurationFactor(), options: ListViewItemAnimationOptions(short: invertOffsetDirection)) } } } else if animateAlpha { if previousFrame == nil { if forceAnimateInsertion { - node.animateInsertion(timestamp, duration: insertionAnimationDuration * UIView.animationDurationFactor(), short: true) + node.animateInsertion(timestamp, duration: insertionAnimationDuration * UIView.animationDurationFactor(), options: ListViewItemAnimationOptions(short: true)) } else if animateFullTransition { node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) node.layer.animateScale(from: 0.7, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) @@ -2617,7 +2621,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel } } - private func replayOperations(animated: Bool, animateAlpha: Bool, animateCrossfade: Bool, animateFullTransition: Bool, synchronous: Bool, synchronousLoads: Bool, animateTopItemVerticalOrigin: Bool, operations: [ListViewStateOperation], requestItemInsertionAnimationsIndices: Set, scrollToItem originalScrollToItem: ListViewScrollToItem?, additionalScrollDistance: CGFloat, updateSizeAndInsets: ListViewUpdateSizeAndInsets?, stationaryItemIndex: Int?, updateOpaqueState: Any?, completion: () -> Void) { + private func replayOperations(animated: Bool, animateAlpha: Bool, animateCrossfade: Bool, animateFullTransition: Bool, synchronous: Bool, synchronousLoads: Bool, animateTopItemVerticalOrigin: Bool, operations: [ListViewStateOperation], requestItemInsertionAnimationsIndices: Set, scrollToItem originalScrollToItem: ListViewScrollToItem?, additionalScrollDistance: CGFloat, updateSizeAndInsets: ListViewUpdateSizeAndInsets?, stationaryItemIndex: Int?, updateOpaqueState: Any?, forceInvertOffsetDirection: Bool = false, completion: () -> Void) { var scrollToItem: ListViewScrollToItem? var isExperimentalSnapToScrollToItem = false if let originalScrollToItem = originalScrollToItem { @@ -2745,7 +2749,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel updatedPreviousFrame = nil } - self.insertNodeAtIndex(animated: nodeAnimated, animateAlpha: animateAlpha, animateFullTransition: animateFullTransition, forceAnimateInsertion: forceAnimateInsertion, previousFrame: updatedPreviousFrame, nodeIndex: index, offsetDirection: offsetDirection, node: node, layout: layout, apply: apply, timestamp: timestamp, listInsets: listInsets, visibleBounds: visibleBounds) + self.insertNodeAtIndex(animated: nodeAnimated, animateAlpha: animateAlpha, animateFullTransition: animateFullTransition, forceAnimateInsertion: forceAnimateInsertion, previousFrame: updatedPreviousFrame, nodeIndex: index, offsetDirection: offsetDirection, node: node, layout: layout, apply: apply, timestamp: timestamp, listInsets: listInsets, visibleBounds: visibleBounds, forceInvertOffsetDirection: forceInvertOffsetDirection) hadInserts = true hadChangesToItemNodes = true if let _ = updatedPreviousFrame { @@ -2806,10 +2810,10 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel if let height = height, let previousLayout = previousLayout { if takenPreviousNodes.contains(referenceNode) { let tempNode = ListViewTempItemNode(layerBacked: true) - self.insertNodeAtIndex(animated: false, animateAlpha: false, animateFullTransition: false, forceAnimateInsertion: false, previousFrame: nil, nodeIndex: index, offsetDirection: offsetDirection, node: tempNode, layout: ListViewItemNodeLayout(contentSize: CGSize(width: self.visibleSize.width, height: height), insets: UIEdgeInsets()), apply: { return (nil, { _ in }) }, timestamp: timestamp, listInsets: listInsets, visibleBounds: visibleBounds) + self.insertNodeAtIndex(animated: false, animateAlpha: false, animateFullTransition: false, forceAnimateInsertion: false, previousFrame: nil, nodeIndex: index, offsetDirection: offsetDirection, node: tempNode, layout: ListViewItemNodeLayout(contentSize: CGSize(width: self.visibleSize.width, height: height), insets: UIEdgeInsets()), apply: { return (nil, { _ in }) }, timestamp: timestamp, listInsets: listInsets, visibleBounds: visibleBounds, forceInvertOffsetDirection: forceInvertOffsetDirection) } else { referenceNode.index = nil - self.insertNodeAtIndex(animated: false, animateAlpha: false, animateFullTransition: false, forceAnimateInsertion: false, previousFrame: nil, nodeIndex: index, offsetDirection: offsetDirection, node: referenceNode, layout: previousLayout, apply: { return (nil, { _ in }) }, timestamp: timestamp, listInsets: listInsets, visibleBounds: visibleBounds) + self.insertNodeAtIndex(animated: false, animateAlpha: false, animateFullTransition: false, forceAnimateInsertion: false, previousFrame: nil, nodeIndex: index, offsetDirection: offsetDirection, node: referenceNode, layout: previousLayout, apply: { return (nil, { _ in }) }, timestamp: timestamp, listInsets: listInsets, visibleBounds: visibleBounds, forceInvertOffsetDirection: forceInvertOffsetDirection) if let verticalScrollIndicator = self.verticalScrollIndicator { self.insertSubnode(referenceNode, belowSubnode: verticalScrollIndicator) } else { @@ -2888,7 +2892,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel let applyContext = ListViewItemApply(isOnScreen: visibleBounds.intersects(apparentFrame), timestamp: timestamp) apply().1(applyContext) - let invertOffsetDirection = applyContext.invertOffsetDirection + let invertOffsetDirection = applyContext.invertOffsetDirection || forceInvertOffsetDirection var offsetRanges = OffsetRanges() @@ -3056,7 +3060,7 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel } } else if let stationaryItemIndex = stationaryItemIndex { for itemNode in self.itemNodes { - if let index = itemNode.index , index == stationaryItemIndex { + if let index = itemNode.index, index == stationaryItemIndex { for (previousNode, previousFrame) in previousApparentFrames { if previousNode === itemNode { let offset = previousFrame.frame.minY - itemNode.frame.minY diff --git a/submodules/Display/Source/ListViewIntermediateState.swift b/submodules/Display/Source/ListViewIntermediateState.swift index 89da117cd23..0d5c6d9ba04 100644 --- a/submodules/Display/Source/ListViewIntermediateState.swift +++ b/submodules/Display/Source/ListViewIntermediateState.swift @@ -130,6 +130,7 @@ public struct ListViewDeleteAndInsertOptions: OptionSet { public static let AnimateCrossfade = ListViewDeleteAndInsertOptions(rawValue: 256) public static let ForceUpdate = ListViewDeleteAndInsertOptions(rawValue: 512) public static let AnimateFullTransition = ListViewDeleteAndInsertOptions(rawValue: 1024) + public static let InvertOffsetDirection = ListViewDeleteAndInsertOptions(rawValue: 2048) } public struct ListViewUpdateSizeAndInsets { diff --git a/submodules/Display/Source/ListViewItem.swift b/submodules/Display/Source/ListViewItem.swift index 4a20af16203..1228c055ac3 100644 --- a/submodules/Display/Source/ListViewItem.swift +++ b/submodules/Display/Source/ListViewItem.swift @@ -51,7 +51,7 @@ public struct ListViewItemConfigureNodeFlags: OptionSet { } public final class ListViewItemApply { - public let isOnScreen: Bool + public private(set) var isOnScreen: Bool public let timestamp: Double? public private(set) var invertOffsetDirection: Bool = false @@ -63,6 +63,10 @@ public final class ListViewItemApply { public func setInvertOffsetDirection() { self.invertOffsetDirection = true } + + public func setIsOffscreen() { + self.isOnScreen = false + } } public protocol ListViewItem { diff --git a/submodules/Display/Source/ListViewItemNode.swift b/submodules/Display/Source/ListViewItemNode.swift index bec5fbf86ca..2d44033cb25 100644 --- a/submodules/Display/Source/ListViewItemNode.swift +++ b/submodules/Display/Source/ListViewItemNode.swift @@ -15,6 +15,16 @@ var testSpringFreeResistance: CGFloat = 0.676197171211243 var testSpringResistanceScrollingLimits: (CGFloat, CGFloat) = (0.1, 1.0) var testSpringScrollingResistance: CGFloat = 0.6721 +public struct ListViewItemAnimationOptions { + public let short: Bool + public let invertOffsetDirection: Bool + + public init(short: Bool = false, invertOffsetDirection: Bool = false) { + self.short = short + self.invertOffsetDirection = invertOffsetDirection + } +} + struct ListViewItemSpring { let stiffness: CGFloat let damping: CGFloat @@ -577,7 +587,7 @@ open class ListViewItemNode: ASDisplayNode, AccessibilityFocusableNode { self.setAnimationForKey("transitionOffset", animation: animation) } - open func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + open func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { } open func animateAdded(_ currentTimestamp: Double, duration: Double) { diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 11d90442d43..03b47bfce27 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -1599,7 +1599,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { videoNode.setBaseRate(self.playbackRate ?? 1.0) } } else { - if self.shouldAutoplayOnCentrality() { + if isAnimated { + self.playOnContentOwnership = true + } else if self.shouldAutoplayOnCentrality() { self.playOnContentOwnership = true } } diff --git a/submodules/HorizontalPeerItem/Sources/HorizontalPeerItem.swift b/submodules/HorizontalPeerItem/Sources/HorizontalPeerItem.swift index cd58a63fe8d..0fb1d85b1d4 100644 --- a/submodules/HorizontalPeerItem/Sources/HorizontalPeerItem.swift +++ b/submodules/HorizontalPeerItem/Sources/HorizontalPeerItem.swift @@ -282,8 +282,8 @@ public final class HorizontalPeerItemNode: ListViewItemNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - super.animateInsertion(currentTimestamp, duration: duration, short: short) + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + super.animateInsertion(currentTimestamp, duration: duration, options: options) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } diff --git a/submodules/ImageTransparency/Sources/ImageTransparency.swift b/submodules/ImageTransparency/Sources/ImageTransparency.swift index 39d6b32450f..e7b2804c03b 100644 --- a/submodules/ImageTransparency/Sources/ImageTransparency.swift +++ b/submodules/ImageTransparency/Sources/ImageTransparency.swift @@ -5,7 +5,7 @@ import Display private func generateHistogram(cgImage: CGImage) -> ([[vImagePixelCount]], Int)? { var sourceBuffer = vImage_Buffer() defer { - free(sourceBuffer.data) + sourceBuffer.data?.deallocate() } var cgImageFormat = vImage_CGImageFormat( diff --git a/submodules/InviteLinksUI/Sources/AdditionalLinkItem.swift b/submodules/InviteLinksUI/Sources/AdditionalLinkItem.swift index 17f0989bf0b..f4f4ea73f77 100644 --- a/submodules/InviteLinksUI/Sources/AdditionalLinkItem.swift +++ b/submodules/InviteLinksUI/Sources/AdditionalLinkItem.swift @@ -490,7 +490,7 @@ public class AdditionalLinkItemNode: ListViewItemNode, ItemListItemNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift b/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift index 5ad2da56541..60730c0c5c1 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkHeaderItem.swift @@ -175,7 +175,7 @@ class InviteLinkHeaderItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/InviteLinksUI/Sources/InviteLinkInviteHeaderItem.swift b/submodules/InviteLinksUI/Sources/InviteLinkInviteHeaderItem.swift index 406b9e456f7..052de0d4a3c 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkInviteHeaderItem.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkInviteHeaderItem.swift @@ -144,7 +144,7 @@ class InviteLinkInviteHeaderItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/InviteLinksUI/Sources/InviteLinkInviteManageItem.swift b/submodules/InviteLinksUI/Sources/InviteLinkInviteManageItem.swift index 1458484564a..64b7f3b3037 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkInviteManageItem.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkInviteManageItem.swift @@ -108,7 +108,7 @@ class InviteLinkInviteManageItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkItem.swift b/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkItem.swift index d3c0bbd7d38..9691ecd2d67 100644 --- a/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkItem.swift @@ -610,7 +610,7 @@ public class ItemListFolderInviteLinkItemNode: ListViewItemNode, ItemListItemNod } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkListItem.swift b/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkListItem.swift index 7e75b08bbe9..ae9b294313c 100644 --- a/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkListItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkListItem.swift @@ -561,7 +561,7 @@ public class ItemListFolderInviteLinkListItemNode: ItemListRevealOptionsItemNode } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/InviteLinksUI/Sources/ItemListInviteLinkDateLimitItem.swift b/submodules/InviteLinksUI/Sources/ItemListInviteLinkDateLimitItem.swift index b07cadd866c..f329c40be70 100644 --- a/submodules/InviteLinksUI/Sources/ItemListInviteLinkDateLimitItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListInviteLinkDateLimitItem.swift @@ -380,7 +380,7 @@ private final class ItemListInviteLinkTimeLimitItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift b/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift index 653fdb75bc7..1abb2b9da89 100644 --- a/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift @@ -687,7 +687,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/InviteLinksUI/Sources/ItemListInviteLinkUsageLimitItem.swift b/submodules/InviteLinksUI/Sources/ItemListInviteLinkUsageLimitItem.swift index b70a4a4acaf..db379ed7ed6 100644 --- a/submodules/InviteLinksUI/Sources/ItemListInviteLinkUsageLimitItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListInviteLinkUsageLimitItem.swift @@ -390,7 +390,7 @@ private final class ItemListInviteLinkUsageLimitItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/InviteLinksUI/Sources/ItemListInviteRequestItem.swift b/submodules/InviteLinksUI/Sources/ItemListInviteRequestItem.swift index 658898edaee..5a66bf6edf2 100644 --- a/submodules/InviteLinksUI/Sources/ItemListInviteRequestItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListInviteRequestItem.swift @@ -858,7 +858,7 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift b/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift index 077cba79247..4c0dee3641f 100644 --- a/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift @@ -560,7 +560,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/ItemListAddressItem/Sources/ItemListAddressItem.swift b/submodules/ItemListAddressItem/Sources/ItemListAddressItem.swift index ee4334e59b1..bf8300a2148 100644 --- a/submodules/ItemListAddressItem/Sources/ItemListAddressItem.swift +++ b/submodules/ItemListAddressItem/Sources/ItemListAddressItem.swift @@ -387,7 +387,7 @@ public class ItemListAddressItemNode: ListViewItemNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift b/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift index d51d900bf3a..c6bce9e12a5 100644 --- a/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift +++ b/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift @@ -373,7 +373,7 @@ public final class ItemListPeerActionItemNode: ListViewItemNode { } } - public override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + public override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/ItemListPeerItem/BUILD b/submodules/ItemListPeerItem/BUILD index a4a85f437a3..b5fb4ad41cd 100644 --- a/submodules/ItemListPeerItem/BUILD +++ b/submodules/ItemListPeerItem/BUILD @@ -29,6 +29,7 @@ swift_library( "//submodules/CheckNode", "//submodules/TelegramUI/Components/AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer", + "//submodules/TelegramUI/Components/TextNodeWithEntities", ], visibility = [ "//visibility:public", diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index c9eadd682a1..f3e178a2feb 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -19,6 +19,7 @@ import EmojiStatusComponent import CheckNode import AnimationCache import MultiAnimationRenderer +import TextNodeWithEntities private final class ShimmerEffectNode: ASDisplayNode { private var currentBackgroundColor: UIColor? @@ -1801,7 +1802,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } @@ -1951,7 +1952,8 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo public final class ItemListPeerItemHeader: ListViewItemHeader { public let id: ListViewItemNode.HeaderId - public let text: String + public let context: AccountContext + public let text: NSAttributedString public let additionalText: String public let stickDirection: ListViewItemHeaderStickDirection = .topEdge public let stickOverInsets: Bool = true @@ -1962,7 +1964,8 @@ public final class ItemListPeerItemHeader: ListViewItemHeader { public let height: CGFloat = 28.0 - public init(theme: PresentationTheme, strings: PresentationStrings, text: String, additionalText: String, actionTitle: String? = nil, id: Int64, action: (() -> Void)? = nil) { + public init(theme: PresentationTheme, strings: PresentationStrings, context: AccountContext, text: NSAttributedString, additionalText: String, actionTitle: String? = nil, id: Int64, action: (() -> Void)? = nil) { + self.context = context self.text = text self.additionalText = additionalText self.id = ListViewItemNode.HeaderId(space: 0, id: id) @@ -1981,7 +1984,7 @@ public final class ItemListPeerItemHeader: ListViewItemHeader { } public func node(synchronousLoad: Bool) -> ListViewItemHeaderNode { - return ItemListPeerItemHeaderNode(theme: self.theme, strings: self.strings, text: self.text, additionalText: self.additionalText, actionTitle: self.actionTitle, action: self.action) + return ItemListPeerItemHeaderNode(theme: self.theme, strings: self.strings, context: self.context, text: self.text, additionalText: self.additionalText, actionTitle: self.actionTitle, action: self.action) } public func updateNode(_ node: ListViewItemHeaderNode, previous: ListViewItemHeader?, next: ListViewItemHeader?) { @@ -1992,6 +1995,7 @@ public final class ItemListPeerItemHeader: ListViewItemHeader { public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode, ItemListHeaderItemNode { private var theme: PresentationTheme private var strings: PresentationStrings + private let context: AccountContext private var actionTitle: String? private var action: (() -> Void)? @@ -2000,14 +2004,15 @@ public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode, ItemListH private let backgroundNode: ASDisplayNode private let snappedBackgroundNode: ASDisplayNode private let separatorNode: ASDisplayNode - private let textNode: ImmediateTextNode + private let textNode: ImmediateTextNodeWithEntities private let additionalTextNode: ImmediateTextNode private let actionTextNode: ImmediateTextNode private let actionButton: HighlightableButtonNode private var stickDistanceFactor: CGFloat? - public init(theme: PresentationTheme, strings: PresentationStrings, text: String, additionalText: String, actionTitle: String?, action: (() -> Void)?) { + public init(theme: PresentationTheme, strings: PresentationStrings, context: AccountContext, text: NSAttributedString, additionalText: String, actionTitle: String?, action: (() -> Void)?) { + self.context = context self.theme = theme self.strings = strings self.actionTitle = actionTitle @@ -2026,10 +2031,18 @@ public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode, ItemListH let titleFont = Font.regular(13.0) - self.textNode = ImmediateTextNode() + self.textNode = ImmediateTextNodeWithEntities() self.textNode.displaysAsynchronously = false self.textNode.maximumNumberOfLines = 1 - self.textNode.attributedText = NSAttributedString(string: text, font: titleFont, textColor: theme.list.sectionHeaderTextColor) + self.textNode.attributedText = text + self.textNode.arguments = TextNodeWithEntities.Arguments( + context: self.context, + cache: self.context.animationCache, + renderer: self.context.animationRenderer, + placeholderColor: theme.list.mediaPlaceholderColor, + attemptSynchronous: true + ) + self.textNode.visibility = true self.additionalTextNode = ImmediateTextNode() self.additionalTextNode.displaysAsynchronously = false @@ -2086,11 +2099,11 @@ public final class ItemListPeerItemHeaderNode: ListViewItemHeaderNode, ItemListH self.actionTextNode.attributedText = NSAttributedString(string: self.actionTextNode.attributedText?.string ?? "", font: titleFont, textColor: theme.list.sectionHeaderTextColor) } - public func update(text: String, additionalText: String, actionTitle: String?, action: (() -> Void)?) { + public func update(text: NSAttributedString, additionalText: String, actionTitle: String?, action: (() -> Void)?) { self.actionTitle = actionTitle self.action = action let titleFont = Font.regular(13.0) - self.textNode.attributedText = NSAttributedString(string: text, font: titleFont, textColor: theme.list.sectionHeaderTextColor) + self.textNode.attributedText = text self.additionalTextNode.attributedText = NSAttributedString(string: additionalText, font: titleFont, textColor: theme.list.sectionHeaderTextColor) self.actionTextNode.attributedText = NSAttributedString(string: actionTitle ?? "", font: titleFont, textColor: action == nil ? theme.list.sectionHeaderTextColor : theme.list.itemAccentColor) self.actionButton.isUserInteractionEnabled = self.action != nil diff --git a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift index c86a2fa0291..8c7b1a1b839 100644 --- a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift +++ b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift @@ -914,7 +914,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/ItemListUI/BUILD b/submodules/ItemListUI/BUILD index 883dd0cbed0..ab4bb1f6cf2 100644 --- a/submodules/ItemListUI/BUILD +++ b/submodules/ItemListUI/BUILD @@ -32,6 +32,7 @@ swift_library( "//submodules/ComponentFlow", "//submodules/TelegramUI/Components/TabSelectorComponent", "//submodules/Components/ComponentDisplayAdapters", + "//submodules/TelegramUI/Components/TextNodeWithEntities", ], visibility = [ "//visibility:public", diff --git a/submodules/ItemListUI/Sources/Items/ItemListActionItem.swift b/submodules/ItemListUI/Sources/Items/ItemListActionItem.swift index 518e61d1082..ad4bb4681f0 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListActionItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListActionItem.swift @@ -318,7 +318,7 @@ public class ItemListActionItemNode: ListViewItemNode, ItemListItemNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/ItemListUI/Sources/Items/ItemListActivityTextItem.swift b/submodules/ItemListUI/Sources/Items/ItemListActivityTextItem.swift index 04bec4fa287..434b524cf09 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListActivityTextItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListActivityTextItem.swift @@ -162,7 +162,7 @@ public class ItemListActivityTextItemNode: ListViewItemNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift b/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift index 6c3d94b45df..bae1cc72f98 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift @@ -401,7 +401,7 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift index 56bed4446f3..d830858c1f2 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift @@ -797,7 +797,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/ItemListUI/Sources/Items/ItemListExpandableSwitchItem.swift b/submodules/ItemListUI/Sources/Items/ItemListExpandableSwitchItem.swift index bdf9190160c..3cf649e9b23 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListExpandableSwitchItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListExpandableSwitchItem.swift @@ -676,7 +676,7 @@ public class ItemListExpandableSwitchItemNode: ListViewItemNode, ItemListItemNod } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.allowsGroupOpacity = true self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4, completion: { [weak self] _ in self?.layer.allowsGroupOpacity = false diff --git a/submodules/ItemListUI/Sources/Items/ItemListInfoItem.swift b/submodules/ItemListUI/Sources/Items/ItemListInfoItem.swift index dcbe736b2b9..10806d9bba5 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListInfoItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListInfoItem.swift @@ -363,7 +363,7 @@ public class InfoItemNode: ListViewItemNode { } } - public override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + public override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/ItemListUI/Sources/Items/ItemListMultilineInputItem.swift b/submodules/ItemListUI/Sources/Items/ItemListMultilineInputItem.swift index c776212adad..5d8a2ddf5f8 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListMultilineInputItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListMultilineInputItem.swift @@ -383,7 +383,7 @@ public class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNod } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/ItemListUI/Sources/Items/ItemListMultilineTextItem.swift b/submodules/ItemListUI/Sources/Items/ItemListMultilineTextItem.swift index 53e377b19de..fc92b444f0b 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListMultilineTextItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListMultilineTextItem.swift @@ -341,7 +341,7 @@ public class ItemListMultilineTextItemNode: ListViewItemNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/ItemListUI/Sources/Items/ItemListPlaceholderItem.swift b/submodules/ItemListUI/Sources/Items/ItemListPlaceholderItem.swift index e6e572d76a2..4a87d0113e9 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListPlaceholderItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListPlaceholderItem.swift @@ -212,7 +212,7 @@ public class ItemListPlaceholderItemNode: ListViewItemNode, ItemListItemNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/ItemListUI/Sources/Items/ItemListSectionHeaderItem.swift b/submodules/ItemListUI/Sources/Items/ItemListSectionHeaderItem.swift index 71c604aadd0..86ad1635c56 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListSectionHeaderItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListSectionHeaderItem.swift @@ -369,7 +369,7 @@ public class ItemListSectionHeaderItemNode: ListViewItemNode { self.item?.action?() } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift b/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift index 3a6b41e5ec4..5e5fcb35f68 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift @@ -445,7 +445,7 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg self.clearButtonNode.isAccessibilityElement = isHidden } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift b/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift index a8a10279f63..ec6688e3a85 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift @@ -619,7 +619,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.allowsGroupOpacity = true self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4, completion: { [weak self] _ in self?.layer.allowsGroupOpacity = false diff --git a/submodules/ItemListUI/Sources/Items/ItemListTextItem.swift b/submodules/ItemListUI/Sources/Items/ItemListTextItem.swift index 2c30236c89e..7eae8757e60 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListTextItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListTextItem.swift @@ -6,11 +6,14 @@ import SwiftSignalKit import TelegramPresentationData import TextFormat import Markdown +import TextNodeWithEntities +import AccountContext public enum ItemListTextItemText { case plain(String) case large(String) case markdown(String) + case custom(context: AccountContext, string: NSAttributedString) } public enum ItemListTextItemLinkAction { @@ -75,7 +78,7 @@ public class ItemListTextItem: ListViewItem, ItemListItem { } public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { - private let textNode: TextNode + private let textNode: TextNodeWithEntities private var linkHighlightingNode: LinkHighlightingNode? private let activateArea: AccessibilityAreaNode @@ -88,17 +91,17 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { } public init() { - self.textNode = TextNode() - self.textNode.isUserInteractionEnabled = false - self.textNode.contentMode = .left - self.textNode.contentsScale = UIScreen.main.scale + self.textNode = TextNodeWithEntities() + self.textNode.textNode.isUserInteractionEnabled = false + self.textNode.textNode.contentMode = .left + self.textNode.textNode.contentsScale = UIScreen.main.scale self.activateArea = AccessibilityAreaNode() self.activateArea.accessibilityTraits = .staticText super.init(layerBacked: false, dynamicBounce: false) - self.addSubnode(self.textNode) + self.addSubnode(self.textNode.textNode) self.addSubnode(self.activateArea) } @@ -118,11 +121,11 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { } public func asyncLayout() -> (_ item: ItemListTextItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { - let makeTitleLayout = TextNode.asyncLayout(self.textNode) + let makeTitleLayout = TextNodeWithEntities.asyncLayout(self.textNode) let currentChevronImage = self.chevronImage let currentItem = self.item - return { item, params, neighbors in + return { [weak self] item, params, neighbors in let leftInset: CGFloat = 15.0 let topInset: CGFloat = 7.0 var bottomInset: CGFloat = 7.0 @@ -156,15 +159,20 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { } } attributedText = mutableAttributedText + case let .custom(_, string): + attributedText = string } let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset * 2.0 - params.leftInset - params.rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let contentSize: CGSize var insets = itemListNeighborsGroupedInsets(neighbors, params) - if case .large = item.text { + switch item.text { + case .large, .custom: insets.top = 14.0 bottomInset = -6.0 + default: + break } contentSize = CGSize(width: params.width, height: titleLayout.size.height + topInset + bottomInset) @@ -174,7 +182,7 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) - return (layout, { [weak self] in + return (layout, { if let strongSelf = self { strongSelf.item = item strongSelf.chevronImage = chevronImage @@ -182,15 +190,25 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height)) strongSelf.activateArea.accessibilityLabel = attributedText.string - let _ = titleApply() + var textArguments: TextNodeWithEntities.Arguments? + if case let .custom(context, _) = item.text { + textArguments = TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, + attemptSynchronous: true + ) + } + let _ = titleApply(textArguments) - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset + params.leftInset, y: topInset), size: titleLayout.size) + strongSelf.textNode.textNode.frame = CGRect(origin: CGPoint(x: leftInset + params.leftInset, y: topInset), size: titleLayout.size) } }) } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } @@ -204,9 +222,9 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { switch gesture { case .tap: - let titleFrame = self.textNode.frame + let titleFrame = self.textNode.textNode.frame if let item = self.item, titleFrame.contains(location) { - if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) { + if let (_, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) { if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { item.linkAction?(.tap(url)) } @@ -225,8 +243,8 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { if let item = self.item { var rects: [CGRect]? if let point = point { - let textNodeFrame = self.textNode.frame - if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + let textNodeFrame = self.textNode.textNode.frame + if let (index, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { let possibleNames: [String] = [ TelegramTextAttributes.URL, TelegramTextAttributes.PeerMention, @@ -236,7 +254,7 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { ] for name in possibleNames { if let _ = attributes[NSAttributedString.Key(rawValue: name)] { - rects = self.textNode.attributeRects(name: name, at: index) + rects = self.textNode.textNode.attributeRects(name: name, at: index) break } } @@ -250,9 +268,9 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { } else { linkHighlightingNode = LinkHighlightingNode(color: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.2)) self.linkHighlightingNode = linkHighlightingNode - self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode) + self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode.textNode) } - linkHighlightingNode.frame = self.textNode.frame + linkHighlightingNode.frame = self.textNode.textNode.frame linkHighlightingNode.updateRects(rects) } else if let linkHighlightingNode = self.linkHighlightingNode { self.linkHighlightingNode = nil diff --git a/submodules/ItemListUI/Sources/Items/ItemListTextWithLabelItem.swift b/submodules/ItemListUI/Sources/Items/ItemListTextWithLabelItem.swift index 1f4a6c2f4a5..2b27d4078fc 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListTextWithLabelItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListTextWithLabelItem.swift @@ -419,7 +419,7 @@ public class ItemListTextWithLabelItemNode: ListViewItemNode { return nil } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/ItemListVenueItem/Sources/ItemListVenueItem.swift b/submodules/ItemListVenueItem/Sources/ItemListVenueItem.swift index 05644288352..7382d2117b4 100644 --- a/submodules/ItemListVenueItem/Sources/ItemListVenueItem.swift +++ b/submodules/ItemListVenueItem/Sources/ItemListVenueItem.swift @@ -447,7 +447,7 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGIconSwitchView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGIconSwitchView.h index a5eff4d4f46..469cc068ffd 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGIconSwitchView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGIconSwitchView.h @@ -5,5 +5,6 @@ - (void)setPositiveContentColor:(UIColor *)color; - (void)setNegativeContentColor:(UIColor *)color; +- (void)updateIsLocked:(bool)isLocked; @end diff --git a/submodules/LegacyComponents/Sources/TGIconSwitchView.m b/submodules/LegacyComponents/Sources/TGIconSwitchView.m index 677863d286d..73a23c727e6 100644 --- a/submodules/LegacyComponents/Sources/TGIconSwitchView.m +++ b/submodules/LegacyComponents/Sources/TGIconSwitchView.m @@ -30,8 +30,10 @@ - (void)setPosition:(CGPoint)center { @interface TGIconSwitchView () { UIImageView *_offIconView; UIImageView *_onIconView; + UIColor *_negativeContentColor; bool _stateIsOn; + bool _isLocked; } @end @@ -41,49 +43,42 @@ @implementation TGIconSwitchView - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self != nil) { - if (iosMajorVersion() >= 8) { - _offIconView = [[UIImageView alloc] initWithImage:TGComponentsImageNamed(@"PermissionSwitchOff.png")]; - _onIconView = [[UIImageView alloc] initWithImage:TGComponentsImageNamed(@"PermissionSwitchOn.png")]; - self.layer.cornerRadius = 17.0f; - self.backgroundColor = [UIColor redColor]; - self.tintColor = [UIColor redColor]; - UIView *handleView = self.subviews[0].subviews.lastObject; - if (iosMajorVersion() >= 13) { - handleView = self.subviews[0].subviews[1].subviews.lastObject; - } else { - handleView = self.subviews[0].subviews.lastObject; - } - - static Class subclass; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - subclass = freedomMakeClass([handleView.layer class], [TGBaseIconSwitch class]); - object_setClass(handleView.layer, subclass); - }); - - CGPoint offset = CGPointZero; - if (iosMajorVersion() >= 12) { - offset = CGPointMake(-7.0, -3.0); - } - - _offIconView.frame = CGRectOffset(_offIconView.bounds, TGScreenPixelFloor(21.5f) + offset.x, TGScreenPixelFloor(14.5f) + offset.y); - _onIconView.frame = CGRectOffset(_onIconView.bounds, 20.0f + offset.x, 15.0f + offset.y); - [handleView addSubview:_onIconView]; - [handleView addSubview:_offIconView]; - - _onIconView.alpha = 0.0f; - - [self addTarget:self action:@selector(currentValueChanged) forControlEvents:UIControlEventValueChanged]; - - __weak TGIconSwitchView *weakSelf = self; - void (^block)(CGPoint) = ^(CGPoint point) { - __strong TGIconSwitchView *strongSelf = weakSelf; - if (strongSelf != nil) { - [strongSelf updateState:point.x > 30.0 animated:true force:false]; - } - }; - objc_setAssociatedObject(handleView.layer, positionChangedKey, [block copy], OBJC_ASSOCIATION_RETAIN); + _offIconView = [[UIImageView alloc] initWithImage:TGComponentsImageNamed(@"PermissionSwitchOff.png")]; + _onIconView = [[UIImageView alloc] initWithImage:TGComponentsImageNamed(@"PermissionSwitchOn.png")]; + self.layer.cornerRadius = 17.0f; + self.backgroundColor = [UIColor redColor]; + self.tintColor = [UIColor redColor]; + UIView *handleView = self.subviews[0].subviews.lastObject; + if (iosMajorVersion() >= 13) { + handleView = self.subviews[0].subviews[1].subviews.lastObject; + } else { + handleView = self.subviews[0].subviews.lastObject; } + + static Class subclass; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + subclass = freedomMakeClass([handleView.layer class], [TGBaseIconSwitch class]); + object_setClass(handleView.layer, subclass); + }); + + [self updateIconFrame]; + + [handleView addSubview:_onIconView]; + [handleView addSubview:_offIconView]; + + _onIconView.alpha = 0.0f; + + [self addTarget:self action:@selector(currentValueChanged) forControlEvents:UIControlEventValueChanged]; + + __weak TGIconSwitchView *weakSelf = self; + void (^block)(CGPoint) = ^(CGPoint point) { + __strong TGIconSwitchView *strongSelf = weakSelf; + if (strongSelf != nil) { + [strongSelf updateState:point.x > 30.0 animated:true force:false]; + } + }; + objc_setAssociatedObject(handleView.layer, positionChangedKey, [block copy], OBJC_ASSOCIATION_RETAIN); } return self; } @@ -94,6 +89,21 @@ - (void)setOn:(BOOL)on animated:(BOOL)animated { [self updateState:on animated:animated force:true]; } +- (void)updateIconFrame { + CGPoint offset = CGPointZero; + if (iosMajorVersion() >= 12) { + offset = CGPointMake(-7.0, -3.0); + } + + if (_isLocked) { + _offIconView.frame = CGRectOffset(_offIconView.bounds, TGScreenPixelFloor(21.5f) + offset.x, TGScreenPixelFloor(12.5f) + offset.y); + } else { + _offIconView.frame = CGRectOffset(_offIconView.bounds, TGScreenPixelFloor(21.5f) + offset.x, TGScreenPixelFloor(14.5f) + offset.y); + } + + _onIconView.frame = CGRectOffset(_onIconView.bounds, 20.0f + offset.x, 15.0f + offset.y); +} + - (void)updateState:(bool)on animated:(bool)animated force:(bool)force { if (_stateIsOn != on || force) { _stateIsOn = on; @@ -127,7 +137,24 @@ - (void)setPositiveContentColor:(UIColor *)color { } - (void)setNegativeContentColor:(UIColor *)color { - _offIconView.image = TGTintedImage(TGComponentsImageNamed(@"PermissionSwitchOff.png"), color); + _negativeContentColor = color; + if (_isLocked) { + _offIconView.image = TGTintedImage(TGComponentsImageNamed(@"Item List/SwitchLockIcon"), color); + _offIconView.frame = CGRectMake(_offIconView.frame.origin.x, _offIconView.frame.origin.y, _offIconView.image.size.width, _offIconView.image.size.height); + } else { + _offIconView.image = TGTintedImage(TGComponentsImageNamed(@"PermissionSwitchOff.png"), color); + } +} + +- (void)updateIsLocked:(bool)isLocked { + if (_isLocked != isLocked) { + _isLocked = isLocked; + + if (_negativeContentColor) { + [self setNegativeContentColor:_negativeContentColor]; + } + [self updateIconFrame]; + } } - (void)currentValueChanged { diff --git a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift index 723d732467f..06912f377d1 100644 --- a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift @@ -540,8 +540,8 @@ public final class ListMessageFileItemNode: ListMessageNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - super.animateInsertion(currentTimestamp, duration: duration, short: short) + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + super.animateInsertion(currentTimestamp, duration: duration, options: options) self.transitionOffset = self.bounds.size.height * 1.6 self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp) diff --git a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift index a585475f196..4bbb6d07fa3 100644 --- a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift @@ -215,8 +215,8 @@ public final class ListMessageSnippetItemNode: ListMessageNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - super.animateInsertion(currentTimestamp, duration: duration, short: short) + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + super.animateInsertion(currentTimestamp, duration: duration, options: options) self.transitionOffset = self.bounds.size.height * 1.6 self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp) @@ -313,7 +313,7 @@ public final class ListMessageSnippetItemNode: ListMessageNode { } } else if let file = content.file { if content.type == "telegram_background" { - if let wallpaper = parseWallpaperUrl(content.url) { + if let wallpaper = parseWallpaperUrl(sharedContext: item.context.sharedContext, url: content.url) { switch wallpaper { case let .slug(slug, _, colors, intensity, angle): previewWallpaperFileReference = .message(message: MessageReference(message), media: file) diff --git a/submodules/LiveLocationManager/Sources/LiveLocationManager.swift b/submodules/LiveLocationManager/Sources/LiveLocationManager.swift index 352e0c9815e..4187f3b4919 100644 --- a/submodules/LiveLocationManager/Sources/LiveLocationManager.swift +++ b/submodules/LiveLocationManager/Sources/LiveLocationManager.swift @@ -68,14 +68,18 @@ public final class LiveLocationManagerImpl: LiveLocationManager { for media in message.media { if let telegramMap = media as? TelegramMediaMap { if let liveBroadcastingTimeout = telegramMap.liveBroadcastingTimeout { - if message.timestamp + liveBroadcastingTimeout > timestamp { + if liveBroadcastingTimeout == liveLocationIndefinitePeriod || message.timestamp + liveBroadcastingTimeout > timestamp { activeLiveBroadcastingTimeout = liveBroadcastingTimeout } } } } if let activeLiveBroadcastingTimeout = activeLiveBroadcastingTimeout { - broadcastToMessageIds[message.id] = message.timestamp + activeLiveBroadcastingTimeout + if activeLiveBroadcastingTimeout == liveLocationIndefinitePeriod { + broadcastToMessageIds[message.id] = activeLiveBroadcastingTimeout + } else { + broadcastToMessageIds[message.id] = message.timestamp + activeLiveBroadcastingTimeout + } } else { stopMessageIds.insert(message.id) } @@ -173,7 +177,7 @@ public final class LiveLocationManagerImpl: LiveLocationManager { let addedStopped = stopMessageIds.subtracting(self.stopMessageIds) self.stopMessageIds = stopMessageIds for id in addedStopped { - self.editMessageDisposables.set((self.engine.messages.requestEditLiveLocation(messageId: id, stop: true, coordinate: nil, heading: nil, proximityNotificationRadius: nil) + self.editMessageDisposables.set((self.engine.messages.requestEditLiveLocation(messageId: id, stop: true, coordinate: nil, heading: nil, proximityNotificationRadius: nil, extendPeriod: nil) |> deliverOn(self.queue)).start(completed: { [weak self] in if let strongSelf = self { strongSelf.editMessageDisposables.set(nil, forKey: id) @@ -228,7 +232,7 @@ public final class LiveLocationManagerImpl: LiveLocationManager { let ids = self.broadcastToMessageIds let remainingIds = Atomic>(value: Set(ids.keys)) for id in ids.keys { - self.editMessageDisposables.set((self.engine.messages.requestEditLiveLocation(messageId: id, stop: false, coordinate: (latitude: coordinate.latitude, longitude: coordinate.longitude, accuracyRadius: Int32(accuracyRadius)), heading: heading.flatMap { Int32($0) }, proximityNotificationRadius: nil) + self.editMessageDisposables.set((self.engine.messages.requestEditLiveLocation(messageId: id, stop: false, coordinate: (latitude: coordinate.latitude, longitude: coordinate.longitude, accuracyRadius: Int32(accuracyRadius)), heading: heading.flatMap { Int32($0) }, proximityNotificationRadius: nil, extendPeriod: nil) |> deliverOn(self.queue)).start(completed: { [weak self] in if let strongSelf = self { strongSelf.editMessageDisposables.set(nil, forKey: id) diff --git a/submodules/LiveLocationTimerNode/Sources/ChatMessageLiveLocationTimerNode.swift b/submodules/LiveLocationTimerNode/Sources/ChatMessageLiveLocationTimerNode.swift index 0039970146f..c8d8e43912f 100644 --- a/submodules/LiveLocationTimerNode/Sources/ChatMessageLiveLocationTimerNode.swift +++ b/submodules/LiveLocationTimerNode/Sources/ChatMessageLiveLocationTimerNode.swift @@ -4,6 +4,7 @@ import AsyncDisplayKit import Display import TelegramPresentationData +private let infinityFont = Font.with(size: 15.0, design: .round, weight: .bold) private let textFont = Font.with(size: 13.0, design: .round, weight: .bold) private let smallTextFont = Font.with(size: 11.0, design: .round, weight: .bold) @@ -62,6 +63,8 @@ public final class ChatMessageLiveLocationTimerNode: ASDisplayNode { }), selector: #selector(RadialTimeoutNodeTimer.event), userInfo: nil, repeats: true) self.animationTimer = animationTimer RunLoop.main.add(animationTimer, forMode: .common) + + self.setNeedsDisplay() } } @@ -72,14 +75,23 @@ public final class ChatMessageLiveLocationTimerNode: ASDisplayNode { let remaining = beginTimestamp + timeout - timestamp value = CGFloat(max(0.0, 1.0 - min(1.0, remaining / timeout))) - let intRemaining = Int32(remaining) let string: String - if intRemaining > 60 * 60 { - let hours = Int32(round(remaining / (60.0 * 60.0))) - string = strings.Map_LiveLocationShortHour("\(hours)").string + if timeout < 0.0 { + value = 0.0 + string = "∞" } else { - let minutes = Int32(round(remaining / (60.0))) - string = "\(minutes)" + let intRemaining = Int32(remaining) + if intRemaining > 60 * 60 { + let hours = Int32(round(remaining / (60.0 * 60.0))) + if hours > 99 { + string = "99+" + } else { + string = strings.Map_LiveLocationShortHour("\(hours)").string + } + } else { + let minutes = Int32(round(remaining / (60.0))) + string = "\(minutes)" + } } return ChatMessageLiveLocationTimerNodeParams(backgroundColor: backgroundColor, foregroundColor: foregroundColor, textColor: textColor, value: value, string: string) @@ -120,16 +132,27 @@ public final class ChatMessageLiveLocationTimerNode: ASDisplayNode { path.lineCapStyle = .round path.stroke() - let attributes: [NSAttributedString.Key: Any] = [.font: parameters.string.count > 2 ? smallTextFont : textFont, .foregroundColor: parameters.foregroundColor] + let font: UIFont + if parameters.string == "∞" { + font = infinityFont + } else if parameters.string.count > 2 { + font = smallTextFont + } else { + font = textFont + } + + let attributes: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: parameters.foregroundColor] let nsString = parameters.string as NSString let size = nsString.size(withAttributes: attributes) - var offset: CGFloat = 0.0 - if parameters.string.count > 2 { - offset = UIScreenPixel + var offset = CGPoint() + if parameters.string == "∞" { + offset = CGPoint(x: 1.0, y: -1.0) + } else if parameters.string.count > 2 { + offset = CGPoint(x: 0.0, y: UIScreenPixel) } - nsString.draw(at: CGPoint(x: floor((bounds.size.width - size.width) / 2.0), y: floor((bounds.size.height - size.height) / 2.0) + offset), withAttributes: attributes) + nsString.draw(at: CGPoint(x: floor((bounds.size.width - size.width) / 2.0) + offset.x, y: floor((bounds.size.height - size.height) / 2.0) + offset.y), withAttributes: attributes) } } } diff --git a/submodules/LiveLocationTimerNode/Sources/LiveLocationWavesNode.swift b/submodules/LiveLocationTimerNode/Sources/LiveLocationWavesNode.swift index 43390801fd0..6521f9ae6df 100644 --- a/submodules/LiveLocationTimerNode/Sources/LiveLocationWavesNode.swift +++ b/submodules/LiveLocationTimerNode/Sources/LiveLocationWavesNode.swift @@ -5,10 +5,12 @@ import AsyncDisplayKit private final class LiveLocationWavesNodeParams: NSObject { let color: UIColor + let extend: Bool let progress: CGFloat - init(color: UIColor, progress: CGFloat) { + init(color: UIColor, extend: Bool, progress: CGFloat) { self.color = color + self.extend = extend self.progress = progress super.init() @@ -26,6 +28,12 @@ public final class LiveLocationWavesNode: ASDisplayNode { } } + public var extend: Bool { + didSet { + self.setNeedsDisplay() + } + } + private var effectiveProgress: CGFloat = 0.0 { didSet { self.setNeedsDisplay() @@ -34,8 +42,9 @@ public final class LiveLocationWavesNode: ASDisplayNode { var animator: ConstantDisplayLinkAnimator? - public init(color: UIColor) { + public init(color: UIColor, extend: Bool = false) { self.color = color + self.extend = extend super.init() @@ -105,7 +114,7 @@ public final class LiveLocationWavesNode: ASDisplayNode { public override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { let t = CACurrentMediaTime() let value: CGFloat = CGFloat(t.truncatingRemainder(dividingBy: 2.0)) / 2.0 - return LiveLocationWavesNodeParams(color: self.color, progress: value) + return LiveLocationWavesNodeParams(color: self.color, extend: self.extend, progress: value) } @objc public override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { @@ -142,7 +151,9 @@ public final class LiveLocationWavesNode: ASDisplayNode { } context.setAlpha(alpha * 0.7) - draw(context, position, false) + if !parameters.extend { + draw(context, position, false) + } draw(context, position, true) var progress = parameters.progress + 0.5 @@ -157,7 +168,9 @@ public final class LiveLocationWavesNode: ASDisplayNode { } context.setAlpha(largerAlpha * 0.7) - draw(context, largerPos, false) + if !parameters.extend { + draw(context, largerPos, false) + } draw(context, largerPos, true) } } diff --git a/submodules/LocalMediaResources/Sources/FetchPhotoLibraryImageResource.swift b/submodules/LocalMediaResources/Sources/FetchPhotoLibraryImageResource.swift index 1abae79c80a..0f2efd43722 100644 --- a/submodules/LocalMediaResources/Sources/FetchPhotoLibraryImageResource.swift +++ b/submodules/LocalMediaResources/Sources/FetchPhotoLibraryImageResource.swift @@ -5,6 +5,7 @@ import Postbox import SwiftSignalKit import ImageCompression import Accelerate.vImage +import CoreImage private final class RequestId { var id: PHImageRequestID? @@ -15,24 +16,43 @@ private func resizedImage(_ image: UIImage, for size: CGSize) -> UIImage? { guard let cgImage = image.cgImage else { return nil } - + + if #available(iOS 14.1, *) { + if cgImage.bitsPerComponent == 10, let ciImage = CIImage(image: image, options: [.applyOrientationProperty: true, .toneMapHDRtoSDR: true]) { + let scaleX = size.width / ciImage.extent.width + + let filter = CIFilter(name: "CILanczosScaleTransform")! + filter.setValue(ciImage, forKey: kCIInputImageKey) + filter.setValue(scaleX, forKey: kCIInputScaleKey) + filter.setValue(1.0, forKey: kCIInputAspectRatioKey) + + guard let outputImage = filter.outputImage else { return nil } + + let ciContext = CIContext() + guard let cgImage = ciContext.createCGImage(outputImage, from: outputImage.extent) else { return nil } + + return UIImage(cgImage: cgImage) + } + } var format = vImage_CGImageFormat(bitsPerComponent: 8, bitsPerPixel: 32, colorSpace: nil, bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.first.rawValue), version: 0, decode: nil, - renderingIntent: .defaultIntent) + renderingIntent: cgImage.renderingIntent) var error: vImage_Error var sourceBuffer = vImage_Buffer() - defer { sourceBuffer.data.deallocate() } + defer { sourceBuffer.data?.deallocate() } error = vImageBuffer_InitWithCGImage(&sourceBuffer, &format, nil, cgImage, vImage_Flags(kvImageNoFlags)) - guard error == kvImageNoError else { return nil } + guard error == kvImageNoError else { + return nil + } var destinationBuffer = vImage_Buffer() error = vImageBuffer_Init(&destinationBuffer, @@ -84,7 +104,7 @@ extension UIImage.Orientation { private let fetchPhotoWorkers = ThreadPool(threadCount: 3, threadPriority: 0.2) -public func fetchPhotoLibraryResource(localIdentifier: String, width: Int32?, height: Int32?, format: MediaImageFormat?, quality: Int32?) -> Signal { +public func fetchPhotoLibraryResource(localIdentifier: String, width: Int32?, height: Int32?, format: MediaImageFormat?, quality: Int32?, useExif: Bool) -> Signal { return Signal { subscriber in let queue = ThreadPoolQueue(threadPool: fetchPhotoWorkers) @@ -96,7 +116,7 @@ public func fetchPhotoLibraryResource(localIdentifier: String, width: Int32?, he option.deliveryMode = .highQualityFormat option.isNetworkAccessAllowed = true option.isSynchronous = false - + let size: CGSize if let width, let height { size = CGSize(width: CGFloat(width), height: CGFloat(height)) @@ -104,11 +124,31 @@ public func fetchPhotoLibraryResource(localIdentifier: String, width: Int32?, he size = CGSize(width: 1280.0, height: 1280.0) } + var targetSize = PHImageManagerMaximumSize + //TODO: figure out how to manually read and resize some weird 10-bit heif photos from third-party cameras + if useExif, min(asset.pixelWidth, asset.pixelHeight) > 3800 { + func encodeText(string: String, key: Int16) -> String { + let nsString = string as NSString + let result = NSMutableString() + for i in 0 ..< nsString.length { + var c: unichar = nsString.character(at: i) + c = unichar(Int16(c) + key) + result.append(NSString(characters: &c, length: 1) as String) + } + return result as String + } + if let values = asset.value(forKeyPath: encodeText(string: "jnbhfQspqfsujft", key: -1)) as? [String: Any] { + if let depth = values["Depth"] as? Int, depth == 10 { + targetSize = size + } + } + } + queue.addTask(ThreadPoolTask({ _ in let startTime = CACurrentMediaTime() let semaphore = DispatchSemaphore(value: 0) - let requestIdValue = PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFit, options: option, resultHandler: { (image, info) -> Void in + let requestIdValue = PHImageManager.default().requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFit, options: option, resultHandler: { (image, info) -> Void in Queue.concurrentDefaultQueue().async { requestId.with { current -> Void in if !current.invalidated { diff --git a/submodules/LocationUI/Sources/LocationActionListItem.swift b/submodules/LocationUI/Sources/LocationActionListItem.swift index aa5ead74492..0b576c95400 100644 --- a/submodules/LocationUI/Sources/LocationActionListItem.swift +++ b/submodules/LocationUI/Sources/LocationActionListItem.swift @@ -15,6 +15,7 @@ public enum LocationActionListItemIcon: Equatable { case location case liveLocation case stopLiveLocation + case extendLiveLocation case venue(TelegramMediaMap) public static func ==(lhs: LocationActionListItemIcon, rhs: LocationActionListItemIcon) -> Bool { @@ -37,6 +38,12 @@ public enum LocationActionListItemIcon: Equatable { } else { return false } + case .extendLiveLocation: + if case .extendLiveLocation = rhs { + return true + } else { + return false + } case let .venue(lhsVenue): if case let .venue(rhsVenue) = rhs, lhsVenue.venue?.id == rhsVenue.venue?.id { return true @@ -63,18 +70,67 @@ private func generateLocationIcon(theme: PresentationTheme) -> UIImage { })! } -private func generateLiveLocationIcon(theme: PresentationTheme, stop: Bool) -> UIImage { +private enum LiveLocationIconType { + case start + case stop + case extend +} +private func generateLiveLocationIcon(theme: PresentationTheme, type: LiveLocationIconType) -> UIImage { return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in + let imageName: String + let color: UIColor + switch type { + case .start: + imageName = "Location/SendLiveLocationIcon" + color = UIColor(rgb: 0x6cc139) + case .stop: + imageName = "Location/SendLocationIcon" + color = UIColor(rgb: 0xff6464) + case .extend: + imageName = "Location/SendLocationIcon" + color = UIColor(rgb: 0x6cc139) + } + context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor(rgb: stop ? 0xff6464 : 0x6cc139).cgColor) + context.setFillColor(color.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) context.translateBy(x: size.width / 2.0, y: size.height / 2.0) context.scaleBy(x: 1.0, y: -1.0) context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - if let image = generateTintedImage(image: UIImage(bundleImageName: stop ? "Location/SendLocationIcon" : "Location/SendLiveLocationIcon"), color: theme.chat.inputPanel.actionControlForegroundColor) { + if let image = generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.chat.inputPanel.actionControlForegroundColor) { context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)) + + if case .extend = type { + context.setStrokeColor(theme.chat.inputPanel.actionControlForegroundColor.cgColor) + context.setLineWidth(2.0 - UIScreenPixel) + context.setLineCap(.round) + + let length: CGFloat = 6.0 + UIScreenPixel + context.move(to: CGPoint(x: 8.0 + 0.0, y: image.size.height / 2.0)) + context.addLine(to: CGPoint(x: 8.0 + length, y: image.size.height / 2.0)) + context.strokePath() + + context.move(to: CGPoint(x: 8.0 + length / 2.0, y: image.size.height / 2.0 - length / 2.0)) + context.addLine(to: CGPoint(x: 8.0 + length / 2.0, y: image.size.height / 2.0 + length / 2.0)) + context.strokePath() + } else if case .stop = type { + context.setStrokeColor(color.cgColor) + context.setLineWidth(5.0) + + context.move(to: CGPoint(x: 10.0, y: size.height - 10.0)) + context.addLine(to: CGPoint(x: size.width - 10.0, y: 10.0)) + context.strokePath() + + context.setStrokeColor(theme.chat.inputPanel.actionControlForegroundColor.cgColor) + context.setLineWidth(2.0 - UIScreenPixel) + context.setLineCap(.round) + + context.move(to: CGPoint(x: 12.0, y: size.height - 12.0)) + context.addLine(to: CGPoint(x: size.width - 12.0, y: 12.0)) + context.strokePath() + } } })! } @@ -273,10 +329,18 @@ final class LocationActionListItemNode: ListViewItemNode { strongSelf.iconNode.isHidden = false strongSelf.venueIconNode.isHidden = true strongSelf.iconNode.image = generateLocationIcon(theme: item.presentationData.theme) - case .liveLocation, .stopLiveLocation: + case .liveLocation: strongSelf.iconNode.isHidden = false strongSelf.venueIconNode.isHidden = true - strongSelf.iconNode.image = generateLiveLocationIcon(theme: item.presentationData.theme, stop: updatedIcon == .stopLiveLocation) + strongSelf.iconNode.image = generateLiveLocationIcon(theme: item.presentationData.theme, type: .start) + case .stopLiveLocation: + strongSelf.iconNode.isHidden = false + strongSelf.venueIconNode.isHidden = true + strongSelf.iconNode.image = generateLiveLocationIcon(theme: item.presentationData.theme, type: .stop) + case .extendLiveLocation: + strongSelf.iconNode.isHidden = false + strongSelf.venueIconNode.isHidden = true + strongSelf.iconNode.image = generateLiveLocationIcon(theme: item.presentationData.theme, type: .extend) case let .venue(venue): strongSelf.iconNode.isHidden = true strongSelf.venueIconNode.isHidden = false @@ -293,13 +357,14 @@ final class LocationActionListItemNode: ListViewItemNode { strongSelf.venueIconNode.setSignal(venueIcon(engine: item.engine, type: type ?? "", flag: flag, background: true)) } - if updatedIcon == .stopLiveLocation { - let wavesNode = LiveLocationWavesNode(color: item.presentationData.theme.chat.inputPanel.actionControlForegroundColor) + switch updatedIcon { + case .stopLiveLocation, .extendLiveLocation: + let wavesNode = LiveLocationWavesNode(color: item.presentationData.theme.chat.inputPanel.actionControlForegroundColor, extend: updatedIcon == .extendLiveLocation) strongSelf.addSubnode(wavesNode) strongSelf.wavesNode = wavesNode - } else if let wavesNode = strongSelf.wavesNode { + default: + strongSelf.wavesNode?.removeFromSupernode() strongSelf.wavesNode = nil - wavesNode.removeFromSupernode() } strongSelf.wavesNode?.color = item.presentationData.theme.chat.inputPanel.actionControlForegroundColor } @@ -349,7 +414,7 @@ final class LocationActionListItemNode: ListViewItemNode { strongSelf.timerNode = timerNode } let timerSize = CGSize(width: 28.0, height: 28.0) - timerNode.update(backgroundColor: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.4), foregroundColor: item.presentationData.theme.list.itemAccentColor, textColor: item.presentationData.theme.list.itemAccentColor, beginTimestamp: beginTimestamp, timeout: timeout, strings: item.presentationData.strings) + timerNode.update(backgroundColor: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.4), foregroundColor: item.presentationData.theme.list.itemAccentColor, textColor: item.presentationData.theme.list.itemAccentColor, beginTimestamp: beginTimestamp, timeout: Int32(timeout) == liveLocationIndefinitePeriod ? -1.0 : timeout, strings: item.presentationData.strings) timerNode.frame = CGRect(origin: CGPoint(x: contentSize.width - 16.0 - timerSize.width, y: floorToScreenPixels((contentSize.height - timerSize.height) / 2.0) - 2.0), size: timerSize) } else if let timerNode = strongSelf.timerNode { strongSelf.timerNode = nil @@ -361,7 +426,7 @@ final class LocationActionListItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } diff --git a/submodules/LocationUI/Sources/LocationAttributionItem.swift b/submodules/LocationUI/Sources/LocationAttributionItem.swift index ba03d71eb67..d090c91b04d 100644 --- a/submodules/LocationUI/Sources/LocationAttributionItem.swift +++ b/submodules/LocationUI/Sources/LocationAttributionItem.swift @@ -120,7 +120,7 @@ private class LocationAttributionItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } diff --git a/submodules/LocationUI/Sources/LocationInfoListItem.swift b/submodules/LocationUI/Sources/LocationInfoListItem.swift index 723b55c99a9..1ce0bb28daa 100644 --- a/submodules/LocationUI/Sources/LocationInfoListItem.swift +++ b/submodules/LocationUI/Sources/LocationInfoListItem.swift @@ -384,7 +384,7 @@ final class LocationInfoListItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } diff --git a/submodules/LocationUI/Sources/LocationLiveListItem.swift b/submodules/LocationUI/Sources/LocationLiveListItem.swift index 98faeaacab3..4c73eb62fc6 100644 --- a/submodules/LocationUI/Sources/LocationLiveListItem.swift +++ b/submodules/LocationUI/Sources/LocationLiveListItem.swift @@ -321,7 +321,14 @@ final class LocationLiveListItemNode: ListViewItemNode { } let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) - if currentTimestamp < item.message.timestamp + liveBroadcastingTimeout { + let remainingTime: Int32 + if liveBroadcastingTimeout == liveLocationIndefinitePeriod { + remainingTime = liveLocationIndefinitePeriod + } else { + remainingTime = max(0, item.message.timestamp + liveBroadcastingTimeout - currentTimestamp) + } + + if remainingTime > 0 { let timerNode: ChatMessageLiveLocationTimerNode if let current = strongSelf.timerNode { timerNode = current @@ -331,7 +338,7 @@ final class LocationLiveListItemNode: ListViewItemNode { strongSelf.timerNode = timerNode } let timerSize = CGSize(width: 28.0, height: 28.0) - timerNode.update(backgroundColor: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.4), foregroundColor: item.presentationData.theme.list.itemAccentColor, textColor: item.presentationData.theme.list.itemAccentColor, beginTimestamp: Double(item.message.timestamp), timeout: Double(liveBroadcastingTimeout), strings: item.presentationData.strings) + timerNode.update(backgroundColor: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.4), foregroundColor: item.presentationData.theme.list.itemAccentColor, textColor: item.presentationData.theme.list.itemAccentColor, beginTimestamp: Double(item.message.timestamp), timeout: Int32(liveBroadcastingTimeout) == liveLocationIndefinitePeriod ? -1.0 : Double(liveBroadcastingTimeout), strings: item.presentationData.strings) timerNode.frame = CGRect(origin: CGPoint(x: contentSize.width - 16.0 - timerSize.width, y: 14.0), size: timerSize) } else if let timerNode = strongSelf.timerNode { strongSelf.timerNode = nil @@ -391,7 +398,7 @@ final class LocationLiveListItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } diff --git a/submodules/LocationUI/Sources/LocationPickerController.swift b/submodules/LocationUI/Sources/LocationPickerController.swift index 6d73920913a..d6430907db3 100644 --- a/submodules/LocationUI/Sources/LocationPickerController.swift +++ b/submodules/LocationUI/Sources/LocationPickerController.swift @@ -80,7 +80,11 @@ public final class LocationPickerController: ViewController, AttachmentContainab public var requestAttachmentMenuExpansion: () -> Void = {} public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } + public var parentController: () -> ViewController? = { + return nil + } public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in } + public var updateTabBarVisibility: (Bool, ContainedViewLayoutTransition) -> Void = { _, _ in } public var cancelPanGesture: () -> Void = { } public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } @@ -142,33 +146,34 @@ public final class LocationPickerController: ViewController, AttachmentContainab return } let controller = ActionSheetController(presentationData: strongSelf.presentationData) - var title = strongSelf.presentationData.strings.Map_LiveLocationGroupDescription + var title = strongSelf.presentationData.strings.Map_LiveLocationGroupNewDescription if case let .share(peer, _, _) = strongSelf.mode, let peer = peer, case .user = peer { - title = strongSelf.presentationData.strings.Map_LiveLocationPrivateDescription(peer.compactDisplayTitle).string + title = strongSelf.presentationData.strings.Map_LiveLocationPrivateNewDescription(peer.compactDisplayTitle).string } + + let sendLiveLocationImpl: (Int32) -> Void = { [weak self, weak controller] period in + controller?.dismissAnimated() + guard let self, let controller else { + return + } + controller.dismissAnimated() + self.completion(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: period), nil, nil, nil, nil) + self.dismiss() + } + controller.setItemGroups([ ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: title), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor15Minutes, color: .accent, action: { [weak self, weak controller] in - controller?.dismissAnimated() - if let strongSelf = self { - strongSelf.completion(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: 15 * 60), nil, nil, nil, nil) - strongSelf.dismiss() - } + ActionSheetTextItem(title: title, font: .large, parseMarkdown: true), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationForMinutes(15), color: .accent, action: { sendLiveLocationImpl(15 * 60) }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor1Hour, color: .accent, action: { [weak self, weak controller] in - controller?.dismissAnimated() - if let strongSelf = self { - strongSelf.completion(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: 60 * 60 - 1), nil, nil, nil, nil) - strongSelf.dismiss() - } + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationForHours(1), color: .accent, action: { + sendLiveLocationImpl(60 * 60 - 1) }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor8Hours, color: .accent, action: { [weak self, weak controller] in - controller?.dismissAnimated() - if let strongSelf = self { - strongSelf.completion(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: 8 * 60 * 60), nil, nil, nil, nil) - strongSelf.dismiss() - } + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationForHours(8), color: .accent, action: { + sendLiveLocationImpl(8 * 60 * 60) + }), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationIndefinite, color: .accent, action: { + sendLiveLocationImpl(liveLocationIndefinitePeriod) }) ]), ActionSheetItemGroup(items: [ diff --git a/submodules/LocationUI/Sources/LocationSectionHeaderItem.swift b/submodules/LocationUI/Sources/LocationSectionHeaderItem.swift index 0d138c093fc..b82ac19664e 100644 --- a/submodules/LocationUI/Sources/LocationSectionHeaderItem.swift +++ b/submodules/LocationUI/Sources/LocationSectionHeaderItem.swift @@ -109,7 +109,7 @@ private class LocationSectionHeaderItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } diff --git a/submodules/LocationUI/Sources/LocationViewController.swift b/submodules/LocationUI/Sources/LocationViewController.swift index 71fd3a7f870..35fb7bb4d46 100644 --- a/submodules/LocationUI/Sources/LocationViewController.swift +++ b/submodules/LocationUI/Sources/LocationViewController.swift @@ -47,12 +47,12 @@ class LocationViewInteraction { let share: () -> Void let setupProximityNotification: (Bool, EngineMessage.Id?) -> Void let updateSendActionHighlight: (Bool) -> Void - let sendLiveLocation: (Int32?) -> Void + let sendLiveLocation: (Int32?, Bool, EngineMessage.Id?) -> Void let stopLiveLocation: () -> Void let updateRightBarButton: (LocationViewRightBarButton) -> Void let present: (ViewController) -> Void - init(toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, toggleTrackingMode: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, requestDirections: @escaping (TelegramMediaMap, String?, OpenInLocationDirections) -> Void, share: @escaping () -> Void, setupProximityNotification: @escaping (Bool, EngineMessage.Id?) -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, sendLiveLocation: @escaping (Int32?) -> Void, stopLiveLocation: @escaping () -> Void, updateRightBarButton: @escaping (LocationViewRightBarButton) -> Void, present: @escaping (ViewController) -> Void) { + init(toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, toggleTrackingMode: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, requestDirections: @escaping (TelegramMediaMap, String?, OpenInLocationDirections) -> Void, share: @escaping () -> Void, setupProximityNotification: @escaping (Bool, EngineMessage.Id?) -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, sendLiveLocation: @escaping (Int32?, Bool, EngineMessage.Id?) -> Void, stopLiveLocation: @escaping () -> Void, updateRightBarButton: @escaping (LocationViewRightBarButton) -> Void, present: @escaping (ViewController) -> Void) { self.toggleMapModeSelection = toggleMapModeSelection self.updateMapMode = updateMapMode self.toggleTrackingMode = toggleTrackingMode @@ -214,7 +214,7 @@ public final class LocationViewController: ViewController { return state } - let _ = context.engine.messages.requestEditLiveLocation(messageId: messageId, stop: false, coordinate: nil, heading: nil, proximityNotificationRadius: 0).start(completed: { [weak self] in + let _ = context.engine.messages.requestEditLiveLocation(messageId: messageId, stop: false, coordinate: nil, heading: nil, proximityNotificationRadius: 0, extendPeriod: nil).start(completed: { [weak self] in guard let strongSelf = self else { return } @@ -283,7 +283,7 @@ public final class LocationViewController: ViewController { return state } - let _ = context.engine.messages.requestEditLiveLocation(messageId: messageId, stop: false, coordinate: nil, heading: nil, proximityNotificationRadius: distance).start(completed: { [weak self] in + let _ = context.engine.messages.requestEditLiveLocation(messageId: messageId, stop: false, coordinate: nil, heading: nil, proximityNotificationRadius: distance, extendPeriod: nil).start(completed: { [weak self] in guard let strongSelf = self else { return } @@ -323,7 +323,7 @@ public final class LocationViewController: ViewController { } else { strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: updatedPresentationData, title: strongSelf.presentationData.strings.Location_LiveLocationRequired_Title, text: strongSelf.presentationData.strings.Location_LiveLocationRequired_Description, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Location_LiveLocationRequired_ShareLocation, action: { completion() - strongSelf.interaction?.sendLiveLocation(distance) + strongSelf.interaction?.sendLiveLocation(distance, false, nil) }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})], actionLayout: .vertical), in: .window(.root)) } completion() @@ -341,7 +341,7 @@ public final class LocationViewController: ViewController { return } strongSelf.controllerNode.updateSendActionHighlight(highlighted) - }, sendLiveLocation: { [weak self] distance in + }, sendLiveLocation: { [weak self] distance, extend, messageId in guard let strongSelf = self else { return } @@ -400,33 +400,47 @@ public final class LocationViewController: ViewController { let _ = (context.account.postbox.loadedPeerWithId(subject.id.peerId) |> deliverOnMainQueue).start(next: { peer in let controller = ActionSheetController(presentationData: strongSelf.presentationData) - var title = strongSelf.presentationData.strings.Map_LiveLocationGroupDescription - if let user = peer as? TelegramUser { - title = strongSelf.presentationData.strings.Map_LiveLocationPrivateDescription(EnginePeer(user).compactDisplayTitle).string + var title: String + if extend { + title = strongSelf.presentationData.strings.Map_LiveLocationExtendDescription + } else { + title = strongSelf.presentationData.strings.Map_LiveLocationGroupNewDescription + if let user = peer as? TelegramUser { + title = strongSelf.presentationData.strings.Map_LiveLocationPrivateNewDescription(EnginePeer(user).compactDisplayTitle).string + } } let sendLiveLocationImpl: (Int32) -> Void = { [weak controller] period in controller?.dismissAnimated() - let _ = (strongSelf.controllerNode.coordinate - |> deliverOnMainQueue).start(next: { coordinate in - params.sendLiveLocation(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: period)) - }) - - strongSelf.controllerNode.showAll() + if extend { + if let messageId { + let _ = context.engine.messages.requestEditLiveLocation(messageId: messageId, stop: false, coordinate: nil, heading: nil, proximityNotificationRadius: nil, extendPeriod: period).start() + } + } else { + let _ = (strongSelf.controllerNode.coordinate + |> deliverOnMainQueue).start(next: { coordinate in + params.sendLiveLocation(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: period)) + }) + + strongSelf.controllerNode.showAll() + } } controller.setItemGroups([ ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: title), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor15Minutes, color: .accent, action: { + ActionSheetTextItem(title: title, font: .large, parseMarkdown: true), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationForMinutes(15), color: .accent, action: { sendLiveLocationImpl(15 * 60) }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor1Hour, color: .accent, action: { + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationForHours(1), color: .accent, action: { sendLiveLocationImpl(60 * 60 - 1) }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor8Hours, color: .accent, action: { + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationForHours(8), color: .accent, action: { sendLiveLocationImpl(8 * 60 * 60) + }), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationIndefinite, color: .accent, action: { + sendLiveLocationImpl(liveLocationIndefinitePeriod) }) ]), ActionSheetItemGroup(items: [ diff --git a/submodules/LocationUI/Sources/LocationViewControllerNode.swift b/submodules/LocationUI/Sources/LocationViewControllerNode.swift index 9f13a79efdc..752e0a5f370 100644 --- a/submodules/LocationUI/Sources/LocationViewControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationViewControllerNode.swift @@ -38,25 +38,26 @@ private struct LocationViewTransaction { let updates: [ListViewUpdateItem] let gotTravelTimes: Bool let count: Int + let animated: Bool } private enum LocationViewEntryId: Hashable { case info - case toggleLiveLocation + case toggleLiveLocation(Bool) case liveLocation(UInt32) } private enum LocationViewEntry: Comparable, Identifiable { case info(PresentationTheme, TelegramMediaMap, String?, Double?, ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime, Bool) - case toggleLiveLocation(PresentationTheme, String, String, Double?, Double?) + case toggleLiveLocation(PresentationTheme, String, String, Double?, Double?, Bool, EngineMessage.Id?) case liveLocation(PresentationTheme, PresentationDateTimeFormat, PresentationPersonNameOrder, EngineMessage, Double?, ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime, Int) var stableId: LocationViewEntryId { switch self { case .info: return .info - case .toggleLiveLocation: - return .toggleLiveLocation + case let .toggleLiveLocation(_, _, _, _, _, additional, _): + return .toggleLiveLocation(additional) case let .liveLocation(_, _, _, message, _, _, _, _, _): return .liveLocation(message.stableId) } @@ -70,8 +71,8 @@ private enum LocationViewEntry: Comparable, Identifiable { } else { return false } - case let .toggleLiveLocation(lhsTheme, lhsTitle, lhsSubtitle, lhsBeginTimestamp, lhsTimeout): - if case let .toggleLiveLocation(rhsTheme, rhsTitle, rhsSubtitle, rhsBeginTimestamp, rhsTimeout) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsBeginTimestamp == rhsBeginTimestamp, lhsTimeout == rhsTimeout { + case let .toggleLiveLocation(lhsTheme, lhsTitle, lhsSubtitle, lhsBeginTimestamp, lhsTimeout, lhsAdditional, lhsMessageId): + if case let .toggleLiveLocation(rhsTheme, rhsTitle, rhsSubtitle, rhsBeginTimestamp, rhsTimeout, rhsAdditional, rhsMessageId) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsBeginTimestamp == rhsBeginTimestamp, lhsTimeout == rhsTimeout, lhsAdditional == rhsAdditional, lhsMessageId == rhsMessageId { return true } else { return false @@ -94,10 +95,12 @@ private enum LocationViewEntry: Comparable, Identifiable { case .toggleLiveLocation, .liveLocation: return true } - case .toggleLiveLocation: + case let .toggleLiveLocation(_, _, _, _, _, lhsAdditional, _): switch rhs { - case .info, .toggleLiveLocation: + case .info: return false + case let .toggleLiveLocation(_, _, _, _, _, rhsAdditional, _): + return !lhsAdditional && rhsAdditional case .liveLocation: return true } @@ -135,18 +138,36 @@ private enum LocationViewEntry: Comparable, Identifiable { }, walkingAction: { interaction?.requestDirections(location, nil, .walking) }) - case let .toggleLiveLocation(_, title, subtitle, beginTimstamp, timeout): - let beginTimeAndTimeout: (Double, Double)? + case let .toggleLiveLocation(_, title, subtitle, beginTimstamp, timeout, additional, messageId): + var beginTimeAndTimeout: (Double, Double)? if let beginTimstamp = beginTimstamp, let timeout = timeout { beginTimeAndTimeout = (beginTimstamp, timeout) } else { beginTimeAndTimeout = nil } - return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), engine: context.engine, title: title, subtitle: subtitle, icon: beginTimeAndTimeout != nil ? .stopLiveLocation : .liveLocation, beginTimeAndTimeout: beginTimeAndTimeout, action: { + + let icon: LocationActionListItemIcon + if let timeout, Int32(timeout) != liveLocationIndefinitePeriod, !additional { + icon = .extendLiveLocation + } else if beginTimeAndTimeout != nil { + icon = .stopLiveLocation + } else { + icon = .liveLocation + } + + return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), engine: context.engine, title: title, subtitle: subtitle, icon: icon, beginTimeAndTimeout: !additional ? beginTimeAndTimeout : nil, action: { if beginTimeAndTimeout != nil { - interaction?.stopLiveLocation() + if let timeout, Int32(timeout) != liveLocationIndefinitePeriod { + if additional { + interaction?.stopLiveLocation() + } else { + interaction?.sendLiveLocation(nil, true, messageId) + } + } else { + interaction?.stopLiveLocation() + } } else { - interaction?.sendLiveLocation(nil) + interaction?.sendLiveLocation(nil, false, nil) } }, highlighted: { highlight in interaction?.updateSendActionHighlight(highlight) @@ -177,14 +198,14 @@ private enum LocationViewEntry: Comparable, Identifiable { } } -private func preparedTransition(from fromEntries: [LocationViewEntry], to toEntries: [LocationViewEntry], context: AccountContext, presentationData: PresentationData, interaction: LocationViewInteraction?, gotTravelTimes: Bool) -> LocationViewTransaction { +private func preparedTransition(from fromEntries: [LocationViewEntry], to toEntries: [LocationViewEntry], context: AccountContext, presentationData: PresentationData, interaction: LocationViewInteraction?, gotTravelTimes: Bool, animated: Bool) -> LocationViewTransaction { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, interaction: interaction), directionHint: nil) } - return LocationViewTransaction(deletions: deletions, insertions: insertions, updates: updates, gotTravelTimes: gotTravelTimes, count: toEntries.count) + return LocationViewTransaction(deletions: deletions, insertions: insertions, updates: updates, gotTravelTimes: gotTravelTimes, count: toEntries.count, animated: animated) } enum LocationViewLocation: Equatable { @@ -421,7 +442,12 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan if case let .channel(channel) = subject.author, case .broadcast = channel.info, activeOwnLiveLocation == nil { } else { - entries.append(.toggleLiveLocation(presentationData.theme, title, subtitle, beginTime, timeout)) + if let timeout, Int32(timeout) != liveLocationIndefinitePeriod { + entries.append(.toggleLiveLocation(presentationData.theme, presentationData.strings.Map_SharingLocation, presentationData.strings.Map_TapToAddTime, beginTime, timeout, false, activeOwnLiveLocation?.id)) + entries.append(.toggleLiveLocation(presentationData.theme, title, subtitle, beginTime, timeout, true, nil)) + } else { + entries.append(.toggleLiveLocation(presentationData.theme, title, subtitle, beginTime, timeout, false, nil)) + } } var sortedLiveLocations: [EngineMessage] = [] @@ -452,7 +478,12 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan if let timeout = location.liveBroadcastingTimeout { liveBroadcastingTimeout = timeout } - let remainingTime = max(0, message.timestamp + liveBroadcastingTimeout - currentTime) + let remainingTime: Int32 + if liveBroadcastingTimeout == liveLocationIndefinitePeriod { + remainingTime = liveLocationIndefinitePeriod + } else { + remainingTime = max(0, message.timestamp + liveBroadcastingTimeout - currentTime) + } if message.flags.contains(.Incoming) && remainingTime != 0 && proximityNotification == nil { proximityNotification = false } @@ -543,7 +574,27 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan let previousState = previousState.swap(state) let previousHadTravelTimes = previousHadTravelTimes.swap(!travelTimes.isEmpty) - let transition = preparedTransition(from: previousEntries ?? [], to: entries, context: context, presentationData: presentationData, interaction: strongSelf.interaction, gotTravelTimes: !travelTimes.isEmpty && !previousHadTravelTimes) + var animated = false + var previousActionsCount = 0 + var actionsCount = 0 + if let previousEntries { + for entry in previousEntries { + if case .toggleLiveLocation = entry { + previousActionsCount += 1 + } + } + } + for entry in entries { + if case .toggleLiveLocation = entry { + actionsCount += 1 + } + } + + if actionsCount < previousActionsCount { + animated = true + } + + let transition = preparedTransition(from: previousEntries ?? [], to: entries, context: context, presentationData: presentationData, interaction: strongSelf.interaction, gotTravelTimes: !travelTimes.isEmpty && !previousHadTravelTimes, animated: animated) strongSelf.enqueueTransition(transition) strongSelf.headerNode.updateState(mapMode: state.mapMode, trackingMode: state.trackingMode, displayingMapModeOptions: state.displayingMapModeOptions, displayingPlacesButton: false, proximityNotification: proximityNotification, animated: false) @@ -757,7 +808,10 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan scrollToItem = nil } - let options = ListViewDeleteAndInsertOptions() + var options = ListViewDeleteAndInsertOptions() + if transition.animated { + options.insert(.AnimateInsertion) + } self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: scrollToItem, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in }) } diff --git a/submodules/MediaPickerUI/Sources/MediaGroupsAlbumGridItem.swift b/submodules/MediaPickerUI/Sources/MediaGroupsAlbumGridItem.swift index 7b1f98b5749..b9f111bc584 100644 --- a/submodules/MediaPickerUI/Sources/MediaGroupsAlbumGridItem.swift +++ b/submodules/MediaPickerUI/Sources/MediaGroupsAlbumGridItem.swift @@ -184,8 +184,8 @@ private final class MediaGroupsGridAlbumItemNode : ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - super.animateInsertion(currentTimestamp, duration: duration, short: short) + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + super.animateInsertion(currentTimestamp, duration: duration, options: options) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } @@ -366,7 +366,7 @@ private class MediaGroupsAlbumGridItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } diff --git a/submodules/MediaPickerUI/Sources/MediaGroupsAlbumItem.swift b/submodules/MediaPickerUI/Sources/MediaGroupsAlbumItem.swift index 670c099d965..7cc617796cf 100644 --- a/submodules/MediaPickerUI/Sources/MediaGroupsAlbumItem.swift +++ b/submodules/MediaPickerUI/Sources/MediaGroupsAlbumItem.swift @@ -312,7 +312,7 @@ class MediaGroupsAlbumItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/MediaPickerUI/Sources/MediaGroupsHeaderItem.swift b/submodules/MediaPickerUI/Sources/MediaGroupsHeaderItem.swift index 2dbaa1e84e7..b934d07d5a1 100644 --- a/submodules/MediaPickerUI/Sources/MediaGroupsHeaderItem.swift +++ b/submodules/MediaPickerUI/Sources/MediaGroupsHeaderItem.swift @@ -100,7 +100,7 @@ private class MediaGroupsHeaderItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } diff --git a/submodules/MediaPickerUI/Sources/MediaGroupsScreen.swift b/submodules/MediaPickerUI/Sources/MediaGroupsScreen.swift index a1a18dd424d..93289e50596 100644 --- a/submodules/MediaPickerUI/Sources/MediaGroupsScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaGroupsScreen.swift @@ -152,7 +152,11 @@ private func preparedTransition(from fromEntries: [MediaGroupsEntry], to toEntri public final class MediaGroupsScreen: ViewController, AttachmentContainable { public var requestAttachmentMenuExpansion: () -> Void = {} public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } + public var parentController: () -> ViewController? = { + return nil + } public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in } + public var updateTabBarVisibility: (Bool, ContainedViewLayoutTransition) -> Void = { _, _ in } public var cancelPanGesture: () -> Void = { } public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index baf8dc41c1b..fd6fc6fcccb 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -206,7 +206,11 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { public var requestAttachmentMenuExpansion: () -> Void = { } public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } + public var parentController: () -> ViewController? = { + return nil + } public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in } + public var updateTabBarVisibility: (Bool, ContainedViewLayoutTransition) -> Void = { _, _ in } public var cancelPanGesture: () -> Void = { } public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } @@ -1596,11 +1600,14 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { if case let .noAccess(cameraAccess) = self.state { var hasCamera = cameraAccess == .authorized var story = false - if let subject = self.controller?.subject, case .assets(_, .story) = subject { - hasCamera = false - story = true - - self.controller?.navigationItem.rightBarButtonItem = nil + if let subject = self.controller?.subject { + if case .assets(_, .story) = subject { + hasCamera = false + story = true + self.controller?.navigationItem.rightBarButtonItem = nil + } else if case .assets(_, .createSticker) = subject { + hasCamera = false + } } var placeholderTransition = transition diff --git a/submodules/MtProtoKit/Sources/MTContext.m b/submodules/MtProtoKit/Sources/MTContext.m index 2ed758d26ae..ef6d24c6cd4 100644 --- a/submodules/MtProtoKit/Sources/MTContext.m +++ b/submodules/MtProtoKit/Sources/MTContext.m @@ -350,6 +350,8 @@ - (void)cleanup id cleanupSessionInfoDisposables = _cleanupSessionInfoDisposables; + NSDictionary *transportSchemeDisposableByDatacenterId = _transportSchemeDisposableByDatacenterId; + [[MTContext contextQueue] dispatchOnQueue:^ { for (NSNumber *nDatacenterId in discoverDatacenterAddressActions) @@ -383,6 +385,12 @@ - (void)cleanup } [cleanupSessionInfoDisposables dispose]; + + for (NSNumber *nDatacenterId in transportSchemeDisposableByDatacenterId) + { + id disposable = transportSchemeDisposableByDatacenterId[nDatacenterId]; + [disposable dispose]; + } }]; } diff --git a/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSetupHeaderItem.swift b/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSetupHeaderItem.swift index 904d8c20c1a..9a87b6001fb 100644 --- a/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSetupHeaderItem.swift +++ b/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSetupHeaderItem.swift @@ -171,7 +171,7 @@ class ChannelDiscussionGroupSetupHeaderItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersController.swift b/submodules/PeerInfoUI/Sources/ChannelMembersController.swift index cbc787a3596..282c219f2fb 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersController.swift @@ -518,7 +518,7 @@ public func channelMembersController(context: AccountContext, updatedPresentatio ) |> deliverOnMainQueue).start(next: { chatPeer, exportedInvitation, members in let disabledIds = members?.compactMap({$0.peer.id}) ?? [] - let contactsController = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, updatedPresentationData: updatedPresentationData, mode: .peerSelection(searchChatList: false, searchGroups: false, searchChannels: false), options: [], filters: [.excludeSelf, .disable(disabledIds)], onlyWriteable: true, isGroupInvitation: true)) + let contactsController = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, updatedPresentationData: updatedPresentationData, mode: .peerSelection(searchChatList: false, searchGroups: false, searchChannels: false), filters: [.excludeSelf, .disable(disabledIds)], onlyWriteable: true, isGroupInvitation: true)) addMembersDisposable.set(( contactsController.result diff --git a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift index 7b768293f38..87898ed2152 100644 --- a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift @@ -2276,7 +2276,7 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta nextImpl = { [weak controller] in if let controller = controller { if case .initialSetup = mode { - let selectionController = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, updatedPresentationData: updatedPresentationData, mode: .channelCreation, options: [], onlyWriteable: true)) + let selectionController = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, updatedPresentationData: updatedPresentationData, mode: .channelCreation, onlyWriteable: true)) (controller.navigationController as? NavigationController)?.replaceAllButRootController(selectionController, animated: true) let _ = (selectionController.result |> deliverOnMainQueue).start(next: { [weak selectionController] result in diff --git a/submodules/PeerInfoUI/Sources/ChatSlowmodeItem.swift b/submodules/PeerInfoUI/Sources/ChatSlowmodeItem.swift index 40811d64ad4..b522dea458e 100644 --- a/submodules/PeerInfoUI/Sources/ChatSlowmodeItem.swift +++ b/submodules/PeerInfoUI/Sources/ChatSlowmodeItem.swift @@ -297,7 +297,7 @@ class ChatSlowmodeItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/PeerInfoUI/Sources/ChatUnrestrictBoostersItem.swift b/submodules/PeerInfoUI/Sources/ChatUnrestrictBoostersItem.swift index 320a91126b5..a42aef2ccf7 100644 --- a/submodules/PeerInfoUI/Sources/ChatUnrestrictBoostersItem.swift +++ b/submodules/PeerInfoUI/Sources/ChatUnrestrictBoostersItem.swift @@ -319,7 +319,7 @@ class ChatUnrestrictBoostersItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift b/submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift index b358a62c364..f9410539382 100644 --- a/submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift +++ b/submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift @@ -316,7 +316,7 @@ class IncreaseLimitHeaderItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/PeerInfoUI/Sources/ItemListCallListItem.swift b/submodules/PeerInfoUI/Sources/ItemListCallListItem.swift index 9ed20538e0a..e9e3d243c47 100644 --- a/submodules/PeerInfoUI/Sources/ItemListCallListItem.swift +++ b/submodules/PeerInfoUI/Sources/ItemListCallListItem.swift @@ -351,7 +351,7 @@ public class ItemListCallListItemNode: ListViewItemNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/PeerInfoUI/Sources/ItemListReactionItem.swift b/submodules/PeerInfoUI/Sources/ItemListReactionItem.swift index 2ade5e10e99..294577c2b27 100644 --- a/submodules/PeerInfoUI/Sources/ItemListReactionItem.swift +++ b/submodules/PeerInfoUI/Sources/ItemListReactionItem.swift @@ -447,7 +447,7 @@ public class ItemListReactionItemNode: ListViewItemNode, ItemListItemNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/PeerInfoUI/Sources/ItemListSecretChatKeyItem.swift b/submodules/PeerInfoUI/Sources/ItemListSecretChatKeyItem.swift index c9318435d1d..a9824f78fde 100644 --- a/submodules/PeerInfoUI/Sources/ItemListSecretChatKeyItem.swift +++ b/submodules/PeerInfoUI/Sources/ItemListSecretChatKeyItem.swift @@ -327,7 +327,7 @@ class ItemListSecretChatKeyItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/PeerInfoUI/Sources/PeerAutoremoveTimeoutItem.swift b/submodules/PeerInfoUI/Sources/PeerAutoremoveTimeoutItem.swift index a7d354ceec0..f45479be7b7 100644 --- a/submodules/PeerInfoUI/Sources/PeerAutoremoveTimeoutItem.swift +++ b/submodules/PeerInfoUI/Sources/PeerAutoremoveTimeoutItem.swift @@ -306,7 +306,7 @@ class PeerRemoveTimeoutItemNode: ListViewItemNode, ItemListItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/PeerInfoUI/Sources/UserInfoEditingPhoneActionItem.swift b/submodules/PeerInfoUI/Sources/UserInfoEditingPhoneActionItem.swift index 5c89c377861..4fe64adbab4 100644 --- a/submodules/PeerInfoUI/Sources/UserInfoEditingPhoneActionItem.swift +++ b/submodules/PeerInfoUI/Sources/UserInfoEditingPhoneActionItem.swift @@ -221,7 +221,7 @@ class UserInfoEditingPhoneActionItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/PeerInfoUI/Sources/UserInfoEditingPhoneItem.swift b/submodules/PeerInfoUI/Sources/UserInfoEditingPhoneItem.swift index 1329820447b..409db1eb162 100644 --- a/submodules/PeerInfoUI/Sources/UserInfoEditingPhoneItem.swift +++ b/submodules/PeerInfoUI/Sources/UserInfoEditingPhoneItem.swift @@ -321,7 +321,7 @@ class UserInfoEditingPhoneItemNode: ItemListRevealOptionsItemNode, ItemListItemN self.item?.delete() } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/PeersNearbyUI/Sources/PeersNearbyHeaderItem.swift b/submodules/PeersNearbyUI/Sources/PeersNearbyHeaderItem.swift index ed3171ce126..57f756bf395 100644 --- a/submodules/PeersNearbyUI/Sources/PeersNearbyHeaderItem.swift +++ b/submodules/PeersNearbyUI/Sources/PeersNearbyHeaderItem.swift @@ -117,7 +117,7 @@ class PeersNearbyHeaderItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/PremiumUI/Sources/GiftOptionItem.swift b/submodules/PremiumUI/Sources/GiftOptionItem.swift index 882912171be..60ca54d94d8 100644 --- a/submodules/PremiumUI/Sources/GiftOptionItem.swift +++ b/submodules/PremiumUI/Sources/GiftOptionItem.swift @@ -670,7 +670,7 @@ class GiftOptionItemNode: ItemListRevealOptionsItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index c66d950c82f..de2053e80c5 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -1425,6 +1425,71 @@ public class PremiumDemoScreen: ViewControllerComponentContainer { case businessChatBots case businessIntro case businessLinks + + public var perk: PremiumPerk { + switch self { + case .doubleLimits: + return .doubleLimits + case .moreUpload: + return .moreUpload + case .fasterDownload: + return .fasterDownload + case .voiceToText: + return .voiceToText + case .noAds: + return .noAds + case .uniqueReactions: + return .uniqueReactions + case .premiumStickers: + return .premiumStickers + case .advancedChatManagement: + return .advancedChatManagement + case .profileBadge: + return .profileBadge + case .animatedUserpics: + return .animatedUserpics + case .appIcons: + return .appIcons + case .animatedEmoji: + return .animatedEmoji + case .emojiStatus: + return .emojiStatus + case .translation: + return .translation + case .stories: + return .stories + case .colors: + return .colors + case .wallpapers: + return .wallpapers + case .messageTags: + return .messageTags + case .lastSeen: + return .lastSeen + case .messagePrivacy: + return .messagePrivacy + case .business: + return .business + case .folderTags: + return .folderTags + case .businessLocation: + return .businessLocation + case .businessHours: + return .businessHours + case .businessGreetingMessage: + return .businessGreetingMessage + case .businessQuickReplies: + return .businessQuickReplies + case .businessAwayMessage: + return .businessAwayMessage + case .businessChatBots: + return .businessChatBots + case .businessIntro: + return .businessIntro + case .businessLinks: + return .businessLinks + } + } } public enum Source: Equatable { diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift index 2922f6bb25d..eddb892c99e 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -1297,6 +1297,7 @@ open class PremiumGiftScreen: ViewControllerComponentContainer { public var animationColor: UIColor? public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in } + public var updateTabBarVisibility: (Bool, ContainedViewLayoutTransition) -> Void = { _, _ in } public let mainButtonStatePromise = Promise(nil) private let mainButtonActionSlot = ActionSlot() diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index f9ba1bffe5f..41ae1775350 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -2016,11 +2016,11 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { lineSpacing: 0.18 ))) ], alignment: .left, spacing: 2.0)), - leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent( + leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent( backgroundColor: gradientColors[i], foregroundColor: .white, iconName: perk.iconName - ))), + )))), action: { [weak state] _ in var demoSubject: PremiumDemoScreen.Subject switch perk { @@ -2183,11 +2183,11 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { lineSpacing: 0.18 ))) ], alignment: .left, spacing: 2.0)), - leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent( + leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent( backgroundColor: gradientColors[min(i, gradientColors.count - 1)], foregroundColor: .white, iconName: perk.iconName - ))), + )))), action: { [weak state] _ in let isPremium = state?.isPremium == true if isPremium { @@ -2367,11 +2367,11 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { lineSpacing: 0.18 ))) ], alignment: .left, spacing: 2.0)), - leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent( + leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent( backgroundColor: UIColor(rgb: 0x676bff), foregroundColor: .white, iconName: "Premium/BusinessPerk/Status" - ))), + )))), icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent( context: context.component.context, color: accentColor, @@ -2408,11 +2408,11 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { lineSpacing: 0.18 ))) ], alignment: .left, spacing: 2.0)), - leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent( + leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent( backgroundColor: UIColor(rgb: 0x4492ff), foregroundColor: .white, iconName: "Premium/BusinessPerk/Tag" - ))), + )))), action: { _ in push(accountContext.sharedContext.makeFilterSettingsController(context: accountContext, modal: false, scrollToTags: true, dismissed: nil)) } @@ -2439,11 +2439,11 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { lineSpacing: 0.18 ))) ], alignment: .left, spacing: 2.0)), - leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent( + leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent( backgroundColor: UIColor(rgb: 0x41a6a5), foregroundColor: .white, iconName: "Premium/Perk/Stories" - ))), + )))), action: { _ in push(accountContext.sharedContext.makeMyStoriesController(context: accountContext, isArchive: false)) } diff --git a/submodules/PremiumUI/Sources/SubscriptionsCountItem.swift b/submodules/PremiumUI/Sources/SubscriptionsCountItem.swift index 9f1d26a4939..2d989a238c7 100644 --- a/submodules/PremiumUI/Sources/SubscriptionsCountItem.swift +++ b/submodules/PremiumUI/Sources/SubscriptionsCountItem.swift @@ -334,7 +334,7 @@ private final class SubscriptionsCountItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 99fdb218283..d413abc2b8a 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -2010,7 +2010,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { })) } case let .category(value): - let resultSignal = self.context.engine.stickers.searchEmoji(emojiString: value) + let resultSignal = self.context.engine.stickers.searchEmoji(category: value) |> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in var items: [EmojiPagerContentComponent.Item] = [] @@ -2082,11 +2082,11 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { fillWithLoadingPlaceholders: true, items: [] ) - ], id: AnyHashable(value), version: version, isPreset: true), isSearching: false) + ], id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) return } - self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value), version: version, isPreset: true), isSearching: false) + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) version += 1 })) } diff --git a/submodules/SectionHeaderItem/Sources/SectionHeaderItem.swift b/submodules/SectionHeaderItem/Sources/SectionHeaderItem.swift index f29e35321c4..37a1f4f9256 100644 --- a/submodules/SectionHeaderItem/Sources/SectionHeaderItem.swift +++ b/submodules/SectionHeaderItem/Sources/SectionHeaderItem.swift @@ -133,7 +133,7 @@ private class SectionHeaderItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index a22fdcba2bd..ce27f3ef1a3 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -82,7 +82,7 @@ swift_library( "//submodules/AuthorizationUI:AuthorizationUI", "//submodules/InstantPageUI:InstantPageUI", "//submodules/CheckNode:CheckNode", - "//submodules/CounterContollerTitleView:CounterContollerTitleView", + "//submodules/CounterControllerTitleView:CounterControllerTitleView", "//submodules/GridMessageSelectionNode:GridMessageSelectionNode", "//submodules/InstantPageCache:InstantPageCache", "//submodules/AppBundle:AppBundle", diff --git a/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift b/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift index 321e6b4d2d1..7e9b78957dd 100644 --- a/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift +++ b/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift @@ -99,12 +99,16 @@ public func ChangePhoneNumberController(context: AccountContext) -> ViewControll guard let codeController else { return } - AuthorizationSequenceController.presentDidNotGetCodeUI(controller: codeController, presentationData: context.sharedContext.currentPresentationData.with({ $0 }), number: phoneNumber) + let carrier = CTCarrier() + let mnc = carrier.mobileNetworkCode ?? "none" + let _ = context.engine.auth.reportMissingCode(phoneNumber: phoneNumber, phoneCodeHash: next.hash, mnc: mnc).start() + + AuthorizationSequenceController.presentDidNotGetCodeUI(controller: codeController, presentationData: context.sharedContext.currentPresentationData.with({ $0 }), phoneNumber: phoneNumber, mnc: mnc) } codeController.openFragment = { url in context.sharedContext.applicationBindings.openUrl(url) } - codeController.updateData(number: formatPhoneNumber(context: context, number: phoneNumber), email: nil, codeType: next.type, nextType: nil, timeout: next.timeout, termsOfService: nil) + codeController.updateData(number: formatPhoneNumber(context: context, number: phoneNumber), email: nil, codeType: next.type, nextType: nil, timeout: next.timeout, termsOfService: nil, previousCodeType: nil, isPrevious: false) dismissImpl = { [weak codeController] in codeController?.dismiss() } diff --git a/submodules/SettingsUI/Sources/Data and Storage/AutodownloadDataUsagePickerItem.swift b/submodules/SettingsUI/Sources/Data and Storage/AutodownloadDataUsagePickerItem.swift index 51a0462c7f2..d0ad752a2fe 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/AutodownloadDataUsagePickerItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/AutodownloadDataUsagePickerItem.swift @@ -369,7 +369,7 @@ private final class AutodownloadDataUsagePickerItemNode: ListViewItemNode { self.activateArea.accessibilityTraits = accessibilityTraits } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/SettingsUI/Sources/Data and Storage/AutodownloadSizeLimitItem.swift b/submodules/SettingsUI/Sources/Data and Storage/AutodownloadSizeLimitItem.swift index 2668b8a3355..a0ea01a3c03 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/AutodownloadSizeLimitItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/AutodownloadSizeLimitItem.swift @@ -287,7 +287,7 @@ private final class AutodownloadSizeLimitItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/SettingsUI/Sources/Data and Storage/CalculatingCacheSizeItem.swift b/submodules/SettingsUI/Sources/Data and Storage/CalculatingCacheSizeItem.swift index 0ebbeeb5710..2d6531d397d 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/CalculatingCacheSizeItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/CalculatingCacheSizeItem.swift @@ -225,7 +225,7 @@ private final class CalculatingCacheSizeItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/SettingsUI/Sources/Data and Storage/EnergyUsageBatteryLevelItem.swift b/submodules/SettingsUI/Sources/Data and Storage/EnergyUsageBatteryLevelItem.swift index 91e094a776d..f9b62b234e6 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/EnergyUsageBatteryLevelItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/EnergyUsageBatteryLevelItem.swift @@ -319,7 +319,7 @@ class EnergyUsageBatteryLevelItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/SettingsUI/Sources/Data and Storage/KeepMediaDurationPickerItem.swift b/submodules/SettingsUI/Sources/Data and Storage/KeepMediaDurationPickerItem.swift index b03ce8a30b4..462287b7d0f 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/KeepMediaDurationPickerItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/KeepMediaDurationPickerItem.swift @@ -286,7 +286,7 @@ private final class KeepMediaDurationPickerItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/SettingsUI/Sources/Data and Storage/MaximumCacheSizePickerItem.swift b/submodules/SettingsUI/Sources/Data and Storage/MaximumCacheSizePickerItem.swift index f9df190a5e9..cb8d9e831d0 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/MaximumCacheSizePickerItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/MaximumCacheSizePickerItem.swift @@ -301,7 +301,7 @@ private final class MaximumCacheSizePickerItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift index 56fa4ceb786..48ea6da9dbd 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift @@ -310,10 +310,10 @@ public enum ProxySettingsControllerMode { public func proxySettingsController(context: AccountContext, mode: ProxySettingsControllerMode = .default) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - return proxySettingsController(accountManager: context.sharedContext.accountManager, context: context, postbox: context.account.postbox, network: context.account.network, mode: mode, presentationData: presentationData, updatedPresentationData: context.sharedContext.presentationData) + return proxySettingsController(accountManager: context.sharedContext.accountManager, sharedContext: context.sharedContext, context: context, postbox: context.account.postbox, network: context.account.network, mode: mode, presentationData: presentationData, updatedPresentationData: context.sharedContext.presentationData) } -public func proxySettingsController(accountManager: AccountManager, context: AccountContext? = nil, postbox: Postbox, network: Network, mode: ProxySettingsControllerMode, presentationData: PresentationData, updatedPresentationData: Signal) -> ViewController { +public func proxySettingsController(accountManager: AccountManager, sharedContext: SharedAccountContext, context: AccountContext? = nil, postbox: Postbox, network: Network, mode: ProxySettingsControllerMode, presentationData: PresentationData, updatedPresentationData: Signal) -> ViewController { var pushControllerImpl: ((ViewController) -> Void)? var dismissImpl: (() -> Void)? let stateValue = Atomic(value: ProxySettingsControllerState()) @@ -341,7 +341,7 @@ public func proxySettingsController(accountManager: AccountManager ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - return proxyServerSettingsController(context: context, presentationData: presentationData, updatedPresentationData: context.sharedContext.presentationData, accountManager: context.sharedContext.accountManager, network: context.account.network, currentSettings: currentSettings) + return proxyServerSettingsController(sharedContext: context.sharedContext, context: context, presentationData: presentationData, updatedPresentationData: context.sharedContext.presentationData, accountManager: context.sharedContext.accountManager, network: context.account.network, currentSettings: currentSettings) } -func proxyServerSettingsController(context: AccountContext? = nil, presentationData: PresentationData, updatedPresentationData: Signal, accountManager: AccountManager, network: Network, currentSettings: ProxyServerSettings?) -> ViewController { +func proxyServerSettingsController(sharedContext: SharedAccountContext, context: AccountContext? = nil, presentationData: PresentationData, updatedPresentationData: Signal, accountManager: AccountManager, network: Network, currentSettings: ProxyServerSettings?) -> ViewController { var currentMode: ProxyServerSettingsControllerMode = .socks5 var currentUsername: String? var currentPassword: String? @@ -285,7 +285,7 @@ func proxyServerSettingsController(context: AccountContext? = nil, presentationD currentMode = .mtp } } else { - if let proxy = parseProxyUrl(UIPasteboard.general.string ?? "") { + if let proxy = parseProxyUrl(sharedContext: sharedContext, url: UIPasteboard.general.string ?? "") { if let secret = proxy.secret, let parsedSecret = MTProxySecret.parseData(secret) { pasteboardSettings = ProxyServerSettings(host: proxy.host, port: proxy.port, connection: .mtp(secret: parsedSecret.serialize())) } else { diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxySettingsActionItem.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxySettingsActionItem.swift index 49cd22a11ad..83aedaff2c5 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxySettingsActionItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxySettingsActionItem.swift @@ -267,7 +267,7 @@ private final class ProxySettingsActionItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxySettingsServerItem.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxySettingsServerItem.swift index c062830a953..0002bc61550 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxySettingsServerItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxySettingsServerItem.swift @@ -473,7 +473,7 @@ private final class ProxySettingsServerItemNode: ItemListRevealOptionsItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageItem.swift b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageItem.swift index c95f6355943..e50a2c31c3c 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageItem.swift @@ -310,7 +310,7 @@ private final class StorageUsageItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserItem.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserItem.swift index f30bf01a7a0..480f42c5f70 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserItem.swift @@ -297,7 +297,7 @@ private final class WebBrowserItemNode: ListViewItemNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/SettingsUI/Sources/DeleteAccountPeersItem.swift b/submodules/SettingsUI/Sources/DeleteAccountPeersItem.swift index 6d314dafe5d..0e7581bdbf3 100644 --- a/submodules/SettingsUI/Sources/DeleteAccountPeersItem.swift +++ b/submodules/SettingsUI/Sources/DeleteAccountPeersItem.swift @@ -297,7 +297,7 @@ class DeleteAccountPeersItemNode: ListViewItemNode, ItemListItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/SettingsUI/Sources/DeleteAccountPhoneItem.swift b/submodules/SettingsUI/Sources/DeleteAccountPhoneItem.swift index 0e0fe77e80e..d15613ecbc1 100644 --- a/submodules/SettingsUI/Sources/DeleteAccountPhoneItem.swift +++ b/submodules/SettingsUI/Sources/DeleteAccountPhoneItem.swift @@ -397,7 +397,7 @@ class DeleteAccountPhoneItemNode: ListViewItemNode, ItemListItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/SettingsUI/Sources/Notifications/NotificationsCategoryItemListItem.swift b/submodules/SettingsUI/Sources/Notifications/NotificationsCategoryItemListItem.swift index ab7c6efc08f..d948c1fadaf 100644 --- a/submodules/SettingsUI/Sources/Notifications/NotificationsCategoryItemListItem.swift +++ b/submodules/SettingsUI/Sources/Notifications/NotificationsCategoryItemListItem.swift @@ -429,7 +429,7 @@ public class NotificationsCategoryItemListItemNode: ListViewItemNode, ItemListIt } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift index 36b4e072380..a118eb72839 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift @@ -316,7 +316,7 @@ class ForwardPrivacyChatPreviewItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveHeaderItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveHeaderItem.swift index 982aec8f9e3..585f1d766df 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveHeaderItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveHeaderItem.swift @@ -99,7 +99,7 @@ class GlobalAutoremoveHeaderItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveScreen.swift b/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveScreen.swift index ca723fc5194..203bc2f9c35 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveScreen.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveScreen.swift @@ -315,7 +315,6 @@ public func globalAutoremoveScreen(context: AccountContext, initialValue: Int32, chatListFilters: nil, displayAutoremoveTimeout: true )), - options: [], filters: [.excludeSelf], isPeerEnabled: { peer in var canManage = false diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index 19ec37b50b5..56c06c7e88c 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -1446,7 +1446,7 @@ public func privacyAndSecurityController( emailChangeCompletion(codeController) })) } - codeController.updateData(number: "", email: email, codeType: .email(emailPattern: "", length: data.length, resetAvailablePeriod: nil, resetPendingDate: nil, appleSignInAllowed: false, setup: true), nextType: nil, timeout: nil, termsOfService: nil) + codeController.updateData(number: "", email: email, codeType: .email(emailPattern: "", length: data.length, resetAvailablePeriod: nil, resetPendingDate: nil, appleSignInAllowed: false, setup: true), nextType: nil, timeout: nil, termsOfService: nil, previousCodeType: nil, isPrevious: false) pushControllerImpl?(codeController, true) dismissCodeControllerImpl = { [weak codeController] in codeController?.dismiss() diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListRecentSessionItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListRecentSessionItem.swift index 209f328de86..bbe89b87897 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListRecentSessionItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListRecentSessionItem.swift @@ -560,7 +560,7 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListWebsiteItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListWebsiteItem.swift index 0618ba3b71b..78127a08f5e 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListWebsiteItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListWebsiteItem.swift @@ -488,7 +488,7 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsHeaderItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsHeaderItem.swift index 120f551b226..d7ce3be983c 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsHeaderItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsHeaderItem.swift @@ -173,7 +173,7 @@ class RecentSessionsHeaderItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift b/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift index 81a65b4ceab..e22d5e2f17c 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift @@ -305,18 +305,6 @@ private class RecentSessionScreenNode: ViewControllerTracingNode, ASScrollViewDe if !session.deviceModel.isEmpty { deviceString = session.deviceModel } -// if !session.platform.isEmpty { -// if !deviceString.isEmpty { -// deviceString += ", " -// } -// deviceString += session.platform -// } -// if !session.systemVersion.isEmpty { -// if !deviceString.isEmpty { -// deviceString += ", " -// } -// deviceString += session.systemVersion -// } title = deviceString device = "\(session.appName) \(appVersion)" location = session.country @@ -801,7 +789,18 @@ private class RecentSessionScreenNode: ViewControllerTracingNode, ASScrollViewDe transition.updateFrame(node: self.cancelButton, frame: cancelFrame) let fieldItemHeight: CGFloat = 44.0 - let fieldFrame = CGRect(x: inset, y: textFrame.maxY + 24.0, width: width - inset * 2.0, height: fieldItemHeight * 3.0) + var fieldFrame = CGRect(x: inset, y: textFrame.maxY + 24.0, width: width - inset * 2.0, height: fieldItemHeight * 2.0) + if !(self.ipValueNode.attributedText?.string ?? "").isEmpty { + fieldFrame.size.height += fieldItemHeight + + self.ipTitleNode.isHidden = false + self.ipValueNode.isHidden = false + self.secondSeparatorNode.isHidden = false + } else { + self.ipTitleNode.isHidden = true + self.ipValueNode.isHidden = true + self.secondSeparatorNode.isHidden = true + } transition.updateFrame(node: self.fieldBackgroundNode, frame: fieldFrame) let maxFieldTitleWidth = (width - inset * 4.0) * 0.4 @@ -827,11 +826,11 @@ private class RecentSessionScreenNode: ViewControllerTracingNode, ASScrollViewDe transition.updateFrame(node: self.secondSeparatorNode, frame: CGRect(x: fieldFrame.minX + inset, y: fieldFrame.minY + fieldItemHeight + fieldItemHeight, width: fieldFrame.width - inset, height: UIScreenPixel)) let locationTitleTextSize = self.locationTitleNode.updateLayout(CGSize(width: maxFieldTitleWidth, height: fieldItemHeight)) - let locationTitleTextFrame = CGRect(origin: CGPoint(x: fieldFrame.minX + inset, y: fieldFrame.minY + fieldItemHeight + fieldItemHeight + floorToScreenPixels((fieldItemHeight - locationTitleTextSize.height) / 2.0)), size: locationTitleTextSize) + let locationTitleTextFrame = CGRect(origin: CGPoint(x: fieldFrame.minX + inset, y: fieldFrame.maxY - fieldItemHeight + floorToScreenPixels((fieldItemHeight - locationTitleTextSize.height) / 2.0)), size: locationTitleTextSize) transition.updateFrame(node: self.locationTitleNode, frame: locationTitleTextFrame) let locationValueTextSize = self.locationValueNode.updateLayout(CGSize(width: fieldFrame.width - inset * 2.0 - locationTitleTextSize.width - 10.0, height: fieldItemHeight)) - let locationValueTextFrame = CGRect(origin: CGPoint(x: fieldFrame.maxX - locationValueTextSize.width - inset, y: fieldFrame.minY + fieldItemHeight + fieldItemHeight + floorToScreenPixels((fieldItemHeight - locationValueTextSize.height) / 2.0)), size: locationValueTextSize) + let locationValueTextFrame = CGRect(origin: CGPoint(x: fieldFrame.maxX - locationValueTextSize.width - inset, y: fieldFrame.maxY - fieldItemHeight + floorToScreenPixels((fieldItemHeight - locationValueTextSize.height) / 2.0)), size: locationValueTextSize) transition.updateFrame(node: self.locationValueNode, frame: locationValueTextFrame) let locationInfoTextSize = self.locationInfoNode.updateLayout(CGSize(width: fieldFrame.width - inset * 2.0, height: fieldItemHeight * 2.0)) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift index b08f6a4d623..4d02db8394a 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift @@ -1141,7 +1141,7 @@ public func selectivePrivacySettingsController( onlyUsers: false, disableChannels: true, disableBots: false - )), options: [], filters: [.excludeSelf])) + )), filters: [.excludeSelf])) addPeerDisposable.set((controller.result |> take(1) |> deliverOnMainQueue).start(next: { [weak controller] result in diff --git a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift index 3f7fc5c00b7..81ecb8434b1 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift @@ -355,7 +355,7 @@ public func selectivePrivacyPeersController(context: AccountContext, title: Stri onlyUsers: false, disableChannels: true, disableBots: false - )), options: [], alwaysEnabled: true)) + )), alwaysEnabled: true)) addPeerDisposable.set((controller.result |> take(1) |> deliverOnMainQueue).start(next: { [weak controller] result in diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchRecentItem.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchRecentItem.swift index 9dc72ac32fd..10f315d55b0 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchRecentItem.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchRecentItem.swift @@ -241,7 +241,7 @@ class SettingsSearchRecentItemNode: ItemListRevealOptionsItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchResultItem.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchResultItem.swift index 88f0a929c10..7e73002311d 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchResultItem.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchResultItem.swift @@ -271,7 +271,7 @@ class SettingsSearchResultItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionItem.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionItem.swift index 2175d31d9c4..f19fe421d9c 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionItem.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionItem.swift @@ -286,7 +286,7 @@ class BubbleSettingsRadiusItemNode: ListViewItemNode, ItemListItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/SettingsUI/Sources/ThemePickerGridItem.swift b/submodules/SettingsUI/Sources/ThemePickerGridItem.swift index ef7c648cb75..96485c5eb6c 100644 --- a/submodules/SettingsUI/Sources/ThemePickerGridItem.swift +++ b/submodules/SettingsUI/Sources/ThemePickerGridItem.swift @@ -536,7 +536,7 @@ class ThemeGridThemeItemNode: ListViewItemNode, ItemListItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift index 102f8473291..d000c3da2b4 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift @@ -9,7 +9,7 @@ import TelegramPresentationData import TelegramUIPreferences import AccountContext import ShareController -import CounterContollerTitleView +import CounterControllerTitleView import WallpaperResources import OverlayStatusController import AppBundle @@ -139,8 +139,8 @@ public final class ThemePreviewController: ViewController { isPreview = true } - let titleView = CounterContollerTitleView(theme: self.previewTheme) - titleView.title = CounterContollerTitle(title: themeName, counter: hasInstallsCount ? " " : "") + let titleView = CounterControllerTitleView(theme: self.previewTheme) + titleView.title = CounterControllerTitle(title: themeName, counter: hasInstallsCount ? " " : "") self.navigationItem.titleView = titleView self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) @@ -154,8 +154,8 @@ public final class ThemePreviewController: ViewController { self.disposable = (combineLatest(self.theme.get(), self.presentationTheme.get()) |> deliverOnMainQueue).start(next: { [weak self] theme, presentationTheme in if let strongSelf = self, let theme = theme { - let titleView = CounterContollerTitleView(theme: strongSelf.previewTheme) - titleView.title = CounterContollerTitle(title: themeName, counter: hasInstallsCount ? strongSelf.presentationData.strings.Theme_UsersCount(max(1, theme.installCount ?? 0)) : "") + let titleView = CounterControllerTitleView(theme: strongSelf.previewTheme) + titleView.title = CounterControllerTitle(title: themeName, counter: hasInstallsCount ? strongSelf.presentationData.strings.Theme_UsersCount(max(1, theme.installCount ?? 0)) : "") strongSelf.navigationItem.titleView = titleView strongSelf.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationTheme: presentationTheme, presentationStrings: strongSelf.presentationData.strings)) } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAccentColorItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAccentColorItem.swift index a0a0be5cfce..9a18c3b9ae2 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAccentColorItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAccentColorItem.swift @@ -429,8 +429,8 @@ private final class ThemeSettingsAccentColorIconItemNode : ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - super.animateInsertion(currentTimestamp, duration: duration, short: short) + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + super.animateInsertion(currentTimestamp, duration: duration, options: options) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } @@ -561,8 +561,8 @@ private final class ThemeSettingsAccentColorPickerItemNode : ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - super.animateInsertion(currentTimestamp, duration: duration, short: short) + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + super.animateInsertion(currentTimestamp, duration: duration, options: options) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } @@ -946,7 +946,7 @@ class ThemeSettingsAccentColorItemNode: ListViewItemNode, ItemListItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift index 6de6a2ef22c..12dc6a6fa65 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift @@ -414,7 +414,7 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsBrightnessItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsBrightnessItem.swift index a7670057b21..c4fa77d58ba 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsBrightnessItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsBrightnessItem.swift @@ -227,7 +227,7 @@ class ThemeSettingsBrightnessItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift index 3cfc9025b24..b431aff3f38 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift @@ -301,7 +301,7 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift index cdc66009189..3928a01f9dc 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift @@ -338,7 +338,7 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { case let .iconItem(theme, strings, icons, isPremium, value): return ThemeSettingsAppIconItem(theme: theme, strings: strings, sectionId: self.section, icons: icons, isPremium: isPremium, currentIconName: value, updated: { icon in arguments.selectAppIcon(icon) - }) + }, tag: ThemeSettingsEntryTag.icon) case .powerSaving: return ItemListDisclosureItem(presentationData: presentationData, icon: nil, title: presentationData.strings.AppearanceSettings_Animations, label: "", labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openPowerSavingSettings() diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsFontSizeItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsFontSizeItem.swift index 09c3501a100..aff462c9ae1 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsFontSizeItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsFontSizeItem.swift @@ -311,7 +311,7 @@ class ThemeSettingsFontSizeItemNode: ListViewItemNode, ItemListItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/ShareItems/Sources/ShareItems.swift b/submodules/ShareItems/Sources/ShareItems.swift index b935a76c84e..4742e79fad3 100644 --- a/submodules/ShareItems/Sources/ShareItems.swift +++ b/submodules/ShareItems/Sources/ShareItems.swift @@ -35,15 +35,11 @@ public enum PreparedShareItems { } private func scalePhotoImage(_ image: UIImage, dimensions: CGSize) -> UIImage? { - if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { - let format = UIGraphicsImageRendererFormat() - format.scale = 1.0 - let renderer = UIGraphicsImageRenderer(size: dimensions, format: format) - return renderer.image { _ in - image.draw(in: CGRect(origin: .zero, size: dimensions)) - } - } else { - return TGScaleImageToPixelSize(image, dimensions) + let format = UIGraphicsImageRendererFormat() + format.scale = 1.0 + let renderer = UIGraphicsImageRenderer(size: dimensions, format: format) + return renderer.image { _ in + image.draw(in: CGRect(origin: .zero, size: dimensions)) } } @@ -234,7 +230,7 @@ private func preparedShareItem(postbox: Postbox, network: Network, to peerId: Pe } ) } else { - let scaledImage = TGScaleImageToPixelSize(image, CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale).fitted(CGSize(width: 1280.0, height: 1280.0)))! + let scaledImage = scalePhotoImage(image, dimensions: CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale).fitted(CGSize(width: 1280.0, height: 1280.0)))! let imageData = scaledImage.jpegData(compressionQuality: 0.54)! return .single(.preparing(false)) |> then( diff --git a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift index fa80ebebbd6..7074cd24541 100644 --- a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift +++ b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift @@ -838,8 +838,12 @@ public final class SolidRoundedButtonNode: ASDisplayNode { let diameter: CGFloat = self.buttonHeight - 22.0 let progressFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(buttonOffset + (buttonWidth - diameter) / 2.0), y: floorToScreenPixels((self.buttonHeight - diameter) / 2.0)), size: CGSize(width: diameter, height: diameter)) progressNode.frame = progressFrame - progressNode.image = generateIndefiniteActivityIndicatorImage(color: self.theme.foregroundColor, diameter: diameter, lineWidth: 3.0) - + + if !self.isEnabled, let disabledForegroundColor = self.theme.disabledForegroundColor { + progressNode.image = generateIndefiniteActivityIndicatorImage(color: disabledForegroundColor, diameter: diameter, lineWidth: 3.0) + } else { + progressNode.image = generateIndefiniteActivityIndicatorImage(color: self.theme.foregroundColor, diameter: diameter, lineWidth: 3.0) + } self.addSubnode(progressNode) } diff --git a/submodules/StatisticsUI/Sources/BoostLevelHeaderItem.swift b/submodules/StatisticsUI/Sources/BoostLevelHeaderItem.swift index 67a0b2cd11f..9270713394e 100644 --- a/submodules/StatisticsUI/Sources/BoostLevelHeaderItem.swift +++ b/submodules/StatisticsUI/Sources/BoostLevelHeaderItem.swift @@ -155,7 +155,7 @@ class IncreaseLimitHeaderItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/StatisticsUI/Sources/BoostsTabsItem.swift b/submodules/StatisticsUI/Sources/BoostsTabsItem.swift index c5e30fa02e5..6a71df18c57 100644 --- a/submodules/StatisticsUI/Sources/BoostsTabsItem.swift +++ b/submodules/StatisticsUI/Sources/BoostsTabsItem.swift @@ -257,7 +257,7 @@ private final class BoostsTabsItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index f693400079f..af89fcc08b0 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -1401,11 +1401,11 @@ private func monetizationEntries( isCreator = true } entries.append(.adsBalanceTitle(presentationData.theme, presentationData.strings.Monetization_BalanceTitle)) - entries.append(.adsBalance(presentationData.theme, data, isCreator && data.availableBalance > 0, monetizationConfiguration.withdrawalAvailable, diamond)) + entries.append(.adsBalance(presentationData.theme, data, isCreator && data.balances.availableBalance > 0, monetizationConfiguration.withdrawalAvailable, diamond)) if isCreator { let withdrawalInfoText: String - if data.availableBalance == 0 { + if data.balances.availableBalance == 0 { withdrawalInfoText = presentationData.strings.Monetization_Balance_ZeroInfo } else if monetizationConfiguration.withdrawalAvailable { withdrawalInfoText = presentationData.strings.Monetization_Balance_AvailableInfo @@ -1579,7 +1579,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD let boostsContext = ChannelBoostersContext(account: context.account, peerId: peerId, gift: false) let giftsContext = ChannelBoostersContext(account: context.account, peerId: peerId, gift: true) - let revenueContext = RevenueStatsContext(postbox: context.account.postbox, network: context.account.network, peerId: peerId) + let revenueContext = RevenueStatsContext(account: context.account, peerId: peerId) let revenueState = Promise() revenueState.set(.single(nil) |> then(revenueContext.state |> map(Optional.init))) @@ -2127,11 +2127,9 @@ public func channelStatsController(context: AccountContext, updatedPresentationD |> deliverOnMainQueue).start(error: { error in let controller = revenueWithdrawalController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, initialError: error, present: { c, _ in presentImpl?(c) - }, completion: { [weak revenueContext] url in + }, completion: { url in let presentationData = context.sharedContext.currentPresentationData.with { $0 } context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) - - revenueContext?.reload() }) presentImpl?(controller) })) diff --git a/submodules/StatisticsUI/Sources/CpmSliderItem.swift b/submodules/StatisticsUI/Sources/CpmSliderItem.swift deleted file mode 100644 index 81862904a3b..00000000000 --- a/submodules/StatisticsUI/Sources/CpmSliderItem.swift +++ /dev/null @@ -1,322 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import SwiftSignalKit -import TelegramCore -import TelegramUIPreferences -import TelegramPresentationData -import LegacyComponents -import ItemListUI -import PresentationDataUtils -import EmojiTextAttachmentView -import TextFormat -import AccountContext -import UIKitRuntimeUtils - -final class CpmSliderItem: ListViewItem, ItemListItem { - let context: AccountContext - let theme: PresentationTheme - let strings: PresentationStrings - let value: Int32 - let animatedEmoji: TelegramMediaFile? - let sectionId: ItemListSectionId - let updated: (Int32) -> Void - - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, value: Int32, enabled: Bool, animatedEmoji: TelegramMediaFile?, sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) { - self.context = context - self.theme = theme - self.strings = strings - self.value = value - self.animatedEmoji = animatedEmoji - self.sectionId = sectionId - self.updated = updated - } - - func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { - async { - let node = CpmSliderItemNode() - let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) - - node.contentSize = layout.contentSize - node.insets = layout.insets - - Queue.mainQueue().async { - completion(node, { - return (nil, { _ in apply() }) - }) - } - } - } - - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { - Queue.mainQueue().async { - if let nodeValue = node() as? CpmSliderItemNode { - let makeLayout = nodeValue.asyncLayout() - - async { - let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) - Queue.mainQueue().async { - completion(layout, { _ in - apply() - }) - } - } - } - } - } -} - -private let allowedValues: [Int32] = [1, 2, 3, 4, 5] - -class CpmSliderItemNode: ListViewItemNode { - private let backgroundNode: ASDisplayNode - private let topStripeNode: ASDisplayNode - private let bottomStripeNode: ASDisplayNode - private let maskNode: ASImageNode - - private let minTextNode: TextNode - private let maxTextNode: TextNode - private let textNode: TextNode - private var sliderView: TGPhotoEditorSliderView? - private var animatedEmojiLayer: InlineStickerItemLayer? - private var maxAnimatedEmojiLayer: InlineStickerItemLayer? - - private var item: CpmSliderItem? - private var layoutParams: ListViewItemLayoutParams? - private var reportedValue: Int32? - - init() { - self.backgroundNode = ASDisplayNode() - self.backgroundNode.isLayerBacked = true - - self.maskNode = ASImageNode() - - self.topStripeNode = ASDisplayNode() - self.topStripeNode.isLayerBacked = true - - self.bottomStripeNode = ASDisplayNode() - self.bottomStripeNode.isLayerBacked = true - - self.textNode = TextNode() - self.textNode.isUserInteractionEnabled = false - self.textNode.displaysAsynchronously = false - - self.minTextNode = TextNode() - self.minTextNode.isUserInteractionEnabled = false - self.minTextNode.displaysAsynchronously = false - - self.maxTextNode = TextNode() - self.maxTextNode.isUserInteractionEnabled = false - self.maxTextNode.displaysAsynchronously = false - - super.init(layerBacked: false, dynamicBounce: false) - - self.addSubnode(self.textNode) - self.addSubnode(self.minTextNode) - self.addSubnode(self.maxTextNode) - } - - override func didLoad() { - super.didLoad() - - self.view.disablesInteractiveTransitionGestureRecognizer = true - - let sliderView = TGPhotoEditorSliderView() - sliderView.enablePanHandling = true - sliderView.trackCornerRadius = 2.0 - sliderView.lineSize = 4.0 - sliderView.dotSize = 5.0 - sliderView.minimumValue = 0.0 - sliderView.maximumValue = 1.0 - sliderView.startValue = 0.0 - sliderView.displayEdges = true - sliderView.disablesInteractiveTransitionGestureRecognizer = true - if let item = self.item, let params = self.layoutParams { - sliderView.value = CGFloat(item.value) / 50.0 - sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor - sliderView.backColor = item.theme.list.itemSwitchColors.frameColor - sliderView.startColor = item.theme.list.itemSwitchColors.frameColor - sliderView.trackColor = item.theme.list.itemAccentColor - sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme) - - sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0)) - sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX) - } - self.view.addSubview(sliderView) - sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged) - self.sliderView = sliderView - } - - func asyncLayout() -> (_ item: CpmSliderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { - let currentItem = self.item - let makeTextLayout = TextNode.asyncLayout(self.textNode) - let makeMinTextLayout = TextNode.asyncLayout(self.minTextNode) - let makeMaxTextLayout = TextNode.asyncLayout(self.maxTextNode) - - return { item, params, neighbors in - var themeUpdated = false - if currentItem?.theme !== item.theme { - themeUpdated = true - } - - let contentSize: CGSize - let insets: UIEdgeInsets - let separatorHeight = UIScreenPixel - - //TODO:localize - let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.value == 0 ? "No Ads" : "\(item.value) CPM", font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) - - let (minTextLayout, minTextApply) = makeMinTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "No Ads", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) - - let (maxTextLayout, maxTextApply) = makeMaxTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "50 CPM", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) - - contentSize = CGSize(width: params.width, height: 88.0) - insets = itemListNeighborsGroupedInsets(neighbors, params) - - let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) - let layoutSize = layout.size - - return (layout, { [weak self] in - if let strongSelf = self { - strongSelf.item = item - strongSelf.layoutParams = params - - strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor - strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor - - if strongSelf.backgroundNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) - } - if strongSelf.topStripeNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) - } - if strongSelf.bottomStripeNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) - } - if strongSelf.maskNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.maskNode, at: 3) - } - let hasCorners = itemListHasRoundedBlockLayout(params) - var hasTopCorners = false - var hasBottomCorners = false - switch neighbors.top { - case .sameSection(false): - strongSelf.topStripeNode.isHidden = true - default: - hasTopCorners = true - strongSelf.topStripeNode.isHidden = hasCorners - } - let bottomStripeInset: CGFloat - let bottomStripeOffset: CGFloat - switch neighbors.bottom { - case .sameSection(false): - bottomStripeInset = 0.0 - bottomStripeOffset = -separatorHeight - strongSelf.bottomStripeNode.isHidden = false - default: - bottomStripeInset = 0.0 - hasBottomCorners = true - strongSelf.bottomStripeNode.isHidden = hasCorners - bottomStripeOffset = 0.0 - } - - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil - - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) - strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) - strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) - strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) - - let _ = textApply() - let textFrame = CGRect(origin: CGPoint(x: floor((params.width - textLayout.size.width) / 2.0), y: 12.0), size: textLayout.size) - strongSelf.textNode.frame = textFrame - - if let animatedEmoji = item.animatedEmoji { - let itemSize = floorToScreenPixels(17.0 * 20.0 / 17.0) - - var itemFrame = CGRect(origin: CGPoint(x: textFrame.minX - itemSize / 2.0 - 1.0, y: textFrame.midY), size: CGSize()).insetBy(dx: -itemSize / 2.0, dy: -itemSize / 2.0) - itemFrame.origin.x = floorToScreenPixels(itemFrame.origin.x) - itemFrame.origin.y = floorToScreenPixels(itemFrame.origin.y) - - let itemLayer: InlineStickerItemLayer - if let current = strongSelf.animatedEmojiLayer { - itemLayer = current - } else { - let pointSize = floor(itemSize * 1.3) - itemLayer = InlineStickerItemLayer(context: item.context, userLocation: .other, attemptSynchronousLoad: true, emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: animatedEmoji.fileId.id, file: animatedEmoji, custom: nil), file: animatedEmoji, cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: item.theme.list.mediaPlaceholderColor, pointSize: CGSize(width: pointSize, height: pointSize), dynamicColor: nil) - strongSelf.animatedEmojiLayer = itemLayer - strongSelf.layer.addSublayer(itemLayer) - - itemLayer.isVisibleForAnimations = true - } - itemLayer.frame = itemFrame - } - - let _ = minTextApply() - strongSelf.minTextNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 16.0, y: 16.0), size: minTextLayout.size) - - let _ = maxTextApply() - let maxTextFrame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 16.0 - maxTextLayout.size.width, y: 16.0), size: maxTextLayout.size) - strongSelf.maxTextNode.frame = maxTextFrame - - if let animatedEmoji = item.animatedEmoji { - let itemSize = floorToScreenPixels(13.0 * 20.0 / 17.0) - - var itemFrame = CGRect(origin: CGPoint(x: maxTextFrame.minX - itemSize / 2.0 - 1.0, y: maxTextFrame.midY), size: CGSize()).insetBy(dx: -itemSize / 2.0, dy: -itemSize / 2.0) - itemFrame.origin.x = floorToScreenPixels(itemFrame.origin.x) - itemFrame.origin.y = floorToScreenPixels(itemFrame.origin.y) - - let itemLayer: InlineStickerItemLayer - if let current = strongSelf.maxAnimatedEmojiLayer { - itemLayer = current - } else { - let pointSize = floor(itemSize * 1.3) - itemLayer = InlineStickerItemLayer(context: item.context, userLocation: .other, attemptSynchronousLoad: true, emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: animatedEmoji.fileId.id, file: animatedEmoji, custom: nil), file: animatedEmoji, cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: item.theme.list.mediaPlaceholderColor, pointSize: CGSize(width: pointSize, height: pointSize), dynamicColor: nil) - strongSelf.maxAnimatedEmojiLayer = itemLayer - strongSelf.layer.addSublayer(itemLayer) - - itemLayer.isVisibleForAnimations = true - - if let filter = makeMonochromeFilter() { - filter.setValue([1.0, 1.0, 1.0, 1.0] as [NSNumber], forKey: "inputColor") - filter.setValue(1.0 as NSNumber, forKey: "inputAmount") - itemLayer.filters = [filter] - } - } - itemLayer.frame = itemFrame - } - - if let sliderView = strongSelf.sliderView { - if themeUpdated { - sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor - sliderView.backColor = item.theme.list.itemSwitchColors.frameColor - sliderView.startColor = item.theme.list.itemSwitchColors.frameColor - sliderView.trackColor = item.theme.list.itemAccentColor - sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme) - } - - sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0)) - sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX) - } - } - }) - } - } - - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) - } - - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { - self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) - } - - @objc func sliderValueChanged() { - guard let item = self.item, let sliderView = self.sliderView else { - return - } - item.updated(Int32(sliderView.value * 50.0)) - } -} diff --git a/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift b/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift index a21ddae0052..2e7ad59bdc7 100644 --- a/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift +++ b/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift @@ -158,13 +158,13 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode { let integralFont = Font.with(size: 48.0, design: .round, weight: .semibold) let fractionalFont = Font.with(size: 24.0, design: .round, weight: .semibold) - let cryptoValue = formatBalanceText(item.stats.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) + let cryptoValue = formatBalanceText(item.stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) let amountString = amountAttributedString(cryptoValue, integralFont: integralFont, fractionalFont: fractionalFont, color: item.presentationData.theme.list.itemPrimaryTextColor) let (balanceLayout, balanceApply) = makeBalanceTextLayout(TextNodeLayoutArguments(attributedString: amountString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) - let value = item.stats.availableBalance == 0 ? "" : "≈\(formatUsdValue(item.stats.availableBalance, rate: item.stats.usdRate))" + let value = item.stats.balances.availableBalance == 0 ? "" : "≈\(formatUsdValue(item.stats.balances.availableBalance, rate: item.stats.usdRate))" let (valueLayout, valueApply) = makeValueTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: value, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let verticalInset: CGFloat = 13.0 @@ -319,7 +319,7 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/StatisticsUI/Sources/StatsGraphItem.swift b/submodules/StatisticsUI/Sources/StatsGraphItem.swift index b963c71b1a5..4a618efcb90 100644 --- a/submodules/StatisticsUI/Sources/StatsGraphItem.swift +++ b/submodules/StatisticsUI/Sources/StatsGraphItem.swift @@ -283,7 +283,7 @@ class StatsGraphItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/StatisticsUI/Sources/StatsMessageItem.swift b/submodules/StatisticsUI/Sources/StatsMessageItem.swift index f3a612ee4c9..64323ad58d9 100644 --- a/submodules/StatisticsUI/Sources/StatsMessageItem.swift +++ b/submodules/StatisticsUI/Sources/StatsMessageItem.swift @@ -754,7 +754,7 @@ final class StatsMessageItemNode: ListViewItemNode, ItemListItemNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/StatisticsUI/Sources/StatsOverviewItem.swift b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift index 2c7d87d7835..80510f2a7d2 100644 --- a/submodules/StatisticsUI/Sources/StatsOverviewItem.swift +++ b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift @@ -732,9 +732,9 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - formatBalanceText(stats.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + formatBalanceText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), item.presentationData.strings.Monetization_Overview_Available, - (stats.availableBalance == 0 ? "" : "≈\(formatUsdValue(stats.availableBalance, rate: stats.usdRate))", .generic), + (stats.balances.availableBalance == 0 ? "" : "≈\(formatUsdValue(stats.balances.availableBalance, rate: stats.usdRate))", .generic), true ) @@ -742,9 +742,9 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - formatBalanceText(stats.currentBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + formatBalanceText(stats.balances.currentBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), item.presentationData.strings.Monetization_Overview_Current, - (stats.currentBalance == 0 ? "" : "≈\(formatUsdValue(stats.currentBalance, rate: stats.usdRate))", .generic), + (stats.balances.currentBalance == 0 ? "" : "≈\(formatUsdValue(stats.balances.currentBalance, rate: stats.usdRate))", .generic), true ) @@ -752,9 +752,9 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - formatBalanceText(stats.overallRevenue, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + formatBalanceText(stats.balances.overallRevenue, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), item.presentationData.strings.Monetization_Overview_Total, - (stats.overallRevenue == 0 ? "" : "≈\(formatUsdValue(stats.overallRevenue, rate: stats.usdRate))", .generic), + (stats.balances.overallRevenue == 0 ? "" : "≈\(formatUsdValue(stats.balances.overallRevenue, rate: stats.usdRate))", .generic), true ) @@ -904,7 +904,7 @@ class StatsOverviewItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/SwitchNode/Sources/IconSwitchNode.swift b/submodules/SwitchNode/Sources/IconSwitchNode.swift index bdf0fe9db9d..83b3b46d61b 100644 --- a/submodules/SwitchNode/Sources/IconSwitchNode.swift +++ b/submodules/SwitchNode/Sources/IconSwitchNode.swift @@ -69,6 +69,8 @@ open class IconSwitchNode: ASDisplayNode { } } + private var _isLocked: Bool = false + override public init() { super.init() @@ -82,8 +84,8 @@ open class IconSwitchNode: ASDisplayNode { (self.view as! UISwitch).backgroundColor = self.backgroundColor (self.view as! UISwitch).tintColor = self.frameColor - //(self.view as! UISwitch).thumbTintColor = self.handleColor (self.view as! UISwitch).onTintColor = self.contentColor + (self.view as? TGIconSwitchView)?.updateIsLocked(self._isLocked) (self.view as! IconSwitchNodeView).setNegativeContentColor(self.negativeContentColor) (self.view as! IconSwitchNodeView).setPositiveContentColor(self.positiveContentColor) @@ -99,6 +101,16 @@ open class IconSwitchNode: ASDisplayNode { } } + public func updateIsLocked(_ value: Bool) { + if self._isLocked == value { + return + } + self._isLocked = value + if self.isNodeLoaded { + (self.view as? TGIconSwitchView)?.updateIsLocked(value) + } + } + override open func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { return CGSize(width: 51.0, height: 31.0) } diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 849ffdcc9c8..eb40aad7f31 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -105,6 +105,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-944407322] = { return Api.BotMenuButton.parse_botMenuButton($0) } dict[1113113093] = { return Api.BotMenuButton.parse_botMenuButtonCommands($0) } dict[1966318984] = { return Api.BotMenuButton.parse_botMenuButtonDefault($0) } + dict[-2076642874] = { return Api.BroadcastRevenueBalances.parse_broadcastRevenueBalances($0) } dict[1434332356] = { return Api.BroadcastRevenueTransaction.parse_broadcastRevenueTransactionProceeds($0) } dict[1121127726] = { return Api.BroadcastRevenueTransaction.parse_broadcastRevenueTransactionRefund($0) } dict[1515784568] = { return Api.BroadcastRevenueTransaction.parse_broadcastRevenueTransactionWithdrawal($0) } @@ -251,6 +252,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1128644211] = { return Api.EmailVerifyPurpose.parse_emailVerifyPurposeLoginSetup($0) } dict[-1141565819] = { return Api.EmailVerifyPurpose.parse_emailVerifyPurposePassport($0) } dict[2056961449] = { return Api.EmojiGroup.parse_emojiGroup($0) } + dict[-2133693241] = { return Api.EmojiGroup.parse_emojiGroupGreeting($0) } + dict[154914612] = { return Api.EmojiGroup.parse_emojiGroupPremium($0) } dict[-709641735] = { return Api.EmojiKeyword.parse_emojiKeyword($0) } dict[594408994] = { return Api.EmojiKeyword.parse_emojiKeywordDeleted($0) } dict[1556570557] = { return Api.EmojiKeywordsDifference.parse_emojiKeywordsDifference($0) } @@ -710,8 +713,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[236446268] = { return Api.PhotoSize.parse_photoSizeEmpty($0) } dict[-96535659] = { return Api.PhotoSize.parse_photoSizeProgressive($0) } dict[-525288402] = { return Api.PhotoSize.parse_photoStrippedSize($0) } - dict[-2032041631] = { return Api.Poll.parse_poll($0) } - dict[1823064809] = { return Api.PollAnswer.parse_pollAnswer($0) } + dict[1484026161] = { return Api.Poll.parse_poll($0) } + dict[-15277366] = { return Api.PollAnswer.parse_pollAnswer($0) } dict[997055186] = { return Api.PollAnswerVoters.parse_pollAnswerVoters($0) } dict[2061444128] = { return Api.PollResults.parse_pollResults($0) } dict[1558266229] = { return Api.PopularContact.parse_popularContact($0) } @@ -925,6 +928,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-997782967] = { return Api.Update.parse_updateBotStopped($0) } dict[-2095595325] = { return Api.Update.parse_updateBotWebhookJSON($0) } dict[-1684914010] = { return Api.Update.parse_updateBotWebhookJSONQuery($0) } + dict[1550177112] = { return Api.Update.parse_updateBroadcastRevenueTransactions($0) } dict[1666927625] = { return Api.Update.parse_updateChannel($0) } dict[-1304443240] = { return Api.Update.parse_updateChannelAvailableMessages($0) } dict[-761649164] = { return Api.Update.parse_updateChannelMessageForwards($0) } @@ -1135,6 +1139,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-2113903484] = { return Api.auth.SentCodeType.parse_sentCodeTypeMissedCall($0) } dict[-1521934870] = { return Api.auth.SentCodeType.parse_sentCodeTypeSetUpEmailRequired($0) } dict[-1073693790] = { return Api.auth.SentCodeType.parse_sentCodeTypeSms($0) } + dict[-1284008785] = { return Api.auth.SentCodeType.parse_sentCodeTypeSmsPhrase($0) } + dict[-1542017919] = { return Api.auth.SentCodeType.parse_sentCodeTypeSmsWord($0) } dict[-391678544] = { return Api.bots.BotInfo.parse_botInfo($0) } dict[-309659827] = { return Api.channels.AdminLogResults.parse_adminLogResults($0) } dict[-541588713] = { return Api.channels.ChannelParticipant.parse_channelParticipant($0) } @@ -1299,7 +1305,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1696454430] = { return Api.premium.MyBoosts.parse_myBoosts($0) } dict[-594852657] = { return Api.smsjobs.EligibilityToJoin.parse_eligibleToJoin($0) } dict[720277905] = { return Api.smsjobs.Status.parse_status($0) } - dict[-797226067] = { return Api.stats.BroadcastRevenueStats.parse_broadcastRevenueStats($0) } + dict[1409802903] = { return Api.stats.BroadcastRevenueStats.parse_broadcastRevenueStats($0) } dict[-2028632986] = { return Api.stats.BroadcastRevenueTransactions.parse_broadcastRevenueTransactions($0) } dict[-328886473] = { return Api.stats.BroadcastRevenueWithdrawalUrl.parse_broadcastRevenueWithdrawalUrl($0) } dict[963421692] = { return Api.stats.BroadcastStats.parse_broadcastStats($0) } @@ -1356,7 +1362,7 @@ public extension Api { return parser(reader) } else { - telegramApiLog("Type constructor \(String(UInt32(bitPattern: signature), radix: 16, uppercase: false)) not found") + telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found") return nil } } @@ -1450,6 +1456,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.BotMenuButton: _1.serialize(buffer, boxed) + case let _1 as Api.BroadcastRevenueBalances: + _1.serialize(buffer, boxed) case let _1 as Api.BroadcastRevenueTransaction: _1.serialize(buffer, boxed) case let _1 as Api.BusinessAwayMessage: diff --git a/submodules/TelegramApi/Sources/Api18.swift b/submodules/TelegramApi/Sources/Api18.swift index a040d113881..86d042f173f 100644 --- a/submodules/TelegramApi/Sources/Api18.swift +++ b/submodules/TelegramApi/Sources/Api18.swift @@ -436,17 +436,17 @@ public extension Api { } public extension Api { enum Poll: TypeConstructorDescription { - case poll(id: Int64, flags: Int32, question: String, answers: [Api.PollAnswer], closePeriod: Int32?, closeDate: Int32?) + case poll(id: Int64, flags: Int32, question: Api.TextWithEntities, answers: [Api.PollAnswer], closePeriod: Int32?, closeDate: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { case .poll(let id, let flags, let question, let answers, let closePeriod, let closeDate): if boxed { - buffer.appendInt32(-2032041631) + buffer.appendInt32(1484026161) } serializeInt64(id, buffer: buffer, boxed: false) serializeInt32(flags, buffer: buffer, boxed: false) - serializeString(question, buffer: buffer, boxed: false) + question.serialize(buffer, true) buffer.appendInt32(481674261) buffer.appendInt32(Int32(answers.count)) for item in answers { @@ -470,8 +470,10 @@ public extension Api { _1 = reader.readInt64() var _2: Int32? _2 = reader.readInt32() - var _3: String? - _3 = parseString(reader) + var _3: Api.TextWithEntities? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.TextWithEntities + } var _4: [Api.PollAnswer]? if let _ = reader.readInt32() { _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PollAnswer.self) @@ -498,15 +500,15 @@ public extension Api { } public extension Api { enum PollAnswer: TypeConstructorDescription { - case pollAnswer(text: String, option: Buffer) + case pollAnswer(text: Api.TextWithEntities, option: Buffer) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { case .pollAnswer(let text, let option): if boxed { - buffer.appendInt32(1823064809) + buffer.appendInt32(-15277366) } - serializeString(text, buffer: buffer, boxed: false) + text.serialize(buffer, true) serializeBytes(option, buffer: buffer, boxed: false) break } @@ -520,8 +522,10 @@ public extension Api { } public static func parse_pollAnswer(_ reader: BufferReader) -> PollAnswer? { - var _1: String? - _1 = parseString(reader) + var _1: Api.TextWithEntities? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.TextWithEntities + } var _2: Buffer? _2 = parseBytes(reader) let _c1 = _1 != nil diff --git a/submodules/TelegramApi/Sources/Api2.swift b/submodules/TelegramApi/Sources/Api2.swift index c803cd59c80..51d1fb1d7fe 100644 --- a/submodules/TelegramApi/Sources/Api2.swift +++ b/submodules/TelegramApi/Sources/Api2.swift @@ -724,6 +724,50 @@ public extension Api { } } +public extension Api { + enum BroadcastRevenueBalances: TypeConstructorDescription { + case broadcastRevenueBalances(currentBalance: Int64, availableBalance: Int64, overallRevenue: Int64) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .broadcastRevenueBalances(let currentBalance, let availableBalance, let overallRevenue): + if boxed { + buffer.appendInt32(-2076642874) + } + serializeInt64(currentBalance, buffer: buffer, boxed: false) + serializeInt64(availableBalance, buffer: buffer, boxed: false) + serializeInt64(overallRevenue, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .broadcastRevenueBalances(let currentBalance, let availableBalance, let overallRevenue): + return ("broadcastRevenueBalances", [("currentBalance", currentBalance as Any), ("availableBalance", availableBalance as Any), ("overallRevenue", overallRevenue as Any)]) + } + } + + public static func parse_broadcastRevenueBalances(_ reader: BufferReader) -> BroadcastRevenueBalances? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int64? + _3 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.BroadcastRevenueBalances.broadcastRevenueBalances(currentBalance: _1!, availableBalance: _2!, overallRevenue: _3!) + } + else { + return nil + } + } + + } +} public extension Api { enum BroadcastRevenueTransaction: TypeConstructorDescription { case broadcastRevenueTransactionProceeds(amount: Int64, fromDate: Int32, toDate: Int32) @@ -1166,49 +1210,3 @@ public extension Api { } } -public extension Api { - enum BusinessLocation: TypeConstructorDescription { - case businessLocation(flags: Int32, geoPoint: Api.GeoPoint?, address: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .businessLocation(let flags, let geoPoint, let address): - if boxed { - buffer.appendInt32(-1403249929) - } - serializeInt32(flags, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {geoPoint!.serialize(buffer, true)} - serializeString(address, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .businessLocation(let flags, let geoPoint, let address): - return ("businessLocation", [("flags", flags as Any), ("geoPoint", geoPoint as Any), ("address", address as Any)]) - } - } - - public static func parse_businessLocation(_ reader: BufferReader) -> BusinessLocation? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.GeoPoint? - if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.GeoPoint - } } - var _3: String? - _3 = parseString(reader) - let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.BusinessLocation.businessLocation(flags: _1!, geoPoint: _2, address: _3!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api24.swift b/submodules/TelegramApi/Sources/Api24.swift index 146871a967d..eb7a764d63d 100644 --- a/submodules/TelegramApi/Sources/Api24.swift +++ b/submodules/TelegramApi/Sources/Api24.swift @@ -72,6 +72,7 @@ public extension Api { case updateBotStopped(userId: Int64, date: Int32, stopped: Api.Bool, qts: Int32) case updateBotWebhookJSON(data: Api.DataJSON) case updateBotWebhookJSONQuery(queryId: Int64, data: Api.DataJSON, timeout: Int32) + case updateBroadcastRevenueTransactions(balances: Api.BroadcastRevenueBalances) case updateChannel(channelId: Int64) case updateChannelAvailableMessages(channelId: Int64, availableMinId: Int32) case updateChannelMessageForwards(channelId: Int64, id: Int32, forwards: Int32) @@ -395,6 +396,12 @@ public extension Api { data.serialize(buffer, true) serializeInt32(timeout, buffer: buffer, boxed: false) break + case .updateBroadcastRevenueTransactions(let balances): + if boxed { + buffer.appendInt32(1550177112) + } + balances.serialize(buffer, true) + break case .updateChannel(let channelId): if boxed { buffer.appendInt32(1666927625) @@ -1406,6 +1413,8 @@ public extension Api { return ("updateBotWebhookJSON", [("data", data as Any)]) case .updateBotWebhookJSONQuery(let queryId, let data, let timeout): return ("updateBotWebhookJSONQuery", [("queryId", queryId as Any), ("data", data as Any), ("timeout", timeout as Any)]) + case .updateBroadcastRevenueTransactions(let balances): + return ("updateBroadcastRevenueTransactions", [("balances", balances as Any)]) case .updateChannel(let channelId): return ("updateChannel", [("channelId", channelId as Any)]) case .updateChannelAvailableMessages(let channelId, let availableMinId): @@ -2098,6 +2107,19 @@ public extension Api { return nil } } + public static func parse_updateBroadcastRevenueTransactions(_ reader: BufferReader) -> Update? { + var _1: Api.BroadcastRevenueBalances? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.BroadcastRevenueBalances + } + let _c1 = _1 != nil + if _c1 { + return Api.Update.updateBroadcastRevenueTransactions(balances: _1!) + } + else { + return nil + } + } public static func parse_updateChannel(_ reader: BufferReader) -> Update? { var _1: Int64? _1 = reader.readInt64() diff --git a/submodules/TelegramApi/Sources/Api27.swift b/submodules/TelegramApi/Sources/Api27.swift index babd3e977ad..56090bb9940 100644 --- a/submodules/TelegramApi/Sources/Api27.swift +++ b/submodules/TelegramApi/Sources/Api27.swift @@ -595,6 +595,8 @@ public extension Api.auth { case sentCodeTypeMissedCall(prefix: String, length: Int32) case sentCodeTypeSetUpEmailRequired(flags: Int32) case sentCodeTypeSms(length: Int32) + case sentCodeTypeSmsPhrase(flags: Int32, beginning: String?) + case sentCodeTypeSmsWord(flags: Int32, beginning: String?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -662,6 +664,20 @@ public extension Api.auth { } serializeInt32(length, buffer: buffer, boxed: false) break + case .sentCodeTypeSmsPhrase(let flags, let beginning): + if boxed { + buffer.appendInt32(-1284008785) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(beginning!, buffer: buffer, boxed: false)} + break + case .sentCodeTypeSmsWord(let flags, let beginning): + if boxed { + buffer.appendInt32(-1542017919) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(beginning!, buffer: buffer, boxed: false)} + break } } @@ -685,6 +701,10 @@ public extension Api.auth { return ("sentCodeTypeSetUpEmailRequired", [("flags", flags as Any)]) case .sentCodeTypeSms(let length): return ("sentCodeTypeSms", [("length", length as Any)]) + case .sentCodeTypeSmsPhrase(let flags, let beginning): + return ("sentCodeTypeSmsPhrase", [("flags", flags as Any), ("beginning", beginning as Any)]) + case .sentCodeTypeSmsWord(let flags, let beginning): + return ("sentCodeTypeSmsWord", [("flags", flags as Any), ("beginning", beginning as Any)]) } } @@ -817,6 +837,34 @@ public extension Api.auth { return nil } } + public static func parse_sentCodeTypeSmsPhrase(_ reader: BufferReader) -> SentCodeType? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + if Int(_1!) & Int(1 << 0) != 0 {_2 = parseString(reader) } + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + if _c1 && _c2 { + return Api.auth.SentCodeType.sentCodeTypeSmsPhrase(flags: _1!, beginning: _2) + } + else { + return nil + } + } + public static func parse_sentCodeTypeSmsWord(_ reader: BufferReader) -> SentCodeType? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + if Int(_1!) & Int(1 << 0) != 0 {_2 = parseString(reader) } + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + if _c1 && _c2 { + return Api.auth.SentCodeType.sentCodeTypeSmsWord(flags: _1!, beginning: _2) + } + else { + return nil + } + } } } @@ -926,139 +974,3 @@ public extension Api.channels { } } -public extension Api.channels { - enum ChannelParticipant: TypeConstructorDescription { - case channelParticipant(participant: Api.ChannelParticipant, chats: [Api.Chat], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .channelParticipant(let participant, let chats, let users): - if boxed { - buffer.appendInt32(-541588713) - } - participant.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .channelParticipant(let participant, let chats, let users): - return ("channelParticipant", [("participant", participant as Any), ("chats", chats as Any), ("users", users as Any)]) - } - } - - public static func parse_channelParticipant(_ reader: BufferReader) -> ChannelParticipant? { - var _1: Api.ChannelParticipant? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.ChannelParticipant - } - var _2: [Api.Chat]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _3: [Api.User]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.channels.ChannelParticipant.channelParticipant(participant: _1!, chats: _2!, users: _3!) - } - else { - return nil - } - } - - } -} -public extension Api.channels { - enum ChannelParticipants: TypeConstructorDescription { - case channelParticipants(count: Int32, participants: [Api.ChannelParticipant], chats: [Api.Chat], users: [Api.User]) - case channelParticipantsNotModified - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .channelParticipants(let count, let participants, let chats, let users): - if boxed { - buffer.appendInt32(-1699676497) - } - serializeInt32(count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(participants.count)) - for item in participants { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - case .channelParticipantsNotModified: - if boxed { - buffer.appendInt32(-266911767) - } - - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .channelParticipants(let count, let participants, let chats, let users): - return ("channelParticipants", [("count", count as Any), ("participants", participants as Any), ("chats", chats as Any), ("users", users as Any)]) - case .channelParticipantsNotModified: - return ("channelParticipantsNotModified", []) - } - } - - public static func parse_channelParticipants(_ reader: BufferReader) -> ChannelParticipants? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.ChannelParticipant]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ChannelParticipant.self) - } - var _3: [Api.Chat]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _4: [Api.User]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.channels.ChannelParticipants.channelParticipants(count: _1!, participants: _2!, chats: _3!, users: _4!) - } - else { - return nil - } - } - public static func parse_channelParticipantsNotModified(_ reader: BufferReader) -> ChannelParticipants? { - return Api.channels.ChannelParticipants.channelParticipantsNotModified - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api28.swift b/submodules/TelegramApi/Sources/Api28.swift index 4e1a1e33d33..04914ae0cb9 100644 --- a/submodules/TelegramApi/Sources/Api28.swift +++ b/submodules/TelegramApi/Sources/Api28.swift @@ -1,3 +1,139 @@ +public extension Api.channels { + enum ChannelParticipant: TypeConstructorDescription { + case channelParticipant(participant: Api.ChannelParticipant, chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .channelParticipant(let participant, let chats, let users): + if boxed { + buffer.appendInt32(-541588713) + } + participant.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .channelParticipant(let participant, let chats, let users): + return ("channelParticipant", [("participant", participant as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_channelParticipant(_ reader: BufferReader) -> ChannelParticipant? { + var _1: Api.ChannelParticipant? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.ChannelParticipant + } + var _2: [Api.Chat]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.channels.ChannelParticipant.channelParticipant(participant: _1!, chats: _2!, users: _3!) + } + else { + return nil + } + } + + } +} +public extension Api.channels { + enum ChannelParticipants: TypeConstructorDescription { + case channelParticipants(count: Int32, participants: [Api.ChannelParticipant], chats: [Api.Chat], users: [Api.User]) + case channelParticipantsNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .channelParticipants(let count, let participants, let chats, let users): + if boxed { + buffer.appendInt32(-1699676497) + } + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(participants.count)) + for item in participants { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + case .channelParticipantsNotModified: + if boxed { + buffer.appendInt32(-266911767) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .channelParticipants(let count, let participants, let chats, let users): + return ("channelParticipants", [("count", count as Any), ("participants", participants as Any), ("chats", chats as Any), ("users", users as Any)]) + case .channelParticipantsNotModified: + return ("channelParticipantsNotModified", []) + } + } + + public static func parse_channelParticipants(_ reader: BufferReader) -> ChannelParticipants? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.ChannelParticipant]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ChannelParticipant.self) + } + var _3: [Api.Chat]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _4: [Api.User]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.channels.ChannelParticipants.channelParticipants(count: _1!, participants: _2!, chats: _3!, users: _4!) + } + else { + return nil + } + } + public static func parse_channelParticipantsNotModified(_ reader: BufferReader) -> ChannelParticipants? { + return Api.channels.ChannelParticipants.channelParticipantsNotModified + } + + } +} public extension Api.channels { enum SendAsPeers: TypeConstructorDescription { case sendAsPeers(peers: [Api.SendAsPeer], chats: [Api.Chat], users: [Api.User]) @@ -1324,101 +1460,3 @@ public extension Api.help { } } -public extension Api.help { - enum DeepLinkInfo: TypeConstructorDescription { - case deepLinkInfo(flags: Int32, message: String, entities: [Api.MessageEntity]?) - case deepLinkInfoEmpty - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .deepLinkInfo(let flags, let message, let entities): - if boxed { - buffer.appendInt32(1783556146) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeString(message, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(entities!.count)) - for item in entities! { - item.serialize(buffer, true) - }} - break - case .deepLinkInfoEmpty: - if boxed { - buffer.appendInt32(1722786150) - } - - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .deepLinkInfo(let flags, let message, let entities): - return ("deepLinkInfo", [("flags", flags as Any), ("message", message as Any), ("entities", entities as Any)]) - case .deepLinkInfoEmpty: - return ("deepLinkInfoEmpty", []) - } - } - - public static func parse_deepLinkInfo(_ reader: BufferReader) -> DeepLinkInfo? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) - } } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - if _c1 && _c2 && _c3 { - return Api.help.DeepLinkInfo.deepLinkInfo(flags: _1!, message: _2!, entities: _3) - } - else { - return nil - } - } - public static func parse_deepLinkInfoEmpty(_ reader: BufferReader) -> DeepLinkInfo? { - return Api.help.DeepLinkInfo.deepLinkInfoEmpty - } - - } -} -public extension Api.help { - enum InviteText: TypeConstructorDescription { - case inviteText(message: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inviteText(let message): - if boxed { - buffer.appendInt32(415997816) - } - serializeString(message, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inviteText(let message): - return ("inviteText", [("message", message as Any)]) - } - } - - public static func parse_inviteText(_ reader: BufferReader) -> InviteText? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.help.InviteText.inviteText(message: _1!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index 188802d4a5b..79877efba2f 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -1,3 +1,101 @@ +public extension Api.help { + enum DeepLinkInfo: TypeConstructorDescription { + case deepLinkInfo(flags: Int32, message: String, entities: [Api.MessageEntity]?) + case deepLinkInfoEmpty + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .deepLinkInfo(let flags, let message, let entities): + if boxed { + buffer.appendInt32(1783556146) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(message, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(entities!.count)) + for item in entities! { + item.serialize(buffer, true) + }} + break + case .deepLinkInfoEmpty: + if boxed { + buffer.appendInt32(1722786150) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .deepLinkInfo(let flags, let message, let entities): + return ("deepLinkInfo", [("flags", flags as Any), ("message", message as Any), ("entities", entities as Any)]) + case .deepLinkInfoEmpty: + return ("deepLinkInfoEmpty", []) + } + } + + public static func parse_deepLinkInfo(_ reader: BufferReader) -> DeepLinkInfo? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: [Api.MessageEntity]? + if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + if _c1 && _c2 && _c3 { + return Api.help.DeepLinkInfo.deepLinkInfo(flags: _1!, message: _2!, entities: _3) + } + else { + return nil + } + } + public static func parse_deepLinkInfoEmpty(_ reader: BufferReader) -> DeepLinkInfo? { + return Api.help.DeepLinkInfo.deepLinkInfoEmpty + } + + } +} +public extension Api.help { + enum InviteText: TypeConstructorDescription { + case inviteText(message: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inviteText(let message): + if boxed { + buffer.appendInt32(415997816) + } + serializeString(message, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inviteText(let message): + return ("inviteText", [("message", message as Any)]) + } + } + + public static func parse_inviteText(_ reader: BufferReader) -> InviteText? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.help.InviteText.inviteText(message: _1!) + } + else { + return nil + } + } + + } +} public extension Api.help { enum PassportConfig: TypeConstructorDescription { case passportConfig(hash: Int32, countriesLangs: Api.DataJSON) @@ -1166,183 +1264,3 @@ public extension Api.messages { } } -public extension Api.messages { - enum BotCallbackAnswer: TypeConstructorDescription { - case botCallbackAnswer(flags: Int32, message: String?, url: String?, cacheTime: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .botCallbackAnswer(let flags, let message, let url, let cacheTime): - if boxed { - buffer.appendInt32(911761060) - } - serializeInt32(flags, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {serializeString(message!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 2) != 0 {serializeString(url!, buffer: buffer, boxed: false)} - serializeInt32(cacheTime, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .botCallbackAnswer(let flags, let message, let url, let cacheTime): - return ("botCallbackAnswer", [("flags", flags as Any), ("message", message as Any), ("url", url as Any), ("cacheTime", cacheTime as Any)]) - } - } - - public static func parse_botCallbackAnswer(_ reader: BufferReader) -> BotCallbackAnswer? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - if Int(_1!) & Int(1 << 0) != 0 {_2 = parseString(reader) } - var _3: String? - if Int(_1!) & Int(1 << 2) != 0 {_3 = parseString(reader) } - var _4: Int32? - _4 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.messages.BotCallbackAnswer.botCallbackAnswer(flags: _1!, message: _2, url: _3, cacheTime: _4!) - } - else { - return nil - } - } - - } -} -public extension Api.messages { - enum BotResults: TypeConstructorDescription { - case botResults(flags: Int32, queryId: Int64, nextOffset: String?, switchPm: Api.InlineBotSwitchPM?, switchWebview: Api.InlineBotWebView?, results: [Api.BotInlineResult], cacheTime: Int32, users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .botResults(let flags, let queryId, let nextOffset, let switchPm, let switchWebview, let results, let cacheTime, let users): - if boxed { - buffer.appendInt32(-534646026) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt64(queryId, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 1) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 2) != 0 {switchPm!.serialize(buffer, true)} - if Int(flags) & Int(1 << 3) != 0 {switchWebview!.serialize(buffer, true)} - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(results.count)) - for item in results { - item.serialize(buffer, true) - } - serializeInt32(cacheTime, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .botResults(let flags, let queryId, let nextOffset, let switchPm, let switchWebview, let results, let cacheTime, let users): - return ("botResults", [("flags", flags as Any), ("queryId", queryId as Any), ("nextOffset", nextOffset as Any), ("switchPm", switchPm as Any), ("switchWebview", switchWebview as Any), ("results", results as Any), ("cacheTime", cacheTime as Any), ("users", users as Any)]) - } - } - - public static func parse_botResults(_ reader: BufferReader) -> BotResults? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int64? - _2 = reader.readInt64() - var _3: String? - if Int(_1!) & Int(1 << 1) != 0 {_3 = parseString(reader) } - var _4: Api.InlineBotSwitchPM? - if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.InlineBotSwitchPM - } } - var _5: Api.InlineBotWebView? - if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() { - _5 = Api.parse(reader, signature: signature) as? Api.InlineBotWebView - } } - var _6: [Api.BotInlineResult]? - if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.BotInlineResult.self) - } - var _7: Int32? - _7 = reader.readInt32() - var _8: [Api.User]? - if let _ = reader.readInt32() { - _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil - let _c6 = _6 != nil - let _c7 = _7 != nil - let _c8 = _8 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { - return Api.messages.BotResults.botResults(flags: _1!, queryId: _2!, nextOffset: _3, switchPm: _4, switchWebview: _5, results: _6!, cacheTime: _7!, users: _8!) - } - else { - return nil - } - } - - } -} -public extension Api.messages { - enum ChatAdminsWithInvites: TypeConstructorDescription { - case chatAdminsWithInvites(admins: [Api.ChatAdminWithInvites], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .chatAdminsWithInvites(let admins, let users): - if boxed { - buffer.appendInt32(-1231326505) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(admins.count)) - for item in admins { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .chatAdminsWithInvites(let admins, let users): - return ("chatAdminsWithInvites", [("admins", admins as Any), ("users", users as Any)]) - } - } - - public static func parse_chatAdminsWithInvites(_ reader: BufferReader) -> ChatAdminsWithInvites? { - var _1: [Api.ChatAdminWithInvites]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ChatAdminWithInvites.self) - } - var _2: [Api.User]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.ChatAdminsWithInvites.chatAdminsWithInvites(admins: _1!, users: _2!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api3.swift b/submodules/TelegramApi/Sources/Api3.swift index 9c5ff72afe4..331ac6b496a 100644 --- a/submodules/TelegramApi/Sources/Api3.swift +++ b/submodules/TelegramApi/Sources/Api3.swift @@ -1,3 +1,49 @@ +public extension Api { + enum BusinessLocation: TypeConstructorDescription { + case businessLocation(flags: Int32, geoPoint: Api.GeoPoint?, address: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .businessLocation(let flags, let geoPoint, let address): + if boxed { + buffer.appendInt32(-1403249929) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {geoPoint!.serialize(buffer, true)} + serializeString(address, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .businessLocation(let flags, let geoPoint, let address): + return ("businessLocation", [("flags", flags as Any), ("geoPoint", geoPoint as Any), ("address", address as Any)]) + } + } + + public static func parse_businessLocation(_ reader: BufferReader) -> BusinessLocation? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.GeoPoint? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.GeoPoint + } } + var _3: String? + _3 = parseString(reader) + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.BusinessLocation.businessLocation(flags: _1!, geoPoint: _2, address: _3!) + } + else { + return nil + } + } + + } +} public extension Api { enum BusinessRecipients: TypeConstructorDescription { case businessRecipients(flags: Int32, users: [Int64]?) diff --git a/submodules/TelegramApi/Sources/Api30.swift b/submodules/TelegramApi/Sources/Api30.swift index 5db0a976991..6475bda11f7 100644 --- a/submodules/TelegramApi/Sources/Api30.swift +++ b/submodules/TelegramApi/Sources/Api30.swift @@ -1,3 +1,183 @@ +public extension Api.messages { + enum BotCallbackAnswer: TypeConstructorDescription { + case botCallbackAnswer(flags: Int32, message: String?, url: String?, cacheTime: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .botCallbackAnswer(let flags, let message, let url, let cacheTime): + if boxed { + buffer.appendInt32(911761060) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(message!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 2) != 0 {serializeString(url!, buffer: buffer, boxed: false)} + serializeInt32(cacheTime, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .botCallbackAnswer(let flags, let message, let url, let cacheTime): + return ("botCallbackAnswer", [("flags", flags as Any), ("message", message as Any), ("url", url as Any), ("cacheTime", cacheTime as Any)]) + } + } + + public static func parse_botCallbackAnswer(_ reader: BufferReader) -> BotCallbackAnswer? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + if Int(_1!) & Int(1 << 0) != 0 {_2 = parseString(reader) } + var _3: String? + if Int(_1!) & Int(1 << 2) != 0 {_3 = parseString(reader) } + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.messages.BotCallbackAnswer.botCallbackAnswer(flags: _1!, message: _2, url: _3, cacheTime: _4!) + } + else { + return nil + } + } + + } +} +public extension Api.messages { + enum BotResults: TypeConstructorDescription { + case botResults(flags: Int32, queryId: Int64, nextOffset: String?, switchPm: Api.InlineBotSwitchPM?, switchWebview: Api.InlineBotWebView?, results: [Api.BotInlineResult], cacheTime: Int32, users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .botResults(let flags, let queryId, let nextOffset, let switchPm, let switchWebview, let results, let cacheTime, let users): + if boxed { + buffer.appendInt32(-534646026) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(queryId, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 2) != 0 {switchPm!.serialize(buffer, true)} + if Int(flags) & Int(1 << 3) != 0 {switchWebview!.serialize(buffer, true)} + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(results.count)) + for item in results { + item.serialize(buffer, true) + } + serializeInt32(cacheTime, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .botResults(let flags, let queryId, let nextOffset, let switchPm, let switchWebview, let results, let cacheTime, let users): + return ("botResults", [("flags", flags as Any), ("queryId", queryId as Any), ("nextOffset", nextOffset as Any), ("switchPm", switchPm as Any), ("switchWebview", switchWebview as Any), ("results", results as Any), ("cacheTime", cacheTime as Any), ("users", users as Any)]) + } + } + + public static func parse_botResults(_ reader: BufferReader) -> BotResults? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: String? + if Int(_1!) & Int(1 << 1) != 0 {_3 = parseString(reader) } + var _4: Api.InlineBotSwitchPM? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.InlineBotSwitchPM + } } + var _5: Api.InlineBotWebView? + if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() { + _5 = Api.parse(reader, signature: signature) as? Api.InlineBotWebView + } } + var _6: [Api.BotInlineResult]? + if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.BotInlineResult.self) + } + var _7: Int32? + _7 = reader.readInt32() + var _8: [Api.User]? + if let _ = reader.readInt32() { + _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return Api.messages.BotResults.botResults(flags: _1!, queryId: _2!, nextOffset: _3, switchPm: _4, switchWebview: _5, results: _6!, cacheTime: _7!, users: _8!) + } + else { + return nil + } + } + + } +} +public extension Api.messages { + enum ChatAdminsWithInvites: TypeConstructorDescription { + case chatAdminsWithInvites(admins: [Api.ChatAdminWithInvites], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .chatAdminsWithInvites(let admins, let users): + if boxed { + buffer.appendInt32(-1231326505) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(admins.count)) + for item in admins { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .chatAdminsWithInvites(let admins, let users): + return ("chatAdminsWithInvites", [("admins", admins as Any), ("users", users as Any)]) + } + } + + public static func parse_chatAdminsWithInvites(_ reader: BufferReader) -> ChatAdminsWithInvites? { + var _1: [Api.ChatAdminWithInvites]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ChatAdminWithInvites.self) + } + var _2: [Api.User]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.ChatAdminsWithInvites.chatAdminsWithInvites(admins: _1!, users: _2!) + } + else { + return nil + } + } + + } +} public extension Api.messages { enum ChatFull: TypeConstructorDescription { case chatFull(fullChat: Api.ChatFull, chats: [Api.Chat], users: [Api.User]) @@ -1300,175 +1480,3 @@ public extension Api.messages { } } -public extension Api.messages { - enum MessageEditData: TypeConstructorDescription { - case messageEditData(flags: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .messageEditData(let flags): - if boxed { - buffer.appendInt32(649453030) - } - serializeInt32(flags, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .messageEditData(let flags): - return ("messageEditData", [("flags", flags as Any)]) - } - } - - public static func parse_messageEditData(_ reader: BufferReader) -> MessageEditData? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return Api.messages.MessageEditData.messageEditData(flags: _1!) - } - else { - return nil - } - } - - } -} -public extension Api.messages { - enum MessageReactionsList: TypeConstructorDescription { - case messageReactionsList(flags: Int32, count: Int32, reactions: [Api.MessagePeerReaction], chats: [Api.Chat], users: [Api.User], nextOffset: String?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .messageReactionsList(let flags, let count, let reactions, let chats, let users, let nextOffset): - if boxed { - buffer.appendInt32(834488621) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(reactions.count)) - for item in reactions { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .messageReactionsList(let flags, let count, let reactions, let chats, let users, let nextOffset): - return ("messageReactionsList", [("flags", flags as Any), ("count", count as Any), ("reactions", reactions as Any), ("chats", chats as Any), ("users", users as Any), ("nextOffset", nextOffset as Any)]) - } - } - - public static func parse_messageReactionsList(_ reader: BufferReader) -> MessageReactionsList? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: [Api.MessagePeerReaction]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessagePeerReaction.self) - } - var _4: [Api.Chat]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _5: [Api.User]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - var _6: String? - if Int(_1!) & Int(1 << 0) != 0 {_6 = parseString(reader) } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.messages.MessageReactionsList.messageReactionsList(flags: _1!, count: _2!, reactions: _3!, chats: _4!, users: _5!, nextOffset: _6) - } - else { - return nil - } - } - - } -} -public extension Api.messages { - enum MessageViews: TypeConstructorDescription { - case messageViews(views: [Api.MessageViews], chats: [Api.Chat], users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .messageViews(let views, let chats, let users): - if boxed { - buffer.appendInt32(-1228606141) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(views.count)) - for item in views { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .messageViews(let views, let chats, let users): - return ("messageViews", [("views", views as Any), ("chats", chats as Any), ("users", users as Any)]) - } - } - - public static func parse_messageViews(_ reader: BufferReader) -> MessageViews? { - var _1: [Api.MessageViews]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageViews.self) - } - var _2: [Api.Chat]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _3: [Api.User]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.messages.MessageViews.messageViews(views: _1!, chats: _2!, users: _3!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api31.swift b/submodules/TelegramApi/Sources/Api31.swift index b3151e37fe2..3f28e123a8c 100644 --- a/submodules/TelegramApi/Sources/Api31.swift +++ b/submodules/TelegramApi/Sources/Api31.swift @@ -1,3 +1,175 @@ +public extension Api.messages { + enum MessageEditData: TypeConstructorDescription { + case messageEditData(flags: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .messageEditData(let flags): + if boxed { + buffer.appendInt32(649453030) + } + serializeInt32(flags, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .messageEditData(let flags): + return ("messageEditData", [("flags", flags as Any)]) + } + } + + public static func parse_messageEditData(_ reader: BufferReader) -> MessageEditData? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.messages.MessageEditData.messageEditData(flags: _1!) + } + else { + return nil + } + } + + } +} +public extension Api.messages { + enum MessageReactionsList: TypeConstructorDescription { + case messageReactionsList(flags: Int32, count: Int32, reactions: [Api.MessagePeerReaction], chats: [Api.Chat], users: [Api.User], nextOffset: String?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .messageReactionsList(let flags, let count, let reactions, let chats, let users, let nextOffset): + if boxed { + buffer.appendInt32(834488621) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(reactions.count)) + for item in reactions { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .messageReactionsList(let flags, let count, let reactions, let chats, let users, let nextOffset): + return ("messageReactionsList", [("flags", flags as Any), ("count", count as Any), ("reactions", reactions as Any), ("chats", chats as Any), ("users", users as Any), ("nextOffset", nextOffset as Any)]) + } + } + + public static func parse_messageReactionsList(_ reader: BufferReader) -> MessageReactionsList? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: [Api.MessagePeerReaction]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessagePeerReaction.self) + } + var _4: [Api.Chat]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _5: [Api.User]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + var _6: String? + if Int(_1!) & Int(1 << 0) != 0 {_6 = parseString(reader) } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.messages.MessageReactionsList.messageReactionsList(flags: _1!, count: _2!, reactions: _3!, chats: _4!, users: _5!, nextOffset: _6) + } + else { + return nil + } + } + + } +} +public extension Api.messages { + enum MessageViews: TypeConstructorDescription { + case messageViews(views: [Api.MessageViews], chats: [Api.Chat], users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .messageViews(let views, let chats, let users): + if boxed { + buffer.appendInt32(-1228606141) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(views.count)) + for item in views { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .messageViews(let views, let chats, let users): + return ("messageViews", [("views", views as Any), ("chats", chats as Any), ("users", users as Any)]) + } + } + + public static func parse_messageViews(_ reader: BufferReader) -> MessageViews? { + var _1: [Api.MessageViews]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageViews.self) + } + var _2: [Api.Chat]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.messages.MessageViews.messageViews(views: _1!, chats: _2!, users: _3!) + } + else { + return nil + } + } + + } +} public extension Api.messages { enum Messages: TypeConstructorDescription { case channelMessages(flags: Int32, pts: Int32, count: Int32, offsetIdOffset: Int32?, messages: [Api.Message], topics: [Api.ForumTopic], chats: [Api.Chat], users: [Api.User]) @@ -1292,115 +1464,3 @@ public extension Api.messages { } } -public extension Api.messages { - enum StickerSetInstallResult: TypeConstructorDescription { - case stickerSetInstallResultArchive(sets: [Api.StickerSetCovered]) - case stickerSetInstallResultSuccess - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .stickerSetInstallResultArchive(let sets): - if boxed { - buffer.appendInt32(904138920) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(sets.count)) - for item in sets { - item.serialize(buffer, true) - } - break - case .stickerSetInstallResultSuccess: - if boxed { - buffer.appendInt32(946083368) - } - - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .stickerSetInstallResultArchive(let sets): - return ("stickerSetInstallResultArchive", [("sets", sets as Any)]) - case .stickerSetInstallResultSuccess: - return ("stickerSetInstallResultSuccess", []) - } - } - - public static func parse_stickerSetInstallResultArchive(_ reader: BufferReader) -> StickerSetInstallResult? { - var _1: [Api.StickerSetCovered]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerSetCovered.self) - } - let _c1 = _1 != nil - if _c1 { - return Api.messages.StickerSetInstallResult.stickerSetInstallResultArchive(sets: _1!) - } - else { - return nil - } - } - public static func parse_stickerSetInstallResultSuccess(_ reader: BufferReader) -> StickerSetInstallResult? { - return Api.messages.StickerSetInstallResult.stickerSetInstallResultSuccess - } - - } -} -public extension Api.messages { - enum Stickers: TypeConstructorDescription { - case stickers(hash: Int64, stickers: [Api.Document]) - case stickersNotModified - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .stickers(let hash, let stickers): - if boxed { - buffer.appendInt32(816245886) - } - serializeInt64(hash, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(stickers.count)) - for item in stickers { - item.serialize(buffer, true) - } - break - case .stickersNotModified: - if boxed { - buffer.appendInt32(-244016606) - } - - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .stickers(let hash, let stickers): - return ("stickers", [("hash", hash as Any), ("stickers", stickers as Any)]) - case .stickersNotModified: - return ("stickersNotModified", []) - } - } - - public static func parse_stickers(_ reader: BufferReader) -> Stickers? { - var _1: Int64? - _1 = reader.readInt64() - var _2: [Api.Document]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.Stickers.stickers(hash: _1!, stickers: _2!) - } - else { - return nil - } - } - public static func parse_stickersNotModified(_ reader: BufferReader) -> Stickers? { - return Api.messages.Stickers.stickersNotModified - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api32.swift b/submodules/TelegramApi/Sources/Api32.swift index 7523af18826..4e07851e3e3 100644 --- a/submodules/TelegramApi/Sources/Api32.swift +++ b/submodules/TelegramApi/Sources/Api32.swift @@ -1,3 +1,115 @@ +public extension Api.messages { + enum StickerSetInstallResult: TypeConstructorDescription { + case stickerSetInstallResultArchive(sets: [Api.StickerSetCovered]) + case stickerSetInstallResultSuccess + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .stickerSetInstallResultArchive(let sets): + if boxed { + buffer.appendInt32(904138920) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(sets.count)) + for item in sets { + item.serialize(buffer, true) + } + break + case .stickerSetInstallResultSuccess: + if boxed { + buffer.appendInt32(946083368) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .stickerSetInstallResultArchive(let sets): + return ("stickerSetInstallResultArchive", [("sets", sets as Any)]) + case .stickerSetInstallResultSuccess: + return ("stickerSetInstallResultSuccess", []) + } + } + + public static func parse_stickerSetInstallResultArchive(_ reader: BufferReader) -> StickerSetInstallResult? { + var _1: [Api.StickerSetCovered]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerSetCovered.self) + } + let _c1 = _1 != nil + if _c1 { + return Api.messages.StickerSetInstallResult.stickerSetInstallResultArchive(sets: _1!) + } + else { + return nil + } + } + public static func parse_stickerSetInstallResultSuccess(_ reader: BufferReader) -> StickerSetInstallResult? { + return Api.messages.StickerSetInstallResult.stickerSetInstallResultSuccess + } + + } +} +public extension Api.messages { + enum Stickers: TypeConstructorDescription { + case stickers(hash: Int64, stickers: [Api.Document]) + case stickersNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .stickers(let hash, let stickers): + if boxed { + buffer.appendInt32(816245886) + } + serializeInt64(hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(stickers.count)) + for item in stickers { + item.serialize(buffer, true) + } + break + case .stickersNotModified: + if boxed { + buffer.appendInt32(-244016606) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .stickers(let hash, let stickers): + return ("stickers", [("hash", hash as Any), ("stickers", stickers as Any)]) + case .stickersNotModified: + return ("stickersNotModified", []) + } + } + + public static func parse_stickers(_ reader: BufferReader) -> Stickers? { + var _1: Int64? + _1 = reader.readInt64() + var _2: [Api.Document]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.Stickers.stickers(hash: _1!, stickers: _2!) + } + else { + return nil + } + } + public static func parse_stickersNotModified(_ reader: BufferReader) -> Stickers? { + return Api.messages.Stickers.stickersNotModified + } + + } +} public extension Api.messages { enum TranscribedAudio: TypeConstructorDescription { case transcribedAudio(flags: Int32, transcriptionId: Int64, text: String, trialRemainsNum: Int32?, trialRemainsUntilDate: Int32?) @@ -1580,213 +1692,3 @@ public extension Api.premium { } } -public extension Api.smsjobs { - enum EligibilityToJoin: TypeConstructorDescription { - case eligibleToJoin(termsUrl: String, monthlySentSms: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .eligibleToJoin(let termsUrl, let monthlySentSms): - if boxed { - buffer.appendInt32(-594852657) - } - serializeString(termsUrl, buffer: buffer, boxed: false) - serializeInt32(monthlySentSms, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .eligibleToJoin(let termsUrl, let monthlySentSms): - return ("eligibleToJoin", [("termsUrl", termsUrl as Any), ("monthlySentSms", monthlySentSms as Any)]) - } - } - - public static func parse_eligibleToJoin(_ reader: BufferReader) -> EligibilityToJoin? { - var _1: String? - _1 = parseString(reader) - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.smsjobs.EligibilityToJoin.eligibleToJoin(termsUrl: _1!, monthlySentSms: _2!) - } - else { - return nil - } - } - - } -} -public extension Api.smsjobs { - enum Status: TypeConstructorDescription { - case status(flags: Int32, recentSent: Int32, recentSince: Int32, recentRemains: Int32, totalSent: Int32, totalSince: Int32, lastGiftSlug: String?, termsUrl: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .status(let flags, let recentSent, let recentSince, let recentRemains, let totalSent, let totalSince, let lastGiftSlug, let termsUrl): - if boxed { - buffer.appendInt32(720277905) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(recentSent, buffer: buffer, boxed: false) - serializeInt32(recentSince, buffer: buffer, boxed: false) - serializeInt32(recentRemains, buffer: buffer, boxed: false) - serializeInt32(totalSent, buffer: buffer, boxed: false) - serializeInt32(totalSince, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 1) != 0 {serializeString(lastGiftSlug!, buffer: buffer, boxed: false)} - serializeString(termsUrl, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .status(let flags, let recentSent, let recentSince, let recentRemains, let totalSent, let totalSince, let lastGiftSlug, let termsUrl): - return ("status", [("flags", flags as Any), ("recentSent", recentSent as Any), ("recentSince", recentSince as Any), ("recentRemains", recentRemains as Any), ("totalSent", totalSent as Any), ("totalSince", totalSince as Any), ("lastGiftSlug", lastGiftSlug as Any), ("termsUrl", termsUrl as Any)]) - } - } - - public static func parse_status(_ reader: BufferReader) -> Status? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - _4 = reader.readInt32() - var _5: Int32? - _5 = reader.readInt32() - var _6: Int32? - _6 = reader.readInt32() - var _7: String? - if Int(_1!) & Int(1 << 1) != 0 {_7 = parseString(reader) } - var _8: String? - _8 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil - let _c8 = _8 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { - return Api.smsjobs.Status.status(flags: _1!, recentSent: _2!, recentSince: _3!, recentRemains: _4!, totalSent: _5!, totalSince: _6!, lastGiftSlug: _7, termsUrl: _8!) - } - else { - return nil - } - } - - } -} -public extension Api.stats { - enum BroadcastRevenueStats: TypeConstructorDescription { - case broadcastRevenueStats(topHoursGraph: Api.StatsGraph, revenueGraph: Api.StatsGraph, currentBalance: Int64, availableBalance: Int64, overallRevenue: Int64, usdRate: Double) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .broadcastRevenueStats(let topHoursGraph, let revenueGraph, let currentBalance, let availableBalance, let overallRevenue, let usdRate): - if boxed { - buffer.appendInt32(-797226067) - } - topHoursGraph.serialize(buffer, true) - revenueGraph.serialize(buffer, true) - serializeInt64(currentBalance, buffer: buffer, boxed: false) - serializeInt64(availableBalance, buffer: buffer, boxed: false) - serializeInt64(overallRevenue, buffer: buffer, boxed: false) - serializeDouble(usdRate, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .broadcastRevenueStats(let topHoursGraph, let revenueGraph, let currentBalance, let availableBalance, let overallRevenue, let usdRate): - return ("broadcastRevenueStats", [("topHoursGraph", topHoursGraph as Any), ("revenueGraph", revenueGraph as Any), ("currentBalance", currentBalance as Any), ("availableBalance", availableBalance as Any), ("overallRevenue", overallRevenue as Any), ("usdRate", usdRate as Any)]) - } - } - - public static func parse_broadcastRevenueStats(_ reader: BufferReader) -> BroadcastRevenueStats? { - var _1: Api.StatsGraph? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.StatsGraph - } - var _2: Api.StatsGraph? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.StatsGraph - } - var _3: Int64? - _3 = reader.readInt64() - var _4: Int64? - _4 = reader.readInt64() - var _5: Int64? - _5 = reader.readInt64() - var _6: Double? - _6 = reader.readDouble() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.stats.BroadcastRevenueStats.broadcastRevenueStats(topHoursGraph: _1!, revenueGraph: _2!, currentBalance: _3!, availableBalance: _4!, overallRevenue: _5!, usdRate: _6!) - } - else { - return nil - } - } - - } -} -public extension Api.stats { - enum BroadcastRevenueTransactions: TypeConstructorDescription { - case broadcastRevenueTransactions(count: Int32, transactions: [Api.BroadcastRevenueTransaction]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .broadcastRevenueTransactions(let count, let transactions): - if boxed { - buffer.appendInt32(-2028632986) - } - serializeInt32(count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(transactions.count)) - for item in transactions { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .broadcastRevenueTransactions(let count, let transactions): - return ("broadcastRevenueTransactions", [("count", count as Any), ("transactions", transactions as Any)]) - } - } - - public static func parse_broadcastRevenueTransactions(_ reader: BufferReader) -> BroadcastRevenueTransactions? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.BroadcastRevenueTransaction]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.BroadcastRevenueTransaction.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.stats.BroadcastRevenueTransactions.broadcastRevenueTransactions(count: _1!, transactions: _2!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api33.swift b/submodules/TelegramApi/Sources/Api33.swift index 8966e6d13ef..5e8eefe5eaf 100644 --- a/submodules/TelegramApi/Sources/Api33.swift +++ b/submodules/TelegramApi/Sources/Api33.swift @@ -1,3 +1,207 @@ +public extension Api.smsjobs { + enum EligibilityToJoin: TypeConstructorDescription { + case eligibleToJoin(termsUrl: String, monthlySentSms: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .eligibleToJoin(let termsUrl, let monthlySentSms): + if boxed { + buffer.appendInt32(-594852657) + } + serializeString(termsUrl, buffer: buffer, boxed: false) + serializeInt32(monthlySentSms, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .eligibleToJoin(let termsUrl, let monthlySentSms): + return ("eligibleToJoin", [("termsUrl", termsUrl as Any), ("monthlySentSms", monthlySentSms as Any)]) + } + } + + public static func parse_eligibleToJoin(_ reader: BufferReader) -> EligibilityToJoin? { + var _1: String? + _1 = parseString(reader) + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.smsjobs.EligibilityToJoin.eligibleToJoin(termsUrl: _1!, monthlySentSms: _2!) + } + else { + return nil + } + } + + } +} +public extension Api.smsjobs { + enum Status: TypeConstructorDescription { + case status(flags: Int32, recentSent: Int32, recentSince: Int32, recentRemains: Int32, totalSent: Int32, totalSince: Int32, lastGiftSlug: String?, termsUrl: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .status(let flags, let recentSent, let recentSince, let recentRemains, let totalSent, let totalSince, let lastGiftSlug, let termsUrl): + if boxed { + buffer.appendInt32(720277905) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(recentSent, buffer: buffer, boxed: false) + serializeInt32(recentSince, buffer: buffer, boxed: false) + serializeInt32(recentRemains, buffer: buffer, boxed: false) + serializeInt32(totalSent, buffer: buffer, boxed: false) + serializeInt32(totalSince, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeString(lastGiftSlug!, buffer: buffer, boxed: false)} + serializeString(termsUrl, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .status(let flags, let recentSent, let recentSince, let recentRemains, let totalSent, let totalSince, let lastGiftSlug, let termsUrl): + return ("status", [("flags", flags as Any), ("recentSent", recentSent as Any), ("recentSince", recentSince as Any), ("recentRemains", recentRemains as Any), ("totalSent", totalSent as Any), ("totalSince", totalSince as Any), ("lastGiftSlug", lastGiftSlug as Any), ("termsUrl", termsUrl as Any)]) + } + } + + public static func parse_status(_ reader: BufferReader) -> Status? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + var _6: Int32? + _6 = reader.readInt32() + var _7: String? + if Int(_1!) & Int(1 << 1) != 0 {_7 = parseString(reader) } + var _8: String? + _8 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil + let _c8 = _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return Api.smsjobs.Status.status(flags: _1!, recentSent: _2!, recentSince: _3!, recentRemains: _4!, totalSent: _5!, totalSince: _6!, lastGiftSlug: _7, termsUrl: _8!) + } + else { + return nil + } + } + + } +} +public extension Api.stats { + enum BroadcastRevenueStats: TypeConstructorDescription { + case broadcastRevenueStats(topHoursGraph: Api.StatsGraph, revenueGraph: Api.StatsGraph, balances: Api.BroadcastRevenueBalances, usdRate: Double) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .broadcastRevenueStats(let topHoursGraph, let revenueGraph, let balances, let usdRate): + if boxed { + buffer.appendInt32(1409802903) + } + topHoursGraph.serialize(buffer, true) + revenueGraph.serialize(buffer, true) + balances.serialize(buffer, true) + serializeDouble(usdRate, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .broadcastRevenueStats(let topHoursGraph, let revenueGraph, let balances, let usdRate): + return ("broadcastRevenueStats", [("topHoursGraph", topHoursGraph as Any), ("revenueGraph", revenueGraph as Any), ("balances", balances as Any), ("usdRate", usdRate as Any)]) + } + } + + public static func parse_broadcastRevenueStats(_ reader: BufferReader) -> BroadcastRevenueStats? { + var _1: Api.StatsGraph? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.StatsGraph + } + var _2: Api.StatsGraph? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.StatsGraph + } + var _3: Api.BroadcastRevenueBalances? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.BroadcastRevenueBalances + } + var _4: Double? + _4 = reader.readDouble() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.stats.BroadcastRevenueStats.broadcastRevenueStats(topHoursGraph: _1!, revenueGraph: _2!, balances: _3!, usdRate: _4!) + } + else { + return nil + } + } + + } +} +public extension Api.stats { + enum BroadcastRevenueTransactions: TypeConstructorDescription { + case broadcastRevenueTransactions(count: Int32, transactions: [Api.BroadcastRevenueTransaction]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .broadcastRevenueTransactions(let count, let transactions): + if boxed { + buffer.appendInt32(-2028632986) + } + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(transactions.count)) + for item in transactions { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .broadcastRevenueTransactions(let count, let transactions): + return ("broadcastRevenueTransactions", [("count", count as Any), ("transactions", transactions as Any)]) + } + } + + public static func parse_broadcastRevenueTransactions(_ reader: BufferReader) -> BroadcastRevenueTransactions? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.BroadcastRevenueTransaction]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.BroadcastRevenueTransaction.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.stats.BroadcastRevenueTransactions.broadcastRevenueTransactions(count: _1!, transactions: _2!) + } + else { + return nil + } + } + + } +} public extension Api.stats { enum BroadcastRevenueWithdrawalUrl: TypeConstructorDescription { case broadcastRevenueWithdrawalUrl(url: String) @@ -1522,55 +1726,3 @@ public extension Api.updates { } } -public extension Api.updates { - enum State: TypeConstructorDescription { - case state(pts: Int32, qts: Int32, date: Int32, seq: Int32, unreadCount: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .state(let pts, let qts, let date, let seq, let unreadCount): - if boxed { - buffer.appendInt32(-1519637954) - } - serializeInt32(pts, buffer: buffer, boxed: false) - serializeInt32(qts, buffer: buffer, boxed: false) - serializeInt32(date, buffer: buffer, boxed: false) - serializeInt32(seq, buffer: buffer, boxed: false) - serializeInt32(unreadCount, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .state(let pts, let qts, let date, let seq, let unreadCount): - return ("state", [("pts", pts as Any), ("qts", qts as Any), ("date", date as Any), ("seq", seq as Any), ("unreadCount", unreadCount as Any)]) - } - } - - public static func parse_state(_ reader: BufferReader) -> State? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - _4 = reader.readInt32() - var _5: Int32? - _5 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.updates.State.state(pts: _1!, qts: _2!, date: _3!, seq: _4!, unreadCount: _5!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api34.swift b/submodules/TelegramApi/Sources/Api34.swift index 81f996c9b3d..a2670b0450f 100644 --- a/submodules/TelegramApi/Sources/Api34.swift +++ b/submodules/TelegramApi/Sources/Api34.swift @@ -1,3 +1,55 @@ +public extension Api.updates { + enum State: TypeConstructorDescription { + case state(pts: Int32, qts: Int32, date: Int32, seq: Int32, unreadCount: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .state(let pts, let qts, let date, let seq, let unreadCount): + if boxed { + buffer.appendInt32(-1519637954) + } + serializeInt32(pts, buffer: buffer, boxed: false) + serializeInt32(qts, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + serializeInt32(seq, buffer: buffer, boxed: false) + serializeInt32(unreadCount, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .state(let pts, let qts, let date, let seq, let unreadCount): + return ("state", [("pts", pts as Any), ("qts", qts as Any), ("date", date as Any), ("seq", seq as Any), ("unreadCount", unreadCount as Any)]) + } + } + + public static func parse_state(_ reader: BufferReader) -> State? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.updates.State.state(pts: _1!, qts: _2!, date: _3!, seq: _4!, unreadCount: _5!) + } + else { + return nil + } + } + + } +} public extension Api.upload { enum CdnFile: TypeConstructorDescription { case cdnFile(bytes: Buffer) diff --git a/submodules/TelegramApi/Sources/Api35.swift b/submodules/TelegramApi/Sources/Api35.swift index 72e36df9e8d..bc3083babea 100644 --- a/submodules/TelegramApi/Sources/Api35.swift +++ b/submodules/TelegramApi/Sources/Api35.swift @@ -2043,6 +2043,23 @@ public extension Api.functions.auth { }) } } +public extension Api.functions.auth { + static func reportMissingCode(phoneNumber: String, phoneCodeHash: String, mnc: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-878841866) + serializeString(phoneNumber, buffer: buffer, boxed: false) + serializeString(phoneCodeHash, buffer: buffer, boxed: false) + serializeString(mnc, buffer: buffer, boxed: false) + return (FunctionDescription(name: "auth.reportMissingCode", parameters: [("phoneNumber", String(describing: phoneNumber)), ("phoneCodeHash", String(describing: phoneCodeHash)), ("mnc", String(describing: mnc))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.auth { static func requestFirebaseSms(flags: Int32, phoneNumber: String, phoneCodeHash: String, safetyNetToken: String?, iosPushSecret: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -5766,6 +5783,21 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func getEmojiStickerGroups(hash: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(500711669) + serializeInt32(hash, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.getEmojiStickerGroups", parameters: [("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.EmojiGroups? in + let reader = BufferReader(buffer) + var result: Api.messages.EmojiGroups? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.EmojiGroups + } + return result + }) + } +} public extension Api.functions.messages { static func getEmojiStickers(hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramApi/Sources/Api6.swift b/submodules/TelegramApi/Sources/Api6.swift index b54c445aa26..e3677b92d23 100644 --- a/submodules/TelegramApi/Sources/Api6.swift +++ b/submodules/TelegramApi/Sources/Api6.swift @@ -231,6 +231,8 @@ public extension Api { public extension Api { enum EmojiGroup: TypeConstructorDescription { case emojiGroup(title: String, iconEmojiId: Int64, emoticons: [String]) + case emojiGroupGreeting(title: String, iconEmojiId: Int64, emoticons: [String]) + case emojiGroupPremium(title: String, iconEmojiId: Int64) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -246,6 +248,25 @@ public extension Api { serializeString(item, buffer: buffer, boxed: false) } break + case .emojiGroupGreeting(let title, let iconEmojiId, let emoticons): + if boxed { + buffer.appendInt32(-2133693241) + } + serializeString(title, buffer: buffer, boxed: false) + serializeInt64(iconEmojiId, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(emoticons.count)) + for item in emoticons { + serializeString(item, buffer: buffer, boxed: false) + } + break + case .emojiGroupPremium(let title, let iconEmojiId): + if boxed { + buffer.appendInt32(154914612) + } + serializeString(title, buffer: buffer, boxed: false) + serializeInt64(iconEmojiId, buffer: buffer, boxed: false) + break } } @@ -253,6 +274,10 @@ public extension Api { switch self { case .emojiGroup(let title, let iconEmojiId, let emoticons): return ("emojiGroup", [("title", title as Any), ("iconEmojiId", iconEmojiId as Any), ("emoticons", emoticons as Any)]) + case .emojiGroupGreeting(let title, let iconEmojiId, let emoticons): + return ("emojiGroupGreeting", [("title", title as Any), ("iconEmojiId", iconEmojiId as Any), ("emoticons", emoticons as Any)]) + case .emojiGroupPremium(let title, let iconEmojiId): + return ("emojiGroupPremium", [("title", title as Any), ("iconEmojiId", iconEmojiId as Any)]) } } @@ -275,6 +300,39 @@ public extension Api { return nil } } + public static func parse_emojiGroupGreeting(_ reader: BufferReader) -> EmojiGroup? { + var _1: String? + _1 = parseString(reader) + var _2: Int64? + _2 = reader.readInt64() + var _3: [String]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.EmojiGroup.emojiGroupGreeting(title: _1!, iconEmojiId: _2!, emoticons: _3!) + } + else { + return nil + } + } + public static func parse_emojiGroupPremium(_ reader: BufferReader) -> EmojiGroup? { + var _1: String? + _1 = parseString(reader) + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.EmojiGroup.emojiGroupPremium(title: _1!, iconEmojiId: _2!) + } + else { + return nil + } + } } } @@ -1032,97 +1090,3 @@ public extension Api { } } -public extension Api { - enum ExportedChatlistInvite: TypeConstructorDescription { - case exportedChatlistInvite(flags: Int32, title: String, url: String, peers: [Api.Peer]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .exportedChatlistInvite(let flags, let title, let url, let peers): - if boxed { - buffer.appendInt32(206668204) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeString(title, buffer: buffer, boxed: false) - serializeString(url, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(peers.count)) - for item in peers { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .exportedChatlistInvite(let flags, let title, let url, let peers): - return ("exportedChatlistInvite", [("flags", flags as Any), ("title", title as Any), ("url", url as Any), ("peers", peers as Any)]) - } - } - - public static func parse_exportedChatlistInvite(_ reader: BufferReader) -> ExportedChatlistInvite? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: String? - _3 = parseString(reader) - var _4: [Api.Peer]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.ExportedChatlistInvite.exportedChatlistInvite(flags: _1!, title: _2!, url: _3!, peers: _4!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum ExportedContactToken: TypeConstructorDescription { - case exportedContactToken(url: String, expires: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .exportedContactToken(let url, let expires): - if boxed { - buffer.appendInt32(1103040667) - } - serializeString(url, buffer: buffer, boxed: false) - serializeInt32(expires, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .exportedContactToken(let url, let expires): - return ("exportedContactToken", [("url", url as Any), ("expires", expires as Any)]) - } - } - - public static func parse_exportedContactToken(_ reader: BufferReader) -> ExportedContactToken? { - var _1: String? - _1 = parseString(reader) - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.ExportedContactToken.exportedContactToken(url: _1!, expires: _2!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api7.swift b/submodules/TelegramApi/Sources/Api7.swift index 60cf7f42908..1b56cb7fbb5 100644 --- a/submodules/TelegramApi/Sources/Api7.swift +++ b/submodules/TelegramApi/Sources/Api7.swift @@ -1,3 +1,97 @@ +public extension Api { + enum ExportedChatlistInvite: TypeConstructorDescription { + case exportedChatlistInvite(flags: Int32, title: String, url: String, peers: [Api.Peer]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .exportedChatlistInvite(let flags, let title, let url, let peers): + if boxed { + buffer.appendInt32(206668204) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + serializeString(url, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(peers.count)) + for item in peers { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .exportedChatlistInvite(let flags, let title, let url, let peers): + return ("exportedChatlistInvite", [("flags", flags as Any), ("title", title as Any), ("url", url as Any), ("peers", peers as Any)]) + } + } + + public static func parse_exportedChatlistInvite(_ reader: BufferReader) -> ExportedChatlistInvite? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + var _4: [Api.Peer]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.ExportedChatlistInvite.exportedChatlistInvite(flags: _1!, title: _2!, url: _3!, peers: _4!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum ExportedContactToken: TypeConstructorDescription { + case exportedContactToken(url: String, expires: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .exportedContactToken(let url, let expires): + if boxed { + buffer.appendInt32(1103040667) + } + serializeString(url, buffer: buffer, boxed: false) + serializeInt32(expires, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .exportedContactToken(let url, let expires): + return ("exportedContactToken", [("url", url as Any), ("expires", expires as Any)]) + } + } + + public static func parse_exportedContactToken(_ reader: BufferReader) -> ExportedContactToken? { + var _1: String? + _1 = parseString(reader) + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.ExportedContactToken.exportedContactToken(url: _1!, expires: _2!) + } + else { + return nil + } + } + + } +} public extension Api { enum ExportedMessageLink: TypeConstructorDescription { case exportedMessageLink(link: String, html: String) @@ -1190,367 +1284,3 @@ public extension Api { } } -public extension Api { - enum InputBotInlineMessage: TypeConstructorDescription { - case inputBotInlineMessageGame(flags: Int32, replyMarkup: Api.ReplyMarkup?) - case inputBotInlineMessageMediaAuto(flags: Int32, message: String, entities: [Api.MessageEntity]?, replyMarkup: Api.ReplyMarkup?) - case inputBotInlineMessageMediaContact(flags: Int32, phoneNumber: String, firstName: String, lastName: String, vcard: String, replyMarkup: Api.ReplyMarkup?) - case inputBotInlineMessageMediaGeo(flags: Int32, geoPoint: Api.InputGeoPoint, heading: Int32?, period: Int32?, proximityNotificationRadius: Int32?, replyMarkup: Api.ReplyMarkup?) - case inputBotInlineMessageMediaInvoice(flags: Int32, title: String, description: String, photo: Api.InputWebDocument?, invoice: Api.Invoice, payload: Buffer, provider: String, providerData: Api.DataJSON, replyMarkup: Api.ReplyMarkup?) - case inputBotInlineMessageMediaVenue(flags: Int32, geoPoint: Api.InputGeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String, replyMarkup: Api.ReplyMarkup?) - case inputBotInlineMessageMediaWebPage(flags: Int32, message: String, entities: [Api.MessageEntity]?, url: String, replyMarkup: Api.ReplyMarkup?) - case inputBotInlineMessageText(flags: Int32, message: String, entities: [Api.MessageEntity]?, replyMarkup: Api.ReplyMarkup?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputBotInlineMessageGame(let flags, let replyMarkup): - if boxed { - buffer.appendInt32(1262639204) - } - serializeInt32(flags, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} - break - case .inputBotInlineMessageMediaAuto(let flags, let message, let entities, let replyMarkup): - if boxed { - buffer.appendInt32(864077702) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeString(message, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(entities!.count)) - for item in entities! { - item.serialize(buffer, true) - }} - if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} - break - case .inputBotInlineMessageMediaContact(let flags, let phoneNumber, let firstName, let lastName, let vcard, let replyMarkup): - if boxed { - buffer.appendInt32(-1494368259) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeString(phoneNumber, buffer: buffer, boxed: false) - serializeString(firstName, buffer: buffer, boxed: false) - serializeString(lastName, buffer: buffer, boxed: false) - serializeString(vcard, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} - break - case .inputBotInlineMessageMediaGeo(let flags, let geoPoint, let heading, let period, let proximityNotificationRadius, let replyMarkup): - if boxed { - buffer.appendInt32(-1768777083) - } - serializeInt32(flags, buffer: buffer, boxed: false) - geoPoint.serialize(buffer, true) - if Int(flags) & Int(1 << 0) != 0 {serializeInt32(heading!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 1) != 0 {serializeInt32(period!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 3) != 0 {serializeInt32(proximityNotificationRadius!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} - break - case .inputBotInlineMessageMediaInvoice(let flags, let title, let description, let photo, let invoice, let payload, let provider, let providerData, let replyMarkup): - if boxed { - buffer.appendInt32(-672693723) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeString(title, buffer: buffer, boxed: false) - serializeString(description, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {photo!.serialize(buffer, true)} - invoice.serialize(buffer, true) - serializeBytes(payload, buffer: buffer, boxed: false) - serializeString(provider, buffer: buffer, boxed: false) - providerData.serialize(buffer, true) - if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} - break - case .inputBotInlineMessageMediaVenue(let flags, let geoPoint, let title, let address, let provider, let venueId, let venueType, let replyMarkup): - if boxed { - buffer.appendInt32(1098628881) - } - serializeInt32(flags, buffer: buffer, boxed: false) - geoPoint.serialize(buffer, true) - serializeString(title, buffer: buffer, boxed: false) - serializeString(address, buffer: buffer, boxed: false) - serializeString(provider, buffer: buffer, boxed: false) - serializeString(venueId, buffer: buffer, boxed: false) - serializeString(venueType, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} - break - case .inputBotInlineMessageMediaWebPage(let flags, let message, let entities, let url, let replyMarkup): - if boxed { - buffer.appendInt32(-1109605104) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeString(message, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(entities!.count)) - for item in entities! { - item.serialize(buffer, true) - }} - serializeString(url, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} - break - case .inputBotInlineMessageText(let flags, let message, let entities, let replyMarkup): - if boxed { - buffer.appendInt32(1036876423) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeString(message, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(entities!.count)) - for item in entities! { - item.serialize(buffer, true) - }} - if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputBotInlineMessageGame(let flags, let replyMarkup): - return ("inputBotInlineMessageGame", [("flags", flags as Any), ("replyMarkup", replyMarkup as Any)]) - case .inputBotInlineMessageMediaAuto(let flags, let message, let entities, let replyMarkup): - return ("inputBotInlineMessageMediaAuto", [("flags", flags as Any), ("message", message as Any), ("entities", entities as Any), ("replyMarkup", replyMarkup as Any)]) - case .inputBotInlineMessageMediaContact(let flags, let phoneNumber, let firstName, let lastName, let vcard, let replyMarkup): - return ("inputBotInlineMessageMediaContact", [("flags", flags as Any), ("phoneNumber", phoneNumber as Any), ("firstName", firstName as Any), ("lastName", lastName as Any), ("vcard", vcard as Any), ("replyMarkup", replyMarkup as Any)]) - case .inputBotInlineMessageMediaGeo(let flags, let geoPoint, let heading, let period, let proximityNotificationRadius, let replyMarkup): - return ("inputBotInlineMessageMediaGeo", [("flags", flags as Any), ("geoPoint", geoPoint as Any), ("heading", heading as Any), ("period", period as Any), ("proximityNotificationRadius", proximityNotificationRadius as Any), ("replyMarkup", replyMarkup as Any)]) - case .inputBotInlineMessageMediaInvoice(let flags, let title, let description, let photo, let invoice, let payload, let provider, let providerData, let replyMarkup): - return ("inputBotInlineMessageMediaInvoice", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("invoice", invoice as Any), ("payload", payload as Any), ("provider", provider as Any), ("providerData", providerData as Any), ("replyMarkup", replyMarkup as Any)]) - case .inputBotInlineMessageMediaVenue(let flags, let geoPoint, let title, let address, let provider, let venueId, let venueType, let replyMarkup): - return ("inputBotInlineMessageMediaVenue", [("flags", flags as Any), ("geoPoint", geoPoint as Any), ("title", title as Any), ("address", address as Any), ("provider", provider as Any), ("venueId", venueId as Any), ("venueType", venueType as Any), ("replyMarkup", replyMarkup as Any)]) - case .inputBotInlineMessageMediaWebPage(let flags, let message, let entities, let url, let replyMarkup): - return ("inputBotInlineMessageMediaWebPage", [("flags", flags as Any), ("message", message as Any), ("entities", entities as Any), ("url", url as Any), ("replyMarkup", replyMarkup as Any)]) - case .inputBotInlineMessageText(let flags, let message, let entities, let replyMarkup): - return ("inputBotInlineMessageText", [("flags", flags as Any), ("message", message as Any), ("entities", entities as Any), ("replyMarkup", replyMarkup as Any)]) - } - } - - public static func parse_inputBotInlineMessageGame(_ reader: BufferReader) -> InputBotInlineMessage? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.ReplyMarkup? - if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup - } } - let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 2) == 0) || _2 != nil - if _c1 && _c2 { - return Api.InputBotInlineMessage.inputBotInlineMessageGame(flags: _1!, replyMarkup: _2) - } - else { - return nil - } - } - public static func parse_inputBotInlineMessageMediaAuto(_ reader: BufferReader) -> InputBotInlineMessage? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) - } } - var _4: Api.ReplyMarkup? - if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup - } } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.InputBotInlineMessage.inputBotInlineMessageMediaAuto(flags: _1!, message: _2!, entities: _3, replyMarkup: _4) - } - else { - return nil - } - } - public static func parse_inputBotInlineMessageMediaContact(_ reader: BufferReader) -> InputBotInlineMessage? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: String? - _3 = parseString(reader) - var _4: String? - _4 = parseString(reader) - var _5: String? - _5 = parseString(reader) - var _6: Api.ReplyMarkup? - if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { - _6 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup - } } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.InputBotInlineMessage.inputBotInlineMessageMediaContact(flags: _1!, phoneNumber: _2!, firstName: _3!, lastName: _4!, vcard: _5!, replyMarkup: _6) - } - else { - return nil - } - } - public static func parse_inputBotInlineMessageMediaGeo(_ reader: BufferReader) -> InputBotInlineMessage? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.InputGeoPoint? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.InputGeoPoint - } - var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 {_3 = reader.readInt32() } - var _4: Int32? - if Int(_1!) & Int(1 << 1) != 0 {_4 = reader.readInt32() } - var _5: Int32? - if Int(_1!) & Int(1 << 3) != 0 {_5 = reader.readInt32() } - var _6: Api.ReplyMarkup? - if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { - _6 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup - } } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.InputBotInlineMessage.inputBotInlineMessageMediaGeo(flags: _1!, geoPoint: _2!, heading: _3, period: _4, proximityNotificationRadius: _5, replyMarkup: _6) - } - else { - return nil - } - } - public static func parse_inputBotInlineMessageMediaInvoice(_ reader: BufferReader) -> InputBotInlineMessage? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: String? - _3 = parseString(reader) - var _4: Api.InputWebDocument? - if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.InputWebDocument - } } - var _5: Api.Invoice? - if let signature = reader.readInt32() { - _5 = Api.parse(reader, signature: signature) as? Api.Invoice - } - var _6: Buffer? - _6 = parseBytes(reader) - var _7: String? - _7 = parseString(reader) - var _8: Api.DataJSON? - if let signature = reader.readInt32() { - _8 = Api.parse(reader, signature: signature) as? Api.DataJSON - } - var _9: Api.ReplyMarkup? - if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { - _9 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup - } } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - let _c7 = _7 != nil - let _c8 = _8 != nil - let _c9 = (Int(_1!) & Int(1 << 2) == 0) || _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return Api.InputBotInlineMessage.inputBotInlineMessageMediaInvoice(flags: _1!, title: _2!, description: _3!, photo: _4, invoice: _5!, payload: _6!, provider: _7!, providerData: _8!, replyMarkup: _9) - } - else { - return nil - } - } - public static func parse_inputBotInlineMessageMediaVenue(_ reader: BufferReader) -> InputBotInlineMessage? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.InputGeoPoint? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.InputGeoPoint - } - var _3: String? - _3 = parseString(reader) - var _4: String? - _4 = parseString(reader) - var _5: String? - _5 = parseString(reader) - var _6: String? - _6 = parseString(reader) - var _7: String? - _7 = parseString(reader) - var _8: Api.ReplyMarkup? - if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { - _8 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup - } } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - let _c7 = _7 != nil - let _c8 = (Int(_1!) & Int(1 << 2) == 0) || _8 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { - return Api.InputBotInlineMessage.inputBotInlineMessageMediaVenue(flags: _1!, geoPoint: _2!, title: _3!, address: _4!, provider: _5!, venueId: _6!, venueType: _7!, replyMarkup: _8) - } - else { - return nil - } - } - public static func parse_inputBotInlineMessageMediaWebPage(_ reader: BufferReader) -> InputBotInlineMessage? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) - } } - var _4: String? - _4 = parseString(reader) - var _5: Api.ReplyMarkup? - if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { - _5 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup - } } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.InputBotInlineMessage.inputBotInlineMessageMediaWebPage(flags: _1!, message: _2!, entities: _3, url: _4!, replyMarkup: _5) - } - else { - return nil - } - } - public static func parse_inputBotInlineMessageText(_ reader: BufferReader) -> InputBotInlineMessage? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) - } } - var _4: Api.ReplyMarkup? - if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup - } } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.InputBotInlineMessage.inputBotInlineMessageText(flags: _1!, message: _2!, entities: _3, replyMarkup: _4) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api8.swift b/submodules/TelegramApi/Sources/Api8.swift index 488716ce45f..429d2a4e3a6 100644 --- a/submodules/TelegramApi/Sources/Api8.swift +++ b/submodules/TelegramApi/Sources/Api8.swift @@ -1,3 +1,367 @@ +public extension Api { + enum InputBotInlineMessage: TypeConstructorDescription { + case inputBotInlineMessageGame(flags: Int32, replyMarkup: Api.ReplyMarkup?) + case inputBotInlineMessageMediaAuto(flags: Int32, message: String, entities: [Api.MessageEntity]?, replyMarkup: Api.ReplyMarkup?) + case inputBotInlineMessageMediaContact(flags: Int32, phoneNumber: String, firstName: String, lastName: String, vcard: String, replyMarkup: Api.ReplyMarkup?) + case inputBotInlineMessageMediaGeo(flags: Int32, geoPoint: Api.InputGeoPoint, heading: Int32?, period: Int32?, proximityNotificationRadius: Int32?, replyMarkup: Api.ReplyMarkup?) + case inputBotInlineMessageMediaInvoice(flags: Int32, title: String, description: String, photo: Api.InputWebDocument?, invoice: Api.Invoice, payload: Buffer, provider: String, providerData: Api.DataJSON, replyMarkup: Api.ReplyMarkup?) + case inputBotInlineMessageMediaVenue(flags: Int32, geoPoint: Api.InputGeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String, replyMarkup: Api.ReplyMarkup?) + case inputBotInlineMessageMediaWebPage(flags: Int32, message: String, entities: [Api.MessageEntity]?, url: String, replyMarkup: Api.ReplyMarkup?) + case inputBotInlineMessageText(flags: Int32, message: String, entities: [Api.MessageEntity]?, replyMarkup: Api.ReplyMarkup?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputBotInlineMessageGame(let flags, let replyMarkup): + if boxed { + buffer.appendInt32(1262639204) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} + break + case .inputBotInlineMessageMediaAuto(let flags, let message, let entities, let replyMarkup): + if boxed { + buffer.appendInt32(864077702) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(message, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(entities!.count)) + for item in entities! { + item.serialize(buffer, true) + }} + if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} + break + case .inputBotInlineMessageMediaContact(let flags, let phoneNumber, let firstName, let lastName, let vcard, let replyMarkup): + if boxed { + buffer.appendInt32(-1494368259) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(phoneNumber, buffer: buffer, boxed: false) + serializeString(firstName, buffer: buffer, boxed: false) + serializeString(lastName, buffer: buffer, boxed: false) + serializeString(vcard, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} + break + case .inputBotInlineMessageMediaGeo(let flags, let geoPoint, let heading, let period, let proximityNotificationRadius, let replyMarkup): + if boxed { + buffer.appendInt32(-1768777083) + } + serializeInt32(flags, buffer: buffer, boxed: false) + geoPoint.serialize(buffer, true) + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(heading!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(period!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 3) != 0 {serializeInt32(proximityNotificationRadius!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} + break + case .inputBotInlineMessageMediaInvoice(let flags, let title, let description, let photo, let invoice, let payload, let provider, let providerData, let replyMarkup): + if boxed { + buffer.appendInt32(-672693723) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + serializeString(description, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {photo!.serialize(buffer, true)} + invoice.serialize(buffer, true) + serializeBytes(payload, buffer: buffer, boxed: false) + serializeString(provider, buffer: buffer, boxed: false) + providerData.serialize(buffer, true) + if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} + break + case .inputBotInlineMessageMediaVenue(let flags, let geoPoint, let title, let address, let provider, let venueId, let venueType, let replyMarkup): + if boxed { + buffer.appendInt32(1098628881) + } + serializeInt32(flags, buffer: buffer, boxed: false) + geoPoint.serialize(buffer, true) + serializeString(title, buffer: buffer, boxed: false) + serializeString(address, buffer: buffer, boxed: false) + serializeString(provider, buffer: buffer, boxed: false) + serializeString(venueId, buffer: buffer, boxed: false) + serializeString(venueType, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} + break + case .inputBotInlineMessageMediaWebPage(let flags, let message, let entities, let url, let replyMarkup): + if boxed { + buffer.appendInt32(-1109605104) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(message, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(entities!.count)) + for item in entities! { + item.serialize(buffer, true) + }} + serializeString(url, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} + break + case .inputBotInlineMessageText(let flags, let message, let entities, let replyMarkup): + if boxed { + buffer.appendInt32(1036876423) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(message, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(entities!.count)) + for item in entities! { + item.serialize(buffer, true) + }} + if Int(flags) & Int(1 << 2) != 0 {replyMarkup!.serialize(buffer, true)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputBotInlineMessageGame(let flags, let replyMarkup): + return ("inputBotInlineMessageGame", [("flags", flags as Any), ("replyMarkup", replyMarkup as Any)]) + case .inputBotInlineMessageMediaAuto(let flags, let message, let entities, let replyMarkup): + return ("inputBotInlineMessageMediaAuto", [("flags", flags as Any), ("message", message as Any), ("entities", entities as Any), ("replyMarkup", replyMarkup as Any)]) + case .inputBotInlineMessageMediaContact(let flags, let phoneNumber, let firstName, let lastName, let vcard, let replyMarkup): + return ("inputBotInlineMessageMediaContact", [("flags", flags as Any), ("phoneNumber", phoneNumber as Any), ("firstName", firstName as Any), ("lastName", lastName as Any), ("vcard", vcard as Any), ("replyMarkup", replyMarkup as Any)]) + case .inputBotInlineMessageMediaGeo(let flags, let geoPoint, let heading, let period, let proximityNotificationRadius, let replyMarkup): + return ("inputBotInlineMessageMediaGeo", [("flags", flags as Any), ("geoPoint", geoPoint as Any), ("heading", heading as Any), ("period", period as Any), ("proximityNotificationRadius", proximityNotificationRadius as Any), ("replyMarkup", replyMarkup as Any)]) + case .inputBotInlineMessageMediaInvoice(let flags, let title, let description, let photo, let invoice, let payload, let provider, let providerData, let replyMarkup): + return ("inputBotInlineMessageMediaInvoice", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("invoice", invoice as Any), ("payload", payload as Any), ("provider", provider as Any), ("providerData", providerData as Any), ("replyMarkup", replyMarkup as Any)]) + case .inputBotInlineMessageMediaVenue(let flags, let geoPoint, let title, let address, let provider, let venueId, let venueType, let replyMarkup): + return ("inputBotInlineMessageMediaVenue", [("flags", flags as Any), ("geoPoint", geoPoint as Any), ("title", title as Any), ("address", address as Any), ("provider", provider as Any), ("venueId", venueId as Any), ("venueType", venueType as Any), ("replyMarkup", replyMarkup as Any)]) + case .inputBotInlineMessageMediaWebPage(let flags, let message, let entities, let url, let replyMarkup): + return ("inputBotInlineMessageMediaWebPage", [("flags", flags as Any), ("message", message as Any), ("entities", entities as Any), ("url", url as Any), ("replyMarkup", replyMarkup as Any)]) + case .inputBotInlineMessageText(let flags, let message, let entities, let replyMarkup): + return ("inputBotInlineMessageText", [("flags", flags as Any), ("message", message as Any), ("entities", entities as Any), ("replyMarkup", replyMarkup as Any)]) + } + } + + public static func parse_inputBotInlineMessageGame(_ reader: BufferReader) -> InputBotInlineMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.ReplyMarkup? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup + } } + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 2) == 0) || _2 != nil + if _c1 && _c2 { + return Api.InputBotInlineMessage.inputBotInlineMessageGame(flags: _1!, replyMarkup: _2) + } + else { + return nil + } + } + public static func parse_inputBotInlineMessageMediaAuto(_ reader: BufferReader) -> InputBotInlineMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: [Api.MessageEntity]? + if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + } } + var _4: Api.ReplyMarkup? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.InputBotInlineMessage.inputBotInlineMessageMediaAuto(flags: _1!, message: _2!, entities: _3, replyMarkup: _4) + } + else { + return nil + } + } + public static func parse_inputBotInlineMessageMediaContact(_ reader: BufferReader) -> InputBotInlineMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + var _4: String? + _4 = parseString(reader) + var _5: String? + _5 = parseString(reader) + var _6: Api.ReplyMarkup? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.InputBotInlineMessage.inputBotInlineMessageMediaContact(flags: _1!, phoneNumber: _2!, firstName: _3!, lastName: _4!, vcard: _5!, replyMarkup: _6) + } + else { + return nil + } + } + public static func parse_inputBotInlineMessageMediaGeo(_ reader: BufferReader) -> InputBotInlineMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.InputGeoPoint? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.InputGeoPoint + } + var _3: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_3 = reader.readInt32() } + var _4: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_4 = reader.readInt32() } + var _5: Int32? + if Int(_1!) & Int(1 << 3) != 0 {_5 = reader.readInt32() } + var _6: Api.ReplyMarkup? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.InputBotInlineMessage.inputBotInlineMessageMediaGeo(flags: _1!, geoPoint: _2!, heading: _3, period: _4, proximityNotificationRadius: _5, replyMarkup: _6) + } + else { + return nil + } + } + public static func parse_inputBotInlineMessageMediaInvoice(_ reader: BufferReader) -> InputBotInlineMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + var _4: Api.InputWebDocument? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.InputWebDocument + } } + var _5: Api.Invoice? + if let signature = reader.readInt32() { + _5 = Api.parse(reader, signature: signature) as? Api.Invoice + } + var _6: Buffer? + _6 = parseBytes(reader) + var _7: String? + _7 = parseString(reader) + var _8: Api.DataJSON? + if let signature = reader.readInt32() { + _8 = Api.parse(reader, signature: signature) as? Api.DataJSON + } + var _9: Api.ReplyMarkup? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _9 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = (Int(_1!) & Int(1 << 2) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.InputBotInlineMessage.inputBotInlineMessageMediaInvoice(flags: _1!, title: _2!, description: _3!, photo: _4, invoice: _5!, payload: _6!, provider: _7!, providerData: _8!, replyMarkup: _9) + } + else { + return nil + } + } + public static func parse_inputBotInlineMessageMediaVenue(_ reader: BufferReader) -> InputBotInlineMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.InputGeoPoint? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.InputGeoPoint + } + var _3: String? + _3 = parseString(reader) + var _4: String? + _4 = parseString(reader) + var _5: String? + _5 = parseString(reader) + var _6: String? + _6 = parseString(reader) + var _7: String? + _7 = parseString(reader) + var _8: Api.ReplyMarkup? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _8 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = (Int(_1!) & Int(1 << 2) == 0) || _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return Api.InputBotInlineMessage.inputBotInlineMessageMediaVenue(flags: _1!, geoPoint: _2!, title: _3!, address: _4!, provider: _5!, venueId: _6!, venueType: _7!, replyMarkup: _8) + } + else { + return nil + } + } + public static func parse_inputBotInlineMessageMediaWebPage(_ reader: BufferReader) -> InputBotInlineMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: [Api.MessageEntity]? + if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + } } + var _4: String? + _4 = parseString(reader) + var _5: Api.ReplyMarkup? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _5 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.InputBotInlineMessage.inputBotInlineMessageMediaWebPage(flags: _1!, message: _2!, entities: _3, url: _4!, replyMarkup: _5) + } + else { + return nil + } + } + public static func parse_inputBotInlineMessageText(_ reader: BufferReader) -> InputBotInlineMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: [Api.MessageEntity]? + if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + } } + var _4: Api.ReplyMarkup? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.InputBotInlineMessage.inputBotInlineMessageText(flags: _1!, message: _2!, entities: _3, replyMarkup: _4) + } + else { + return nil + } + } + + } +} public extension Api { enum InputBotInlineMessageID: TypeConstructorDescription { case inputBotInlineMessageID(dcId: Int32, id: Int64, accessHash: Int64) @@ -928,317 +1292,3 @@ public extension Api { } } -public extension Api { - enum InputContact: TypeConstructorDescription { - case inputPhoneContact(clientId: Int64, phone: String, firstName: String, lastName: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputPhoneContact(let clientId, let phone, let firstName, let lastName): - if boxed { - buffer.appendInt32(-208488460) - } - serializeInt64(clientId, buffer: buffer, boxed: false) - serializeString(phone, buffer: buffer, boxed: false) - serializeString(firstName, buffer: buffer, boxed: false) - serializeString(lastName, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputPhoneContact(let clientId, let phone, let firstName, let lastName): - return ("inputPhoneContact", [("clientId", clientId as Any), ("phone", phone as Any), ("firstName", firstName as Any), ("lastName", lastName as Any)]) - } - } - - public static func parse_inputPhoneContact(_ reader: BufferReader) -> InputContact? { - var _1: Int64? - _1 = reader.readInt64() - var _2: String? - _2 = parseString(reader) - var _3: String? - _3 = parseString(reader) - var _4: String? - _4 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.InputContact.inputPhoneContact(clientId: _1!, phone: _2!, firstName: _3!, lastName: _4!) - } - else { - return nil - } - } - - } -} -public extension Api { - indirect enum InputDialogPeer: TypeConstructorDescription { - case inputDialogPeer(peer: Api.InputPeer) - case inputDialogPeerFolder(folderId: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputDialogPeer(let peer): - if boxed { - buffer.appendInt32(-55902537) - } - peer.serialize(buffer, true) - break - case .inputDialogPeerFolder(let folderId): - if boxed { - buffer.appendInt32(1684014375) - } - serializeInt32(folderId, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputDialogPeer(let peer): - return ("inputDialogPeer", [("peer", peer as Any)]) - case .inputDialogPeerFolder(let folderId): - return ("inputDialogPeerFolder", [("folderId", folderId as Any)]) - } - } - - public static func parse_inputDialogPeer(_ reader: BufferReader) -> InputDialogPeer? { - var _1: Api.InputPeer? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.InputPeer - } - let _c1 = _1 != nil - if _c1 { - return Api.InputDialogPeer.inputDialogPeer(peer: _1!) - } - else { - return nil - } - } - public static func parse_inputDialogPeerFolder(_ reader: BufferReader) -> InputDialogPeer? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return Api.InputDialogPeer.inputDialogPeerFolder(folderId: _1!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum InputDocument: TypeConstructorDescription { - case inputDocument(id: Int64, accessHash: Int64, fileReference: Buffer) - case inputDocumentEmpty - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputDocument(let id, let accessHash, let fileReference): - if boxed { - buffer.appendInt32(448771445) - } - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - serializeBytes(fileReference, buffer: buffer, boxed: false) - break - case .inputDocumentEmpty: - if boxed { - buffer.appendInt32(1928391342) - } - - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputDocument(let id, let accessHash, let fileReference): - return ("inputDocument", [("id", id as Any), ("accessHash", accessHash as Any), ("fileReference", fileReference as Any)]) - case .inputDocumentEmpty: - return ("inputDocumentEmpty", []) - } - } - - public static func parse_inputDocument(_ reader: BufferReader) -> InputDocument? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int64? - _2 = reader.readInt64() - var _3: Buffer? - _3 = parseBytes(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.InputDocument.inputDocument(id: _1!, accessHash: _2!, fileReference: _3!) - } - else { - return nil - } - } - public static func parse_inputDocumentEmpty(_ reader: BufferReader) -> InputDocument? { - return Api.InputDocument.inputDocumentEmpty - } - - } -} -public extension Api { - enum InputEncryptedChat: TypeConstructorDescription { - case inputEncryptedChat(chatId: Int32, accessHash: Int64) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputEncryptedChat(let chatId, let accessHash): - if boxed { - buffer.appendInt32(-247351839) - } - serializeInt32(chatId, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputEncryptedChat(let chatId, let accessHash): - return ("inputEncryptedChat", [("chatId", chatId as Any), ("accessHash", accessHash as Any)]) - } - } - - public static func parse_inputEncryptedChat(_ reader: BufferReader) -> InputEncryptedChat? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int64? - _2 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputEncryptedChat.inputEncryptedChat(chatId: _1!, accessHash: _2!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum InputEncryptedFile: TypeConstructorDescription { - case inputEncryptedFile(id: Int64, accessHash: Int64) - case inputEncryptedFileBigUploaded(id: Int64, parts: Int32, keyFingerprint: Int32) - case inputEncryptedFileEmpty - case inputEncryptedFileUploaded(id: Int64, parts: Int32, md5Checksum: String, keyFingerprint: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputEncryptedFile(let id, let accessHash): - if boxed { - buffer.appendInt32(1511503333) - } - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - break - case .inputEncryptedFileBigUploaded(let id, let parts, let keyFingerprint): - if boxed { - buffer.appendInt32(767652808) - } - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt32(parts, buffer: buffer, boxed: false) - serializeInt32(keyFingerprint, buffer: buffer, boxed: false) - break - case .inputEncryptedFileEmpty: - if boxed { - buffer.appendInt32(406307684) - } - - break - case .inputEncryptedFileUploaded(let id, let parts, let md5Checksum, let keyFingerprint): - if boxed { - buffer.appendInt32(1690108678) - } - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt32(parts, buffer: buffer, boxed: false) - serializeString(md5Checksum, buffer: buffer, boxed: false) - serializeInt32(keyFingerprint, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputEncryptedFile(let id, let accessHash): - return ("inputEncryptedFile", [("id", id as Any), ("accessHash", accessHash as Any)]) - case .inputEncryptedFileBigUploaded(let id, let parts, let keyFingerprint): - return ("inputEncryptedFileBigUploaded", [("id", id as Any), ("parts", parts as Any), ("keyFingerprint", keyFingerprint as Any)]) - case .inputEncryptedFileEmpty: - return ("inputEncryptedFileEmpty", []) - case .inputEncryptedFileUploaded(let id, let parts, let md5Checksum, let keyFingerprint): - return ("inputEncryptedFileUploaded", [("id", id as Any), ("parts", parts as Any), ("md5Checksum", md5Checksum as Any), ("keyFingerprint", keyFingerprint as Any)]) - } - } - - public static func parse_inputEncryptedFile(_ reader: BufferReader) -> InputEncryptedFile? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int64? - _2 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputEncryptedFile.inputEncryptedFile(id: _1!, accessHash: _2!) - } - else { - return nil - } - } - public static func parse_inputEncryptedFileBigUploaded(_ reader: BufferReader) -> InputEncryptedFile? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.InputEncryptedFile.inputEncryptedFileBigUploaded(id: _1!, parts: _2!, keyFingerprint: _3!) - } - else { - return nil - } - } - public static func parse_inputEncryptedFileEmpty(_ reader: BufferReader) -> InputEncryptedFile? { - return Api.InputEncryptedFile.inputEncryptedFileEmpty - } - public static func parse_inputEncryptedFileUploaded(_ reader: BufferReader) -> InputEncryptedFile? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int32? - _2 = reader.readInt32() - var _3: String? - _3 = parseString(reader) - var _4: Int32? - _4 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.InputEncryptedFile.inputEncryptedFileUploaded(id: _1!, parts: _2!, md5Checksum: _3!, keyFingerprint: _4!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api9.swift b/submodules/TelegramApi/Sources/Api9.swift index 7ae35b59dd2..551e5d648a5 100644 --- a/submodules/TelegramApi/Sources/Api9.swift +++ b/submodules/TelegramApi/Sources/Api9.swift @@ -1,3 +1,317 @@ +public extension Api { + enum InputContact: TypeConstructorDescription { + case inputPhoneContact(clientId: Int64, phone: String, firstName: String, lastName: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputPhoneContact(let clientId, let phone, let firstName, let lastName): + if boxed { + buffer.appendInt32(-208488460) + } + serializeInt64(clientId, buffer: buffer, boxed: false) + serializeString(phone, buffer: buffer, boxed: false) + serializeString(firstName, buffer: buffer, boxed: false) + serializeString(lastName, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputPhoneContact(let clientId, let phone, let firstName, let lastName): + return ("inputPhoneContact", [("clientId", clientId as Any), ("phone", phone as Any), ("firstName", firstName as Any), ("lastName", lastName as Any)]) + } + } + + public static func parse_inputPhoneContact(_ reader: BufferReader) -> InputContact? { + var _1: Int64? + _1 = reader.readInt64() + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + var _4: String? + _4 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.InputContact.inputPhoneContact(clientId: _1!, phone: _2!, firstName: _3!, lastName: _4!) + } + else { + return nil + } + } + + } +} +public extension Api { + indirect enum InputDialogPeer: TypeConstructorDescription { + case inputDialogPeer(peer: Api.InputPeer) + case inputDialogPeerFolder(folderId: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputDialogPeer(let peer): + if boxed { + buffer.appendInt32(-55902537) + } + peer.serialize(buffer, true) + break + case .inputDialogPeerFolder(let folderId): + if boxed { + buffer.appendInt32(1684014375) + } + serializeInt32(folderId, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputDialogPeer(let peer): + return ("inputDialogPeer", [("peer", peer as Any)]) + case .inputDialogPeerFolder(let folderId): + return ("inputDialogPeerFolder", [("folderId", folderId as Any)]) + } + } + + public static func parse_inputDialogPeer(_ reader: BufferReader) -> InputDialogPeer? { + var _1: Api.InputPeer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputPeer + } + let _c1 = _1 != nil + if _c1 { + return Api.InputDialogPeer.inputDialogPeer(peer: _1!) + } + else { + return nil + } + } + public static func parse_inputDialogPeerFolder(_ reader: BufferReader) -> InputDialogPeer? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.InputDialogPeer.inputDialogPeerFolder(folderId: _1!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum InputDocument: TypeConstructorDescription { + case inputDocument(id: Int64, accessHash: Int64, fileReference: Buffer) + case inputDocumentEmpty + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputDocument(let id, let accessHash, let fileReference): + if boxed { + buffer.appendInt32(448771445) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + serializeBytes(fileReference, buffer: buffer, boxed: false) + break + case .inputDocumentEmpty: + if boxed { + buffer.appendInt32(1928391342) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputDocument(let id, let accessHash, let fileReference): + return ("inputDocument", [("id", id as Any), ("accessHash", accessHash as Any), ("fileReference", fileReference as Any)]) + case .inputDocumentEmpty: + return ("inputDocumentEmpty", []) + } + } + + public static func parse_inputDocument(_ reader: BufferReader) -> InputDocument? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + var _3: Buffer? + _3 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.InputDocument.inputDocument(id: _1!, accessHash: _2!, fileReference: _3!) + } + else { + return nil + } + } + public static func parse_inputDocumentEmpty(_ reader: BufferReader) -> InputDocument? { + return Api.InputDocument.inputDocumentEmpty + } + + } +} +public extension Api { + enum InputEncryptedChat: TypeConstructorDescription { + case inputEncryptedChat(chatId: Int32, accessHash: Int64) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputEncryptedChat(let chatId, let accessHash): + if boxed { + buffer.appendInt32(-247351839) + } + serializeInt32(chatId, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputEncryptedChat(let chatId, let accessHash): + return ("inputEncryptedChat", [("chatId", chatId as Any), ("accessHash", accessHash as Any)]) + } + } + + public static func parse_inputEncryptedChat(_ reader: BufferReader) -> InputEncryptedChat? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputEncryptedChat.inputEncryptedChat(chatId: _1!, accessHash: _2!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum InputEncryptedFile: TypeConstructorDescription { + case inputEncryptedFile(id: Int64, accessHash: Int64) + case inputEncryptedFileBigUploaded(id: Int64, parts: Int32, keyFingerprint: Int32) + case inputEncryptedFileEmpty + case inputEncryptedFileUploaded(id: Int64, parts: Int32, md5Checksum: String, keyFingerprint: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputEncryptedFile(let id, let accessHash): + if boxed { + buffer.appendInt32(1511503333) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + break + case .inputEncryptedFileBigUploaded(let id, let parts, let keyFingerprint): + if boxed { + buffer.appendInt32(767652808) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt32(parts, buffer: buffer, boxed: false) + serializeInt32(keyFingerprint, buffer: buffer, boxed: false) + break + case .inputEncryptedFileEmpty: + if boxed { + buffer.appendInt32(406307684) + } + + break + case .inputEncryptedFileUploaded(let id, let parts, let md5Checksum, let keyFingerprint): + if boxed { + buffer.appendInt32(1690108678) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt32(parts, buffer: buffer, boxed: false) + serializeString(md5Checksum, buffer: buffer, boxed: false) + serializeInt32(keyFingerprint, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputEncryptedFile(let id, let accessHash): + return ("inputEncryptedFile", [("id", id as Any), ("accessHash", accessHash as Any)]) + case .inputEncryptedFileBigUploaded(let id, let parts, let keyFingerprint): + return ("inputEncryptedFileBigUploaded", [("id", id as Any), ("parts", parts as Any), ("keyFingerprint", keyFingerprint as Any)]) + case .inputEncryptedFileEmpty: + return ("inputEncryptedFileEmpty", []) + case .inputEncryptedFileUploaded(let id, let parts, let md5Checksum, let keyFingerprint): + return ("inputEncryptedFileUploaded", [("id", id as Any), ("parts", parts as Any), ("md5Checksum", md5Checksum as Any), ("keyFingerprint", keyFingerprint as Any)]) + } + } + + public static func parse_inputEncryptedFile(_ reader: BufferReader) -> InputEncryptedFile? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputEncryptedFile.inputEncryptedFile(id: _1!, accessHash: _2!) + } + else { + return nil + } + } + public static func parse_inputEncryptedFileBigUploaded(_ reader: BufferReader) -> InputEncryptedFile? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.InputEncryptedFile.inputEncryptedFileBigUploaded(id: _1!, parts: _2!, keyFingerprint: _3!) + } + else { + return nil + } + } + public static func parse_inputEncryptedFileEmpty(_ reader: BufferReader) -> InputEncryptedFile? { + return Api.InputEncryptedFile.inputEncryptedFileEmpty + } + public static func parse_inputEncryptedFileUploaded(_ reader: BufferReader) -> InputEncryptedFile? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.InputEncryptedFile.inputEncryptedFileUploaded(id: _1!, parts: _2!, md5Checksum: _3!, keyFingerprint: _4!) + } + else { + return nil + } + } + + } +} public extension Api { enum InputFile: TypeConstructorDescription { case inputFile(id: Int64, parts: Int32, name: String, md5Checksum: String) diff --git a/submodules/TelegramBaseController/Sources/LocationBroadcastActionSheetItem.swift b/submodules/TelegramBaseController/Sources/LocationBroadcastActionSheetItem.swift index ab090a4db7a..30bdac85067 100644 --- a/submodules/TelegramBaseController/Sources/LocationBroadcastActionSheetItem.swift +++ b/submodules/TelegramBaseController/Sources/LocationBroadcastActionSheetItem.swift @@ -107,7 +107,7 @@ public class LocationBroadcastActionSheetItemNode: ActionSheetItemNode { self.avatarNode.setPeer(context: item.context, theme: (item.context.sharedContext.currentPresentationData.with { $0 }).theme, peer: EnginePeer(item.peer)) - self.timerNode.update(backgroundColor: self.theme.controlAccentColor.withAlphaComponent(0.4), foregroundColor: self.theme.controlAccentColor, textColor: self.theme.controlAccentColor, beginTimestamp: item.beginTimestamp, timeout: item.timeout, strings: item.strings) + self.timerNode.update(backgroundColor: self.theme.controlAccentColor.withAlphaComponent(0.4), foregroundColor: self.theme.controlAccentColor, textColor: self.theme.controlAccentColor, beginTimestamp: item.beginTimestamp, timeout: Int32(item.timeout) == liveLocationIndefinitePeriod ? -1.0 : item.timeout, strings: item.strings) } public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatActionItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatActionItem.swift index d547845763c..320041dea4b 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatActionItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatActionItem.swift @@ -273,7 +273,7 @@ class VoiceChatActionItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatFullscreenParticipantItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatFullscreenParticipantItem.swift index af8d6213185..c07a15d7050 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatFullscreenParticipantItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatFullscreenParticipantItem.swift @@ -1009,7 +1009,7 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { self.updateIsHighlighted(transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate) } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift index 3ab4562186f..ee4c548c91f 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift @@ -1291,7 +1291,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { self.updateIsHighlighted(transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate) } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index 79c3c4658a1..b761ba9e682 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -128,6 +128,7 @@ enum AccountStateMutationOperation { case UpdateStorySentReaction(peerId: PeerId, id: Int32, reaction: Api.Reaction) case UpdateNewAuthorization(isUnconfirmed: Bool, hash: Int64, date: Int32, device: String, location: String) case UpdateWallpaper(peerId: PeerId, wallpaper: TelegramWallpaper?) + case UpdateRevenueBalances(RevenueStats.Balances) } struct HoleFromPreviousState { @@ -524,7 +525,7 @@ struct AccountMutableState { mutating func updatePeersNearby(_ peersNearby: [PeerNearby]) { self.addOperation(.UpdatePeersNearby(peersNearby)) } - + mutating func updateTheme(_ theme: TelegramTheme) { self.addOperation(.UpdateTheme(theme)) } @@ -673,9 +674,13 @@ struct AccountMutableState { self.addOperation(.UpdateNewAuthorization(isUnconfirmed: isUnconfirmed, hash: hash, date: date, device: device, location: location)) } + mutating func updateRevenueBalances(_ balances: RevenueStats.Balances) { + self.addOperation(.UpdateRevenueBalances(balances)) + } + mutating func addOperation(_ operation: AccountStateMutationOperation) { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .UpdateWallpaper, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .ReadOutbox, .ReadGroupFeedInbox, .MergePeerPresences, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdatePeerChatUnreadMark, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .UpdateWallpaper, .SyncChatListFilters, .UpdateChatListFilterOrder, .UpdateChatListFilter, .UpdateReadThread, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateMessagesPinned, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateRevenueBalances: break case let .AddMessages(messages, location): for message in messages { @@ -819,6 +824,7 @@ struct AccountReplayedFinalState { let updatedOutgoingThreadReadStates: [MessageId: MessageId.Id] let updateConfig: Bool let isPremiumUpdated: Bool + let updatedRevenueBalances: RevenueStats.Balances? } struct AccountFinalStateEvents { @@ -845,12 +851,13 @@ struct AccountFinalStateEvents { let updatedOutgoingThreadReadStates: [MessageId: MessageId.Id] let updateConfig: Bool let isPremiumUpdated: Bool + let updatedRevenueBalances: RevenueStats.Balances? var isEmpty: Bool { - return self.addedIncomingMessageIds.isEmpty && self.addedReactionEvents.isEmpty && self.wasScheduledMessageIds.isEmpty && self.deletedMessageIds.isEmpty && self.updatedTypingActivities.isEmpty && self.updatedWebpages.isEmpty && self.updatedCalls.isEmpty && self.addedCallSignalingData.isEmpty && self.updatedGroupCallParticipants.isEmpty && self.storyUpdates.isEmpty && self.updatedPeersNearby?.isEmpty ?? true && self.isContactUpdates.isEmpty && self.displayAlerts.isEmpty && self.dismissBotWebViews.isEmpty && self.delayNotificatonsUntil == nil && self.updatedMaxMessageId == nil && self.updatedQts == nil && self.externallyUpdatedPeerId.isEmpty && !authorizationListUpdated && self.updatedIncomingThreadReadStates.isEmpty && self.updatedOutgoingThreadReadStates.isEmpty && !self.updateConfig && !isPremiumUpdated + return self.addedIncomingMessageIds.isEmpty && self.addedReactionEvents.isEmpty && self.wasScheduledMessageIds.isEmpty && self.deletedMessageIds.isEmpty && self.updatedTypingActivities.isEmpty && self.updatedWebpages.isEmpty && self.updatedCalls.isEmpty && self.addedCallSignalingData.isEmpty && self.updatedGroupCallParticipants.isEmpty && self.storyUpdates.isEmpty && self.updatedPeersNearby?.isEmpty ?? true && self.isContactUpdates.isEmpty && self.displayAlerts.isEmpty && self.dismissBotWebViews.isEmpty && self.delayNotificatonsUntil == nil && self.updatedMaxMessageId == nil && self.updatedQts == nil && self.externallyUpdatedPeerId.isEmpty && !authorizationListUpdated && self.updatedIncomingThreadReadStates.isEmpty && self.updatedOutgoingThreadReadStates.isEmpty && !self.updateConfig && !self.isPremiumUpdated && self.updatedRevenueBalances == nil } - init(addedIncomingMessageIds: [MessageId] = [], addedReactionEvents: [(reactionAuthor: Peer, reaction: MessageReaction.Reaction, message: Message, timestamp: Int32)] = [], wasScheduledMessageIds: [MessageId] = [], deletedMessageIds: [DeletedMessageId] = [], updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] = [:], updatedWebpages: [MediaId: TelegramMediaWebpage] = [:], updatedCalls: [Api.PhoneCall] = [], addedCallSignalingData: [(Int64, Data)] = [], updatedGroupCallParticipants: [(Int64, GroupCallParticipantsContext.Update)] = [], storyUpdates: [InternalStoryUpdate] = [], updatedPeersNearby: [PeerNearby]? = nil, isContactUpdates: [(PeerId, Bool)] = [], displayAlerts: [(text: String, isDropAuth: Bool)] = [], dismissBotWebViews: [Int64] = [], delayNotificatonsUntil: Int32? = nil, updatedMaxMessageId: Int32? = nil, updatedQts: Int32? = nil, externallyUpdatedPeerId: Set = Set(), authorizationListUpdated: Bool = false, updatedIncomingThreadReadStates: [MessageId: MessageId.Id] = [:], updatedOutgoingThreadReadStates: [MessageId: MessageId.Id] = [:], updateConfig: Bool = false, isPremiumUpdated: Bool = false) { + init(addedIncomingMessageIds: [MessageId] = [], addedReactionEvents: [(reactionAuthor: Peer, reaction: MessageReaction.Reaction, message: Message, timestamp: Int32)] = [], wasScheduledMessageIds: [MessageId] = [], deletedMessageIds: [DeletedMessageId] = [], updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] = [:], updatedWebpages: [MediaId: TelegramMediaWebpage] = [:], updatedCalls: [Api.PhoneCall] = [], addedCallSignalingData: [(Int64, Data)] = [], updatedGroupCallParticipants: [(Int64, GroupCallParticipantsContext.Update)] = [], storyUpdates: [InternalStoryUpdate] = [], updatedPeersNearby: [PeerNearby]? = nil, isContactUpdates: [(PeerId, Bool)] = [], displayAlerts: [(text: String, isDropAuth: Bool)] = [], dismissBotWebViews: [Int64] = [], delayNotificatonsUntil: Int32? = nil, updatedMaxMessageId: Int32? = nil, updatedQts: Int32? = nil, externallyUpdatedPeerId: Set = Set(), authorizationListUpdated: Bool = false, updatedIncomingThreadReadStates: [MessageId: MessageId.Id] = [:], updatedOutgoingThreadReadStates: [MessageId: MessageId.Id] = [:], updateConfig: Bool = false, isPremiumUpdated: Bool = false, updatedRevenueBalances: RevenueStats.Balances? = nil) { self.addedIncomingMessageIds = addedIncomingMessageIds self.addedReactionEvents = addedReactionEvents self.wasScheduledMessageIds = wasScheduledMessageIds @@ -874,6 +881,7 @@ struct AccountFinalStateEvents { self.updatedOutgoingThreadReadStates = updatedOutgoingThreadReadStates self.updateConfig = updateConfig self.isPremiumUpdated = isPremiumUpdated + self.updatedRevenueBalances = updatedRevenueBalances } init(state: AccountReplayedFinalState) { @@ -900,6 +908,7 @@ struct AccountFinalStateEvents { self.updatedOutgoingThreadReadStates = state.updatedOutgoingThreadReadStates self.updateConfig = state.updateConfig self.isPremiumUpdated = state.isPremiumUpdated + self.updatedRevenueBalances = state.updatedRevenueBalances } func union(with other: AccountFinalStateEvents) -> AccountFinalStateEvents { @@ -929,6 +938,6 @@ struct AccountFinalStateEvents { let isPremiumUpdated = self.isPremiumUpdated || other.isPremiumUpdated - return AccountFinalStateEvents(addedIncomingMessageIds: self.addedIncomingMessageIds + other.addedIncomingMessageIds, addedReactionEvents: self.addedReactionEvents + other.addedReactionEvents, wasScheduledMessageIds: self.wasScheduledMessageIds + other.wasScheduledMessageIds, deletedMessageIds: self.deletedMessageIds + other.deletedMessageIds, updatedTypingActivities: self.updatedTypingActivities, updatedWebpages: self.updatedWebpages, updatedCalls: self.updatedCalls + other.updatedCalls, addedCallSignalingData: self.addedCallSignalingData + other.addedCallSignalingData, updatedGroupCallParticipants: self.updatedGroupCallParticipants + other.updatedGroupCallParticipants, storyUpdates: self.storyUpdates + other.storyUpdates, isContactUpdates: self.isContactUpdates + other.isContactUpdates, displayAlerts: self.displayAlerts + other.displayAlerts, dismissBotWebViews: self.dismissBotWebViews + other.dismissBotWebViews, delayNotificatonsUntil: delayNotificatonsUntil, updatedMaxMessageId: updatedMaxMessageId, updatedQts: updatedQts, externallyUpdatedPeerId: externallyUpdatedPeerId, authorizationListUpdated: authorizationListUpdated, updatedIncomingThreadReadStates: self.updatedIncomingThreadReadStates.merging(other.updatedIncomingThreadReadStates, uniquingKeysWith: { lhs, _ in lhs }), updateConfig: updateConfig, isPremiumUpdated: isPremiumUpdated) + return AccountFinalStateEvents(addedIncomingMessageIds: self.addedIncomingMessageIds + other.addedIncomingMessageIds, addedReactionEvents: self.addedReactionEvents + other.addedReactionEvents, wasScheduledMessageIds: self.wasScheduledMessageIds + other.wasScheduledMessageIds, deletedMessageIds: self.deletedMessageIds + other.deletedMessageIds, updatedTypingActivities: self.updatedTypingActivities, updatedWebpages: self.updatedWebpages, updatedCalls: self.updatedCalls + other.updatedCalls, addedCallSignalingData: self.addedCallSignalingData + other.addedCallSignalingData, updatedGroupCallParticipants: self.updatedGroupCallParticipants + other.updatedGroupCallParticipants, storyUpdates: self.storyUpdates + other.storyUpdates, isContactUpdates: self.isContactUpdates + other.isContactUpdates, displayAlerts: self.displayAlerts + other.displayAlerts, dismissBotWebViews: self.dismissBotWebViews + other.dismissBotWebViews, delayNotificatonsUntil: delayNotificatonsUntil, updatedMaxMessageId: updatedMaxMessageId, updatedQts: updatedQts, externallyUpdatedPeerId: externallyUpdatedPeerId, authorizationListUpdated: authorizationListUpdated, updatedIncomingThreadReadStates: self.updatedIncomingThreadReadStates.merging(other.updatedIncomingThreadReadStates, uniquingKeysWith: { lhs, _ in lhs }), updateConfig: updateConfig, isPremiumUpdated: isPremiumUpdated, updatedRevenueBalances: self.updatedRevenueBalances != nil ? self.updatedRevenueBalances : other.updatedRevenueBalances) } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index b3e4c2f189b..d6b2e230e1f 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -433,7 +433,16 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI } else { kind = .poll(multipleAnswers: (flags & (1 << 2)) != 0) } - return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod), nil, nil, nil, nil) + + let questionText: String + let questionEntities: [MessageTextEntity] + switch question { + case let .textWithEntities(text, entities): + questionText = text + questionEntities = messageTextEntitiesFromApiEntities(entities) + } + + return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: questionText, textEntities: questionEntities, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod), nil, nil, nil, nil) } case let .messageMediaDice(value, emoticon): return (TelegramMediaDice(emoji: emoticon, value: value), nil, nil, nil, nil) diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaPoll.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaPoll.swift index 3d58b377396..6fb5933183b 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaPoll.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaPoll.swift @@ -6,13 +6,21 @@ import TelegramApi extension TelegramMediaPollOption { init(apiOption: Api.PollAnswer) { switch apiOption { - case let .pollAnswer(text, option): - self.init(text: text, opaqueIdentifier: option.makeData()) + case let .pollAnswer(text, option): + let answerText: String + let answerEntities: [MessageTextEntity] + switch text { + case let .textWithEntities(text, entities): + answerText = text + answerEntities = messageTextEntitiesFromApiEntities(entities) + } + + self.init(text: answerText, entities: answerEntities, opaqueIdentifier: option.makeData()) } } var apiOption: Api.PollAnswer { - return .pollAnswer(text: self.text, option: Buffer(data: self.opaqueIdentifier)) + return .pollAnswer(text: .textWithEntities(text: self.text, entities: apiEntitiesFromMessageTextEntities(self.entities, associatedPeers: SimpleDictionary())), option: Buffer(data: self.opaqueIdentifier)) } } diff --git a/submodules/TelegramCore/Sources/Authorization.swift b/submodules/TelegramCore/Sources/Authorization.swift index f3377ee7f2b..6ccab4d3aa9 100644 --- a/submodules/TelegramCore/Sources/Authorization.swift +++ b/submodules/TelegramCore/Sources/Authorization.swift @@ -157,7 +157,7 @@ private func sendFirebaseAuthorizationCode(accountManager: AccountManager, account: UnauthorizedAccount, phoneNumber: String, apiId: Int32, apiHash: String, pushNotificationConfiguration: AuthorizationCodePushNotificationConfiguration?, firebaseSecretStream: Signal<[String: String], NoError>, syncContacts: Bool, forcedPasswordSetupNotice: @escaping (Int32) -> (NoticeEntryKey, CodableEntry)?) -> Signal { +public func sendAuthorizationCode(accountManager: AccountManager, account: UnauthorizedAccount, phoneNumber: String, apiId: Int32, apiHash: String, pushNotificationConfiguration: AuthorizationCodePushNotificationConfiguration?, firebaseSecretStream: Signal<[String: String], NoError>, syncContacts: Bool, disableAuthTokens: Bool = false, forcedPasswordSetupNotice: @escaping (Int32) -> (NoticeEntryKey, CodableEntry)?) -> Signal { var cloudValue: [Data] = [] if let list = NSUbiquitousKeyValueStore.default.object(forKey: "T_SLTokens") as? [String] { cloudValue = list.compactMap { string -> Data? in @@ -167,9 +167,6 @@ public func sendAuthorizationCode(accountManager: AccountManager [Data] in return transaction.getStoredLoginTokens() } @@ -177,16 +174,20 @@ public func sendAuthorizationCode(accountManager: AccountManager mapToSignal { localAuthTokens -> Signal in var authTokens = localAuthTokens - #if DEBUG - authTokens.removeAll() - #endif - for data in cloudValue { if !authTokens.contains(data) { authTokens.insert(data, at: 0) } } + if disableAuthTokens { + authTokens.removeAll() + } + +#if DEBUG + authTokens.removeAll() +#endif + var flags: Int32 = 0 flags |= 1 << 5 //allowMissedCall flags |= 1 << 6 //tokens @@ -283,6 +284,7 @@ public func sendAuthorizationCode(accountManager: AccountManager mapToSignal { success -> Signal in if success { return account.postbox.transaction { transaction -> SendAuthorizationCodeResult in - transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: phoneNumber, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts))) + var previousCodeEntry: UnauthorizedAccountStateContents? + if let state = transaction.getState() as? UnauthorizedAccountState, case let .confirmationCodeEntry(_, type, _, _, _, _, previousCodeEntryValue, _) = state.contents { + if let previousCodeEntryValue { + previousCodeEntry = previousCodeEntryValue + } else { + switch type { + case .word, .phrase: + previousCodeEntry = state.contents + default: + switch parsedType { + case .word, .phrase: + previousCodeEntry = state.contents + default: + break + } + } + } + } + transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: phoneNumber, type: parsedType, hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts, previousCodeEntry: previousCodeEntry, usePrevious: false))) return .sentCode(account) } @@ -332,8 +352,26 @@ public func sendAuthorizationCode(accountManager: AccountManager Signal in switch sentCode { case let .sentCode(_, type, phoneCodeHash, nextType, codeTimeout): + let parsedType = SentAuthorizationCodeType(apiType: type) var parsedNextType: AuthorizationCodeNextType? if let nextType = nextType { parsedNextType = AuthorizationCodeNextType(apiType: nextType) @@ -430,7 +469,25 @@ private func internalResendAuthorizationCode(accountManager: AccountManager mapToSignal { success -> Signal in if success { return account.postbox.transaction { transaction -> SendAuthorizationCodeResult in - transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: number, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts))) + var previousCodeEntry: UnauthorizedAccountStateContents? + if let state = transaction.getState() as? UnauthorizedAccountState, case let .confirmationCodeEntry(_, type, _, _, _, _, previousCodeEntryValue, _) = state.contents { + if let previousCodeEntryValue { + previousCodeEntry = previousCodeEntryValue + } else { + switch type { + case .word, .phrase: + previousCodeEntry = state.contents + default: + switch parsedType { + case .word, .phrase: + previousCodeEntry = state.contents + default: + break + } + } + } + } + transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: number, type: parsedType, hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts, previousCodeEntry: previousCodeEntry, usePrevious: false))) return .sentCode(account) } @@ -442,7 +499,25 @@ private func internalResendAuthorizationCode(accountManager: AccountManager Signal in if let state = transaction.getState() as? UnauthorizedAccountState { switch state.contents { - case let .confirmationCodeEntry(number, _, hash, _, nextType, syncContacts): + case let .confirmationCodeEntry(number, type, hash, _, nextType, syncContacts, previousCodeEntryValue, _): if nextType != nil { return account.network.request(Api.functions.auth.resendCode(phoneNumber: number, phoneCodeHash: hash), automaticFloodWait: false) |> mapError { error -> AuthorizationCodeRequestError in @@ -479,13 +554,31 @@ public func resendAuthorizationCode(accountManager: AccountManager mapToSignal { sentCode -> Signal in return account.postbox.transaction { transaction -> Signal in switch sentCode { - case let .sentCode(_, type, phoneCodeHash, nextType, codeTimeout): + case let .sentCode(_, newType, phoneCodeHash, nextType, codeTimeout): + let parsedType = SentAuthorizationCodeType(apiType: newType) + var previousCodeEntry: UnauthorizedAccountStateContents? + if let previousCodeEntryValue { + previousCodeEntry = previousCodeEntryValue + } else { + switch type { + case .word, .phrase: + previousCodeEntry = state.contents + default: + switch parsedType { + case .word, .phrase: + previousCodeEntry = state.contents + default: + break + } + } + } + var parsedNextType: AuthorizationCodeNextType? if let nextType = nextType { parsedNextType = AuthorizationCodeNextType(apiType: nextType) } - if case let .sentCodeTypeFirebaseSms(_, _, receipt, pushTimeout, _) = type { + if case let .sentCodeTypeFirebaseSms(_, _, receipt, pushTimeout, _) = newType { return firebaseSecretStream |> map { mapping -> String? in guard let receipt = receipt else { @@ -518,7 +611,7 @@ public func resendAuthorizationCode(accountManager: AccountManager mapToSignal { success -> Signal in if success { return account.postbox.transaction { transaction -> SendAuthorizationCodeResult in - transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: number, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts))) + transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: number, type: parsedType, hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts, previousCodeEntry: previousCodeEntry, usePrevious: false))) return .sentCode(account) } @@ -531,7 +624,7 @@ public func resendAuthorizationCode(accountManager: AccountManager map { _ -> Void in return Void() } } - transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: number, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts))) + transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: number, type: parsedType, hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts, previousCodeEntry: previousCodeEntry, usePrevious: false))) case .sentCodeSuccess: break } @@ -690,7 +783,7 @@ public func sendLoginEmailCode(account: UnauthorizedAccount, email: String) -> S return account.postbox.transaction { transaction -> Signal in if let state = transaction.getState() as? UnauthorizedAccountState { switch state.contents { - case let .confirmationCodeEntry(phoneNumber, _, phoneCodeHash, _, _, syncContacts): + case let .confirmationCodeEntry(phoneNumber, _, phoneCodeHash, _, _, syncContacts, _, _): return account.network.request(Api.functions.account.sendVerifyEmailCode(purpose: .emailVerifyPurposeLoginSetup(phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash), email: email), automaticFloodWait: false) |> `catch` { error -> Signal in let errorDescription = error.errorDescription ?? "" @@ -709,8 +802,8 @@ public func sendLoginEmailCode(account: UnauthorizedAccount, email: String) -> S |> mapToSignal { result -> Signal in return account.postbox.transaction { transaction -> Signal in switch result { - case let .sentEmailCode(emailPattern, length): - transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: phoneNumber, type: .email(emailPattern: emailPattern, length: length, resetAvailablePeriod: nil, resetPendingDate: nil, appleSignInAllowed: false, setup: true), hash: phoneCodeHash, timeout: nil, nextType: nil, syncContacts: syncContacts))) + case let .sentEmailCode(emailPattern, length): + transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: phoneNumber, type: .email(emailPattern: emailPattern, length: length, resetAvailablePeriod: nil, resetPendingDate: nil, appleSignInAllowed: false, setup: true), hash: phoneCodeHash, timeout: nil, nextType: nil, syncContacts: syncContacts, previousCodeEntry: nil, usePrevious: false))) } return .complete() } @@ -769,7 +862,7 @@ public func verifyLoginEmailSetup(account: UnauthorizedAccount, code: Authorizat return account.postbox.transaction { transaction -> Signal in if let state = transaction.getState() as? UnauthorizedAccountState { switch state.contents { - case let .confirmationCodeEntry(phoneNumber, _, phoneCodeHash, _, _, syncContacts): + case let .confirmationCodeEntry(phoneNumber, _, phoneCodeHash, _, _, syncContacts, _, _): let verification: Api.EmailVerification switch code { case let .emailCode(code): @@ -808,7 +901,7 @@ public func verifyLoginEmailSetup(account: UnauthorizedAccount, code: Authorizat parsedNextType = AuthorizationCodeNextType(apiType: nextType) } - transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: phoneNumber, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: timeout, nextType: parsedNextType, syncContacts: syncContacts))) + transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: phoneNumber, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: timeout, nextType: parsedNextType, syncContacts: syncContacts, previousCodeEntry: nil, usePrevious: false))) case .sentCodeSuccess: break } @@ -846,7 +939,7 @@ public func resetLoginEmail(account: UnauthorizedAccount, phoneNumber: String, p return account.postbox.transaction { transaction -> Signal in if let state = transaction.getState() as? UnauthorizedAccountState { switch state.contents { - case let .confirmationCodeEntry(phoneNumber, _, phoneCodeHash, _, _, syncContacts): + case let .confirmationCodeEntry(phoneNumber, _, phoneCodeHash, _, _, syncContacts, _, _): return account.network.request(Api.functions.auth.resetLoginEmail(phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash), automaticFloodWait: false) |> `catch` { error -> Signal in let errorDescription = error.errorDescription ?? "" @@ -869,7 +962,7 @@ public func resetLoginEmail(account: UnauthorizedAccount, phoneNumber: String, p parsedNextType = AuthorizationCodeNextType(apiType: nextType) } - transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: phoneNumber, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts))) + transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: phoneNumber, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts, previousCodeEntry: nil, usePrevious: false))) return .complete() case .sentCodeSuccess: @@ -898,7 +991,7 @@ public func authorizeWithCode(accountManager: AccountManager Signal in if let state = transaction.getState() as? UnauthorizedAccountState { switch state.contents { - case let .confirmationCodeEntry(number, _, hash, _, _, syncContacts): + case let .confirmationCodeEntry(number, _, hash, _, _, syncContacts, _, _): var flags: Int32 = 0 var phoneCode: String? var emailVerification: Api.EmailVerification? @@ -1340,3 +1433,28 @@ public func resetAuthorizationState(account: UnauthorizedAccount, to value: Auth } } } + +public func togglePreviousCodeEntry(account: UnauthorizedAccount) -> Signal { + return account.postbox.transaction { transaction -> Void in + if let state = transaction.getState() as? UnauthorizedAccountState { + if case let .confirmationCodeEntry(number, type, hash, timeout, nextType, syncContacts, previousCodeEntry, usePrevious) = state.contents, let previousCodeEntry { + transaction.setState(UnauthorizedAccountState(isTestingEnvironment: state.isTestingEnvironment, masterDatacenterId: state.masterDatacenterId, contents: .confirmationCodeEntry(number: number, type: type, hash: hash, timeout: timeout, nextType: nextType, syncContacts: syncContacts, previousCodeEntry: previousCodeEntry, usePrevious: !usePrevious))) + } + } + } + |> ignoreValues +} + +public enum ReportMissingCodeError { + case generic +} + +func _internal_reportMissingCode(network: Network, phoneNumber: String, phoneCodeHash: String, mnc: String) -> Signal { + return network.request(Api.functions.auth.reportMissingCode(phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, mnc: mnc)) + |> mapError { error -> ReportMissingCodeError in + return .generic + } + |> mapToSignal { result -> Signal in + return .complete() + } +} diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index 895c8fb945b..b2fa27871d4 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -251,7 +251,7 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post mappedSolutionEntities = apiTextAttributeEntities(TextEntitiesMessageAttribute(entities: solution.entities), associatedPeers: SimpleDictionary()) pollMediaFlags |= 1 << 1 } - let inputPoll = Api.InputMedia.inputMediaPoll(flags: pollMediaFlags, poll: Api.Poll.poll(id: 0, flags: pollFlags, question: poll.text, answers: poll.options.map({ $0.apiOption }), closePeriod: poll.deadlineTimeout, closeDate: nil), correctAnswers: correctAnswers, solution: mappedSolution, solutionEntities: mappedSolutionEntities) + let inputPoll = Api.InputMedia.inputMediaPoll(flags: pollMediaFlags, poll: Api.Poll.poll(id: 0, flags: pollFlags, question: .textWithEntities(text: poll.text, entities: apiEntitiesFromMessageTextEntities(poll.textEntities, associatedPeers: SimpleDictionary())), answers: poll.options.map({ $0.apiOption }), closePeriod: poll.deadlineTimeout, closeDate: nil), correctAnswers: correctAnswers, solution: mappedSolution, solutionEntities: mappedSolutionEntities) return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputPoll, text), reuploadInfo: nil, cacheReferenceKey: nil))) } else if let media = media as? TelegramMediaDice { let inputDice = Api.InputMedia.inputMediaDice(emoticon: media.emoji) diff --git a/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift index 87447920860..d9dd993fbfa 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift @@ -263,7 +263,7 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, } } -func _internal_requestEditLiveLocation(postbox: Postbox, network: Network, stateManager: AccountStateManager, messageId: MessageId, stop: Bool, coordinate: (latitude: Double, longitude: Double, accuracyRadius: Int32?)?, heading: Int32?, proximityNotificationRadius: Int32?) -> Signal { +func _internal_requestEditLiveLocation(postbox: Postbox, network: Network, stateManager: AccountStateManager, messageId: MessageId, stop: Bool, coordinate: (latitude: Double, longitude: Double, accuracyRadius: Int32?)?, heading: Int32?, proximityNotificationRadius: Int32?, extendPeriod: Int32?) -> Signal { return postbox.transaction { transaction -> (Api.InputPeer, TelegramMediaMap)? in guard let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) else { return nil @@ -305,7 +305,19 @@ func _internal_requestEditLiveLocation(postbox: Postbox, network: Network, state if let _ = proximityNotificationRadius { flags |= 1 << 3 } - inputMedia = .inputMediaGeoLive(flags: flags, geoPoint: inputGeoPoint, heading: heading, period: liveBroadcastingTimeout, proximityNotificationRadius: proximityNotificationRadius) + + let period: Int32 + if let extendPeriod { + if extendPeriod == liveLocationIndefinitePeriod { + period = extendPeriod + } else { + period = liveBroadcastingTimeout + extendPeriod + } + } else { + period = liveBroadcastingTimeout + } + + inputMedia = .inputMediaGeoLive(flags: flags, geoPoint: inputGeoPoint, heading: heading, period: period, proximityNotificationRadius: proximityNotificationRadius) } else { inputMedia = .inputMediaGeoLive(flags: 1 << 0, geoPoint: .inputGeoPoint(flags: 0, lat: media.latitude, long: media.longitude, accuracyRadius: nil), heading: nil, period: nil, proximityNotificationRadius: nil) } @@ -319,7 +331,7 @@ func _internal_requestEditLiveLocation(postbox: Postbox, network: Network, state if let updates = updates { stateManager.addUpdates(updates) } - if coordinate == nil && proximityNotificationRadius == nil { + if coordinate == nil && proximityNotificationRadius == nil && extendPeriod == nil { return postbox.transaction { transaction -> Void in transaction.updateMessage(messageId, update: { currentMessage in var storeForwardInfo: StoreMessageForwardInfo? diff --git a/submodules/TelegramCore/Sources/State/AccountState.swift b/submodules/TelegramCore/Sources/State/AccountState.swift index dfb3b5ec792..18757d2fb63 100644 --- a/submodules/TelegramCore/Sources/State/AccountState.swift +++ b/submodules/TelegramCore/Sources/State/AccountState.swift @@ -38,6 +38,10 @@ extension SentAuthorizationCodeType { self = .fragment(url: url, length: length) case let .sentCodeTypeFirebaseSms(_, _, _, pushTimeout, length): self = .firebase(pushTimeout: pushTimeout, length: length) + case let .sentCodeTypeSmsWord(_, beginning): + self = .word(startsWith: beginning) + case let .sentCodeTypeSmsPhrase(_, beginning): + self = .phrase(startsWith: beginning) } } } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 5b6ffebfd78..efc40febf2a 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1776,6 +1776,8 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: updatedState.updateNewAuthorization(isUnconfirmed: isUnconfirmed, hash: hash, date: date ?? 0, device: device ?? "", location: location ?? "") case let .updatePeerWallpaper(_, peer, wallpaper): updatedState.updateWallpaper(peerId: peer.peerId, wallpaper: wallpaper.flatMap { TelegramWallpaper(apiWallpaper: $0) }) + case let .updateBroadcastRevenueTransactions(balances): + updatedState.updateRevenueBalances(RevenueStats.Balances(apiRevenueBalances: balances)) default: break } @@ -3267,7 +3269,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation]) var currentAddQuickReplyMessages: OptimizeAddMessagesState? for operation in operations { switch operation { - case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateWallpaper: + case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateWallpaper, .UpdateRevenueBalances: if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty { result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location)) } @@ -3400,6 +3402,7 @@ func replayFinalState( var deletedMessageIds: [DeletedMessageId] = [] var syncAttachMenuBots = false var updateConfig = false + var updatedRevenueBalances: RevenueStats.Balances? var holesFromPreviousStateMessageIds: [MessageId] = [] var clearHolesFromPreviousStateForChannelMessagesWithPts: [PeerIdAndMessageNamespace: Int32] = [:] @@ -3897,7 +3900,16 @@ func replayFinalState( } else { kind = .poll(multipleAnswers: (flags & (1 << 2)) != 0) } - updatedPoll = TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: poll.results, isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod) + + let questionText: String + let questionEntities: [MessageTextEntity] + switch question { + case let .textWithEntities(text, entities): + questionText = text + questionEntities = messageTextEntitiesFromApiEntities(entities) + } + + updatedPoll = TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: questionText, textEntities: questionEntities, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: poll.results, isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod) } } updatedPoll = updatedPoll.withUpdatedResults(TelegramMediaPollResults(apiResults: results), min: resultsMin) @@ -4816,6 +4828,8 @@ func replayFinalState( } else { transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.NewSessionReviews, itemId: id.rawValue) } + case let .UpdateRevenueBalances(balances): + updatedRevenueBalances = balances } } @@ -5310,5 +5324,5 @@ func replayFinalState( } } - return AccountReplayedFinalState(state: finalState, addedIncomingMessageIds: addedIncomingMessageIds, addedReactionEvents: addedReactionEvents, wasScheduledMessageIds: wasScheduledMessageIds, addedSecretMessageIds: addedSecretMessageIds, deletedMessageIds: deletedMessageIds, updatedTypingActivities: updatedTypingActivities, updatedWebpages: updatedWebpages, updatedCalls: updatedCalls, addedCallSignalingData: addedCallSignalingData, updatedGroupCallParticipants: updatedGroupCallParticipants, storyUpdates: storyUpdates, updatedPeersNearby: updatedPeersNearby, isContactUpdates: isContactUpdates, delayNotificatonsUntil: delayNotificatonsUntil, updatedIncomingThreadReadStates: updatedIncomingThreadReadStates, updatedOutgoingThreadReadStates: updatedOutgoingThreadReadStates, updateConfig: updateConfig, isPremiumUpdated: isPremiumUpdated) + return AccountReplayedFinalState(state: finalState, addedIncomingMessageIds: addedIncomingMessageIds, addedReactionEvents: addedReactionEvents, wasScheduledMessageIds: wasScheduledMessageIds, addedSecretMessageIds: addedSecretMessageIds, deletedMessageIds: deletedMessageIds, updatedTypingActivities: updatedTypingActivities, updatedWebpages: updatedWebpages, updatedCalls: updatedCalls, addedCallSignalingData: addedCallSignalingData, updatedGroupCallParticipants: updatedGroupCallParticipants, storyUpdates: storyUpdates, updatedPeersNearby: updatedPeersNearby, isContactUpdates: isContactUpdates, delayNotificatonsUntil: delayNotificatonsUntil, updatedIncomingThreadReadStates: updatedIncomingThreadReadStates, updatedOutgoingThreadReadStates: updatedOutgoingThreadReadStates, updateConfig: updateConfig, isPremiumUpdated: isPremiumUpdated, updatedRevenueBalances: updatedRevenueBalances) } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManager.swift b/submodules/TelegramCore/Sources/State/AccountStateManager.swift index 1f00d155dc1..b364a339f60 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManager.swift @@ -44,6 +44,10 @@ private final class UpdatedPeersNearbySubscriberContext { let subscribers = Bag<([PeerNearby]) -> Void>() } +private final class UpdatedRevenueBalancesSubscriberContext { + let subscribers = Bag<(RevenueStats.Balances) -> Void>() +} + public enum DeletedMessageId: Hashable { case global(Int32) case messageId(MessageId) @@ -277,6 +281,7 @@ public final class AccountStateManager { private var updatedWebpageContexts: [MediaId: UpdatedWebpageSubscriberContext] = [:] private var updatedPeersNearbyContext = UpdatedPeersNearbySubscriberContext() + private var updatedRevenueBalancesContext = UpdatedRevenueBalancesSubscriberContext() private let delayNotificatonsUntil = Atomic(value: nil) private let appliedMaxMessageIdPromise = Promise(nil) @@ -1022,6 +1027,9 @@ public final class AccountStateManager { if let updatedPeersNearby = events.updatedPeersNearby { strongSelf.notifyUpdatedPeersNearby(updatedPeersNearby) } + if let updatedRevenueBalances = events.updatedRevenueBalances { + strongSelf.notifyUpdatedRevenueBalances(updatedRevenueBalances) + } if !events.updatedCalls.isEmpty { for call in events.updatedCalls { strongSelf.callSessionManager?.updateSession(call, completion: { _ in }) @@ -1594,6 +1602,33 @@ public final class AccountStateManager { } } + public func updatedRevenueBalances() -> Signal { + let queue = self.queue + return Signal { [weak self] subscriber in + let disposable = MetaDisposable() + queue.async { + if let strongSelf = self { + let index = strongSelf.updatedRevenueBalancesContext.subscribers.add({ revenueBalances in + subscriber.putNext(revenueBalances) + }) + + disposable.set(ActionDisposable { + if let strongSelf = self { + strongSelf.updatedRevenueBalancesContext.subscribers.remove(index) + } + }) + } + } + return disposable + } + } + + private func notifyUpdatedRevenueBalances(_ updatedRevenueBalances: RevenueStats.Balances) { + for subscriber in self.updatedRevenueBalancesContext.subscribers.copyItems() { + subscriber(updatedRevenueBalances) + } + } + func notifyDeletedMessages(messageIds: [MessageId]) { self.deletedMessagesPipe.putNext(messageIds.map { .messageId($0) }) } @@ -1881,6 +1916,12 @@ public final class AccountStateManager { } } + public func updatedRevenueBalances() -> Signal { + return self.impl.signalWith { impl, subscriber in + return impl.updatedRevenueBalances().start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion) + } + } + func addCustomOperation(_ f: Signal) -> Signal { return self.impl.signalWith { impl, subscriber in return impl.addCustomOperation(f).start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion) diff --git a/submodules/TelegramCore/Sources/State/AccountTaskManager.swift b/submodules/TelegramCore/Sources/State/AccountTaskManager.swift index 186090cc6c6..7f92d7df97f 100644 --- a/submodules/TelegramCore/Sources/State/AccountTaskManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountTaskManager.swift @@ -93,6 +93,7 @@ final class AccountTaskManager { tasks.add(managedSynchronizeEmojiSearchCategories(postbox: self.stateManager.postbox, network: self.stateManager.network, kind: .emoji).start()) tasks.add(managedSynchronizeEmojiSearchCategories(postbox: self.stateManager.postbox, network: self.stateManager.network, kind: .status).start()) tasks.add(managedSynchronizeEmojiSearchCategories(postbox: self.stateManager.postbox, network: self.stateManager.network, kind: .avatar).start()) + tasks.add(managedSynchronizeEmojiSearchCategories(postbox: self.stateManager.postbox, network: self.stateManager.network, kind: .combinedChatStickers).start()) tasks.add(managedSynchronizeAttachMenuBots(accountPeerId: self.accountPeerId, postbox: self.stateManager.postbox, network: self.stateManager.network, force: true).start()) tasks.add(managedSynchronizeNotificationSoundList(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) tasks.add(managedChatListFilters(postbox: self.stateManager.postbox, network: self.stateManager.network, accountPeerId: self.stateManager.accountPeerId).start()) diff --git a/submodules/TelegramCore/Sources/State/EmojiSearchCategories.swift b/submodules/TelegramCore/Sources/State/EmojiSearchCategories.swift index 67c481223bf..c0af005c09e 100644 --- a/submodules/TelegramCore/Sources/State/EmojiSearchCategories.swift +++ b/submodules/TelegramCore/Sources/State/EmojiSearchCategories.swift @@ -8,6 +8,7 @@ public final class EmojiSearchCategories: Equatable, Codable { case emoji = 0 case status = 1 case avatar = 2 + case combinedChatStickers = 3 } public struct Group: Codable, Equatable { @@ -15,16 +16,25 @@ public final class EmojiSearchCategories: Equatable, Codable { case id case title case identifiers + case kind + } + + public enum Kind: Int32, Codable { + case generic + case greeting + case premium } public var id: Int64 public var title: String public var identifiers: [String] + public var kind: Kind - public init(id: Int64, title: String, identifiers: [String]) { + public init(id: Int64, title: String, identifiers: [String], kind: Kind) { self.id = id self.title = title self.identifiers = identifiers + self.kind = kind } public init(from decoder: Decoder) throws { @@ -33,6 +43,16 @@ public final class EmojiSearchCategories: Equatable, Codable { self.id = try container.decode(Int64.self, forKey: .id) self.title = try container.decode(String.self, forKey: .title) self.identifiers = try container.decode([String].self, forKey: .identifiers) + self.kind = ((try container.decodeIfPresent(Int32.self, forKey: .kind)).flatMap(Kind.init(rawValue:))) ?? .generic + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.id, forKey: .id) + try container.encode(self.title, forKey: .title) + try container.encode(self.identifiers, forKey: .identifiers) + try container.encode(self.kind.rawValue, forKey: .kind) } } @@ -128,6 +148,11 @@ func managedSynchronizeEmojiSearchCategories(postbox: Postbox, network: Network, |> `catch` { _ -> Signal in return .single(.emojiGroupsNotModified) } + case .combinedChatStickers: + signal = network.request(Api.functions.messages.getEmojiStickerGroups(hash: current?.hash ?? 0)) + |> `catch` { _ -> Signal in + return .single(.emojiGroupsNotModified) + } } return signal @@ -137,12 +162,28 @@ func managedSynchronizeEmojiSearchCategories(postbox: Postbox, network: Network, case let .emojiGroups(hash, groups): let categories = EmojiSearchCategories( hash: hash, - groups: groups.map { item -> EmojiSearchCategories.Group in + groups: groups.compactMap { item -> EmojiSearchCategories.Group? in switch item { case let .emojiGroup(title, iconEmojiId, emoticons): return EmojiSearchCategories.Group( id: iconEmojiId, - title: title, identifiers: emoticons + title: title, + identifiers: emoticons, + kind: .generic + ) + case let .emojiGroupGreeting(title, iconEmojiId, emoticons): + return EmojiSearchCategories.Group( + id: iconEmojiId, + title: title, + identifiers: emoticons, + kind: .greeting + ) + case let .emojiGroupPremium(title, iconEmojiId): + return EmojiSearchCategories.Group( + id: iconEmojiId, + title: title, + identifiers: [], + kind: .premium ) } } diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index de13906cf0e..e133e4f83e7 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 178 + return 179 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/Statistics/RevenueStatistics.swift b/submodules/TelegramCore/Sources/Statistics/RevenueStatistics.swift index 7b2a799e4d1..2be2c409cb9 100644 --- a/submodules/TelegramCore/Sources/Statistics/RevenueStatistics.swift +++ b/submodules/TelegramCore/Sources/Statistics/RevenueStatistics.swift @@ -5,19 +5,21 @@ import TelegramApi import MtProtoKit public struct RevenueStats: Equatable { + public struct Balances: Equatable { + public let currentBalance: Int64 + public let availableBalance: Int64 + public let overallRevenue: Int64 + } + public let topHoursGraph: StatsGraph public let revenueGraph: StatsGraph - public let currentBalance: Int64 - public let availableBalance: Int64 - public let overallRevenue: Int64 + public let balances: Balances public let usdRate: Double - init(topHoursGraph: StatsGraph, revenueGraph: StatsGraph, currentBalance: Int64, availableBalance: Int64, overallRevenue: Int64, usdRate: Double) { + init(topHoursGraph: StatsGraph, revenueGraph: StatsGraph, balances: Balances, usdRate: Double) { self.topHoursGraph = topHoursGraph self.revenueGraph = revenueGraph - self.currentBalance = currentBalance - self.availableBalance = availableBalance - self.overallRevenue = overallRevenue + self.balances = balances self.usdRate = usdRate } @@ -28,13 +30,7 @@ public struct RevenueStats: Equatable { if lhs.revenueGraph != rhs.revenueGraph { return false } - if lhs.currentBalance != rhs.currentBalance { - return false - } - if lhs.availableBalance != rhs.availableBalance { - return false - } - if lhs.overallRevenue != rhs.overallRevenue { + if lhs.balances != rhs.balances { return false } if lhs.usdRate != rhs.usdRate { @@ -44,11 +40,31 @@ public struct RevenueStats: Equatable { } } +public extension RevenueStats { + func withUpdated(balances: RevenueStats.Balances) -> RevenueStats { + return RevenueStats( + topHoursGraph: self.topHoursGraph, + revenueGraph: self.revenueGraph, + balances: balances, + usdRate: self.usdRate + ) + } +} + extension RevenueStats { init(apiRevenueStats: Api.stats.BroadcastRevenueStats, peerId: PeerId) { switch apiRevenueStats { - case let .broadcastRevenueStats(topHoursGraph, revenueGraph, currentBalance, availableBalance, overallRevenue, usdRate): - self.init(topHoursGraph: StatsGraph(apiStatsGraph: topHoursGraph), revenueGraph: StatsGraph(apiStatsGraph: revenueGraph), currentBalance: currentBalance, availableBalance: availableBalance, overallRevenue: overallRevenue, usdRate: usdRate) + case let .broadcastRevenueStats(topHoursGraph, revenueGraph, balances, usdRate): + self.init(topHoursGraph: StatsGraph(apiStatsGraph: topHoursGraph), revenueGraph: StatsGraph(apiStatsGraph: revenueGraph), balances: RevenueStats.Balances(apiRevenueBalances: balances), usdRate: usdRate) + } + } +} + +extension RevenueStats.Balances { + init(apiRevenueBalances: Api.BroadcastRevenueBalances) { + switch apiRevenueBalances { + case let .broadcastRevenueBalances(currentBalance, availableBalance, overallRevenue): + self.init(currentBalance: currentBalance, availableBalance: availableBalance, overallRevenue: overallRevenue) } } } @@ -82,8 +98,7 @@ private func requestRevenueStats(postbox: Postbox, network: Network, peerId: Pee } private final class RevenueStatsContextImpl { - private let postbox: Postbox - private let network: Network + private let account: Account private let peerId: PeerId private var _state: RevenueStatsContextState { @@ -100,11 +115,10 @@ private final class RevenueStatsContextImpl { private let disposable = MetaDisposable() - init(postbox: Postbox, network: Network, peerId: PeerId) { + init(account: Account, peerId: PeerId) { assert(Queue.mainQueue().isCurrent()) - self.postbox = postbox - self.network = network + self.account = account self.peerId = peerId self._state = RevenueStatsContextState(stats: nil) self._statePromise.set(.single(self._state)) @@ -120,7 +134,22 @@ private final class RevenueStatsContextImpl { fileprivate func load() { assert(Queue.mainQueue().isCurrent()) - self.disposable.set((requestRevenueStats(postbox: self.postbox, network: self.network, peerId: self.peerId) + let account = self.account + let signal = requestRevenueStats(postbox: self.account.postbox, network: self.account.network, peerId: self.peerId) + |> mapToSignal { initial -> Signal in + guard let initial else { + return .single(nil) + } + return .single(initial) + |> then( + account.stateManager.updatedRevenueBalances() + |> map { balances in + return initial.withUpdated(balances: balances) + } + ) + } + + self.disposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] stats in if let strongSelf = self { strongSelf._state = RevenueStatsContextState(stats: stats) @@ -131,7 +160,7 @@ private final class RevenueStatsContextImpl { func loadDetailedGraph(_ graph: StatsGraph, x: Int64) -> Signal { if let token = graph.token { - return requestGraph(postbox: self.postbox, network: self.network, peerId: self.peerId, token: token, x: x) + return requestGraph(postbox: self.account.postbox, network: self.account.network, peerId: self.peerId, token: token, x: x) } else { return .single(nil) } @@ -153,9 +182,9 @@ public final class RevenueStatsContext { } } - public init(postbox: Postbox, network: Network, peerId: PeerId) { + public init(account: Account, peerId: PeerId) { self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: { - return RevenueStatsContextImpl(postbox: postbox, network: network, peerId: peerId) + return RevenueStatsContextImpl(account: account, peerId: peerId) }) } @@ -184,6 +213,7 @@ private final class RevenueStatsTransactionsContextImpl { private let account: Account private let peerId: EnginePeer.Id private let disposable = MetaDisposable() + private var updateDisposable: Disposable? private var isLoadingMore: Bool = false private var hasLoadedOnce: Bool = false private var canLoadMore: Bool = true @@ -201,13 +231,21 @@ private final class RevenueStatsTransactionsContextImpl { self.count = 0 self.loadMore() + + self.updateDisposable = (account.stateManager.updatedRevenueBalances() + |> deliverOn(self.queue)).startStrict(next: { [weak self] _ in + self?.reload() + }) } deinit { self.disposable.dispose() + self.updateDisposable?.dispose() } func reload() { + self.lastOffset = nil + self.loadMore() } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaMap.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaMap.swift index e4fa801154a..2039d62d8b1 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaMap.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaMap.swift @@ -1,5 +1,7 @@ import Postbox +public let liveLocationIndefinitePeriod: Int32 = 0x7fffffff + public final class NamedGeoPlace: PostboxCoding, Equatable { public let country: String? public let state: String? diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaPoll.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaPoll.swift index b70b1876d64..4f8cf01d5ea 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaPoll.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaPoll.swift @@ -3,20 +3,24 @@ import Postbox public struct TelegramMediaPollOption: Equatable, PostboxCoding { public let text: String + public let entities: [MessageTextEntity] public let opaqueIdentifier: Data - public init(text: String, opaqueIdentifier: Data) { + public init(text: String, entities: [MessageTextEntity], opaqueIdentifier: Data) { self.text = text + self.entities = entities self.opaqueIdentifier = opaqueIdentifier } public init(decoder: PostboxDecoder) { self.text = decoder.decodeStringForKey("t", orElse: "") + self.entities = decoder.decodeObjectArrayWithDecoderForKey("et") self.opaqueIdentifier = decoder.decodeDataForKey("i") ?? Data() } public func encode(_ encoder: PostboxEncoder) { encoder.encodeString(self.text, forKey: "t") + encoder.encodeObjectArray(self.entities, forKey: "et") encoder.encodeData(self.opaqueIdentifier, forKey: "i") } } @@ -150,17 +154,19 @@ public final class TelegramMediaPoll: Media, Equatable { public let kind: TelegramMediaPollKind public let text: String + public let textEntities: [MessageTextEntity] public let options: [TelegramMediaPollOption] public let correctAnswers: [Data]? public let results: TelegramMediaPollResults public let isClosed: Bool public let deadlineTimeout: Int32? - public init(pollId: MediaId, publicity: TelegramMediaPollPublicity, kind: TelegramMediaPollKind, text: String, options: [TelegramMediaPollOption], correctAnswers: [Data]?, results: TelegramMediaPollResults, isClosed: Bool, deadlineTimeout: Int32?) { + public init(pollId: MediaId, publicity: TelegramMediaPollPublicity, kind: TelegramMediaPollKind, text: String, textEntities: [MessageTextEntity], options: [TelegramMediaPollOption], correctAnswers: [Data]?, results: TelegramMediaPollResults, isClosed: Bool, deadlineTimeout: Int32?) { self.pollId = pollId self.publicity = publicity self.kind = kind self.text = text + self.textEntities = textEntities self.options = options self.correctAnswers = correctAnswers self.results = results @@ -177,6 +183,7 @@ public final class TelegramMediaPoll: Media, Equatable { self.publicity = TelegramMediaPollPublicity(rawValue: decoder.decodeInt32ForKey("pb", orElse: 0)) ?? TelegramMediaPollPublicity.anonymous self.kind = decoder.decodeObjectForKey("kn", decoder: { TelegramMediaPollKind(decoder: $0) }) as? TelegramMediaPollKind ?? TelegramMediaPollKind.poll(multipleAnswers: false) self.text = decoder.decodeStringForKey("t", orElse: "") + self.textEntities = decoder.decodeObjectArrayWithDecoderForKey("te") self.options = decoder.decodeObjectArrayWithDecoderForKey("os") self.correctAnswers = decoder.decodeOptionalDataArrayForKey("ca") self.results = decoder.decodeObjectForKey("rs", decoder: { TelegramMediaPollResults(decoder: $0) }) as? TelegramMediaPollResults ?? TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [], solution: nil) @@ -191,6 +198,7 @@ public final class TelegramMediaPoll: Media, Equatable { encoder.encodeObject(self.kind, forKey: "kn") encoder.encodeBytes(buffer, forKey: "i") encoder.encodeString(self.text, forKey: "t") + encoder.encodeObjectArray(self.textEntities, forKey: "te") encoder.encodeObjectArray(self.options, forKey: "os") if let correctAnswers = self.correctAnswers { encoder.encodeDataArray(correctAnswers, forKey: "ca") @@ -230,6 +238,9 @@ public final class TelegramMediaPoll: Media, Equatable { if lhs.text != rhs.text { return false } + if lhs.textEntities != rhs.textEntities { + return false + } if lhs.options != rhs.options { return false } @@ -273,6 +284,6 @@ public final class TelegramMediaPoll: Media, Equatable { } else { updatedResults = results } - return TelegramMediaPoll(pollId: self.pollId, publicity: self.publicity, kind: self.kind, text: self.text, options: self.options, correctAnswers: self.correctAnswers, results: updatedResults, isClosed: self.isClosed, deadlineTimeout: self.deadlineTimeout) + return TelegramMediaPoll(pollId: self.pollId, publicity: self.publicity, kind: self.kind, text: self.text, textEntities: self.textEntities, options: self.options, correctAnswers: self.correctAnswers, results: updatedResults, isClosed: self.isClosed, deadlineTimeout: self.deadlineTimeout) } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_UnauthorizedAccountState.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_UnauthorizedAccountState.swift index 921608044c8..9bab8302f81 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_UnauthorizedAccountState.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_UnauthorizedAccountState.swift @@ -10,6 +10,8 @@ private enum SentAuthorizationCodeTypeValue: Int32 { case emailSetupRequired = 6 case fragment = 7 case firebase = 8 + case word = 9 + case phrase = 10 } public enum SentAuthorizationCodeType: PostboxCoding, Equatable { @@ -22,6 +24,8 @@ public enum SentAuthorizationCodeType: PostboxCoding, Equatable { case emailSetupRequired(appleSignInAllowed: Bool) case fragment(url: String, length: Int32) case firebase(pushTimeout: Int32?, length: Int32) + case word(startsWith: String?) + case phrase(startsWith: String?) public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("v", orElse: 0) { @@ -43,6 +47,10 @@ public enum SentAuthorizationCodeType: PostboxCoding, Equatable { self = .fragment(url: decoder.decodeStringForKey("u", orElse: ""), length: decoder.decodeInt32ForKey("l", orElse: 0)) case SentAuthorizationCodeTypeValue.firebase.rawValue: self = .firebase(pushTimeout: decoder.decodeOptionalInt32ForKey("pushTimeout"), length: decoder.decodeInt32ForKey("length", orElse: 0)) + case SentAuthorizationCodeTypeValue.word.rawValue: + self = .word(startsWith: decoder.decodeOptionalStringForKey("w")) + case SentAuthorizationCodeTypeValue.phrase.rawValue: + self = .phrase(startsWith: decoder.decodeOptionalStringForKey("ph")) default: preconditionFailure() } @@ -97,6 +105,20 @@ public enum SentAuthorizationCodeType: PostboxCoding, Equatable { encoder.encodeNil(forKey: "pushTimeout") } encoder.encodeInt32(length, forKey: "length") + case let .word(startsWith): + encoder.encodeInt32(SentAuthorizationCodeTypeValue.word.rawValue, forKey: "v") + if let startsWith = startsWith { + encoder.encodeString(startsWith, forKey: "w") + } else { + encoder.encodeNil(forKey: "w") + } + case let .phrase(startsWith): + encoder.encodeInt32(SentAuthorizationCodeTypeValue.phrase.rawValue, forKey: "v") + if let startsWith = startsWith { + encoder.encodeString(startsWith, forKey: "ph") + } else { + encoder.encodeNil(forKey: "ph") + } } } } @@ -151,10 +173,10 @@ public struct UnauthorizedAccountTermsOfService: PostboxCoding, Equatable { } } -public enum UnauthorizedAccountStateContents: PostboxCoding, Equatable { +public indirect enum UnauthorizedAccountStateContents: PostboxCoding, Equatable { case empty case phoneEntry(countryCode: Int32, number: String) - case confirmationCodeEntry(number: String, type: SentAuthorizationCodeType, hash: String, timeout: Int32?, nextType: AuthorizationCodeNextType?, syncContacts: Bool) + case confirmationCodeEntry(number: String, type: SentAuthorizationCodeType, hash: String, timeout: Int32?, nextType: AuthorizationCodeNextType?, syncContacts: Bool, previousCodeEntry: UnauthorizedAccountStateContents?, usePrevious: Bool) case passwordEntry(hint: String, number: String?, code: AuthorizationCode?, suggestReset: Bool, syncContacts: Bool) case passwordRecovery(hint: String, number: String?, code: AuthorizationCode?, emailPattern: String, syncContacts: Bool) case awaitingAccountReset(protectedUntil: Int32, number: String?, syncContacts: Bool) @@ -171,7 +193,7 @@ public enum UnauthorizedAccountStateContents: PostboxCoding, Equatable { if let value = decoder.decodeOptionalInt32ForKey("nt") { nextType = AuthorizationCodeNextType(rawValue: value) } - self = .confirmationCodeEntry(number: decoder.decodeStringForKey("num", orElse: ""), type: decoder.decodeObjectForKey("t", decoder: { SentAuthorizationCodeType(decoder: $0) }) as! SentAuthorizationCodeType, hash: decoder.decodeStringForKey("h", orElse: ""), timeout: decoder.decodeOptionalInt32ForKey("tm"), nextType: nextType, syncContacts: decoder.decodeInt32ForKey("syncContacts", orElse: 1) != 0) + self = .confirmationCodeEntry(number: decoder.decodeStringForKey("num", orElse: ""), type: decoder.decodeObjectForKey("t", decoder: { SentAuthorizationCodeType(decoder: $0) }) as! SentAuthorizationCodeType, hash: decoder.decodeStringForKey("h", orElse: ""), timeout: decoder.decodeOptionalInt32ForKey("tm"), nextType: nextType, syncContacts: decoder.decodeInt32ForKey("syncContacts", orElse: 1) != 0, previousCodeEntry: decoder.decodeObjectForKey("previousCodeEntry", decoder: { UnauthorizedAccountStateContents(decoder: $0) }) as? UnauthorizedAccountStateContents, usePrevious: decoder.decodeInt32ForKey("usePrevious", orElse: 1) != 0) case UnauthorizedAccountStateContentsValue.passwordEntry.rawValue: var code: AuthorizationCode? if let modernCode = decoder.decodeObjectForKey("modernCode", decoder: { AuthorizationCode(decoder: $0) }) as? AuthorizationCode { @@ -206,7 +228,7 @@ public enum UnauthorizedAccountStateContents: PostboxCoding, Equatable { encoder.encodeInt32(UnauthorizedAccountStateContentsValue.phoneEntry.rawValue, forKey: "v") encoder.encodeInt32(countryCode, forKey: "cc") encoder.encodeString(number, forKey: "n") - case let .confirmationCodeEntry(number, type, hash, timeout, nextType, syncContacts): + case let .confirmationCodeEntry(number, type, hash, timeout, nextType, syncContacts, previousCodeEntry, usePrevious): encoder.encodeInt32(UnauthorizedAccountStateContentsValue.confirmationCodeEntry.rawValue, forKey: "v") encoder.encodeString(number, forKey: "num") encoder.encodeObject(type, forKey: "t") @@ -222,6 +244,14 @@ public enum UnauthorizedAccountStateContents: PostboxCoding, Equatable { encoder.encodeNil(forKey: "nt") } encoder.encodeInt32(syncContacts ? 1 : 0, forKey: "syncContacts") + + if let previousCodeEntry = previousCodeEntry { + encoder.encodeObject(previousCodeEntry, forKey: "previousCodeEntry") + encoder.encodeInt32(usePrevious ? 1 : 0, forKey: "usePrevious") + } else { + encoder.encodeNil(forKey: "previousCodeEntry") + encoder.encodeInt32(0, forKey: "usePrevious") + } case let .passwordEntry(hint, number, code, suggestReset, syncContacts): encoder.encodeInt32(UnauthorizedAccountStateContentsValue.passwordEntry.rawValue, forKey: "v") encoder.encodeString(hint, forKey: "h") @@ -290,8 +320,8 @@ public enum UnauthorizedAccountStateContents: PostboxCoding, Equatable { } else { return false } - case let .confirmationCodeEntry(lhsNumber, lhsType, lhsHash, lhsTimeout, lhsNextType, lhsSyncContacts): - if case let .confirmationCodeEntry(rhsNumber, rhsType, rhsHash, rhsTimeout, rhsNextType, rhsSyncContacts) = rhs { + case let .confirmationCodeEntry(lhsNumber, lhsType, lhsHash, lhsTimeout, lhsNextType, lhsSyncContacts, lhsPreviousCodeEntry, lhsUsePrevious): + if case let .confirmationCodeEntry(rhsNumber, rhsType, rhsHash, rhsTimeout, rhsNextType, rhsSyncContacts, rhsPreviousCodeEntry, rhsUsePrevious) = rhs { if lhsNumber != rhsNumber { return false } @@ -310,6 +340,12 @@ public enum UnauthorizedAccountStateContents: PostboxCoding, Equatable { if lhsSyncContacts != rhsSyncContacts { return false } + if lhsPreviousCodeEntry != rhsPreviousCodeEntry { + return false + } + if lhsUsePrevious != rhsUsePrevious { + return false + } return true } else { return false diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Auth/TelegramEngineAuth.swift b/submodules/TelegramCore/Sources/TelegramEngine/Auth/TelegramEngineAuth.swift index cc54474cc2d..4a8493f994a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Auth/TelegramEngineAuth.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Auth/TelegramEngineAuth.swift @@ -52,6 +52,10 @@ public extension TelegramEngineUnauthorized { return _internal_uploadedPeerVideo(postbox: self.account.postbox, network: self.account.network, messageMediaPreuploadManager: nil, resource: resource) } + public func reportMissingCode(phoneNumber: String, phoneCodeHash: String, mnc: String) -> Signal { + return _internal_reportMissingCode(network: self.account.network, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, mnc: mnc) + } + public func state() -> Signal { return self.account.postbox.stateView() |> map { view -> TelegramEngineAuthorizationState? in @@ -202,6 +206,10 @@ public extension TelegramEngine { public func invalidateLoginCodes(codes: [String]) -> Signal { return _internal_invalidateLoginCodes(network: self.account.network, codes: codes) } + + public func reportMissingCode(phoneNumber: String, phoneCodeHash: String, mnc: String) -> Signal { + return _internal_reportMissingCode(network: self.account.network, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, mnc: mnc) + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index 5c1b9925089..f8538240fbc 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -521,6 +521,34 @@ public extension TelegramEngine.EngineData.Item { } } + public struct CanSetStickerPack: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = Bool + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .cachedPeerData(peerId: self.id) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? CachedPeerDataView else { + preconditionFailure() + } + if let cachedData = view.cachedPeerData as? CachedChannelData { + return cachedData.flags.contains(.canSetStickerSet) + } else { + return false + } + } + } + public struct StickerPack: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { public typealias Result = StickerPackCollectionInfo? diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PeerLiveLocationsContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PeerLiveLocationsContext.swift index 7c8255b7ecd..0791de33fd6 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PeerLiveLocationsContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PeerLiveLocationsContext.swift @@ -19,7 +19,7 @@ func _internal_topPeerActiveLiveLocationMessages(viewTracker: AccountViewTracker for entry in view.entries { for media in entry.message.media { if let location = media as? TelegramMediaMap, let liveBroadcastingTimeout = location.liveBroadcastingTimeout { - if entry.message.timestamp + liveBroadcastingTimeout > timestamp { + if liveBroadcastingTimeout == liveLocationIndefinitePeriod || entry.message.timestamp + liveBroadcastingTimeout > timestamp { result.append(entry.message) } } else { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Polls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Polls.swift index d7174c3e951..259a7ccf17a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Polls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Polls.swift @@ -44,7 +44,14 @@ func _internal_requestMessageSelectPollOption(account: Account, messageId: Messa } else { kind = .poll(multipleAnswers: (flags & (1 << 2)) != 0) } - resultPoll = TelegramMediaPoll(pollId: pollId, publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod) + let questionText: String + let questionEntities: [MessageTextEntity] + switch question { + case let .textWithEntities(text, entities): + questionText = text + questionEntities = messageTextEntitiesFromApiEntities(entities) + } + resultPoll = TelegramMediaPoll(pollId: pollId, publicity: publicity, kind: kind, text: questionText, textEntities: questionEntities, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod) } } @@ -135,7 +142,7 @@ func _internal_requestClosePoll(postbox: Postbox, network: Network, stateManager pollMediaFlags |= 1 << 1 } - return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: nil, media: .inputMediaPoll(flags: pollMediaFlags, poll: .poll(id: poll.pollId.id, flags: pollFlags, question: poll.text, answers: poll.options.map({ $0.apiOption }), closePeriod: poll.deadlineTimeout, closeDate: nil), correctAnswers: correctAnswers, solution: mappedSolution, solutionEntities: mappedSolutionEntities), replyMarkup: nil, entities: nil, scheduleDate: nil, quickReplyShortcutId: nil)) + return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: nil, media: .inputMediaPoll(flags: pollMediaFlags, poll: .poll(id: poll.pollId.id, flags: pollFlags, question: .textWithEntities(text: poll.text, entities: apiEntitiesFromMessageTextEntities(poll.textEntities, associatedPeers: SimpleDictionary())), answers: poll.options.map({ $0.apiOption }), closePeriod: poll.deadlineTimeout, closeDate: nil), correctAnswers: correctAnswers, solution: mappedSolution, solutionEntities: mappedSolutionEntities), replyMarkup: nil, entities: nil, scheduleDate: nil, quickReplyShortcutId: nil)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index a410e33f9e9..50645e56945 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -1518,12 +1518,6 @@ public func _internal_pollPeerStories(postbox: Postbox, network: Network, accoun return .complete() } - #if DEBUG - if "".isEmpty { - return .complete() - } - #endif - return network.request(Api.functions.stories.getPeerStories(peer: inputPeer)) |> map(Optional.init) |> `catch` { _ -> Signal in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 314d66fce47..546eeaf58c9 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -144,8 +144,8 @@ public extension TelegramEngine { return _internal_requestEditMessage(account: self.account, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime) } - public func requestEditLiveLocation(messageId: MessageId, stop: Bool, coordinate: (latitude: Double, longitude: Double, accuracyRadius: Int32?)?, heading: Int32?, proximityNotificationRadius: Int32?) -> Signal { - return _internal_requestEditLiveLocation(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, messageId: messageId, stop: stop, coordinate: coordinate, heading: heading, proximityNotificationRadius: proximityNotificationRadius) + public func requestEditLiveLocation(messageId: MessageId, stop: Bool, coordinate: (latitude: Double, longitude: Double, accuracyRadius: Int32?)?, heading: Int32?, proximityNotificationRadius: Int32?, extendPeriod: Int32?) -> Signal { + return _internal_requestEditLiveLocation(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, messageId: messageId, stop: stop, coordinate: coordinate, heading: heading, proximityNotificationRadius: proximityNotificationRadius, extendPeriod: extendPeriod) } public func addSecretChatMessageScreenshot(peerId: PeerId) -> Signal { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift index 3ece75f7326..d113a071bc4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift @@ -181,6 +181,35 @@ func channelAdminLogEvents(accountPeerId: PeerId, postbox: Postbox, network: Net var events: [AdminLogEvent] = [] + func renderedMessage(message: StoreMessage) -> Message? { + var associatedThreadInfo: Message.AssociatedThreadInfo? + if let threadId = message.threadId, let threadInfo = transaction.getMessageHistoryThreadInfo(peerId: message.id.peerId, threadId: threadId) { + associatedThreadInfo = postbox.seedConfiguration.decodeMessageThreadInfo(threadInfo.data) + } + var associatedMessages: SimpleDictionary = SimpleDictionary() + if let replyAttribute = message.attributes.first(where: { $0 is ReplyMessageAttribute }) as? ReplyMessageAttribute { + var foundDeletedReplyMessage = false + for event in apiEvents { + switch event { + case let .channelAdminLogEvent(_, _, _, apiAction): + switch apiAction { + case let .channelAdminLogEventActionDeleteMessage(apiMessage): + if let messageId = apiMessage.id(namespace: Namespaces.Message.Cloud), messageId == replyAttribute.messageId, let message = StoreMessage(apiMessage: apiMessage, accountPeerId: accountPeerId, peerIsForum: peer.isForum), let replyMessage = locallyRenderedMessage(message: message, peers: peers, associatedThreadInfo: associatedThreadInfo) { + associatedMessages[replyMessage.id] = replyMessage + foundDeletedReplyMessage = true + } + default: + break + } + } + } + if !foundDeletedReplyMessage, let replyMessage = transaction.getMessage(replyAttribute.messageId) { + associatedMessages[replyMessage.id] = replyMessage + } + } + return locallyRenderedMessage(message: message, peers: peers, associatedThreadInfo: associatedThreadInfo, associatedMessages: associatedMessages) + } + for event in apiEvents { switch event { case let .channelAdminLogEvent(id, date, userId, apiAction): @@ -205,16 +234,16 @@ func channelAdminLogEvents(accountPeerId: PeerId, postbox: Postbox, network: Net case .messageEmpty: action = .updatePinned(nil) default: - if let message = StoreMessage(apiMessage: new, accountPeerId: accountPeerId, peerIsForum: peer.isForum), let rendered = locallyRenderedMessage(message: message, peers: peers) { + if let message = StoreMessage(apiMessage: new, accountPeerId: accountPeerId, peerIsForum: peer.isForum), let rendered = renderedMessage(message: message) { action = .updatePinned(rendered) } } case let .channelAdminLogEventActionEditMessage(prev, new): - if let prev = StoreMessage(apiMessage: prev, accountPeerId: accountPeerId, peerIsForum: peer.isForum), let prevRendered = locallyRenderedMessage(message: prev, peers: peers), let new = StoreMessage(apiMessage: new, accountPeerId: accountPeerId, peerIsForum: peer.isForum), let newRendered = locallyRenderedMessage(message: new, peers: peers) { + if let prev = StoreMessage(apiMessage: prev, accountPeerId: accountPeerId, peerIsForum: peer.isForum), let prevRendered = renderedMessage(message: prev), let new = StoreMessage(apiMessage: new, accountPeerId: accountPeerId, peerIsForum: peer.isForum), let newRendered = renderedMessage(message: new) { action = .editMessage(prev: prevRendered, new: newRendered) } case let .channelAdminLogEventActionDeleteMessage(message): - if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForum), let rendered = locallyRenderedMessage(message: message, peers: peers) { + if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForum), let rendered = renderedMessage(message: message) { action = .deleteMessage(rendered) } case .channelAdminLogEventActionParticipantJoin: @@ -248,7 +277,7 @@ func channelAdminLogEvents(accountPeerId: PeerId, postbox: Postbox, network: Net case let .channelAdminLogEventActionDefaultBannedRights(prevBannedRights, newBannedRights): action = .updateDefaultBannedRights(prev: TelegramChatBannedRights(apiBannedRights: prevBannedRights), new: TelegramChatBannedRights(apiBannedRights: newBannedRights)) case let .channelAdminLogEventActionStopPoll(message): - if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForum), let rendered = locallyRenderedMessage(message: message, peers: peers) { + if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForum), let rendered = renderedMessage(message: message) { action = .pollStopped(rendered) } case let .channelAdminLogEventActionChangeLinkedChat(prevValue, newValue): @@ -287,7 +316,7 @@ func channelAdminLogEvents(accountPeerId: PeerId, postbox: Postbox, network: Net case let .channelAdminLogEventActionToggleNoForwards(new): action = .toggleCopyProtection(boolFromApiValue(new)) case let .channelAdminLogEventActionSendMessage(message): - if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForum), let rendered = locallyRenderedMessage(message: message, peers: peers) { + if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForum), let rendered = renderedMessage(message: message) { action = .sendMessage(rendered) } case let .channelAdminLogEventActionChangeAvailableReactions(prevValue, newValue): diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 3cfd6892cff..6f1e12f55ba 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/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift index b9198a4b06d..70ff4f51902 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift @@ -385,6 +385,391 @@ func _internal_searchStickers(account: Account, query: [String], scope: SearchSt } } +func _internal_searchStickers(account: Account, category: EmojiSearchCategories.Group, scope: SearchStickersScope = [.installed, .remote]) -> Signal<(items: [FoundStickerItem], isFinalResult: Bool), NoError> { + if scope.isEmpty { + return .single(([], true)) + } + + var query = category.identifiers + if query == ["\u{2764}"] { + query = ["\u{2764}\u{FE0F}"] + } + + return account.postbox.transaction { transaction -> ([FoundStickerItem], CachedStickerQueryResult?, Bool, SearchStickersConfiguration) in + let isPremium = transaction.getPeer(account.peerId)?.isPremium ?? false + + var result: [FoundStickerItem] = [] + if scope.contains(.installed) { + for entry in transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudSavedStickers) { + if let item = entry.contents.get(SavedStickerItem.self) { + if case .premium = category.kind { + if item.file.isPremiumSticker { + result.append(FoundStickerItem(file: item.file, stringRepresentations: item.stringRepresentations)) + } + } else { + for representation in item.stringRepresentations { + for queryItem in query { + if representation.hasPrefix(queryItem) { + result.append(FoundStickerItem(file: item.file, stringRepresentations: item.stringRepresentations)) + break + } + } + } + } + } + } + + var currentItems = Set(result.map { $0.file.fileId }) + var recentItems: [TelegramMediaFile] = [] + var recentAnimatedItems: [TelegramMediaFile] = [] + var recentItemsIds = Set() + var matchingRecentItemsIds = Set() + + for entry in transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudRecentStickers) { + if let item = entry.contents.get(RecentMediaItem.self) { + let file = item.media + if file.isPremiumSticker && !isPremium { + continue + } + + if !currentItems.contains(file.fileId) { + currentItems.insert(file.fileId) + + for case let .Sticker(displayText, _, _) in file.attributes { + if case .premium = category.kind { + if file.isPremiumSticker { + matchingRecentItemsIds.insert(file.fileId) + } + } else { + for queryItem in query { + if displayText.hasPrefix(queryItem) { + matchingRecentItemsIds.insert(file.fileId) + break + } + } + } + recentItemsIds.insert(file.fileId) + if file.isAnimatedSticker || file.isVideoSticker { + recentAnimatedItems.append(file) + } else { + recentItems.append(file) + } + break + } + } + } + } + + var searchQueries: [ItemCollectionSearchQuery] = query.map { queryItem -> ItemCollectionSearchQuery in + return .exact(ValueBoxKey(queryItem)) + } + if query == ["\u{2764}"] { + searchQueries = [.any([ValueBoxKey("\u{2764}"), ValueBoxKey("\u{2764}\u{FE0F}")])] + } + + var installedItems: [FoundStickerItem] = [] + var installedAnimatedItems: [FoundStickerItem] = [] + var installedPremiumItems: [FoundStickerItem] = [] + + if case .premium = category.kind { + for (_, _, items) in transaction.getCollectionsItems(namespace: Namespaces.ItemCollection.CloudStickerPacks) { + for item in items { + guard let item = item as? StickerPackItem else { + continue + } + if item.file.isPremiumSticker { + if currentItems.contains(item.file.fileId) { + continue + } + currentItems.insert(item.file.fileId) + + if !recentItemsIds.contains(item.file.fileId) { + if item.file.isPremiumSticker { + installedPremiumItems.append(FoundStickerItem(file: item.file, stringRepresentations: [])) + } else if item.file.isAnimatedSticker || item.file.isVideoSticker { + installedAnimatedItems.append(FoundStickerItem(file: item.file, stringRepresentations: [])) + } else { + installedItems.append(FoundStickerItem(file: item.file, stringRepresentations: [])) + } + } else { + matchingRecentItemsIds.insert(item.file.fileId) + if item.file.isAnimatedSticker || item.file.isVideoSticker { + recentAnimatedItems.append(item.file) + } else { + recentItems.append(item.file) + } + } + } + } + } + + for item in transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudAllPremiumStickers) { + guard let item = item.contents.get(RecentMediaItem.self) else { + continue + } + if currentItems.contains(item.media.fileId) { + continue + } + currentItems.insert(item.media.fileId) + + if !recentItemsIds.contains(item.media.fileId) { + if item.media.isPremiumSticker { + installedPremiumItems.append(FoundStickerItem(file: item.media, stringRepresentations: [])) + } else if item.media.isAnimatedSticker || item.media.isVideoSticker { + installedAnimatedItems.append(FoundStickerItem(file: item.media, stringRepresentations: [])) + } else { + installedItems.append(FoundStickerItem(file: item.media, stringRepresentations: [])) + } + } else { + matchingRecentItemsIds.insert(item.media.fileId) + if item.media.isAnimatedSticker || item.media.isVideoSticker { + recentAnimatedItems.append(item.media) + } else { + recentItems.append(item.media) + } + } + } + } else { + for searchQuery in searchQueries { + for item in transaction.searchItemCollection(namespace: Namespaces.ItemCollection.CloudStickerPacks, query: searchQuery) { + if let item = item as? StickerPackItem { + if !currentItems.contains(item.file.fileId) { + currentItems.insert(item.file.fileId) + + let stringRepresentations = item.getStringRepresentationsOfIndexKeys() + if !recentItemsIds.contains(item.file.fileId) { + if item.file.isPremiumSticker { + installedPremiumItems.append(FoundStickerItem(file: item.file, stringRepresentations: stringRepresentations)) + } else if item.file.isAnimatedSticker || item.file.isVideoSticker { + installedAnimatedItems.append(FoundStickerItem(file: item.file, stringRepresentations: stringRepresentations)) + } else { + installedItems.append(FoundStickerItem(file: item.file, stringRepresentations: stringRepresentations)) + } + } else { + matchingRecentItemsIds.insert(item.file.fileId) + if item.file.isAnimatedSticker || item.file.isVideoSticker { + recentAnimatedItems.append(item.file) + } else { + recentItems.append(item.file) + } + } + } + } + } + } + } + + for file in recentAnimatedItems { + if file.isPremiumSticker && !isPremium { + continue + } + if matchingRecentItemsIds.contains(file.fileId) { + result.append(FoundStickerItem(file: file, stringRepresentations: query)) + } + } + + for file in recentItems { + if file.isPremiumSticker && !isPremium { + continue + } + if matchingRecentItemsIds.contains(file.fileId) { + result.append(FoundStickerItem(file: file, stringRepresentations: query)) + } + } + + result.append(contentsOf: installedPremiumItems) + result.append(contentsOf: installedAnimatedItems) + result.append(contentsOf: installedItems) + } + + var cached: CachedStickerQueryResult? + let appConfiguration: AppConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue + let searchStickersConfiguration = SearchStickersConfiguration.with(appConfiguration: appConfiguration) + + if case .premium = category.kind { + } else { + let combinedQuery = query.joined(separator: "") + cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerQueryResults, key: CachedStickerQueryResult.cacheKey(combinedQuery)))?.get(CachedStickerQueryResult.self) + + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + + if let currentCached = cached, currentTime > currentCached.timestamp + searchStickersConfiguration.cacheTimeout { + cached = nil + } + } + + return (result, cached, isPremium, searchStickersConfiguration) + } + |> mapToSignal { localItems, cached, isPremium, searchStickersConfiguration -> Signal<(items: [FoundStickerItem], isFinalResult: Bool), NoError> in + if !scope.contains(.remote) { + return .single((localItems, true)) + } + if case .premium = category.kind { + return .single((localItems, true)) + } + + var tempResult: [FoundStickerItem] = [] + let currentItemIds = Set(localItems.map { $0.file.fileId }) + + var premiumItems: [FoundStickerItem] = [] + var otherItems: [FoundStickerItem] = [] + + for item in localItems { + if item.file.isPremiumSticker { + premiumItems.append(item) + } else { + otherItems.append(item) + } + } + + if let cached = cached { + var cachedItems: [FoundStickerItem] = [] + var cachedAnimatedItems: [FoundStickerItem] = [] + var cachedPremiumItems: [FoundStickerItem] = [] + + for file in cached.items { + if !currentItemIds.contains(file.fileId) { + if file.isPremiumSticker { + cachedPremiumItems.append(FoundStickerItem(file: file, stringRepresentations: [])) + } else if file.isAnimatedSticker || file.isVideoSticker { + cachedAnimatedItems.append(FoundStickerItem(file: file, stringRepresentations: [])) + } else { + cachedItems.append(FoundStickerItem(file: file, stringRepresentations: [])) + } + } + } + + otherItems.append(contentsOf: cachedAnimatedItems) + otherItems.append(contentsOf: cachedItems) + + let allPremiumItems = premiumItems + cachedPremiumItems + let allOtherItems = otherItems + cachedAnimatedItems + cachedItems + + if isPremium { + let batchCount = Int(searchStickersConfiguration.normalStickersPerPremiumCount) + if batchCount == 0 { + tempResult.append(contentsOf: allPremiumItems) + tempResult.append(contentsOf: allOtherItems) + } else { + if allPremiumItems.isEmpty { + tempResult.append(contentsOf: allOtherItems) + } else { + var i = 0 + for premiumItem in allPremiumItems { + if i < allOtherItems.count { + for j in i ..< min(i + batchCount, allOtherItems.count) { + tempResult.append(allOtherItems[j]) + } + i += batchCount + } + tempResult.append(premiumItem) + } + if i < allOtherItems.count { + for j in i ..< allOtherItems.count { + tempResult.append(allOtherItems[j]) + } + } + } + } + } else { + tempResult.append(contentsOf: allOtherItems) + tempResult.append(contentsOf: allPremiumItems.prefix(max(0, Int(searchStickersConfiguration.premiumStickersCount)))) + } + } + + let remote = account.network.request(Api.functions.messages.getStickers(emoticon: query.joined(separator: ""), hash: cached?.hash ?? 0)) + |> `catch` { _ -> Signal in + return .single(.stickersNotModified) + } + |> mapToSignal { result -> Signal<(items: [FoundStickerItem], isFinalResult: Bool), NoError> in + return account.postbox.transaction { transaction -> (items: [FoundStickerItem], isFinalResult: Bool) in + switch result { + case let .stickers(hash, stickers): + var result: [FoundStickerItem] = [] + let currentItemIds = Set(localItems.map { $0.file.fileId }) + + var premiumItems: [FoundStickerItem] = [] + var otherItems: [FoundStickerItem] = [] + + for item in localItems { + if item.file.isPremiumSticker { + premiumItems.append(item) + } else { + otherItems.append(item) + } + } + + var foundItems: [FoundStickerItem] = [] + var foundAnimatedItems: [FoundStickerItem] = [] + var foundPremiumItems: [FoundStickerItem] = [] + + var files: [TelegramMediaFile] = [] + for sticker in stickers { + if let file = telegramMediaFileFromApiDocument(sticker), let id = file.id { + files.append(file) + if !currentItemIds.contains(id) { + if file.isPremiumSticker { + foundPremiumItems.append(FoundStickerItem(file: file, stringRepresentations: [])) + } else if file.isAnimatedSticker || file.isVideoSticker { + foundAnimatedItems.append(FoundStickerItem(file: file, stringRepresentations: [])) + } else { + foundItems.append(FoundStickerItem(file: file, stringRepresentations: [])) + } + } + } + } + + let allPremiumItems = premiumItems + foundPremiumItems + let allOtherItems = otherItems + foundAnimatedItems + foundItems + + if isPremium { + let batchCount = Int(searchStickersConfiguration.normalStickersPerPremiumCount) + if batchCount == 0 { + result.append(contentsOf: allPremiumItems) + result.append(contentsOf: allOtherItems) + } else { + if allPremiumItems.isEmpty { + result.append(contentsOf: allOtherItems) + } else { + var i = 0 + for premiumItem in allPremiumItems { + if i < allOtherItems.count { + for j in i ..< min(i + batchCount, allOtherItems.count) { + result.append(allOtherItems[j]) + } + i += batchCount + } + result.append(premiumItem) + } + if i < allOtherItems.count { + for j in i ..< allOtherItems.count { + result.append(allOtherItems[j]) + } + } + } + } + } else { + result.append(contentsOf: allOtherItems) + result.append(contentsOf: allPremiumItems.prefix(max(0, Int(searchStickersConfiguration.premiumStickersCount)))) + } + + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + if let entry = CodableEntry(CachedStickerQueryResult(items: files, hash: hash, timestamp: currentTime)) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerQueryResults, key: CachedStickerQueryResult.cacheKey(query.joined(separator: ""))), entry: entry) + } + + return (result, true) + case .stickersNotModified: + break + } + return (tempResult, true) + } + } + return .single((tempResult, false)) + |> then(remote) + } +} + func _internal_searchEmoji(account: Account, query: [String], scope: SearchStickersScope = [.installed, .remote]) -> Signal<(items: [FoundStickerItem], isFinalResult: Bool), NoError> { if scope.isEmpty { return .single(([], true)) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift index a29f884883a..42ee3b92fa0 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift @@ -33,6 +33,10 @@ public extension TelegramEngine { public func searchStickers(query: [String], scope: SearchStickersScope = [.installed, .remote]) -> Signal<(items: [FoundStickerItem], isFinalResult: Bool), NoError> { return _internal_searchStickers(account: self.account, query: query, scope: scope) } + + public func searchStickers(category: EmojiSearchCategories.Group, scope: SearchStickersScope = [.installed, .remote]) -> Signal<(items: [FoundStickerItem], isFinalResult: Bool), NoError> { + return _internal_searchStickers(account: self.account, category: category, scope: scope) + } public func searchStickerSetsRemotely(query: String) -> Signal { return _internal_searchStickerSetsRemotely(network: self.account.network, query: query) @@ -286,6 +290,13 @@ public extension TelegramEngine { } } + public func searchEmoji(category: EmojiSearchCategories.Group) -> Signal<(items: [TelegramMediaFile], isFinalResult: Bool), NoError> { + return _internal_searchEmoji(account: self.account, query: category.identifiers) + |> map { items, isFinalResult -> (items: [TelegramMediaFile], isFinalResult: Bool) in + return (items.map(\.file), isFinalResult) + } + } + public func addRecentlyUsedSticker(fileReference: FileMediaReference) { let _ = self.account.postbox.transaction({ transaction -> Void in TelegramCore.addRecentlyUsedSticker(transaction: transaction, fileReference: fileReference) diff --git a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift index ce7d685b42c..e234e47effc 100644 --- a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift @@ -211,7 +211,7 @@ func messagesIdsGroupedByPeerId(_ ids: [MessageAndThreadId]) -> [PeerAndThreadId return dict } -func locallyRenderedMessage(message: StoreMessage, peers: [PeerId: Peer], associatedThreadInfo: Message.AssociatedThreadInfo? = nil) -> Message? { +func locallyRenderedMessage(message: StoreMessage, peers: [PeerId: Peer], associatedThreadInfo: Message.AssociatedThreadInfo? = nil, associatedMessages: SimpleDictionary = SimpleDictionary()) -> Message? { guard case let .Id(id) = message.id else { return nil } @@ -264,7 +264,7 @@ func locallyRenderedMessage(message: StoreMessage, peers: [PeerId: Peer], associ let second = UInt32(hashValue & 0xffffffff) let stableId = first &+ second - return Message(stableId: stableId, stableVersion: 0, id: id, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: message.threadId, timestamp: message.timestamp, flags: MessageFlags(message.flags), tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, customTags: [], forwardInfo: forwardInfo, author: author, text: message.text, attributes: message.attributes, media: message.media, peers: messagePeers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: associatedThreadInfo, associatedStories: [:]) + return Message(stableId: stableId, stableVersion: 0, id: id, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: message.threadId, timestamp: message.timestamp, flags: MessageFlags(message.flags), tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, customTags: [], forwardInfo: forwardInfo, author: author, text: message.text, attributes: message.attributes, media: message.media, peers: messagePeers, associatedMessages: associatedMessages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: associatedThreadInfo, associatedStories: [:]) } func locallyRenderedMessage(message: StoreMessage, peers: AccumulatedPeers, associatedThreadInfo: Message.AssociatedThreadInfo? = nil) -> Message? { diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index b76f7ba8cbc..cfd2add5c02 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -320,6 +320,8 @@ public enum PresentationResourceKey: Int32 { case hideIconImage case peerStatusLockedImage + case expandDownArrowImage + case expandSmallDownArrowImage } public enum ChatExpiredStoryIndicatorType: Hashable { diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift index 91f5f1c975d..81e5b19064e 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift @@ -414,4 +414,16 @@ public struct PresentationResourcesItemList { return generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/SmallLock"), color: theme.list.itemSecondaryTextColor) }) } + + public static func expandDownArrowImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.expandDownArrowImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Item List/ExpandingItemVerticalRegularArrow"), color: .white)?.withRenderingMode(.alwaysTemplate) + }) + } + + public static func expandSmallDownArrowImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.expandSmallDownArrowImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Item List/ExpandingItemVerticalSmallArrow"), color: .white)?.withRenderingMode(.alwaysTemplate) + }) + } } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 75d5a5eaa03..860d13951c4 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -231,7 +231,7 @@ swift_library( "//submodules/QrCode:QrCode", "//submodules/WallpaperResources:WallpaperResources", "//submodules/AuthorizationUI:AuthorizationUI", - "//submodules/CounterContollerTitleView:CounterContollerTitleView", + "//submodules/CounterControllerTitleView:CounterControllerTitleView", "//submodules/GridMessageSelectionNode:GridMessageSelectionNode", "//submodules/InstantPageCache:InstantPageCache", "//submodules/PersistentStringHash:PersistentStringHash", @@ -477,6 +477,7 @@ swift_library( "//submodules/TelegramUI/Components/Ads/AdsInfoScreen", "//submodules/TelegramUI/Components/Ads/AdsReportScreen", "//submodules/TelegramUI/Components/Settings/BotSettingsScreen", + "//submodules/TelegramUI/Components/AdminUserActionsSheet", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/AdminUserActionsSheet/BUILD b/submodules/TelegramUI/Components/AdminUserActionsSheet/BUILD new file mode 100644 index 00000000000..d6ef632670b --- /dev/null +++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/BUILD @@ -0,0 +1,37 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "AdminUserActionsSheet", + module_name = "AdminUserActionsSheet", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/PresentationDataUtils", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/AvatarNode", + "//submodules/CheckNode", + "//submodules/UndoUI", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsPeerComponent copy.swift b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsPeerComponent copy.swift new file mode 100644 index 00000000000..dcd78b898ff --- /dev/null +++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsPeerComponent copy.swift @@ -0,0 +1,342 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import AccountContext +import TelegramCore +import MultilineTextComponent +import AvatarNode +import TelegramPresentationData +import CheckNode +import TelegramStringFormatting +import ListSectionComponent + +/*final class AdminUserActionsSwitchComponent: Component { + enum SelectionState: Equatable { + case none + case editing(isSelected: Bool) + } + + enum SubtitleIcon { + case lock + } + + enum Subtitle: Equatable { + case presence(EnginePeer.Presence?) + case text(text: String, icon: SubtitleIcon) + } + + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let sideInset: CGFloat + let title: String + let subtitle: Subtitle + let peer: EnginePeer? + let selectionState: SelectionState + let hasNext: Bool + let action: (EnginePeer) -> Void + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + sideInset: CGFloat, + title: String, + subtitle: Subtitle, + peer: EnginePeer?, + selectionState: SelectionState, + hasNext: Bool, + action: @escaping (EnginePeer) -> Void + ) { + self.context = context + self.theme = theme + self.strings = strings + self.sideInset = sideInset + self.title = title + self.subtitle = subtitle + self.peer = peer + self.selectionState = selectionState + self.hasNext = hasNext + self.action = action + } + + static func ==(lhs: AdminUserActionsSwitchComponent, rhs: AdminUserActionsSwitchComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.sideInset != rhs.sideInset { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.subtitle != rhs.subtitle { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.selectionState != rhs.selectionState { + return false + } + if lhs.hasNext != rhs.hasNext { + return false + } + return true + } + + final class View: UIView { + private let containerButton: HighlightTrackingButton + + private let title = ComponentView() + private let label = ComponentView() + private let separatorLayer: SimpleLayer + private let avatarNode: AvatarNode + + private var labelIconView: UIImageView? + private var checkLayer: CheckLayer? + + private var component: AdminUserActionsSwitchComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.separatorLayer = SimpleLayer() + + self.containerButton = HighlightTrackingButton() + + self.avatarNode = AvatarNode(font: avatarFont) + self.avatarNode.isLayerBacked = true + + super.init(frame: frame) + + self.layer.addSublayer(self.separatorLayer) + self.addSubview(self.containerButton) + self.containerButton.layer.addSublayer(self.avatarNode.layer) + + self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + guard let component = self.component, let peer = component.peer else { + return + } + component.action(peer) + } + + func update(component: AdminUserActionsSwitchComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + + var hasSelectionUpdated = false + if let previousComponent = self.component { + switch previousComponent.selectionState { + case .none: + if case .none = component.selectionState { + } else { + hasSelectionUpdated = true + } + case .editing: + if case .editing = component.selectionState { + } else { + hasSelectionUpdated = true + } + } + } + + self.component = component + self.state = state + + let contextInset: CGFloat = 0.0 + + let height: CGFloat = 60.0 + let verticalInset: CGFloat = 1.0 + let leftInset: CGFloat = 62.0 + component.sideInset + var rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset + let avatarLeftInset: CGFloat = component.sideInset + 10.0 + + if case let .editing(isSelected) = component.selectionState { + rightInset += 48.0 + + let checkSize: CGFloat = 22.0 + + let checkLayer: CheckLayer + if let current = self.checkLayer { + checkLayer = current + if themeUpdated { + checkLayer.theme = CheckNodeTheme(theme: component.theme, style: .plain) + } + checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate) + } else { + checkLayer = CheckLayer(theme: CheckNodeTheme(theme: component.theme, style: .plain)) + self.checkLayer = checkLayer + self.containerButton.layer.addSublayer(checkLayer) + checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)) + checkLayer.setSelected(isSelected, animated: false) + checkLayer.setNeedsDisplay() + } + transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: availableSize.width - rightInset + floor((48.0 - checkSize) * 0.5), y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))) + } else { + if let checkLayer = self.checkLayer { + self.checkLayer = nil + transition.setPosition(layer: checkLayer, position: CGPoint(x: -checkLayer.bounds.width * 0.5, y: checkLayer.position.y), completion: { [weak checkLayer] _ in + checkLayer?.removeFromSuperlayer() + }) + } + } + + let avatarSize: CGFloat = 40.0 + + let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: floor((height - verticalInset * 2.0 - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)) + if self.avatarNode.bounds.isEmpty { + self.avatarNode.frame = avatarFrame + } else { + transition.setFrame(layer: self.avatarNode.layer, frame: avatarFrame) + } + if let peer = component.peer { + let clipStyle: AvatarNodeClipStyle + if case let .channel(channel) = peer, channel.flags.contains(.isForum) { + clipStyle = .roundedRect + } else { + clipStyle = .round + } + if peer.id == component.context.account.peerId { + self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, overrideImage: .savedMessagesIcon, clipStyle: clipStyle, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) + } else { + self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: clipStyle, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) + } + } + + var labelIcon: UIImage? + let labelData: (String, Bool) + switch component.subtitle { + case let .presence(presence): + if let presence { + labelData = stringAndActivityForUserPresence(strings: component.strings, dateTimeFormat: PresentationDateTimeFormat(), presence: presence, relativeTo: Int32(Date().timeIntervalSince1970)) + } else { + labelData = (component.strings.LastSeen_Offline, false) + } + case let .text(text, icon): + switch icon { + case .lock: + labelIcon = PresentationResourcesItemList.peerStatusLockedImage(component.theme) + } + labelData = (text, false) + } + + var maxTextSize = availableSize.width - leftInset - rightInset + if labelIcon != nil { + maxTextSize -= 48.0 + } + + let labelSize = self.label.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: labelData.0, font: Font.regular(15.0), textColor: labelData.1 ? component.theme.list.itemAccentColor : component.theme.list.itemSecondaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: maxTextSize, height: 100.0) + ) + + let previousTitleFrame = self.title.view?.frame + var previousTitleContents: UIView? + if hasSelectionUpdated && !"".isEmpty { + previousTitleContents = self.title.view?.snapshotView(afterScreenUpdates: false) + } + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: maxTextSize, height: 100.0) + ) + + let titleSpacing: CGFloat = 1.0 + let centralContentHeight: CGFloat = titleSize.height + labelSize.height + titleSpacing + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.containerButton.addSubview(titleView) + } + titleView.frame = titleFrame + if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x { + transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true) + } + + if let previousTitleFrame, let previousTitleContents, previousTitleFrame.size != titleSize { + previousTitleContents.frame = CGRect(origin: previousTitleFrame.origin, size: previousTitleFrame.size) + self.addSubview(previousTitleContents) + + transition.setFrame(view: previousTitleContents, frame: CGRect(origin: titleFrame.origin, size: previousTitleFrame.size)) + transition.setAlpha(view: previousTitleContents, alpha: 0.0, completion: { [weak previousTitleContents] _ in + previousTitleContents?.removeFromSuperview() + }) + transition.animateAlpha(view: titleView, from: 0.0, to: 1.0) + } + } + + if let labelIcon { + let labelIconView: UIImageView + if let current = self.labelIconView { + labelIconView = current + } else { + labelIconView = UIImageView() + self.labelIconView = labelIconView + self.containerButton.addSubview(labelIconView) + } + labelIconView.image = labelIcon + + let labelIconFrame = CGRect(origin: CGPoint(x: availableSize.width - rightInset - 48.0 + floor((48.0 - labelIcon.size.width) * 0.5), y: floor((height - verticalInset * 2.0 - labelIcon.size.height) / 2.0)), size: CGSize(width: labelIcon.size.width, height: labelIcon.size.height)) + transition.setFrame(view: labelIconView, frame: labelIconFrame) + } else { + if let labelIconView = self.labelIconView { + self.labelIconView = nil + labelIconView.removeFromSuperview() + } + } + + if let labelView = self.label.view { + if labelView.superview == nil { + labelView.isUserInteractionEnabled = false + self.containerButton.addSubview(labelView) + } + transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + titleSpacing), size: labelSize)) + } + + if themeUpdated { + self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor + } + transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel))) + self.separatorLayer.isHidden = !component.hasNext + + let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0)) + transition.setFrame(view: self.containerButton, frame: containerFrame) + + return CGSize(width: availableSize.width, height: height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} +*/ diff --git a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsPeerComponent.swift b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsPeerComponent.swift new file mode 100644 index 00000000000..dba2a48811f --- /dev/null +++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsPeerComponent.swift @@ -0,0 +1,274 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import AccountContext +import TelegramCore +import MultilineTextComponent +import AvatarNode +import TelegramPresentationData +import CheckNode +import TelegramStringFormatting +import ListSectionComponent + +private let avatarFont = avatarPlaceholderFont(size: 15.0) + +private func cancelContextGestures(view: UIView) { + if let gestureRecognizers = view.gestureRecognizers { + for gesture in gestureRecognizers { + if let gesture = gesture as? ContextGesture { + gesture.cancel() + } + } + } + for subview in view.subviews { + cancelContextGestures(view: subview) + } +} + +final class AdminUserActionsPeerComponent: Component { + enum SelectionState: Equatable { + case none + case editing(isSelected: Bool) + } + + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let sideInset: CGFloat + let title: String + let peer: EnginePeer? + let selectionState: SelectionState + let action: (EnginePeer) -> Void + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + sideInset: CGFloat, + title: String, + peer: EnginePeer?, + selectionState: SelectionState, + action: @escaping (EnginePeer) -> Void + ) { + self.context = context + self.theme = theme + self.strings = strings + self.sideInset = sideInset + self.title = title + self.peer = peer + self.selectionState = selectionState + self.action = action + } + + static func ==(lhs: AdminUserActionsPeerComponent, rhs: AdminUserActionsPeerComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.sideInset != rhs.sideInset { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.selectionState != rhs.selectionState { + return false + } + return true + } + + final class View: UIView, ListSectionComponent.ChildView { + private let containerButton: HighlightTrackingButton + + private let title = ComponentView() + private let label = ComponentView() + private let avatarNode: AvatarNode + + private var labelIconView: UIImageView? + private var checkLayer: CheckLayer? + + private var component: AdminUserActionsPeerComponent? + private weak var state: EmptyComponentState? + + public var customUpdateIsHighlighted: ((Bool) -> Void)? + public var separatorInset: CGFloat = 0.0 + + override init(frame: CGRect) { + self.containerButton = HighlightTrackingButton() + + self.avatarNode = AvatarNode(font: avatarFont) + self.avatarNode.isLayerBacked = true + + super.init(frame: frame) + + self.addSubview(self.containerButton) + self.containerButton.layer.addSublayer(self.avatarNode.layer) + + self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + guard let component = self.component, let peer = component.peer else { + return + } + component.action(peer) + } + + func update(component: AdminUserActionsPeerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + + var hasSelectionUpdated = false + if let previousComponent = self.component { + switch previousComponent.selectionState { + case .none: + if case .none = component.selectionState { + } else { + hasSelectionUpdated = true + } + case .editing: + if case .editing = component.selectionState { + } else { + hasSelectionUpdated = true + } + } + } + + self.component = component + self.state = state + + let contextInset: CGFloat = 0.0 + + let height: CGFloat = 44.0 + let verticalInset: CGFloat = 1.0 + let leftInset: CGFloat = 30.0 + component.sideInset + var rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset + var avatarLeftInset: CGFloat = component.sideInset + 10.0 + + if case let .editing(isSelected) = component.selectionState { + rightInset += 46.0 + avatarLeftInset += 24.0 + + let checkSize: CGFloat = 22.0 + + let checkLayer: CheckLayer + if let current = self.checkLayer { + checkLayer = current + if themeUpdated { + checkLayer.theme = CheckNodeTheme(theme: component.theme, style: .plain) + } + checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate) + } else { + checkLayer = CheckLayer(theme: CheckNodeTheme(theme: component.theme, style: .plain)) + self.checkLayer = checkLayer + self.containerButton.layer.addSublayer(checkLayer) + checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)) + checkLayer.setSelected(isSelected, animated: false) + checkLayer.setNeedsDisplay() + } + transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: floor((22.0 - checkSize) * 0.5), y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))) + } else { + if let checkLayer = self.checkLayer { + self.checkLayer = nil + transition.setPosition(layer: checkLayer, position: CGPoint(x: -checkLayer.bounds.width * 0.5, y: checkLayer.position.y), completion: { [weak checkLayer] _ in + checkLayer?.removeFromSuperlayer() + }) + } + } + + let avatarSize: CGFloat = 30.0 + + let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: floor((height - verticalInset * 2.0 - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)) + if self.avatarNode.bounds.isEmpty { + self.avatarNode.frame = avatarFrame + } else { + transition.setFrame(layer: self.avatarNode.layer, frame: avatarFrame) + } + if let peer = component.peer { + let clipStyle: AvatarNodeClipStyle + if case let .channel(channel) = peer, channel.flags.contains(.isForum) { + clipStyle = .roundedRect + } else { + clipStyle = .round + } + if peer.id == component.context.account.peerId { + self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, overrideImage: .savedMessagesIcon, clipStyle: clipStyle, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) + } else { + self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: clipStyle, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) + } + } + + let avatarTitleSpacing: CGFloat = 5.0 + let maxTextSize = availableSize.width - avatarLeftInset - avatarSize - avatarTitleSpacing - rightInset + + let previousTitleFrame = self.title.view?.frame + var previousTitleContents: UIView? + if hasSelectionUpdated && !"".isEmpty { + previousTitleContents = self.title.view?.snapshotView(afterScreenUpdates: false) + } + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: maxTextSize, height: 100.0) + ) + + let centralContentHeight: CGFloat = titleSize.height + + let titleFrame = CGRect(origin: CGPoint(x: avatarLeftInset + avatarSize + avatarTitleSpacing, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.containerButton.addSubview(titleView) + } + titleView.frame = titleFrame + if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x { + transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true) + } + + if let previousTitleFrame, let previousTitleContents, previousTitleFrame.size != titleSize { + previousTitleContents.frame = CGRect(origin: previousTitleFrame.origin, size: previousTitleFrame.size) + self.addSubview(previousTitleContents) + + transition.setFrame(view: previousTitleContents, frame: CGRect(origin: titleFrame.origin, size: previousTitleFrame.size)) + transition.setAlpha(view: previousTitleContents, alpha: 0.0, completion: { [weak previousTitleContents] _ in + previousTitleContents?.removeFromSuperview() + }) + transition.animateAlpha(view: titleView, from: 0.0, to: 1.0) + } + } + + let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0)) + transition.setFrame(view: self.containerButton, frame: containerFrame) + + self.separatorInset = leftInset + + return CGSize(width: availableSize.width, height: height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift new file mode 100644 index 00000000000..2a042c2b4cd --- /dev/null +++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift @@ -0,0 +1,1759 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData +import AccountContext +import TelegramCore +import MultilineTextComponent +import ButtonComponent +import PresentationDataUtils +import Markdown +import UndoUI +import AvatarNode +import TelegramStringFormatting +import ListSectionComponent +import ListActionItemComponent +import PlainButtonComponent + +struct MediaRight: OptionSet, Hashable { + var rawValue: Int + + static let photos = MediaRight(rawValue: 1 << 0) + static let videos = MediaRight(rawValue: 1 << 1) + static let stickersAndGifs = MediaRight(rawValue: 1 << 2) + static let music = MediaRight(rawValue: 1 << 3) + static let files = MediaRight(rawValue: 1 << 4) + static let voiceMessages = MediaRight(rawValue: 1 << 5) + static let videoMessages = MediaRight(rawValue: 1 << 6) + static let links = MediaRight(rawValue: 1 << 7) + static let polls = MediaRight(rawValue: 1 << 8) +} + +extension MediaRight { + var count: Int { + var result = 0 + var index = 0 + while index < 31 { + let currentValue = self.rawValue >> UInt32(index) + index += 1 + if currentValue == 0 { + break + } + + if (currentValue & 1) != 0 { + result += 1 + } + } + return result + } +} + +private struct ParticipantRight: OptionSet { + var rawValue: Int + + static let sendMessages = ParticipantRight(rawValue: 1 << 0) + static let addMembers = ParticipantRight(rawValue: 1 << 2) + static let pinMessages = ParticipantRight(rawValue: 1 << 3) + static let changeInfo = ParticipantRight(rawValue: 1 << 4) +} + +private func rightsFromBannedRights(_ rights: TelegramChatBannedRightsFlags) -> (participantRights: ParticipantRight, mediaRights: MediaRight) { + var participantResult: ParticipantRight = [ + .sendMessages, + .addMembers, + .pinMessages, + .changeInfo + ] + var mediaResult: MediaRight = [ + .photos, + .videos, + .stickersAndGifs, + .music, + .files, + .voiceMessages, + .videoMessages, + .links, + .polls + ] + + if rights.contains(.banSendText) { + participantResult.remove(.sendMessages) + } + if rights.contains(.banAddMembers) { + participantResult.remove(.addMembers) + } + if rights.contains(.banPinMessages) { + participantResult.remove(.pinMessages) + } + if rights.contains(.banChangeInfo) { + participantResult.remove(.changeInfo) + } + + if rights.contains(.banSendPhotos) { + mediaResult.remove(.photos) + } + if rights.contains(.banSendVideos) { + mediaResult.remove(.videos) + } + if rights.contains(.banSendStickers) || rights.contains(.banSendGifs) || rights.contains(.banSendGames) || rights.contains(.banSendInline) { + mediaResult.remove(.stickersAndGifs) + } + if rights.contains(.banSendMusic) { + mediaResult.remove(.music) + } + if rights.contains(.banSendFiles) { + mediaResult.remove(.files) + } + if rights.contains(.banSendVoice) { + mediaResult.remove(.voiceMessages) + } + if rights.contains(.banSendInstantVideos) { + mediaResult.remove(.videoMessages) + } + if rights.contains(.banEmbedLinks) { + mediaResult.remove(.links) + } + if rights.contains(.banSendPolls) { + mediaResult.remove(.polls) + } + + return (participantResult, mediaResult) +} + +private func rightFlagsFromRights(participantRights: ParticipantRight, mediaRights: MediaRight) -> TelegramChatBannedRightsFlags { + var result: TelegramChatBannedRightsFlags = [] + + if !participantRights.contains(.sendMessages) { + result.insert(.banSendText) + } + if !participantRights.contains(.addMembers) { + result.insert(.banAddMembers) + } + if !participantRights.contains(.pinMessages) { + result.insert(.banPinMessages) + } + if !participantRights.contains(.changeInfo) { + result.insert(.banChangeInfo) + } + + if !mediaRights.contains(.photos) { + result.insert(.banSendPhotos) + } + if !mediaRights.contains(.videos) { + result.insert(.banSendVideos) + } + if !mediaRights.contains(.stickersAndGifs) { + result.insert(.banSendStickers) + result.insert(.banSendGifs) + result.insert(.banSendGames) + result.insert(.banSendInline) + } + if !mediaRights.contains(.music) { + result.insert(.banSendMusic) + } + if !mediaRights.contains(.files) { + result.insert(.banSendFiles) + } + if !mediaRights.contains(.voiceMessages) { + result.insert(.banSendVoice) + } + if !mediaRights.contains(.videoMessages) { + result.insert(.banSendInstantVideos) + } + if !mediaRights.contains(.links) { + result.insert(.banEmbedLinks) + } + if !mediaRights.contains(.polls) { + result.insert(.banSendPolls) + } + + return result +} + +private let allMediaRightItems: [MediaRight] = [ + .photos, + .videos, + .stickersAndGifs, + .music, + .files, + .voiceMessages, + .videoMessages, + .links, + .polls +] + +private final class AdminUserActionsSheetComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let chatPeer: EnginePeer + let peers: [RenderedChannelParticipant] + let messageCount: Int + let completion: (AdminUserActionsSheet.Result) -> Void + + init( + context: AccountContext, + chatPeer: EnginePeer, + peers: [RenderedChannelParticipant], + messageCount: Int, + completion: @escaping (AdminUserActionsSheet.Result) -> Void + ) { + self.context = context + self.chatPeer = chatPeer + self.peers = peers + self.messageCount = messageCount + self.completion = completion + } + + static func ==(lhs: AdminUserActionsSheetComponent, rhs: AdminUserActionsSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.chatPeer != rhs.chatPeer { + return false + } + if lhs.peers != rhs.peers { + return false + } + if lhs.messageCount != rhs.messageCount { + return false + } + return true + } + + private struct ItemLayout: Equatable { + var containerSize: CGSize + var containerInset: CGFloat + var bottomInset: CGFloat + var topInset: CGFloat + + init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat) { + self.containerSize = containerSize + self.containerInset = containerInset + self.bottomInset = bottomInset + self.topInset = topInset + } + } + + private final class ScrollView: UIScrollView { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return super.hitTest(point, with: event) + } + } + + final class View: UIView, UIScrollViewDelegate { + private let dimView: UIView + private let backgroundLayer: SimpleLayer + private let navigationBarContainer: SparseContainerView + private let navigationBackgroundView: BlurredBackgroundView + private let navigationBarSeparator: SimpleLayer + private let scrollView: ScrollView + private let scrollContentClippingView: SparseContainerView + private let scrollContentView: UIView + + private let leftButton = ComponentView() + + private let title = ComponentView() + private let actionButton = ComponentView() + + private let optionsSection = ComponentView() + private let optionsFooter = ComponentView() + private let configSection = ComponentView() + + private let bottomOverscrollLimit: CGFloat + + private var ignoreScrolling: Bool = false + + private var component: AdminUserActionsSheetComponent? + private weak var state: EmptyComponentState? + private var environment: ViewControllerComponentContainer.Environment? + private var isUpdating: Bool = false + + private var itemLayout: ItemLayout? + + private var topOffsetDistance: CGFloat? + + private var isOptionReportExpanded: Bool = false + private var optionReportSelectedPeers = Set() + private var isOptionDeleteAllExpanded: Bool = false + private var optionDeleteAllSelectedPeers = Set() + private var isOptionBanExpanded: Bool = false + private var optionBanSelectedPeers = Set() + + private var isConfigurationExpanded: Bool = false + private var isMediaSectionExpanded: Bool = false + + private var allowedParticipantRights: ParticipantRight = [] + private var allowedMediaRights: MediaRight = [] + private var participantRights: ParticipantRight = [] + private var mediaRights: MediaRight = [] + + private var previousWasConfigurationExpanded: Bool = false + + override init(frame: CGRect) { + self.bottomOverscrollLimit = 200.0 + + self.dimView = UIView() + + self.backgroundLayer = SimpleLayer() + self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.backgroundLayer.cornerRadius = 10.0 + + self.navigationBarContainer = SparseContainerView() + + self.navigationBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + self.navigationBarSeparator = SimpleLayer() + + self.scrollView = ScrollView() + + self.scrollContentClippingView = SparseContainerView() + self.scrollContentClippingView.clipsToBounds = true + + self.scrollContentView = UIView() + + super.init(frame: frame) + + self.addSubview(self.dimView) + self.layer.addSublayer(self.backgroundLayer) + + self.scrollView.delaysContentTouches = true + self.scrollView.canCancelContentTouches = true + self.scrollView.clipsToBounds = false + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.alwaysBounceVertical = true + self.scrollView.scrollsToTop = false + self.scrollView.delegate = self + self.scrollView.clipsToBounds = true + + self.addSubview(self.scrollContentClippingView) + self.scrollContentClippingView.addSubview(self.scrollView) + + self.scrollView.addSubview(self.scrollContentView) + + self.addSubview(self.navigationBarContainer) + + self.navigationBarContainer.addSubview(self.navigationBackgroundView) + self.navigationBarContainer.layer.addSublayer(self.navigationBarSeparator) + + self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + } + } + + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + /*guard let itemLayout = self.itemLayout, let topOffsetDistance = self.topOffsetDistance else { + return + } + + var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset + topOffset = max(0.0, topOffset) + + if topOffset < topOffsetDistance { + targetContentOffset.pointee.y = scrollView.contentOffset.y + scrollView.setContentOffset(CGPoint(x: 0.0, y: itemLayout.topInset), animated: true) + }*/ + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.bounds.contains(point) { + return nil + } + if !self.backgroundLayer.frame.contains(point) { + return self.dimView + } + + if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) { + return result + } + + let result = super.hitTest(point, with: event) + return result + } + + @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + guard let environment = self.environment, let controller = environment.controller() else { + return + } + controller.dismiss() + } + } + + private func calculateResult() -> AdminUserActionsSheet.Result { + var reportSpamPeers: [EnginePeer.Id] = [] + var deleteAllFromPeers: [EnginePeer.Id] = [] + var banPeers: [EnginePeer.Id] = [] + var updateBannedRights: [EnginePeer.Id: TelegramChatBannedRights] = [:] + + for id in self.optionReportSelectedPeers.sorted() { + reportSpamPeers.append(id) + } + for id in self.optionDeleteAllSelectedPeers.sorted() { + deleteAllFromPeers.append(id) + } + + if !self.isConfigurationExpanded { + for id in self.optionBanSelectedPeers.sorted() { + banPeers.append(id) + } + } else { + var banFlags: TelegramChatBannedRightsFlags = [] + banFlags = rightFlagsFromRights(participantRights: self.participantRights, mediaRights: self.mediaRights) + + let bannedRights = TelegramChatBannedRights(flags: banFlags, untilDate: Int32.max) + for id in self.optionBanSelectedPeers.sorted() { + updateBannedRights[id] = bannedRights + } + } + + return AdminUserActionsSheet.Result( + reportSpamPeers: reportSpamPeers, + deleteAllFromPeers: deleteAllFromPeers, + banPeers: banPeers, + updateBannedRights: updateBannedRights + ) + } + + private func updateScrolling(transition: Transition) { + guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else { + return + } + var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset + + let navigationAlpha: CGFloat = 1.0 - max(0.0, min(1.0, (topOffset + 20.0) / 20.0)) + transition.setAlpha(view: self.navigationBackgroundView, alpha: navigationAlpha) + transition.setAlpha(layer: self.navigationBarSeparator, alpha: navigationAlpha) + + topOffset = max(0.0, topOffset) + transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) + + transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) + + let topOffsetDistance: CGFloat = min(200.0, floor(itemLayout.containerSize.height * 0.25)) + self.topOffsetDistance = topOffsetDistance + var topOffsetFraction = topOffset / topOffsetDistance + topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) + + let transitionFactor: CGFloat = 1.0 - topOffsetFraction + if self.isUpdating { + DispatchQueue.main.async { [weak controller] in + guard let controller else { + return + } + controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: transition.containedViewLayoutTransition) + } + } else { + controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: transition.containedViewLayoutTransition) + } + } + + func animateIn() { + self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY + self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + if let actionButtonView = self.actionButton.view { + actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + } + + func animateOut(completion: @escaping () -> Void) { + let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY + + self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in + completion() + }) + self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + if let actionButtonView = self.actionButton.view { + actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + } + + if let environment = self.environment, let controller = environment.controller() { + controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .easeInOut)) + } + } + + func update(component: AdminUserActionsSheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + let themeUpdated = self.environment?.theme !== environment.theme + + let resetScrolling = self.scrollView.bounds.width != availableSize.width + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + + if self.component == nil { + var (allowedParticipantRights, allowedMediaRights) = rightsFromBannedRights([]) + if case let .channel(channel) = component.chatPeer { + (allowedParticipantRights, allowedMediaRights) = rightsFromBannedRights(channel.defaultBannedRights?.flags ?? []) + } + + var (commonParticipantRights, commonMediaRights) = rightsFromBannedRights([]) + + loop: for peer in component.peers { + var (peerParticipantRights, peerMediaRights) = rightsFromBannedRights([]) + switch peer.participant { + case .creator: + allowedParticipantRights = [] + allowedMediaRights = [] + break loop + case let .member(_, _, adminInfo, banInfo, _): + if adminInfo != nil { + allowedParticipantRights = [] + allowedMediaRights = [] + break loop + } else if let banInfo { + (peerParticipantRights, peerMediaRights) = rightsFromBannedRights(banInfo.rights.flags) + } + } + peerParticipantRights = peerParticipantRights.intersection(allowedParticipantRights) + peerMediaRights = peerMediaRights.intersection(allowedMediaRights) + + commonParticipantRights = commonParticipantRights.intersection(peerParticipantRights) + commonMediaRights = commonMediaRights.intersection(peerMediaRights) + } + + commonParticipantRights = commonParticipantRights.intersection(allowedParticipantRights) + commonMediaRights = commonMediaRights.intersection(allowedMediaRights) + + self.allowedParticipantRights = allowedParticipantRights + self.participantRights = commonParticipantRights + + self.allowedMediaRights = allowedMediaRights + self.mediaRights = commonMediaRights + } + + self.component = component + self.state = state + self.environment = environment + + if themeUpdated { + self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + self.backgroundLayer.backgroundColor = environment.theme.list.blocksBackgroundColor.cgColor + + self.navigationBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + self.navigationBarSeparator.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor + } + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) + + transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) + + var contentHeight: CGFloat = 0.0 + contentHeight += 54.0 + contentHeight += 16.0 + + let leftButtonSize = self.leftButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: environment.theme.list.itemAccentColor)), + action: { [weak self] in + guard let self, let controller = self.environment?.controller() else { + return + } + controller.dismiss() + } + ).minSize(CGSize(width: 44.0, height: 56.0))), + environment: {}, + containerSize: CGSize(width: 120.0, height: 100.0) + ) + let leftButtonFrame = CGRect(origin: CGPoint(x: 16.0 + environment.safeInsets.left, y: 0.0), size: leftButtonSize) + if let leftButtonView = self.leftButton.view { + if leftButtonView.superview == nil { + self.navigationBarContainer.addSubview(leftButtonView) + } + transition.setFrame(view: leftButtonView, frame: leftButtonFrame) + } + + let containerInset: CGFloat = environment.statusBarHeight + 10.0 + + let clippingY: CGFloat + + enum OptionsSection { + case report + case deleteAll + case ban + } + + var availableOptions: [OptionsSection] = [] + availableOptions.append(.report) + + if case let .channel(channel) = component.chatPeer { + if channel.hasPermission(.deleteAllMessages) { + availableOptions.append(.deleteAll) + + if channel.hasPermission(.banMembers) { + var canBanEveryone = true + for peer in component.peers { + if peer.peer.id == component.context.account.peerId { + canBanEveryone = false + continue + } + + switch peer.participant { + case .creator: + canBanEveryone = false + case let .member(_, _, adminInfo, banInfo, _): + let _ = banInfo + if let adminInfo { + if channel.flags.contains(.isCreator) { + } else if adminInfo.promotedBy == component.context.account.peerId { + } else { + canBanEveryone = false + } + } + } + } + + if canBanEveryone { + availableOptions.append(.ban) + } + } + } + } + + let optionsItem: (OptionsSection) -> AnyComponentWithIdentity = { section in + let sectionId: AnyHashable + let selectedPeers: Set + let isExpanded: Bool + let title: String + + switch section { + case .report: + sectionId = "report" + selectedPeers = self.optionReportSelectedPeers + isExpanded = self.isOptionReportExpanded + + title = environment.strings.Chat_AdminActionSheet_ReportSpam + case .deleteAll: + sectionId = "delete-all" + selectedPeers = self.optionDeleteAllSelectedPeers + isExpanded = self.isOptionDeleteAllExpanded + + if component.peers.count == 1 { + title = environment.strings.Chat_AdminActionSheet_DeleteAllSingle(EnginePeer(component.peers[0].peer).compactDisplayTitle).string + } else { + title = environment.strings.Chat_AdminActionSheet_DeleteAllMultiple + } + case .ban: + sectionId = "ban" + selectedPeers = self.optionBanSelectedPeers + isExpanded = self.isOptionBanExpanded + + let banTitle: String + let restrictTitle: String + if component.peers.count == 1 { + banTitle = environment.strings.Chat_AdminActionSheet_BanSingle(EnginePeer(component.peers[0].peer).compactDisplayTitle).string + restrictTitle = environment.strings.Chat_AdminActionSheet_RestrictSingle(EnginePeer(component.peers[0].peer).compactDisplayTitle).string + } else { + banTitle = environment.strings.Chat_AdminActionSheet_BanMultiple + restrictTitle = environment.strings.Chat_AdminActionSheet_RestrictMultiple + } + title = self.isConfigurationExpanded ? restrictTitle : banTitle + } + + var accessory: ListActionItemComponent.Accessory? + if component.peers.count > 1 { + accessory = .custom(ListActionItemComponent.CustomAccessory( + component: AnyComponentWithIdentity(id: 0, component: AnyComponent(PlainButtonComponent( + content: AnyComponent(OptionSectionExpandIndicatorComponent( + theme: environment.theme, + count: selectedPeers.isEmpty ? component.peers.count : selectedPeers.count, + isExpanded: isExpanded + )), + effectAlignment: .center, + action: { [weak self] in + guard let self else { + return + } + + switch section { + case .report: + self.isOptionReportExpanded = !self.isOptionReportExpanded + case .deleteAll: + self.isOptionDeleteAllExpanded = !self.isOptionDeleteAllExpanded + case .ban: + self.isOptionBanExpanded = !self.isOptionBanExpanded + } + + self.state?.updated(transition: .spring(duration: 0.35)) + }, + animateScale: false + ))), + insets: UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 2.0), + isInteractive: true + )) + } + + return AnyComponentWithIdentity(id: sectionId, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: title, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + leftIcon: .check(ListActionItemComponent.LeftIcon.Check( + isSelected: !selectedPeers.isEmpty, + toggle: { [weak self] in + guard let self, let component = self.component else { + return + } + + var selectedPeers: Set + switch section { + case .report: + selectedPeers = self.optionReportSelectedPeers + case .deleteAll: + selectedPeers = self.optionDeleteAllSelectedPeers + case .ban: + selectedPeers = self.optionBanSelectedPeers + } + + if selectedPeers.isEmpty { + for peer in component.peers { + selectedPeers.insert(peer.peer.id) + } + } else { + selectedPeers.removeAll() + } + + switch section { + case .report: + self.optionReportSelectedPeers = selectedPeers + case .deleteAll: + self.optionDeleteAllSelectedPeers = selectedPeers + case .ban: + self.optionBanSelectedPeers = selectedPeers + if self.isConfigurationExpanded && self.optionBanSelectedPeers.isEmpty { + self.isConfigurationExpanded = false + } + } + + self.state?.updated(transition: .spring(duration: 0.35)) + } + )), + icon: .none, + accessory: accessory, + action: { [weak self] _ in + guard let self else { + return + } + + var selectedPeers: Set + switch section { + case .report: + selectedPeers = self.optionReportSelectedPeers + case .deleteAll: + selectedPeers = self.optionDeleteAllSelectedPeers + case .ban: + selectedPeers = self.optionBanSelectedPeers + } + + if selectedPeers.isEmpty { + for peer in component.peers { + selectedPeers.insert(peer.peer.id) + } + } else { + selectedPeers.removeAll() + } + + switch section { + case .report: + self.optionReportSelectedPeers = selectedPeers + case .deleteAll: + self.optionDeleteAllSelectedPeers = selectedPeers + case .ban: + self.optionBanSelectedPeers = selectedPeers + } + + self.state?.updated(transition: .spring(duration: 0.35)) + }, + highlighting: .disabled + ))) + } + + let expandedPeersItem: (OptionsSection) -> AnyComponentWithIdentity = { section in + let sectionId: AnyHashable + let selectedPeers: Set + switch section { + case .report: + sectionId = "report-peers" + selectedPeers = self.optionReportSelectedPeers + case .deleteAll: + sectionId = "delete-all-peers" + selectedPeers = self.optionDeleteAllSelectedPeers + case .ban: + sectionId = "ban-peers" + selectedPeers = self.optionBanSelectedPeers + } + + var peerItems: [AnyComponentWithIdentity] = [] + for peer in component.peers { + peerItems.append(AnyComponentWithIdentity(id: peer.peer.id, component: AnyComponent(AdminUserActionsPeerComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + sideInset: 0.0, + title: EnginePeer(peer.peer).displayTitle(strings: environment.strings, displayOrder: .firstLast), + peer: EnginePeer(peer.peer), + selectionState: .editing(isSelected: selectedPeers.contains(peer.peer.id)), + action: { [weak self] peer in + guard let self else { + return + } + + var selectedPeers: Set + switch section { + case .report: + selectedPeers = self.optionReportSelectedPeers + case .deleteAll: + selectedPeers = self.optionDeleteAllSelectedPeers + case .ban: + selectedPeers = self.optionBanSelectedPeers + } + + if selectedPeers.contains(peer.id) { + selectedPeers.remove(peer.id) + } else { + selectedPeers.insert(peer.id) + } + + switch section { + case .report: + self.optionReportSelectedPeers = selectedPeers + case .deleteAll: + self.optionDeleteAllSelectedPeers = selectedPeers + case .ban: + self.optionBanSelectedPeers = selectedPeers + } + + self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) + } + )))) + } + return AnyComponentWithIdentity(id: sectionId, component: AnyComponent(ListSubSectionComponent( + theme: environment.theme, + leftInset: 62.0, + items: peerItems + ))) + } + + let titleString: String = environment.strings.Chat_AdminActionSheet_DeleteTitle(Int32(component.messageCount)) + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: titleString, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((54.0 - titleSize.height) * 0.5)), size: titleSize) + if let titleView = title.view { + if titleView.superview == nil { + self.navigationBarContainer.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + + let navigationBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: 54.0)) + transition.setFrame(view: self.navigationBackgroundView, frame: navigationBackgroundFrame) + self.navigationBackgroundView.update(size: navigationBackgroundFrame.size, cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition) + transition.setFrame(layer: self.navigationBarSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: availableSize.width, height: UIScreenPixel))) + + var optionsSectionItems: [AnyComponentWithIdentity] = [] + + for option in availableOptions { + let isOptionExpanded: Bool + switch option { + case .report: + isOptionExpanded = self.isOptionReportExpanded + case .deleteAll: + isOptionExpanded = self.isOptionDeleteAllExpanded + case .ban: + isOptionExpanded = self.isOptionBanExpanded + } + + optionsSectionItems.append(optionsItem(option)) + if isOptionExpanded { + optionsSectionItems.append(expandedPeersItem(option)) + } + } + + var optionsSectionTransition = transition + if self.previousWasConfigurationExpanded != self.isConfigurationExpanded { + self.previousWasConfigurationExpanded = self.isConfigurationExpanded + optionsSectionTransition = optionsSectionTransition.withAnimation(.none) + } + let optionsSectionSize = self.optionsSection.update( + transition: optionsSectionTransition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Chat_AdminActionSheet_RestrictSectionHeader, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: optionsSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100000.0) + ) + + let optionsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: optionsSectionSize) + if let optionsSectionView = self.optionsSection.view { + if optionsSectionView.superview == nil { + self.scrollContentView.addSubview(optionsSectionView) + self.optionsSection.parentState = state + } + transition.setFrame(view: optionsSectionView, frame: optionsSectionFrame) + } + contentHeight += optionsSectionSize.height + + let partiallyRestrictTitle: String + let fullyBanTitle: String + if component.peers.count == 1 { + partiallyRestrictTitle = environment.strings.Chat_AdminActionSheet_RestrictFooterSingle + fullyBanTitle = environment.strings.Chat_AdminActionSheet_BanFooterSingle + } else { + partiallyRestrictTitle = environment.strings.Chat_AdminActionSheet_RestrictFooterMultiple + fullyBanTitle = environment.strings.Chat_AdminActionSheet_BanFooterMultiple + } + + let optionsFooterSize = self.optionsFooter.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(OptionsSectionFooterComponent( + theme: environment.theme, + text: self.isConfigurationExpanded ? fullyBanTitle : partiallyRestrictTitle, + fontSize: presentationData.listsFontSize.itemListBaseHeaderFontSize, + isExpanded: self.isConfigurationExpanded + )), + effectAlignment: .left, + contentInsets: UIEdgeInsets(), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + self.isConfigurationExpanded = !self.isConfigurationExpanded + if self.isConfigurationExpanded && self.optionBanSelectedPeers.isEmpty { + for peer in component.peers { + self.optionBanSelectedPeers.insert(peer.peer.id) + } + } + self.state?.updated(transition: .spring(duration: 0.35)) + }, + animateAlpha: true, + animateScale: false, + animateContents: true + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + + var configSectionItems: [AnyComponentWithIdentity] = [] + + enum ConfigItem: Hashable, CaseIterable { + case sendMessages + case sendMedia + case addUsers + case pinMessages + case changeInfo + } + + var allConfigItems: [(ConfigItem, Bool)] = [] + if !self.allowedMediaRights.isEmpty || !self.allowedParticipantRights.isEmpty { + for configItem in ConfigItem.allCases { + let isEnabled: Bool + switch configItem { + case .sendMessages: + isEnabled = self.allowedParticipantRights.contains(.sendMessages) + case .sendMedia: + isEnabled = !self.allowedMediaRights.isEmpty + case .addUsers: + isEnabled = self.allowedParticipantRights.contains(.addMembers) + case .pinMessages: + isEnabled = self.allowedParticipantRights.contains(.pinMessages) + case .changeInfo: + isEnabled = self.allowedParticipantRights.contains(.changeInfo) + } + allConfigItems.append((configItem, isEnabled)) + } + } + + loop: for (configItem, isEnabled) in allConfigItems { + let itemTitle: AnyComponent + let itemValue: Bool + switch configItem { + case .sendMessages: + itemTitle = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Channel_BanUser_PermissionSendMessages, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )) + itemValue = self.participantRights.contains(.sendMessages) + case .sendMedia: + if isEnabled { + itemTitle = AnyComponent(HStack([ + AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Channel_BanUser_PermissionSendMedia, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(MediaSectionExpandIndicatorComponent( + theme: environment.theme, + title: "\(self.mediaRights.count)/\(self.allowedMediaRights.count)", + isExpanded: self.isMediaSectionExpanded + ))) + ], spacing: 7.0)) + } else { + itemTitle = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Channel_BanUser_PermissionSendMedia, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )) + } + + itemValue = !self.mediaRights.isEmpty + case .addUsers: + itemTitle = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Channel_BanUser_PermissionAddMembers, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )) + itemValue = self.participantRights.contains(.addMembers) + case .pinMessages: + itemTitle = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Channel_EditAdmin_PermissionPinMessages, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )) + itemValue = self.participantRights.contains(.pinMessages) + case .changeInfo: + itemTitle = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Channel_BanUser_PermissionChangeGroupInfo, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )) + itemValue = self.participantRights.contains(.changeInfo) + } + + configSectionItems.append(AnyComponentWithIdentity(id: configItem, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: itemTitle, + accessory: .toggle(ListActionItemComponent.Toggle( + style: isEnabled ? .icons : .lock, + isOn: itemValue, + isInteractive: isEnabled, + action: isEnabled ? { [weak self] _ in + guard let self else { + return + } + switch configItem { + case .sendMessages: + if self.participantRights.contains(.sendMessages) { + self.participantRights.remove(.sendMessages) + } else { + self.participantRights.insert(.sendMessages) + } + case .sendMedia: + if self.mediaRights.isEmpty { + self.mediaRights = self.allowedMediaRights + } else { + self.mediaRights = [] + } + case .addUsers: + if self.participantRights.contains(.addMembers) { + self.participantRights.remove(.addMembers) + } else { + self.participantRights.insert(.addMembers) + } + case .pinMessages: + if self.participantRights.contains(.pinMessages) { + self.participantRights.remove(.pinMessages) + } else { + self.participantRights.insert(.pinMessages) + } + case .changeInfo: + if self.participantRights.contains(.changeInfo) { + self.participantRights.remove(.changeInfo) + } else { + self.participantRights.insert(.changeInfo) + } + } + self.state?.updated(transition: .spring(duration: 0.35)) + } : nil + )), + action: (isEnabled && configItem == .sendMedia) ? { [weak self] _ in + guard let self else { + return + } + self.isMediaSectionExpanded = !self.isMediaSectionExpanded + self.state?.updated(transition: .spring(duration: 0.35)) + } : nil, + highlighting: .disabled + )))) + + if isEnabled, case .sendMedia = configItem, self.isMediaSectionExpanded { + var mediaItems: [AnyComponentWithIdentity] = [] + mediaRightsLoop: for possibleMediaItem in allMediaRightItems { + if !self.allowedMediaRights.contains(possibleMediaItem) { + continue + } + + let mediaItemTitle: String + switch possibleMediaItem { + case .photos: + mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendPhoto + case .videos: + mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendVideo + case .stickersAndGifs: + mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendStickersAndGifs + case .music: + mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendMusic + case .files: + mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendFile + case .voiceMessages: + mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendVoiceMessage + case .videoMessages: + mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendVideoMessage + case .links: + mediaItemTitle = environment.strings.Channel_BanUser_PermissionEmbedLinks + case .polls: + mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendPolls + default: + continue mediaRightsLoop + } + + mediaItems.append(AnyComponentWithIdentity(id: possibleMediaItem, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: mediaItemTitle, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + leftIcon: .check(ListActionItemComponent.LeftIcon.Check( + isSelected: self.mediaRights.contains(possibleMediaItem), + toggle: { [weak self] in + guard let self else { + return + } + + if self.mediaRights.contains(possibleMediaItem) { + self.mediaRights.remove(possibleMediaItem) + } else { + self.mediaRights.insert(possibleMediaItem) + } + + self.state?.updated(transition: .spring(duration: 0.35)) + } + )), + icon: .none, + accessory: .none, + action: { [weak self] _ in + guard let self else { + return + } + + if self.mediaRights.contains(possibleMediaItem) { + self.mediaRights.remove(possibleMediaItem) + } else { + self.mediaRights.insert(possibleMediaItem) + } + + self.state?.updated(transition: .spring(duration: 0.35)) + }, + highlighting: .disabled + )))) + } + configSectionItems.append(AnyComponentWithIdentity(id: "media-sub", component: AnyComponent(ListSubSectionComponent( + theme: environment.theme, + leftInset: 0.0, + items: mediaItems + )))) + } + } + + let configSectionSize = self.configSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Chat_AdminActionSheet_PermissionsSectionHeader, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: configSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100000.0) + ) + let configSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + 30.0), size: configSectionSize) + if let configSectionView = self.configSection.view { + if configSectionView.superview == nil { + configSectionView.clipsToBounds = true + configSectionView.layer.cornerRadius = 11.0 + self.scrollContentView.addSubview(configSectionView) + self.configSection.parentState = state + } + let effectiveConfigSectionFrame: CGRect + if self.isConfigurationExpanded { + effectiveConfigSectionFrame = configSectionFrame + } else { + effectiveConfigSectionFrame = CGRect(origin: CGPoint(x: configSectionFrame.minX, y: configSectionFrame.minY - 30.0), size: CGSize(width: configSectionFrame.width, height: 0.0)) + } + transition.setFrame(view: configSectionView, frame: effectiveConfigSectionFrame) + transition.setAlpha(view: configSectionView, alpha: self.isConfigurationExpanded ? 1.0 : 0.0) + } + + if availableOptions.contains(.ban) && !configSectionItems.isEmpty { + let optionsFooterFrame: CGRect + if self.isConfigurationExpanded { + contentHeight += 30.0 + contentHeight += configSectionSize.height + contentHeight += 7.0 + optionsFooterFrame = CGRect(origin: CGPoint(x: sideInset + 16.0, y: contentHeight), size: optionsFooterSize) + contentHeight += optionsFooterSize.height + } else { + contentHeight += 7.0 + optionsFooterFrame = CGRect(origin: CGPoint(x: sideInset + 16.0, y: contentHeight), size: optionsFooterSize) + contentHeight += optionsFooterSize.height + } + if let optionsFooterView = self.optionsFooter.view { + if optionsFooterView.superview == nil { + self.scrollContentView.addSubview(optionsFooterView) + } + transition.setFrame(view: optionsFooterView, frame: optionsFooterFrame) + transition.setAlpha(view: optionsFooterView, alpha: 1.0) + } + } else { + if let optionsFooterView = self.optionsFooter.view { + if optionsFooterView.superview == nil { + self.scrollContentView.addSubview(optionsFooterView) + } + let optionsFooterFrame = CGRect(origin: CGPoint(x: sideInset + 16.0, y: contentHeight), size: optionsFooterSize) + transition.setFrame(view: optionsFooterView, frame: optionsFooterFrame) + transition.setAlpha(view: optionsFooterView, alpha: 0.0) + } + } + + contentHeight += 30.0 + + let actionButtonSize = self.actionButton.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: environment.theme.list.itemCheckColors.fillColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(ButtonTextContentComponent( + text: environment.strings.Chat_AdminActionSheet_ActionButton, + badge: 0, + textColor: environment.theme.list.itemCheckColors.foregroundColor, + badgeBackground: environment.theme.list.itemCheckColors.foregroundColor, + badgeForeground: environment.theme.list.itemCheckColors.fillColor + )) + ), + isEnabled: true, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + self.environment?.controller()?.dismiss() + component.completion(self.calculateResult()) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + let bottomPanelHeight = 8.0 + environment.safeInsets.bottom + actionButtonSize.height + let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) + if let actionButtonView = actionButton.view { + if actionButtonView.superview == nil { + self.addSubview(actionButtonView) + } + transition.setFrame(view: actionButtonView, frame: actionButtonFrame) + } + + contentHeight += bottomPanelHeight + + clippingY = actionButtonFrame.minY - 24.0 + + let topInset: CGFloat = max(0.0, availableSize.height - containerInset - contentHeight) + + let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset) + + self.scrollContentClippingView.layer.cornerRadius = 10.0 + + self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset) + + transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight))) + + transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) + transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize)) + + let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset), size: CGSize(width: availableSize.width - sideInset * 2.0, height: clippingY - containerInset)) + transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) + transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) + + self.ignoreScrolling = true + let previousBounds = self.scrollView.bounds + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) + let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight) + if contentSize != self.scrollView.contentSize { + self.scrollView.contentSize = contentSize + } + if resetScrolling { + self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize) + } else { + if !previousBounds.isEmpty, !transition.animation.isImmediate { + let bounds = self.scrollView.bounds + if bounds.maxY != previousBounds.maxY { + let offsetY = previousBounds.maxY - bounds.maxY + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) + } + } + } + self.ignoreScrolling = false + self.updateScrolling(transition: transition) + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public class AdminUserActionsSheet: ViewControllerComponentContainer { + public final class Result { + public let reportSpamPeers: [EnginePeer.Id] + public let deleteAllFromPeers: [EnginePeer.Id] + public let banPeers: [EnginePeer.Id] + public let updateBannedRights: [EnginePeer.Id: TelegramChatBannedRights] + + init(reportSpamPeers: [EnginePeer.Id], deleteAllFromPeers: [EnginePeer.Id], banPeers: [EnginePeer.Id], updateBannedRights: [EnginePeer.Id: TelegramChatBannedRights]) { + self.reportSpamPeers = reportSpamPeers + self.deleteAllFromPeers = deleteAllFromPeers + self.banPeers = banPeers + self.updateBannedRights = updateBannedRights + } + } + + private let context: AccountContext + + private var isDismissed: Bool = false + + public init(context: AccountContext, chatPeer: EnginePeer, peers: [RenderedChannelParticipant], messageCount: Int, completion: @escaping (Result) -> Void) { + self.context = context + + super.init(context: context, component: AdminUserActionsSheetComponent(context: context, chatPeer: chatPeer, peers: peers, messageCount: messageCount, completion: completion), navigationBarAppearance: .none) + + self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal + self.blocksBackgroundWhenInOverlay = true + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.view.disablesInteractiveModalDismiss = true + + if let componentView = self.node.hostView.componentView as? AdminUserActionsSheetComponent.View { + componentView.animateIn() + } + } + + override public func dismiss(completion: (() -> Void)? = nil) { + if !self.isDismissed { + self.isDismissed = true + + if let componentView = self.node.hostView.componentView as? AdminUserActionsSheetComponent.View { + componentView.animateOut(completion: { [weak self] in + completion?() + self?.dismiss(animated: false) + }) + } else { + self.dismiss(animated: false) + } + } + } +} + +private let optionExpandUsersIcon: UIImage? = { + let sourceImage = UIImage(bundleImageName: "Item List/InlineIconUsers")! + return generateImage(CGSize(width: sourceImage.size.width, height: sourceImage.size.height), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + UIGraphicsPushContext(context) + sourceImage.draw(at: CGPoint(x: 0.0, y: 0.0)) + UIGraphicsPopContext() + })!.precomposed().withRenderingMode(.alwaysTemplate) +}() + +private final class OptionSectionExpandIndicatorComponent: Component { + let theme: PresentationTheme + let count: Int + let isExpanded: Bool + + init( + theme: PresentationTheme, + count: Int, + isExpanded: Bool + ) { + self.theme = theme + self.count = count + self.isExpanded = isExpanded + } + + static func ==(lhs: OptionSectionExpandIndicatorComponent, rhs: OptionSectionExpandIndicatorComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.count != rhs.count { + return false + } + if lhs.isExpanded != rhs.isExpanded { + return false + } + return true + } + + final class View: UIView { + private let iconView: UIImageView + private let arrowView: UIImageView + private let count = ComponentView() + + override init(frame: CGRect) { + self.iconView = UIImageView() + self.arrowView = UIImageView() + + super.init(frame: frame) + + self.addSubview(self.iconView) + self.addSubview(self.arrowView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: OptionSectionExpandIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let countArrowSpacing: CGFloat = 1.0 + let iconCountSpacing: CGFloat = 1.0 + + if self.iconView.image == nil { + self.iconView.image = optionExpandUsersIcon + } + self.iconView.tintColor = component.theme.list.itemPrimaryTextColor + let iconSize = self.iconView.image?.size ?? CGSize(width: 12.0, height: 12.0) + + if self.arrowView.image == nil { + self.arrowView.image = PresentationResourcesItemList.expandDownArrowImage(component.theme) + } + self.arrowView.tintColor = component.theme.list.itemPrimaryTextColor + let arrowSize = self.arrowView.image?.size ?? CGSize(width: 1.0, height: 1.0) + + let countSize = self.count.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "\(component.count)", font: Font.semibold(13.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + let size = CGSize(width: 60.0, height: availableSize.height) + + let arrowFrame = CGRect(origin: CGPoint(x: size.width - arrowSize.width - 12.0, y: floor((size.height - arrowSize.height) * 0.5)), size: arrowSize) + + let countFrame = CGRect(origin: CGPoint(x: arrowFrame.minX - countArrowSpacing - countSize.width, y: floor((size.height - countSize.height) * 0.5)), size: countSize) + + let iconFrame = CGRect(origin: CGPoint(x: countFrame.minX - iconCountSpacing - iconSize.width, y: floor((size.height - iconSize.height) * 0.5)), size: iconSize) + + if let countView = self.count.view { + if countView.superview == nil { + self.addSubview(countView) + } + countView.frame = countFrame + } + + self.arrowView.center = arrowFrame.center + self.arrowView.bounds = CGRect(origin: CGPoint(), size: arrowFrame.size) + transition.setTransform(view: self.arrowView, transform: CATransform3DTranslate(CATransform3DMakeRotation(component.isExpanded ? CGFloat.pi : 0.0, 0.0, 0.0, 1.0), 0.0, component.isExpanded ? 1.0 : 0.0, 0.0)) + + self.iconView.frame = iconFrame + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class MediaSectionExpandIndicatorComponent: Component { + let theme: PresentationTheme + let title: String + let isExpanded: Bool + + init( + theme: PresentationTheme, + title: String, + isExpanded: Bool + ) { + self.theme = theme + self.title = title + self.isExpanded = isExpanded + } + + static func ==(lhs: MediaSectionExpandIndicatorComponent, rhs: MediaSectionExpandIndicatorComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.isExpanded != rhs.isExpanded { + return false + } + return true + } + + final class View: UIView { + private let arrowView: UIImageView + private let title = ComponentView() + + override init(frame: CGRect) { + self.arrowView = UIImageView() + + super.init(frame: frame) + + self.addSubview(self.arrowView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: MediaSectionExpandIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let titleArrowSpacing: CGFloat = 1.0 + + if self.arrowView.image == nil { + self.arrowView.image = PresentationResourcesItemList.expandDownArrowImage(component.theme) + } + self.arrowView.tintColor = component.theme.list.itemPrimaryTextColor + let arrowSize = self.arrowView.image?.size ?? CGSize(width: 1.0, height: 1.0) + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.semibold(13.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + let size = CGSize(width: titleSize.width + titleArrowSpacing + arrowSize.width, height: titleSize.height) + + let titleFrame = CGRect(origin: CGPoint(x: 0.0, y: floor((size.height - titleSize.height) * 0.5)), size: titleSize) + let arrowFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + titleArrowSpacing, y: floor((size.height - arrowSize.height) * 0.5) + 2.0), size: arrowSize) + + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.frame = titleFrame + } + + self.arrowView.center = arrowFrame.center + self.arrowView.bounds = CGRect(origin: CGPoint(), size: arrowFrame.size) + transition.setTransform(view: self.arrowView, transform: CATransform3DTranslate(CATransform3DMakeRotation(component.isExpanded ? CGFloat.pi : 0.0, 0.0, 0.0, 1.0), 0.0, component.isExpanded ? 1.0 : -1.0, 0.0)) + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class OptionsSectionFooterComponent: Component { + let theme: PresentationTheme + let text: String + let fontSize: CGFloat + let isExpanded: Bool + + init( + theme: PresentationTheme, + text: String, + fontSize: CGFloat, + isExpanded: Bool + ) { + self.theme = theme + self.text = text + self.fontSize = fontSize + self.isExpanded = isExpanded + } + + static func ==(lhs: OptionsSectionFooterComponent, rhs: OptionsSectionFooterComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.fontSize != rhs.fontSize { + return false + } + if lhs.isExpanded != rhs.isExpanded { + return false + } + return true + } + + final class View: UIView { + private let arrowView: UIImageView + private let textView: ImmediateTextView + + override init(frame: CGRect) { + self.arrowView = UIImageView() + + self.textView = ImmediateTextView() + self.textView.maximumNumberOfLines = 0 + + super.init(frame: frame) + + self.addSubview(self.arrowView) + self.addSubview(self.textView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: OptionsSectionFooterComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + if self.arrowView.image == nil { + self.arrowView.image = PresentationResourcesItemList.expandSmallDownArrowImage(component.theme) + } + self.arrowView.tintColor = component.theme.list.itemAccentColor + let arrowSize = self.arrowView.image?.size ?? CGSize(width: 1.0, height: 1.0) + + let attributedText = NSMutableAttributedString(attributedString: NSAttributedString(string: component.text, font: Font.regular(component.fontSize), textColor: component.theme.list.itemAccentColor)) + attributedText.append(NSAttributedString(string: ">", font: Font.regular(component.fontSize), textColor: .clear)) + self.textView.attributedText = attributedText + let textLayout = self.textView.updateLayoutFullInfo(availableSize) + + let size = textLayout.size + let textFrame = CGRect(origin: CGPoint(), size: textLayout.size) + self.textView.frame = textFrame + + var arrowFrame = CGRect() + if let lineRect = textLayout.linesRects().last { + arrowFrame = CGRect(origin: CGPoint(x: textFrame.minX + lineRect.maxX - arrowSize.width + 6.0, y: textFrame.minY + lineRect.maxY - lineRect.height - arrowSize.height - 1.0), size: arrowSize) + } + + self.arrowView.center = arrowFrame.center + self.arrowView.bounds = CGRect(origin: CGPoint(), size: arrowFrame.size) + transition.setTransform(view: self.arrowView, transform: CATransform3DMakeRotation(component.isExpanded ? CGFloat.pi : 0.0, 0.0, 0.0, 1.0)) + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + diff --git a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/RecentActionsSettingsSheet.swift b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/RecentActionsSettingsSheet.swift new file mode 100644 index 00000000000..24df544b7f3 --- /dev/null +++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/RecentActionsSettingsSheet.swift @@ -0,0 +1,1098 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData +import AccountContext +import TelegramCore +import MultilineTextComponent +import ButtonComponent +import PresentationDataUtils +import Markdown +import UndoUI +import TelegramStringFormatting +import ListSectionComponent +import ListActionItemComponent +import PlainButtonComponent + +private enum ActionTypeSection: Hashable, CaseIterable { + case members + case settings + case messages +} + +private enum MembersActionType: Hashable, CaseIterable { + case newAdminRights + case newExceptions + case newMembers + case leftMembers + + func title(isGroup: Bool, strings: PresentationStrings) -> String { + switch self { + case .newAdminRights: + return strings.Channel_AdminLogFilter_EventsAdminRights + case .newExceptions: + return strings.Channel_AdminLogFilter_EventsExceptions + case .newMembers: + return isGroup ? strings.Channel_AdminLogFilter_EventsNewMembers : strings.Channel_AdminLogFilter_EventsNewSubscribers + case .leftMembers: + return isGroup ? strings.Channel_AdminLogFilter_EventsLeavingGroup : strings.Channel_AdminLogFilter_EventsLeavingChannel + } + } + + var eventFlags: AdminLogEventsFlags { + switch self { + case .newAdminRights: + return [.promote, .demote] + case .newExceptions: + return [.ban, .unban, .kick, .unkick] + case .newMembers: + return [.invite, .join] + case .leftMembers: + return [.leave] + } + } + + static func actionTypesFromFlags(_ eventFlags: AdminLogEventsFlags) -> [Self] { + var actionTypes: [Self] = [] + for actionType in Self.allCases { + if !actionType.eventFlags.intersection(eventFlags).isEmpty { + actionTypes.append(actionType) + } + } + return actionTypes + } +} + +private enum SettingsActionType: Hashable, CaseIterable { + case groupInfo + case inviteLinks + case videoChats + + func title(isGroup: Bool, strings: PresentationStrings) -> String { + switch self { + case .groupInfo: + return isGroup ? strings.Channel_AdminLogFilter_EventsInfo : strings.Channel_AdminLogFilter_ChannelEventsInfo + case .inviteLinks: + return strings.Channel_AdminLogFilter_EventsInviteLinks + case .videoChats: + return isGroup ? strings.Channel_AdminLogFilter_EventsCalls : strings.Channel_AdminLogFilter_EventsLiveStreams + } + } + + var eventFlags: AdminLogEventsFlags { + switch self { + case .groupInfo: + return [.info, .settings] + case .inviteLinks: + return [.invites] + case .videoChats: + return [.calls] + } + } + + static func actionTypesFromFlags(_ eventFlags: AdminLogEventsFlags) -> [Self] { + var actionTypes: [Self] = [] + for actionType in Self.allCases { + if !actionType.eventFlags.intersection(eventFlags).isEmpty { + actionTypes.append(actionType) + } + } + return actionTypes + } +} + +private enum MessagesActionType: Hashable, CaseIterable { + case deletedMessages + case editedMessages + case pinnedMessages + + func title(strings: PresentationStrings) -> String { + switch self { + case .deletedMessages: + return strings.Channel_AdminLogFilter_EventsDeletedMessages + case .editedMessages: + return strings.Channel_AdminLogFilter_EventsEditedMessages + case .pinnedMessages: + return strings.Channel_AdminLogFilter_EventsPinned + } + } + + var eventFlags: AdminLogEventsFlags { + switch self { + case .deletedMessages: + return [.deleteMessages] + case .editedMessages: + return [.editMessages] + case .pinnedMessages: + return [.pinnedMessages] + } + } + + static func actionTypesFromFlags(_ eventFlags: AdminLogEventsFlags) -> [Self] { + var actionTypes: [Self] = [] + for actionType in Self.allCases { + if !actionType.eventFlags.intersection(eventFlags).isEmpty { + actionTypes.append(actionType) + } + } + return actionTypes + } +} + +private enum ActionType: Hashable { + case members(MembersActionType) + case settings(SettingsActionType) + case messages(MessagesActionType) + + func title(isGroup: Bool, strings: PresentationStrings) -> String { + switch self { + case let .members(value): + return value.title(isGroup: isGroup, strings: strings) + case let .settings(value): + return value.title(isGroup: isGroup, strings: strings) + case let .messages(value): + return value.title(strings: strings) + } + } +} + +private final class RecentActionsSettingsSheetComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let peer: EnginePeer + let adminPeers: [EnginePeer] + let initialValue: RecentActionsSettingsSheet.Value + let completion: (RecentActionsSettingsSheet.Value) -> Void + + init( + context: AccountContext, + peer: EnginePeer, + adminPeers: [EnginePeer], + initialValue: RecentActionsSettingsSheet.Value, + completion: @escaping (RecentActionsSettingsSheet.Value) -> Void + ) { + self.context = context + self.peer = peer + self.adminPeers = adminPeers + self.initialValue = initialValue + self.completion = completion + } + + static func ==(lhs: RecentActionsSettingsSheetComponent, rhs: RecentActionsSettingsSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.adminPeers != rhs.adminPeers { + return false + } + return true + } + + private struct ItemLayout: Equatable { + var containerSize: CGSize + var containerInset: CGFloat + var bottomInset: CGFloat + var topInset: CGFloat + + init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat) { + self.containerSize = containerSize + self.containerInset = containerInset + self.bottomInset = bottomInset + self.topInset = topInset + } + } + + private final class ScrollView: UIScrollView { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return super.hitTest(point, with: event) + } + } + + final class View: UIView, UIScrollViewDelegate { + private let dimView: UIView + private let backgroundLayer: SimpleLayer + private let navigationBarContainer: SparseContainerView + private let navigationBackgroundView: BlurredBackgroundView + private let navigationBarSeparator: SimpleLayer + private let scrollView: ScrollView + private let scrollContentClippingView: SparseContainerView + private let scrollContentView: UIView + + private let leftButton = ComponentView() + + private let title = ComponentView() + private let actionButton = ComponentView() + + private let optionsSection = ComponentView() + private let adminsSection = ComponentView() + + private let bottomOverscrollLimit: CGFloat + + private var ignoreScrolling: Bool = false + + private var component: RecentActionsSettingsSheetComponent? + private weak var state: EmptyComponentState? + private var environment: ViewControllerComponentContainer.Environment? + private var isUpdating: Bool = false + + private var itemLayout: ItemLayout? + + private var topOffsetDistance: CGFloat? + + private var expandedSections = Set() + private var selectedMembersActions = Set() + private var selectedSettingsActions = Set() + private var selectedMessagesActions = Set() + private var selectedAdmins = Set() + + override init(frame: CGRect) { + self.bottomOverscrollLimit = 200.0 + + self.dimView = UIView() + + self.backgroundLayer = SimpleLayer() + self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.backgroundLayer.cornerRadius = 10.0 + + self.navigationBarContainer = SparseContainerView() + + self.navigationBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + self.navigationBarSeparator = SimpleLayer() + + self.scrollView = ScrollView() + + self.scrollContentClippingView = SparseContainerView() + self.scrollContentClippingView.clipsToBounds = true + + self.scrollContentView = UIView() + + super.init(frame: frame) + + self.addSubview(self.dimView) + self.layer.addSublayer(self.backgroundLayer) + + self.scrollView.delaysContentTouches = true + self.scrollView.canCancelContentTouches = true + self.scrollView.clipsToBounds = false + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.alwaysBounceVertical = true + self.scrollView.scrollsToTop = false + self.scrollView.delegate = self + self.scrollView.clipsToBounds = true + + self.addSubview(self.scrollContentClippingView) + self.scrollContentClippingView.addSubview(self.scrollView) + + self.scrollView.addSubview(self.scrollContentView) + + self.addSubview(self.navigationBarContainer) + + self.navigationBarContainer.addSubview(self.navigationBackgroundView) + self.navigationBarContainer.layer.addSublayer(self.navigationBarSeparator) + + self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + } + } + + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.bounds.contains(point) { + return nil + } + if !self.backgroundLayer.frame.contains(point) { + return self.dimView + } + + if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) { + return result + } + + let result = super.hitTest(point, with: event) + return result + } + + @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + guard let environment = self.environment, let controller = environment.controller() else { + return + } + controller.dismiss() + } + } + + private func calculateResult() -> RecentActionsSettingsSheet.Value { + var events: AdminLogEventsFlags = [] + var admins: [EnginePeer.Id] = [] + for action in self.selectedMembersActions { + events.formUnion(action.eventFlags) + } + for action in self.selectedSettingsActions { + events.formUnion(action.eventFlags) + } + for action in self.selectedMessagesActions { + events.formUnion(action.eventFlags) + } + for peerId in self.selectedAdmins { + admins.append(peerId) + } + return RecentActionsSettingsSheet.Value( + events: events, + admins: admins + ) + } + + private func updateScrolling(isFirstTime: Bool = false, transition: Transition) { + guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else { + return + } + var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset + + let navigationAlpha: CGFloat = 1.0 - max(0.0, min(1.0, (topOffset + 20.0) / 20.0)) + transition.setAlpha(view: self.navigationBackgroundView, alpha: navigationAlpha) + transition.setAlpha(layer: self.navigationBarSeparator, alpha: navigationAlpha) + + topOffset = max(0.0, topOffset) + transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) + + transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) + + let topOffsetDistance: CGFloat = min(200.0, floor(itemLayout.containerSize.height * 0.25)) + self.topOffsetDistance = topOffsetDistance + var topOffsetFraction = topOffset / topOffsetDistance + topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) + + let modalStyleOverlayTransition: ContainedViewLayoutTransition + if isFirstTime { + modalStyleOverlayTransition = .animated(duration: 0.4, curve: .spring) + } else { + modalStyleOverlayTransition = transition.containedViewLayoutTransition + } + + let transitionFactor: CGFloat = 1.0 - topOffsetFraction + if self.isUpdating { + DispatchQueue.main.async { [weak controller] in + guard let controller else { + return + } + controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: modalStyleOverlayTransition) + } + } else { + controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: modalStyleOverlayTransition) + } + } + + func animateIn() { + self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY + self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + if let actionButtonView = self.actionButton.view { + actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + } + + func animateOut(completion: @escaping () -> Void) { + let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY + + self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in + completion() + }) + self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + if let actionButtonView = self.actionButton.view { + actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + } + + if let environment = self.environment, let controller = environment.controller() { + controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .easeInOut)) + } + } + + func update(component: RecentActionsSettingsSheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + let themeUpdated = self.environment?.theme !== environment.theme + + let resetScrolling = self.scrollView.bounds.width != availableSize.width + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + + var isFirstTime = false + if self.component == nil { + isFirstTime = true + self.selectedMembersActions = Set(MembersActionType.actionTypesFromFlags(component.initialValue.events)) + self.selectedSettingsActions = Set(SettingsActionType.actionTypesFromFlags(component.initialValue.events)) + self.selectedMessagesActions = Set(MessagesActionType.actionTypesFromFlags(component.initialValue.events)) + self.selectedAdmins = component.initialValue.admins.flatMap { Set($0) } ?? Set(component.adminPeers.map(\.id)) + } + + var isGroup = true + if case let .channel(channel) = component.peer, case .broadcast = channel.info { + isGroup = false + } + + self.component = component + self.state = state + self.environment = environment + + if themeUpdated { + self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + self.backgroundLayer.backgroundColor = environment.theme.list.blocksBackgroundColor.cgColor + + self.navigationBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + self.navigationBarSeparator.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor + } + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) + + transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) + + var contentHeight: CGFloat = 0.0 + contentHeight += 54.0 + contentHeight += 16.0 + + let leftButtonSize = self.leftButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: environment.theme.list.itemAccentColor)), + action: { [weak self] in + guard let self, let controller = self.environment?.controller() else { + return + } + controller.dismiss() + } + ).minSize(CGSize(width: 44.0, height: 56.0))), + environment: {}, + containerSize: CGSize(width: 120.0, height: 100.0) + ) + let leftButtonFrame = CGRect(origin: CGPoint(x: 16.0 + environment.safeInsets.left, y: 0.0), size: leftButtonSize) + if let leftButtonView = self.leftButton.view { + if leftButtonView.superview == nil { + self.navigationBarContainer.addSubview(leftButtonView) + } + transition.setFrame(view: leftButtonView, frame: leftButtonFrame) + } + + let containerInset: CGFloat = environment.statusBarHeight + 10.0 + + let clippingY: CGFloat + + let actionTypeSectionItem: (ActionTypeSection) -> AnyComponentWithIdentity = { actionTypeSection in + let sectionId: AnyHashable + let totalCount: Int + let selectedCount: Int + let isExpanded: Bool + let title: String + + sectionId = actionTypeSection + isExpanded = self.expandedSections.contains(actionTypeSection) + + switch actionTypeSection { + case .members: + totalCount = MembersActionType.allCases.count + selectedCount = self.selectedMembersActions.count + title = isGroup ? environment.strings.Channel_AdminLogFilter_Section_MembersGroup : environment.strings.Channel_AdminLogFilter_Section_MembersChannel + case .settings: + totalCount = SettingsActionType.allCases.count + selectedCount = self.selectedSettingsActions.count + title = isGroup ? environment.strings.Channel_AdminLogFilter_Section_SettingsGroup : environment.strings.Channel_AdminLogFilter_Section_SettingsChannel + case .messages: + totalCount = MessagesActionType.allCases.count + selectedCount = self.selectedMessagesActions.count + title = environment.strings.Channel_AdminLogFilter_Section_Messages + } + + let itemTitle: AnyComponent = AnyComponent(HStack([ + AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: title, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(MediaSectionExpandIndicatorComponent( + theme: environment.theme, + title: "\(selectedCount)/\(totalCount)", + isExpanded: isExpanded + ))) + ], spacing: 7.0)) + + let toggleAction: () -> Void = { [weak self] in + guard let self else { + return + } + + switch actionTypeSection { + case .members: + if self.selectedMembersActions.isEmpty { + self.selectedMembersActions = Set(MembersActionType.allCases) + } else { + self.selectedMembersActions.removeAll() + } + case .settings: + if self.selectedSettingsActions.isEmpty { + self.selectedSettingsActions = Set(SettingsActionType.allCases) + } else { + self.selectedSettingsActions.removeAll() + } + case .messages: + if self.selectedMessagesActions.isEmpty { + self.selectedMessagesActions = Set(MessagesActionType.allCases) + } else { + self.selectedMessagesActions.removeAll() + } + } + + self.state?.updated(transition: .spring(duration: 0.35)) + } + + return AnyComponentWithIdentity(id: sectionId, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: itemTitle, + leftIcon: .check(ListActionItemComponent.LeftIcon.Check( + isSelected: selectedCount == totalCount, + toggle: { + toggleAction() + } + )), + icon: .none, + accessory: nil, + action: { [weak self] _ in + guard let self else { + return + } + if self.expandedSections.contains(actionTypeSection) { + self.expandedSections.remove(actionTypeSection) + } else { + self.expandedSections.insert(actionTypeSection) + } + + self.state?.updated(transition: .spring(duration: 0.35)) + }, + highlighting: .disabled + ))) + } + + let expandedActionTypeSectionItem: (ActionTypeSection) -> AnyComponentWithIdentity = { actionTypeSection in + let sectionId: AnyHashable + let selectedActionTypes: Set + let actionTypes: [ActionType] + switch actionTypeSection { + case .members: + sectionId = "members-sub" + actionTypes = MembersActionType.allCases.map(ActionType.members) + selectedActionTypes = Set(self.selectedMembersActions.map(ActionType.members)) + case .settings: + sectionId = "settings-sub" + actionTypes = SettingsActionType.allCases.map(ActionType.settings) + selectedActionTypes = Set(self.selectedSettingsActions.map(ActionType.settings)) + case .messages: + sectionId = "messages-sub" + actionTypes = MessagesActionType.allCases.map(ActionType.messages) + selectedActionTypes = Set(self.selectedMessagesActions.map(ActionType.messages)) + } + + var subItems: [AnyComponentWithIdentity] = [] + for actionType in actionTypes { + let actionItemTitle: String = actionType.title(isGroup: isGroup, strings: environment.strings) + + let subItemToggleAction: () -> Void = { [weak self] in + guard let self else { + return + } + + switch actionType { + case let .members(value): + if self.selectedMembersActions.contains(value) { + self.selectedMembersActions.remove(value) + } else { + self.selectedMembersActions.insert(value) + } + case let .settings(value): + if self.selectedSettingsActions.contains(value) { + self.selectedSettingsActions.remove(value) + } else { + self.selectedSettingsActions.insert(value) + } + case let .messages(value): + if self.selectedMessagesActions.contains(value) { + self.selectedMessagesActions.remove(value) + } else { + self.selectedMessagesActions.insert(value) + } + } + + self.state?.updated(transition: .spring(duration: 0.35)) + } + + subItems.append(AnyComponentWithIdentity(id: actionType, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: actionItemTitle, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + leftIcon: .check(ListActionItemComponent.LeftIcon.Check( + isSelected: selectedActionTypes.contains(actionType), + toggle: { + subItemToggleAction() + } + )), + icon: .none, + accessory: .none, + action: { _ in + subItemToggleAction() + }, + highlighting: .disabled + )))) + } + + return AnyComponentWithIdentity(id: sectionId, component: AnyComponent(ListSubSectionComponent( + theme: environment.theme, + leftInset: 62.0, + items: subItems + ))) + } + + let titleString: String = environment.strings.Channel_AdminLogFilter_RecentActionsTitle + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: titleString, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((54.0 - titleSize.height) * 0.5)), size: titleSize) + if let titleView = title.view { + if titleView.superview == nil { + self.navigationBarContainer.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + + let navigationBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: 54.0)) + transition.setFrame(view: self.navigationBackgroundView, frame: navigationBackgroundFrame) + self.navigationBackgroundView.update(size: navigationBackgroundFrame.size, cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition) + transition.setFrame(layer: self.navigationBarSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: availableSize.width, height: UIScreenPixel))) + + var optionsSectionItems: [AnyComponentWithIdentity] = [] + for actionTypeSection in ActionTypeSection.allCases { + optionsSectionItems.append(actionTypeSectionItem(actionTypeSection)) + if self.expandedSections.contains(actionTypeSection) { + optionsSectionItems.append(expandedActionTypeSectionItem(actionTypeSection)) + } + } + + let optionsSectionTransition = transition + let optionsSectionSize = self.optionsSection.update( + transition: optionsSectionTransition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Channel_AdminLogFilter_FilterActionsTypeTitle, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: optionsSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100000.0) + ) + let optionsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: optionsSectionSize) + if let optionsSectionView = self.optionsSection.view { + if optionsSectionView.superview == nil { + self.scrollContentView.addSubview(optionsSectionView) + self.optionsSection.parentState = state + } + transition.setFrame(view: optionsSectionView, frame: optionsSectionFrame) + } + contentHeight += optionsSectionSize.height + contentHeight += 24.0 + + var peerItems: [AnyComponentWithIdentity] = [] + for peer in component.adminPeers { + peerItems.append(AnyComponentWithIdentity(id: peer.id, component: AnyComponent(AdminUserActionsPeerComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + sideInset: 0.0, + title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), + peer: peer, + selectionState: .editing(isSelected: self.selectedAdmins.contains(peer.id)), + action: { [weak self] peer in + guard let self else { + return + } + + if self.selectedAdmins.contains(peer.id) { + self.selectedAdmins.remove(peer.id) + } else { + self.selectedAdmins.insert(peer.id) + } + + self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .easeInOut))) + } + )))) + } + + var adminsSectionItems: [AnyComponentWithIdentity] = [] + let allAdminsToggleAction: () -> Void = { [weak self] in + guard let self, let component = self.component else { + return + } + + if self.selectedAdmins.isEmpty { + self.selectedAdmins = Set(component.adminPeers.map(\.id)) + } else { + self.selectedAdmins.removeAll() + } + + self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .easeInOut))) + } + adminsSectionItems.append(AnyComponentWithIdentity(id: adminsSectionItems.count, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Channel_AdminLogFilter_ShowAllAdminsActions, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + leftIcon: .check(ListActionItemComponent.LeftIcon.Check( + isSelected: self.selectedAdmins.count == component.adminPeers.count, + toggle: { + allAdminsToggleAction() + } + )), + icon: .none, + accessory: .none, + action: { _ in + allAdminsToggleAction() + }, + highlighting: .disabled + )))) + adminsSectionItems.append(AnyComponentWithIdentity(id: adminsSectionItems.count, component: AnyComponent(ListSubSectionComponent( + theme: environment.theme, + leftInset: 62.0, + items: peerItems + )))) + let adminsSectionSize = self.adminsSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Channel_AdminLogFilter_FilterActionsAdminsTitle, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: adminsSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100000.0) + ) + let adminsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: adminsSectionSize) + if let adminsSectionView = self.adminsSection.view { + if adminsSectionView.superview == nil { + self.scrollContentView.addSubview(adminsSectionView) + self.adminsSection.parentState = state + } + transition.setFrame(view: adminsSectionView, frame: adminsSectionFrame) + } + contentHeight += adminsSectionSize.height + + contentHeight += 30.0 + + let actionButtonSize = self.actionButton.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: environment.theme.list.itemCheckColors.fillColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(ButtonTextContentComponent( + text: environment.strings.Channel_AdminLogFilter_ApplyFilter, + badge: 0, + textColor: environment.theme.list.itemCheckColors.foregroundColor, + badgeBackground: environment.theme.list.itemCheckColors.foregroundColor, + badgeForeground: environment.theme.list.itemCheckColors.fillColor + )) + ), + isEnabled: true, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + self.environment?.controller()?.dismiss() + component.completion(self.calculateResult()) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + let bottomPanelHeight = 8.0 + environment.safeInsets.bottom + actionButtonSize.height + let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) + if let actionButtonView = actionButton.view { + if actionButtonView.superview == nil { + self.addSubview(actionButtonView) + } + transition.setFrame(view: actionButtonView, frame: actionButtonFrame) + } + + contentHeight += bottomPanelHeight + + clippingY = actionButtonFrame.minY - 24.0 + + let topInset: CGFloat = max(0.0, availableSize.height - containerInset - contentHeight) + + let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset) + + self.scrollContentClippingView.layer.cornerRadius = 10.0 + + self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset) + + transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight))) + + transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) + transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize)) + + let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset), size: CGSize(width: availableSize.width - sideInset * 2.0, height: clippingY - containerInset)) + transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) + transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) + + self.ignoreScrolling = true + let previousBounds = self.scrollView.bounds + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) + let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight) + if contentSize != self.scrollView.contentSize { + self.scrollView.contentSize = contentSize + } + if resetScrolling { + self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize) + } else { + if !previousBounds.isEmpty, !transition.animation.isImmediate { + let bounds = self.scrollView.bounds + if bounds.maxY != previousBounds.maxY { + let offsetY = previousBounds.maxY - bounds.maxY + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) + } + } + } + self.ignoreScrolling = false + self.updateScrolling(isFirstTime: isFirstTime, transition: transition) + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public class RecentActionsSettingsSheet: ViewControllerComponentContainer { + public final class Value { + public let events: AdminLogEventsFlags + public let admins: [EnginePeer.Id]? + + public init(events: AdminLogEventsFlags, admins: [EnginePeer.Id]?) { + self.events = events + self.admins = admins + } + } + + private let context: AccountContext + + private var isDismissed: Bool = false + + public init(context: AccountContext, peer: EnginePeer, adminPeers: [EnginePeer], initialValue: Value, completion: @escaping (Value) -> Void) { + self.context = context + + super.init(context: context, component: RecentActionsSettingsSheetComponent(context: context, peer: peer, adminPeers: adminPeers, initialValue: initialValue, completion: completion), navigationBarAppearance: .none) + + self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal + self.blocksBackgroundWhenInOverlay = true + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.view.disablesInteractiveModalDismiss = true + + if let componentView = self.node.hostView.componentView as? RecentActionsSettingsSheetComponent.View { + componentView.animateIn() + } + } + + override public func dismiss(completion: (() -> Void)? = nil) { + if !self.isDismissed { + self.isDismissed = true + + if let componentView = self.node.hostView.componentView as? RecentActionsSettingsSheetComponent.View { + componentView.animateOut(completion: { [weak self] in + completion?() + self?.dismiss(animated: false) + }) + } else { + self.dismiss(animated: false) + } + } + } +} + +private final class MediaSectionExpandIndicatorComponent: Component { + let theme: PresentationTheme + let title: String + let isExpanded: Bool + + init( + theme: PresentationTheme, + title: String, + isExpanded: Bool + ) { + self.theme = theme + self.title = title + self.isExpanded = isExpanded + } + + static func ==(lhs: MediaSectionExpandIndicatorComponent, rhs: MediaSectionExpandIndicatorComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.isExpanded != rhs.isExpanded { + return false + } + return true + } + + final class View: UIView { + private let arrowView: UIImageView + private let title = ComponentView() + + override init(frame: CGRect) { + self.arrowView = UIImageView() + + super.init(frame: frame) + + self.addSubview(self.arrowView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: MediaSectionExpandIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let titleArrowSpacing: CGFloat = 1.0 + + if self.arrowView.image == nil { + self.arrowView.image = PresentationResourcesItemList.expandDownArrowImage(component.theme) + } + self.arrowView.tintColor = component.theme.list.itemPrimaryTextColor + let arrowSize = self.arrowView.image?.size ?? CGSize(width: 1.0, height: 1.0) + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.semibold(13.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + let size = CGSize(width: titleSize.width + titleArrowSpacing + arrowSize.width, height: titleSize.height) + + let titleFrame = CGRect(origin: CGPoint(x: 0.0, y: floor((size.height - titleSize.height) * 0.5)), size: titleSize) + let arrowFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + titleArrowSpacing, y: floor((size.height - arrowSize.height) * 0.5) + 2.0), size: arrowSize) + + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.frame = titleFrame + } + + self.arrowView.center = arrowFrame.center + self.arrowView.bounds = CGRect(origin: CGPoint(), size: arrowFrame.size) + transition.setTransform(view: self.arrowView, transform: CATransform3DTranslate(CATransform3DMakeRotation(component.isExpanded ? CGFloat.pi : 0.0, 0.0, 0.0, 1.0), 0.0, component.isExpanded ? 1.0 : -1.0, 0.0)) + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift b/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift index 5e6410d81d8..cb1bcc02fba 100644 --- a/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift +++ b/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift @@ -625,10 +625,10 @@ public final class AdsReportScreen: ViewControllerComponentContainer { }) case .premiumRequired: var replaceImpl: ((ViewController) -> Void)? - let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .noAds, action: { + 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) } diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift index 4b545754f93..563cd08d21c 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift @@ -483,7 +483,7 @@ final class AvatarEditorScreenComponent: Component { })) } case let .category(value): - let resultSignal = context.engine.stickers.searchEmoji(emojiString: value) + let resultSignal = context.engine.stickers.searchEmoji(category: value) |> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in var items: [EmojiPagerContentComponent.Item] = [] @@ -557,11 +557,11 @@ final class AvatarEditorScreenComponent: Component { fillWithLoadingPlaceholders: true, items: [] ) - ], id: AnyHashable(value), version: version, isPreset: true), isSearching: false) + ], id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) return } - self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value), version: version, isPreset: true), isSearching: false) + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) version += 1 })) } @@ -1542,8 +1542,7 @@ public final class AvatarEditorScreen: ViewControllerComponentContainer { hasTrending: false, forceHasPremium: true, searchIsPlaceholderOnly: false, - isProfilePhotoEmojiSelection: !isGroup, - isGroupPhotoEmojiSelection: isGroup + subject: isGroup ? .groupPhotoEmojiSelection : .profilePhotoEmojiSelection ) let signal = combineLatest(queue: .mainQueue(), diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 01f4be6b9ff..3051825902e 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -2198,7 +2198,7 @@ public class CameraScreen: ViewController { self.animatedIn = false let destinationInnerFrame = destinationView.convert(transitionOut.destinationRect, to: self.previewContainerView) - let initialCenter = self.mainPreviewView.layer.position + let initialCenter = self.mainPreviewAnimationWrapperView.layer.position self.mainPreviewAnimationWrapperView.center = destinationInnerFrame.center self.mainPreviewAnimationWrapperView.layer.animatePosition(from: initialCenter, to: self.mainPreviewAnimationWrapperView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in diff --git a/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift b/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift index 6a9820e0ebf..335db1704d7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift @@ -363,7 +363,7 @@ public final class ChatBotInfoItemNode: ListViewItemNode { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift index c8ca0fefd54..d0066f268fb 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift @@ -27,6 +27,8 @@ private func attributedServiceMessageString(theme: ChatPresentationThemeData, st } public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { + public var expandHighlightingNode: LinkHighlightingNode? + public let labelNode: TextNodeWithEntities private var dustNode: InvisibleInkDustNode? public var backgroundNode: WallpaperBubbleBackgroundNode? @@ -364,6 +366,24 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { let baseBackgroundFrame = labelFrame.offsetBy(dx: 0.0, dy: -11.0) + if var rect = strongSelf.labelNode.textNode.cachedLayout?.allAttributeRects(name: TelegramTextAttributes.Button).first?.1 { + rect = rect.insetBy(dx: -2.0, dy: 2.0).offsetBy(dx: 0.0, dy: 1.0 - UIScreenPixel) + let highlightNode: LinkHighlightingNode + if let current = strongSelf.expandHighlightingNode { + highlightNode = current + } else { + highlightNode = LinkHighlightingNode(color: UIColor(rgb: 0x000000, alpha: 0.1)) + highlightNode.outerRadius = 7.5 + strongSelf.insertSubnode(highlightNode, belowSubnode: strongSelf.labelNode.textNode) + strongSelf.expandHighlightingNode = highlightNode + } + highlightNode.frame = strongSelf.labelNode.textNode.frame + highlightNode.updateRects([rect]) + } else { + strongSelf.expandHighlightingNode?.removeFromSupernode() + strongSelf.expandHighlightingNode = nil + } + if let (offset, image) = backgroundMaskImage { if item.context.sharedContext.energyUsageSettings.fullTranslucency { if strongSelf.backgroundNode == nil { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift index 08017ba7739..af49b7cd3cd 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift @@ -395,7 +395,7 @@ public final class ChatMessageActionButtonsNode: ASDisplayNode { public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { self.absolutePosition = (rect, containerSize) - for button in buttonNodes { + for button in self.buttonNodes { var buttonFrame = button.frame buttonFrame.origin.x += rect.minX buttonFrame.origin.y += rect.minY diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index f015c81d2f5..fa3972edfbc 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -1665,14 +1665,24 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } strongSelf.addSubnode(actionButtonsNode) + + if animation.isAnimated { + actionButtonsNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } } else { if case let .System(duration, _) = animation { actionButtonsNode.layer.animateFrame(from: previousFrame, to: actionButtonsFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) } } } else if let actionButtonsNode = strongSelf.actionButtonsNode { - actionButtonsNode.removeFromSupernode() strongSelf.actionButtonsNode = nil + if animation.isAnimated { + actionButtonsNode.layer.animateAlpha(from: actionButtonsNode.alpha, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + actionButtonsNode.removeFromSupernode() + }) + } else { + actionButtonsNode.removeFromSupernode() + } } if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply { @@ -2622,8 +2632,8 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { self.layer.removeAllAnimations() } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - super.animateInsertion(currentTimestamp, duration: duration, short: short) + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + super.animateInsertion(currentTimestamp, duration: duration, options: options) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index fb16b0f37e2..58a9ed5d877 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -982,8 +982,9 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { self.inlineStickerLayers = [stickerLayer] } stickerLayer.isVisibleForAnimations = self.visibility != .none + stickerLayer.dynamicColor = file.isCustomTemplateEmoji ? mainColor : nil stickerLayer.frame = inlineMediaFrame - } else if contentAnimatedFilesValue.count == 4 { + } else if contentAnimatedFilesValue.count == 4, let file = contentAnimatedFilesValue.first { var stickerLayers: [InlineStickerItemLayer] = [] if self.inlineStickerLayers.count == contentAnimatedFilesValue.count { stickerLayers = self.inlineStickerLayers @@ -995,14 +996,16 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { } self.inlineStickerLayers = stickerLayers } + var frames: [CGRect] = [] let smallSize = CGSize(width: inlineMediaFrame.width / 2.0, height: inlineMediaFrame.width / 2.0) - frames.append(CGRect(origin: inlineMediaFrame.origin, size: smallSize)) - frames.append(CGRect(origin: inlineMediaFrame.origin.offsetBy(dx: smallSize.width, dy: 0.0), size: smallSize)) - frames.append(CGRect(origin: inlineMediaFrame.origin.offsetBy(dx: 0.0, dy: smallSize.height), size: smallSize)) - frames.append(CGRect(origin: inlineMediaFrame.origin.offsetBy(dx: smallSize.width, dy: smallSize.height), size: smallSize)) + frames.append(CGRect(origin: inlineMediaFrame.origin, size: smallSize).insetBy(dx: 2.0, dy: 2.0)) + frames.append(CGRect(origin: inlineMediaFrame.origin.offsetBy(dx: smallSize.width, dy: 0.0), size: smallSize).insetBy(dx: 2.0, dy: 2.0)) + frames.append(CGRect(origin: inlineMediaFrame.origin.offsetBy(dx: 0.0, dy: smallSize.height), size: smallSize).insetBy(dx: 2.0, dy: 2.0)) + frames.append(CGRect(origin: inlineMediaFrame.origin.offsetBy(dx: smallSize.width, dy: smallSize.height), size: smallSize).insetBy(dx: 2.0, dy: 2.0)) for i in 0 ..< stickerLayers.count { stickerLayers[i].frame = frames[i] + stickerLayers[i].dynamicColor = file.isCustomTemplateEmoji ? mainColor : nil } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index d7ff8277830..172361b64d4 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -327,6 +327,8 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ case let .eventLogPreviousLink(previousMessage): result.append((previousMessage, ChatMessageEventLogPreviousLinkContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false + case .eventLogGroupedMessages: + break } } @@ -863,8 +865,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI process(node: self) } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - super.animateInsertion(currentTimestamp, duration: duration, short: short) + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + super.animateInsertion(currentTimestamp, duration: duration, options: options) self.shadowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) @@ -1221,13 +1223,23 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } recognizer.highlight = { [weak self] point in if let strongSelf = self { - if let replyInfoNode = strongSelf.replyInfoNode { - var translatedPoint: CGPoint? - let convertedNodeFrame = replyInfoNode.view.convert(replyInfoNode.bounds, to: strongSelf.view) - if let point = point, convertedNodeFrame.insetBy(dx: -4.0, dy: -4.0).contains(point) { - translatedPoint = strongSelf.view.convert(point, to: replyInfoNode.view) + if strongSelf.selectionNode == nil { + if let replyInfoNode = strongSelf.replyInfoNode { + var translatedPoint: CGPoint? + let convertedNodeFrame = replyInfoNode.view.convert(replyInfoNode.bounds, to: strongSelf.view) + if let point = point, convertedNodeFrame.insetBy(dx: -4.0, dy: -4.0).contains(point) { + translatedPoint = strongSelf.view.convert(point, to: replyInfoNode.view) + } + replyInfoNode.updateTouchesAtPoint(translatedPoint) + } + if let forwardInfoNode = strongSelf.forwardInfoNode { + var translatedPoint: CGPoint? + let convertedNodeFrame = forwardInfoNode.view.convert(forwardInfoNode.bounds, to: strongSelf.view) + if let point = point, convertedNodeFrame.insetBy(dx: -4.0, dy: -4.0).contains(point) { + translatedPoint = strongSelf.view.convert(point, to: forwardInfoNode.view) + } + forwardInfoNode.updateTouchesAtPoint(translatedPoint) } - replyInfoNode.updateTouchesAtPoint(translatedPoint) } for contentNode in strongSelf.contentNodes { var translatedPoint: CGPoint? @@ -2134,7 +2146,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } else { firstNodeTopPosition = .None(topNodeMergeStatus) } - let lastNodeTopPosition: ChatMessageBubbleRelativePosition = .None(bottomNodeMergeStatus) + var lastNodeTopPosition: ChatMessageBubbleRelativePosition = .None(bottomNodeMergeStatus) var calculatedGroupFramesAndSize: ([(CGRect, MosaicItemPosition)], CGSize)? var mosaicStatusSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)? @@ -2396,6 +2408,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI let sizeAndApply = forwardInfoLayout(item.context, item.presentationData, item.presentationData.strings, .bubble(incoming: incoming), forwardSource, forwardAuthorSignature, forwardPsaType, nil, CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude)) forwardInfoSizeApply = (sizeAndApply.0, { width in sizeAndApply.1(width) }) + headerSize.height += 2.0 forwardInfoOriginY = headerSize.height headerSize.width = max(headerSize.width, forwardInfoSizeApply.0.width + bubbleWidthInsets) headerSize.height += forwardInfoSizeApply.0.height @@ -2425,6 +2438,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if storyType != .regular { headerSize.height += 6.0 + } else { + headerSize.height += 2.0 } forwardInfoOriginY = headerSize.height @@ -2433,11 +2448,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if storyType != .regular { headerSize.height += 16.0 + } else { + headerSize.height += 2.0 } } var hasReply = replyMessage != nil || replyForward != nil || replyStory != nil - if !isInstantVideo, case let .peer(peerId) = item.chatLocation, (peerId == replyMessage?.id.peerId || item.message.threadId == 1), let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, channel.flags.contains(.isForum), item.message.associatedThreadInfo != nil { + if !isInstantVideo, case let .peer(peerId) = item.chatLocation, (peerId == replyMessage?.id.peerId || item.message.threadId == 1 || item.associatedData.isRecentActions), let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, channel.flags.contains(.isForum), item.message.associatedThreadInfo != nil { if let threadId = item.message.threadId, let replyMessage = replyMessage, Int64(replyMessage.id.id) == threadId { hasReply = false } @@ -2554,7 +2571,27 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI var maxContentWidth: CGFloat = headerSize.width var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode))? - if let replyMarkup = replyMarkup, !item.presentationData.isPreview { + if let additionalContent = item.additionalContent, case let .eventLogGroupedMessages(messages, hasButton) = additionalContent, hasButton { + let (minWidth, buttonsLayout) = actionButtonsLayout( + item.context, + item.presentationData.theme, + item.presentationData.chatBubbleCorners, + item.presentationData.strings, + item.controllerInteraction.presentationContext.backgroundNode, + ReplyMarkupMessageAttribute( + rows: [ + ReplyMarkupRow( + buttons: [ReplyMarkupButton(title: item.presentationData.strings.Channel_AdminLog_ShowMoreMessages(Int32(messages.count - 1)), titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: MemoryBuffer(data: Data())))] + ) + ], + flags: [], + placeholder: nil + ), item.message, maximumNodeWidth) + maxContentWidth = max(maxContentWidth, minWidth) + actionButtonsFinalize = buttonsLayout + + lastNodeTopPosition = .None(.Both) + } else if let replyMarkup = replyMarkup, !item.presentationData.isPreview { let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, item.controllerInteraction.presentationContext.backgroundNode, replyMarkup, item.message, maximumNodeWidth) maxContentWidth = max(maxContentWidth, minWidth) actionButtonsFinalize = buttonsLayout @@ -4058,14 +4095,24 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } strongSelf.insertSubnode(actionButtonsNode, belowSubnode: strongSelf.messageAccessibilityArea) actionButtonsNode.frame = actionButtonsFrame + + if animation.isAnimated { + actionButtonsNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } } else { animation.animator.updateFrame(layer: actionButtonsNode.layer, frame: actionButtonsFrame, completion: nil) } reactionButtonsOffset += actionButtonsSizeAndApply.0.height } else if let actionButtonsNode = strongSelf.actionButtonsNode { - actionButtonsNode.removeFromSupernode() strongSelf.actionButtonsNode = nil + if animation.isAnimated { + actionButtonsNode.layer.animateAlpha(from: actionButtonsNode.alpha, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + actionButtonsNode.removeFromSupernode() + }) + } else { + actionButtonsNode.removeFromSupernode() + } } if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply { @@ -4616,7 +4663,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI item.controllerInteraction.openPeer(EnginePeer(peer), peer is TelegramUser ? .info(nil) : .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default) } else if let _ = forwardInfo.authorSignature { var subRect: CGRect? - if let textNode = forwardInfoNode.textNode { + if let textNode = forwardInfoNode.nameNode { subRect = textNode.frame } item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, forwardInfoNode, subRect) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/BUILD index 84fcec074b1..dd8301c5f5a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/BUILD @@ -25,6 +25,7 @@ swift_library( "//submodules/TelegramUI/Components/TextNodeWithEntities", "//submodules/TelegramUI/Components/AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer", + "//submodules/AvatarNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift index f924d4be481..8d79dea5e71 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift @@ -7,6 +7,7 @@ import TelegramCore import TelegramPresentationData import LocalizedPeerData import AccountContext +import AvatarNode public enum ChatMessageForwardInfoType: Equatable { case bubble(incoming: Bool) @@ -73,10 +74,18 @@ public class ChatMessageForwardInfoNode: ASDisplayNode { } } - public private(set) var textNode: TextNode? + public private(set) var titleNode: TextNode? + public private(set) var nameNode: TextNode? private var credibilityIconNode: ASImageNode? private var infoNode: InfoButtonNode? private var expiredStoryIconView: UIImageView? + private var avatarNode: AvatarNode? + + private var theme: PresentationTheme? + private var highlightColor: UIColor? + private var linkHighlightingNode: LinkHighlightingNode? + + private var previousPeer: Peer? public var openPsa: ((String, ASDisplayNode) -> Void)? @@ -107,17 +116,98 @@ public class ChatMessageForwardInfoNode: ASDisplayNode { } } + public func getBoundingRects() -> [CGRect] { + var initialRects: [CGRect] = [] + let addRects: (TextNode, CGPoint, CGFloat) -> Void = { textNode, offset, additionalWidth in + guard let cachedLayout = textNode.cachedLayout else { + return + } + for rect in cachedLayout.linesRects() { + var rect = rect + rect.size.width += rect.origin.x + additionalWidth + rect.origin.x = 0.0 + initialRects.append(rect.offsetBy(dx: offset.x, dy: offset.y)) + } + } + + let offsetY: CGFloat = -12.0 + if let titleNode = self.titleNode { + addRects(titleNode, CGPoint(x: titleNode.frame.minX, y: offsetY + titleNode.frame.minY), 0.0) + + if let nameNode = self.nameNode { + addRects(nameNode, CGPoint(x: titleNode.frame.minX, y: offsetY + nameNode.frame.minY), nameNode.frame.minX - titleNode.frame.minX) + } + } + + return initialRects + } + + public func updateTouchesAtPoint(_ point: CGPoint?) { + var isHighlighted = false + if point != nil { + isHighlighted = true + } + + var initialRects: [CGRect] = [] + let addRects: (TextNode, CGPoint, CGFloat) -> Void = { textNode, offset, additionalWidth in + guard let cachedLayout = textNode.cachedLayout else { + return + } + for rect in cachedLayout.linesRects() { + var rect = rect + rect.size.width += rect.origin.x + additionalWidth + rect.origin.x = 0.0 + initialRects.append(rect.offsetBy(dx: offset.x, dy: offset.y)) + } + } + + let offsetY: CGFloat = -12.0 + if let titleNode = self.titleNode { + addRects(titleNode, CGPoint(x: titleNode.frame.minX, y: offsetY + titleNode.frame.minY), 0.0) + + if let nameNode = self.nameNode { + addRects(nameNode, CGPoint(x: titleNode.frame.minX, y: offsetY + nameNode.frame.minY), nameNode.frame.minX - titleNode.frame.minX) + } + } + + if isHighlighted, !initialRects.isEmpty, let highlightColor = self.highlightColor { + let rects = initialRects + + let linkHighlightingNode: LinkHighlightingNode + if let current = self.linkHighlightingNode { + linkHighlightingNode = current + } else { + linkHighlightingNode = LinkHighlightingNode(color: highlightColor) + self.linkHighlightingNode = linkHighlightingNode + self.addSubnode(linkHighlightingNode) + } + linkHighlightingNode.frame = self.bounds + linkHighlightingNode.updateRects(rects) + } else if let linkHighlightingNode = self.linkHighlightingNode { + self.linkHighlightingNode = nil + linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in + linkHighlightingNode?.removeFromSupernode() + }) + } + } + public static func asyncLayout(_ maybeNode: ChatMessageForwardInfoNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ strings: PresentationStrings, _ type: ChatMessageForwardInfoType, _ peer: Peer?, _ authorName: String?, _ psaType: String?, _ storyData: StoryData?, _ constrainedSize: CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode) { - let textNodeLayout = TextNode.asyncLayout(maybeNode?.textNode) + let titleNodeLayout = TextNode.asyncLayout(maybeNode?.titleNode) + let nameNodeLayout = TextNode.asyncLayout(maybeNode?.nameNode) + + let previousPeer = maybeNode?.previousPeer return { context, presentationData, strings, type, peer, authorName, psaType, storyData, constrainedSize in - let fontSize = floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0) + let originalPeer = peer + let peer = peer ?? previousPeer + + let fontSize = floor(presentationData.fontSize.baseDisplaySize * 14.0 / 17.0) let prefixFont = Font.regular(fontSize) let peerFont = Font.medium(fontSize) let peerString: String if let peer = peer { - if let authorName = authorName { + if let authorName = authorName, originalPeer === peer { peerString = "\(EnginePeer(peer).displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)) (\(authorName))" } else { peerString = EnginePeer(peer).displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder) @@ -134,87 +224,93 @@ public class ChatMessageForwardInfoNode: ASDisplayNode { } let titleColor: UIColor - let completeSourceString: PresentationStrings.FormattedString + let titleString: PresentationStrings.FormattedString + var authorString: String? switch type { - case let .bubble(incoming): - if let psaType = psaType { - titleColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.barPositive : presentationData.theme.theme.chat.message.outgoing.polls.barPositive - - var customFormat: String? - let key = "Message.ForwardedPsa.\(psaType)" - if let string = presentationData.strings.primaryComponent.dict[key] { - customFormat = string - } else if let string = presentationData.strings.secondaryComponent?.dict[key] { - customFormat = string - } - - if let customFormat = customFormat { - if let range = customFormat.range(of: "%@") { - let leftPart = String(customFormat[customFormat.startIndex ..< range.lowerBound]) - let rightPart = String(customFormat[range.upperBound...]) - - let formattedText = leftPart + peerString + rightPart - completeSourceString = PresentationStrings.FormattedString(string: formattedText, ranges: [PresentationStrings.FormattedString.Range(index: 0, range: NSRange(location: leftPart.count, length: peerString.count))]) - } else { - completeSourceString = PresentationStrings.FormattedString(string: customFormat, ranges: []) - } + case let .bubble(incoming): + if let psaType = psaType { + titleColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.barPositive : presentationData.theme.theme.chat.message.outgoing.polls.barPositive + + var customFormat: String? + let key = "Message.ForwardedPsa.\(psaType)" + if let string = presentationData.strings.primaryComponent.dict[key] { + customFormat = string + } else if let string = presentationData.strings.secondaryComponent?.dict[key] { + customFormat = string + } + + if let customFormat = customFormat { + if let range = customFormat.range(of: "%@") { + let leftPart = String(customFormat[customFormat.startIndex ..< range.lowerBound]) + let rightPart = String(customFormat[range.upperBound...]) + + let formattedText = leftPart + peerString + rightPart + titleString = PresentationStrings.FormattedString(string: formattedText, ranges: [PresentationStrings.FormattedString.Range(index: 0, range: NSRange(location: leftPart.count, length: peerString.count))]) } else { - completeSourceString = strings.Message_GenericForwardedPsa(peerString) + titleString = PresentationStrings.FormattedString(string: customFormat, ranges: []) } } else { - if incoming { - if let nameColor = peer?.nameColor { - titleColor = context.peerNameColors.get(nameColor, dark: presentationData.theme.theme.overallDarkAppearance).main - } else { - titleColor = presentationData.theme.theme.chat.message.incoming.accentTextColor - } - } else { - titleColor = presentationData.theme.theme.chat.message.outgoing.accentTextColor - } - - if let storyData = storyData { - switch storyData.storyType { - case .regular: - completeSourceString = strings.Message_ForwardedStoryShort(peerString) - case .expired: - completeSourceString = strings.Message_ForwardedExpiredStoryShort(peerString) - case .unavailable: - completeSourceString = strings.Message_ForwardedUnavailableStoryShort(peerString) - } + titleString = strings.Message_GenericForwardedPsa(peerString) + } + } else { + if incoming { + if let nameColor = peer?.nameColor { + titleColor = context.peerNameColors.get(nameColor, dark: presentationData.theme.theme.overallDarkAppearance).main } else { - completeSourceString = strings.Message_ForwardedMessageShort(peerString) + titleColor = presentationData.theme.theme.chat.message.incoming.accentTextColor } + } else { + titleColor = presentationData.theme.theme.chat.message.outgoing.accentTextColor } - case .standalone: - let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) - titleColor = serviceColor.primaryText - if let psaType = psaType { - var customFormat: String? - let key = "Message.ForwardedPsa.\(psaType)" - if let string = presentationData.strings.primaryComponent.dict[key] { - customFormat = string - } else if let string = presentationData.strings.secondaryComponent?.dict[key] { - customFormat = string + if let storyData = storyData { + switch storyData.storyType { + case .regular: + titleString = PresentationStrings.FormattedString(string: presentationData.strings.Chat_MessageForwardInfo_StoryHeader, ranges: []) + authorString = peerString + case .expired: + titleString = PresentationStrings.FormattedString(string: presentationData.strings.Chat_MessageForwardInfo_ExpiredStoryHeader, ranges: []) + authorString = peerString + case .unavailable: + titleString = PresentationStrings.FormattedString(string: presentationData.strings.Chat_MessageForwardInfo_UnavailableStoryHeader, ranges: []) + authorString = peerString } - - if let customFormat = customFormat { - if let range = customFormat.range(of: "%@") { - let leftPart = String(customFormat[customFormat.startIndex ..< range.lowerBound]) - let rightPart = String(customFormat[range.upperBound...]) - - let formattedText = leftPart + peerString + rightPart - completeSourceString = PresentationStrings.FormattedString(string: formattedText, ranges: [PresentationStrings.FormattedString.Range(index: 0, range: NSRange(location: leftPart.count, length: peerString.count))]) - } else { - completeSourceString = PresentationStrings.FormattedString(string: customFormat, ranges: []) - } + } else { + titleString = PresentationStrings.FormattedString(string: presentationData.strings.Chat_MessageForwardInfo_MessageHeader, ranges: []) + authorString = peerString + } + } + case .standalone: + let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) + titleColor = serviceColor.primaryText + + if let psaType = psaType { + var customFormat: String? + let key = "Message.ForwardedPsa.\(psaType)" + if let string = presentationData.strings.primaryComponent.dict[key] { + customFormat = string + } else if let string = presentationData.strings.secondaryComponent?.dict[key] { + customFormat = string + } + + if let customFormat = customFormat { + if let range = customFormat.range(of: "%@") { + let leftPart = String(customFormat[customFormat.startIndex ..< range.lowerBound]) + let rightPart = String(customFormat[range.upperBound...]) + + let formattedText = leftPart + peerString + rightPart + titleString = PresentationStrings.FormattedString(string: formattedText, ranges: [PresentationStrings.FormattedString.Range(index: 0, range: NSRange(location: leftPart.count, length: peerString.count))]) } else { - completeSourceString = strings.Message_GenericForwardedPsa(peerString) + titleString = PresentationStrings.FormattedString(string: customFormat, ranges: []) } } else { - completeSourceString = strings.Message_ForwardedMessageShort(peerString) + titleString = strings.Message_GenericForwardedPsa(peerString) } + } else { + titleString = PresentationStrings.FormattedString(string: presentationData.strings.Chat_MessageForwardInfo_MessageHeader, ranges: []) + authorString = peerString + } } var currentCredibilityIconImage: UIImage? @@ -230,17 +326,17 @@ public class ChatMessageForwardInfoNode: ASDisplayNode { if peer.isFake { switch type { - case let .bubble(incoming): - currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(presentationData.theme.theme, strings: presentationData.strings, type: incoming ? .regular : .outgoing) - case .standalone: - currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(presentationData.theme.theme, strings: presentationData.strings, type: .service) + case let .bubble(incoming): + currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(presentationData.theme.theme, strings: presentationData.strings, type: incoming ? .regular : .outgoing) + case .standalone: + currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(presentationData.theme.theme, strings: presentationData.strings, type: .service) } } else if peer.isScam { switch type { - case let .bubble(incoming): - currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(presentationData.theme.theme, strings: presentationData.strings, type: incoming ? .regular : .outgoing) - case .standalone: - currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(presentationData.theme.theme, strings: presentationData.strings, type: .service) + case let .bubble(incoming): + currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(presentationData.theme.theme, strings: presentationData.strings, type: incoming ? .regular : .outgoing) + case .standalone: + currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(presentationData.theme.theme, strings: presentationData.strings, type: .service) } } else { currentCredibilityIconImage = nil @@ -249,10 +345,9 @@ public class ChatMessageForwardInfoNode: ASDisplayNode { highlight = false } - //let completeString: NSString = (completeSourceString.string.replacingOccurrences(of: "\n", with: " \n")) as NSString - let completeString: NSString = completeSourceString.string as NSString - let string = NSMutableAttributedString(string: completeString as String, attributes: [NSAttributedString.Key.foregroundColor: titleColor, NSAttributedString.Key.font: prefixFont]) - if highlight, let range = completeSourceString.ranges.first?.range { + let rawTitleString: NSString = titleString.string as NSString + let string = NSMutableAttributedString(string: rawTitleString as String, attributes: [NSAttributedString.Key.foregroundColor: titleColor, NSAttributedString.Key.font: prefixFont]) + if highlight, let range = titleString.ranges.first?.range { string.addAttributes([NSAttributedString.Key.font: peerFont], range: range) } @@ -278,9 +373,32 @@ public class ChatMessageForwardInfoNode: ASDisplayNode { } } - let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: constrainedSize.width - credibilityIconWidth - infoWidth, height: constrainedSize.height), alignment: .natural, cutout: cutout, insets: UIEdgeInsets())) + let (titleLayout, titleApply) = titleNodeLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: constrainedSize.width - credibilityIconWidth - infoWidth, height: constrainedSize.height), alignment: .natural, cutout: cutout, insets: UIEdgeInsets())) + + var authorAvatarInset: CGFloat = 0.0 + authorAvatarInset = 20.0 + + var nameLayoutAndApply: (TextNodeLayout, () -> TextNode)? + if let authorString { + nameLayoutAndApply = nameNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: authorString, font: peer != nil ? peerFont : prefixFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: constrainedSize.width - credibilityIconWidth - infoWidth - authorAvatarInset, height: constrainedSize.height), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + } + + let titleAuthorSpacing: CGFloat = 0.0 - return (CGSize(width: textLayout.size.width + credibilityIconWidth + infoWidth, height: textLayout.size.height), { width in + let resultSize: CGSize + if let nameLayoutAndApply { + resultSize = CGSize( + width: max( + titleLayout.size.width + credibilityIconWidth + infoWidth, + authorAvatarInset + nameLayoutAndApply.0.size.width + ), + height: titleLayout.size.height + titleAuthorSpacing + nameLayoutAndApply.0.size.height + ) + } else { + resultSize = CGSize(width: titleLayout.size.width + credibilityIconWidth + infoWidth, height: titleLayout.size.height) + } + + return (resultSize, { width in let node: ChatMessageForwardInfoNode if let maybeNode = maybeNode { node = maybeNode @@ -288,15 +406,65 @@ public class ChatMessageForwardInfoNode: ASDisplayNode { node = ChatMessageForwardInfoNode() } - let textNode = textApply() - textNode.displaysAsynchronously = !presentationData.isPreview + node.theme = presentationData.theme.theme + node.highlightColor = titleColor.withMultipliedAlpha(0.1) + + node.previousPeer = peer - if node.textNode == nil { - textNode.isUserInteractionEnabled = false - node.textNode = textNode - node.addSubnode(textNode) + let titleNode = titleApply() + titleNode.displaysAsynchronously = !presentationData.isPreview + + if node.titleNode == nil { + titleNode.isUserInteractionEnabled = false + node.titleNode = titleNode + node.addSubnode(titleNode) + } + titleNode.frame = CGRect(origin: CGPoint(x: leftOffset, y: 0.0), size: titleLayout.size) + + if let (nameLayout, nameApply) = nameLayoutAndApply { + let nameNode = nameApply() + if node.nameNode == nil { + nameNode.isUserInteractionEnabled = false + node.nameNode = nameNode + node.addSubnode(nameNode) + } + nameNode.frame = CGRect(origin: CGPoint(x: leftOffset + authorAvatarInset, y: titleLayout.size.height + titleAuthorSpacing), size: nameLayout.size) + + if authorAvatarInset != 0.0 { + let avatarNode: AvatarNode + if let current = node.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 8.0)) + node.avatarNode = avatarNode + node.addSubnode(avatarNode) + } + let avatarSize = CGSize(width: 16.0, height: 16.0) + avatarNode.frame = CGRect(origin: CGPoint(x: leftOffset, y: titleLayout.size.height + titleAuthorSpacing), size: avatarSize) + avatarNode.updateSize(size: avatarSize) + if let peer { + avatarNode.setPeer(context: context, theme: presentationData.theme.theme, peer: EnginePeer(peer), displayDimensions: avatarSize) + } else if let authorName, !authorName.isEmpty { + avatarNode.setCustomLetters([String(authorName[authorName.startIndex])]) + } else { + avatarNode.setCustomLetters([" "]) + } + } else { + if let avatarNode = node.avatarNode { + node.avatarNode = nil + avatarNode.removeFromSupernode() + } + } + } else { + if let nameNode = node.nameNode { + node.nameNode = nil + nameNode.removeFromSupernode() + } + if let avatarNode = node.avatarNode { + node.avatarNode = nil + avatarNode.removeFromSupernode() + } } - textNode.frame = CGRect(origin: CGPoint(x: leftOffset, y: 0.0), size: textLayout.size) if let storyData, case .expired = storyData.storyType { let expiredStoryIconView: UIImageView @@ -334,7 +502,7 @@ public class ChatMessageForwardInfoNode: ASDisplayNode { node.credibilityIconNode = credibilityIconNode node.addSubnode(credibilityIconNode) } - credibilityIconNode.frame = CGRect(origin: CGPoint(x: textLayout.size.width + 4.0, y: 16.0), size: credibilityIconImage.size) + credibilityIconNode.frame = CGRect(origin: CGPoint(x: titleLayout.size.width + 4.0, y: 16.0), size: credibilityIconImage.size) credibilityIconNode.image = credibilityIconImage } else { node.credibilityIconNode?.removeFromSupernode() diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift index 6874321c3c0..2d222805e02 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift @@ -893,14 +893,24 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco } } strongSelf.addSubnode(actionButtonsNode) + + if animation.isAnimated { + actionButtonsNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } } else { if case let .System(duration, _) = animation { actionButtonsNode.layer.animateFrame(from: previousFrame, to: actionButtonsFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) } } } else if let actionButtonsNode = strongSelf.actionButtonsNode { - actionButtonsNode.removeFromSupernode() strongSelf.actionButtonsNode = nil + if animation.isAnimated { + actionButtonsNode.layer.animateAlpha(from: actionButtonsNode.alpha, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + actionButtonsNode.removeFromSupernode() + }) + } else { + actionButtonsNode.removeFromSupernode() + } } } @@ -1253,8 +1263,8 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco self.layer.removeAllAnimations() } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - super.animateInsertion(currentTimestamp, duration: duration, short: short) + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + super.animateInsertion(currentTimestamp, duration: duration, options: options) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift index 7c11d7204f4..d5d6de10ccb 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift @@ -391,10 +391,10 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { let tipController = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_voiceToText", scale: 0.065, colors: [:], title: nil, text: presentationData.strings.Message_AudioTranscription_SubscribeToPremium, customUndoText: presentationData.strings.Message_AudioTranscription_SubscribeToPremiumAction, timeout: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { action in if case .undo = action { var replaceImpl: ((ViewController) -> Void)? - let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, action: { + let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, forceDark: false, action: { let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil) replaceImpl?(controller) - }) + }, dismissed: nil) replaceImpl = { [weak controller] c in controller?.replace(with: c) } @@ -665,10 +665,10 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { let tipController = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "Transcribe", scale: 0.06, colors: [:], title: nil, text: text, customUndoText: nil, timeout: timeout), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { action in if case .info = action { var replaceImpl: ((ViewController) -> Void)? - let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, action: { + let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, forceDark: false, action: { let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil) replaceImpl?(controller) - }) + }, dismissed: nil) replaceImpl = { [weak controller] c in controller?.replace(with: c) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index e10b3d40c74..b6ab4de49f7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -1829,10 +1829,10 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { if case .undo = action { let context = item.context var replaceImpl: ((ViewController) -> Void)? - let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, action: { + let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, forceDark: false, action: { let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil) replaceImpl?(controller) - }) + }, dismissed: nil) replaceImpl = { [weak controller] c in controller?.replace(with: c) } @@ -1941,10 +1941,10 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { let tipController = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "Transcribe", scale: 0.06, colors: [:], title: nil, text: text, customUndoText: nil, timeout: timeout), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { action in if case .info = action { var replaceImpl: ((ViewController) -> Void)? - let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, action: { + let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, forceDark: false, action: { let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil) replaceImpl?(controller) - }) + }, dismissed: nil) replaceImpl = { [weak controller] c in controller?.replace(with: c) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift index ac098658ff3..2a856c79054 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItem/Sources/ChatMessageItem.swift @@ -83,6 +83,7 @@ public enum ChatMessageItemAdditionalContent { case eventLogPreviousMessage(Message) case eventLogPreviousDescription(Message) case eventLogPreviousLink(Message) + case eventLogGroupedMessages([Message], Bool) } public enum ChatMessageMerge: Int32 { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift index 861ba8756c8..c45fdab34e0 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift @@ -487,7 +487,10 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible Queue.mainQueue().async { completion(node, { - return (nil, { _ in apply(.None, ListViewItemApply(isOnScreen: false), synchronousLoads) }) + return (nil, { info in + info.setIsOffscreen() + apply(.None, info, synchronousLoads) + }) }) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatReplyCountItem.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatReplyCountItem.swift index e16d544a70f..aa9b14c971b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatReplyCountItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatReplyCountItem.swift @@ -91,8 +91,8 @@ public class ChatReplyCountItemNode: ListViewItemNode { self.canBeUsedAsScrollToItemAnchor = false } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - super.animateInsertion(currentTimestamp, duration: duration, short: short) + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + super.animateInsertion(currentTimestamp, duration: duration, options: options) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatUnreadItem.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatUnreadItem.swift index 95604423a56..28e53f9b3ac 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatUnreadItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatUnreadItem.swift @@ -108,8 +108,8 @@ public class ChatUnreadItemNode: ListViewItemNode { self.canBeUsedAsScrollToItemAnchor = false } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - super.animateInsertion(currentTimestamp, duration: duration, short: short) + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + super.animateInsertion(currentTimestamp, duration: duration, options: options) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift index c4356309f2e..fc902d7b49b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift @@ -697,11 +697,11 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { open func cancelInsertionAnimations() { } - override open func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - if short { + override open func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + if options.short { //self.layer.animateBoundsOriginYAdditive(from: -self.bounds.size.height, to: 0.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) } else { - self.transitionOffset = -self.bounds.size.height * 1.6 + self.transitionOffset = options.invertOffsetDirection ? self.bounds.size.height * 1.4 : -self.bounds.size.height * 1.6 self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift index c1f43229378..b5a1394c367 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift @@ -85,7 +85,7 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { selectedMedia = telegramMap if let liveBroadcastingTimeout = telegramMap.liveBroadcastingTimeout { let timestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) - if item.message.timestamp != scheduleWhenOnlineTimestamp && item.message.timestamp + liveBroadcastingTimeout > timestamp { + if item.message.timestamp != scheduleWhenOnlineTimestamp && (liveBroadcastingTimeout == liveLocationIndefinitePeriod || item.message.timestamp + liveBroadcastingTimeout > timestamp) { activeLiveBroadcastingTimeout = liveBroadcastingTimeout } } @@ -402,7 +402,7 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { let timerForegroundColor: UIColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.accentControlColor : item.presentationData.theme.theme.chat.message.outgoing.accentControlColor let timerTextColor: UIColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.secondaryTextColor : item.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor - strongSelf.liveTimerNode?.update(backgroundColor: timerForegroundColor.withAlphaComponent(0.4), foregroundColor: timerForegroundColor, textColor: timerTextColor, beginTimestamp: Double(item.message.timestamp), timeout: Double(activeLiveBroadcastingTimeout), strings: item.presentationData.strings) + strongSelf.liveTimerNode?.update(backgroundColor: timerForegroundColor.withAlphaComponent(0.4), foregroundColor: timerForegroundColor, textColor: timerTextColor, beginTimestamp: Double(item.message.timestamp), timeout: activeLiveBroadcastingTimeout == liveLocationIndefinitePeriod ? -1.0 : Double(activeLiveBroadcastingTimeout), strings: item.presentationData.strings) if strongSelf.liveTextNode == nil { let liveTextNode = ChatMessageLiveLocationTextNode() @@ -421,20 +421,25 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.liveTextNode?.update(color: timerTextColor, timestamp: Double(updateTimestamp), strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat) - let timeoutDeadline = item.message.timestamp + activeLiveBroadcastingTimeout - if strongSelf.timeoutTimer?.1 != timeoutDeadline { + if activeLiveBroadcastingTimeout != liveLocationIndefinitePeriod { + let timeoutDeadline = item.message.timestamp + activeLiveBroadcastingTimeout + if strongSelf.timeoutTimer?.1 != timeoutDeadline { + strongSelf.timeoutTimer?.0.invalidate() + let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + + let timer = SwiftSignalKit.Timer(timeout: Double(max(0, timeoutDeadline - currentTimestamp)), repeat: false, completion: { + if let strongSelf = self { + strongSelf.timeoutTimer?.0.invalidate() + strongSelf.timeoutTimer = nil + item.controllerInteraction.requestMessageUpdate(item.message.id, false) + } + }, queue: Queue.mainQueue()) + strongSelf.timeoutTimer = (timer, timeoutDeadline) + timer.start() + } + } else { strongSelf.timeoutTimer?.0.invalidate() - let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) - - let timer = SwiftSignalKit.Timer(timeout: Double(max(0, timeoutDeadline - currentTimestamp)), repeat: false, completion: { - if let strongSelf = self { - strongSelf.timeoutTimer?.0.invalidate() - strongSelf.timeoutTimer = nil - item.controllerInteraction.requestMessageUpdate(item.message.id, false) - } - }, queue: Queue.mainQueue()) - strongSelf.timeoutTimer = (timer, timeoutDeadline) - timer.start() + strongSelf.timeoutTimer = nil } } else { if let liveTimerNode = strongSelf.liveTimerNode { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/BUILD index cbea42ae83c..7a038fc1013 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/BUILD @@ -26,6 +26,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", "//submodules/TelegramUI/Components/Chat/PollBubbleTimerNode", "//submodules/TelegramUI/Components/Chat/MergedAvatarsNode", + "//submodules/TelegramUI/Components/TextNodeWithEntities", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index 940ca9feb7b..d6bfff73f43 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -16,6 +16,7 @@ import ChatMessageBubbleContentNode import ChatMessageItemCommon import PollBubbleTimerNode import MergedAvatarsNode +import TextNodeWithEntities private final class ChatMessagePollOptionRadioNodeParameters: NSObject { let timestamp: Double @@ -386,7 +387,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { private(set) var radioNode: ChatMessagePollOptionRadioNode? private let percentageNode: ASDisplayNode private var percentageImage: UIImage? - private var titleNode: TextNode? + private var titleNode: TextNodeWithEntities? private let buttonNode: HighlightTrackingButtonNode let separatorNode: ASDisplayNode private let resultBarNode: ASImageNode @@ -400,6 +401,20 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { weak var previousOptionNode: ChatMessagePollOptionNode? + var visibilityRect: CGRect? { + didSet { + if self.visibilityRect != oldValue { + if let titleNode = self.titleNode { + if let visibilityRect = self.visibilityRect { + titleNode.visibilityRect = visibilityRect.offsetBy(dx: 0.0, dy: titleNode.textNode.frame.minY) + } else { + titleNode.visibilityRect = nil + } + } + } + } + } + override init() { self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode.alpha = 0.0 @@ -476,19 +491,35 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { } } - static func asyncLayout(_ maybeNode: ChatMessagePollOptionNode?) -> (_ accountPeerId: PeerId, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollOptionNode))) { - let makeTitleLayout = TextNode.asyncLayout(maybeNode?.titleNode) + static func asyncLayout(_ maybeNode: ChatMessagePollOptionNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode))) { + let makeTitleLayout = TextNodeWithEntities.asyncLayout(maybeNode?.titleNode) let currentResult = maybeNode?.currentResult let currentSelection = maybeNode?.currentSelection let currentTheme = maybeNode?.theme - return { accountPeerId, presentationData, message, poll, option, optionResult, constrainedWidth in + return { context, presentationData, message, poll, option, optionResult, constrainedWidth in let leftInset: CGFloat = 50.0 let rightInset: CGFloat = 12.0 - let incoming = message.effectivelyIncoming(accountPeerId) + let incoming = message.effectivelyIncoming(context.account.peerId) + + let optionTextColor: UIColor = incoming ? presentationData.theme.theme.chat.message.incoming.primaryTextColor : presentationData.theme.theme.chat.message.outgoing.primaryTextColor + let optionAttributedText = stringWithAppliedEntities( + option.text, + entities: option.entities, + baseColor: optionTextColor, + linkColor: optionTextColor, + baseFont: presentationData.messageFont, + linkFont: presentationData.messageFont, + boldFont: presentationData.messageFont, + italicFont: presentationData.messageFont, + boldItalicFont: presentationData.messageFont, + fixedFont: presentationData.messageFont, + blockQuoteFont: presentationData.messageFont, + message: message + ) - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: option.text, font: presentationData.messageFont, textColor: incoming ? presentationData.theme.theme.chat.message.incoming.primaryTextColor : presentationData.theme.theme.chat.message.outgoing.primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: max(1.0, constrainedWidth - leftInset - rightInset), height: CGFloat.greatestFiniteMagnitude), alignment: .left, cutout: nil, insets: UIEdgeInsets(top: 1.0, left: 0.0, bottom: 1.0, right: 0.0))) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: optionAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: max(1.0, constrainedWidth - leftInset - rightInset), height: CGFloat.greatestFiniteMagnitude), alignment: .left, cutout: nil, insets: UIEdgeInsets(top: 1.0, left: 0.0, bottom: 1.0, right: 0.0))) let contentHeight: CGFloat = max(46.0, titleLayout.size.height + 22.0) @@ -578,7 +609,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { } return (titleLayout.size.width + leftInset + rightInset, { width in - return (CGSize(width: width, height: contentHeight), { animated, inProgress in + return (CGSize(width: width, height: contentHeight), { animated, inProgress, attemptSynchronous in let node: ChatMessagePollOptionNode if let maybeNode = maybeNode { node = maybeNode @@ -596,17 +627,29 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { node.buttonNode.accessibilityLabel = option.text - let titleNode = titleApply() - if node.titleNode !== titleNode { - node.titleNode = titleNode - node.addSubnode(titleNode) - titleNode.isUserInteractionEnabled = false - } + let titleNode = titleApply(TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: incoming ? presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor, + attemptSynchronous: attemptSynchronous + )) + let titleNodeFrame: CGRect if titleLayout.hasRTL { - titleNode.frame = CGRect(origin: CGPoint(x: width - rightInset - titleLayout.size.width, y: 11.0), size: titleLayout.size) + titleNodeFrame = CGRect(origin: CGPoint(x: width - rightInset - titleLayout.size.width, y: 11.0), size: titleLayout.size) } else { - titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size) + titleNodeFrame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size) + } + if node.titleNode !== titleNode { + node.titleNode = titleNode + node.addSubnode(titleNode.textNode) + titleNode.textNode.isUserInteractionEnabled = false + + if let visibilityRect = node.visibilityRect { + titleNode.visibilityRect = visibilityRect.offsetBy(dx: 0.0, dy: titleNodeFrame.minY) + } } + titleNode.textNode.frame = titleNodeFrame if shouldHaveRadioNode { let radioNode: ChatMessagePollOptionRadioNode @@ -773,7 +816,7 @@ private final class SolutionButtonNode: HighlightableButtonNode { } public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { - private let textNode: TextNode + private let textNode: TextNodeWithEntities private let typeNode: TextNode private var timerNode: PollBubbleTimerNode? private let solutionButtonNode: SolutionButtonNode @@ -792,12 +835,34 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { return self.solutionButtonNode } + override public var visibility: ListViewItemNodeVisibility { + didSet { + if oldValue != self.visibility { + switch self.visibility { + case .none: + self.textNode.visibilityRect = nil + for optionNode in self.optionNodes { + optionNode.visibilityRect = nil + } + case let .visible(_, subRect): + var subRect = subRect + subRect.origin.x = 0.0 + subRect.size.width = 10000.0 + self.textNode.visibilityRect = subRect.offsetBy(dx: 0.0, dy: -self.textNode.textNode.frame.minY) + for optionNode in self.optionNodes { + optionNode.visibilityRect = subRect.offsetBy(dx: 0.0, dy: -optionNode.frame.minY) + } + } + } + } + } + required public init() { - self.textNode = TextNode() - self.textNode.isUserInteractionEnabled = false - self.textNode.contentMode = .topLeft - self.textNode.contentsScale = UIScreenScale - self.textNode.displaysAsynchronously = false + self.textNode = TextNodeWithEntities() + self.textNode.textNode.isUserInteractionEnabled = false + self.textNode.textNode.contentMode = .topLeft + self.textNode.textNode.contentsScale = UIScreenScale + self.textNode.textNode.displaysAsynchronously = false self.typeNode = TextNode() self.typeNode.isUserInteractionEnabled = false @@ -844,7 +909,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { super.init() - self.addSubnode(self.textNode) + self.addSubnode(self.textNode.textNode) self.addSubnode(self.typeNode) self.addSubnode(self.avatarsNode) self.addSubnode(self.votersNode) @@ -914,7 +979,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { - let makeTextLayout = TextNode.asyncLayout(self.textNode) + let makeTextLayout = TextNodeWithEntities.asyncLayout(self.textNode) let makeTypeLayout = TextNode.asyncLayout(self.typeNode) let makeVotersLayout = TextNode.asyncLayout(self.votersNode) let makeSubmitInactiveTextLayout = TextNode.asyncLayout(self.buttonSubmitInactiveTextNode) @@ -931,7 +996,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } - var previousOptionNodeLayouts: [Data: (_ accountPeerId: PeerId, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollOptionNode)))] = [:] + var previousOptionNodeLayouts: [Data: (_ contet: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode)))] = [:] for optionNode in self.optionNodes { if let option = optionNode.option { previousOptionNodeLayouts[option.opaqueIdentifier] = ChatMessagePollOptionNode.asyncLayout(optionNode) @@ -1049,7 +1114,25 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { let messageTheme = incoming ? item.presentationData.theme.theme.chat.message.incoming : item.presentationData.theme.theme.chat.message.outgoing - let attributedText = NSAttributedString(string: poll?.text ?? "", font: item.presentationData.messageBoldFont, textColor: messageTheme.primaryTextColor) + let attributedText: NSAttributedString + if let poll { + attributedText = stringWithAppliedEntities( + poll.text, + entities: poll.textEntities, + baseColor: messageTheme.primaryTextColor, + linkColor: messageTheme.linkTextColor, + baseFont: item.presentationData.messageBoldFont, + linkFont: item.presentationData.messageBoldFont, + boldFont: item.presentationData.messageBoldFont, + italicFont: item.presentationData.messageBoldFont, + boldItalicFont: item.presentationData.messageBoldFont, + fixedFont: item.presentationData.messageBoldFont, + blockQuoteFont: item.presentationData.messageBoldFont, + message: message + ) + } else { + attributedText = NSAttributedString(string: "", font: item.presentationData.messageBoldFont, textColor: messageTheme.primaryTextColor) + } let textInsets = UIEdgeInsets(top: 2.0, left: 0.0, bottom: 5.0, right: 0.0) @@ -1144,7 +1227,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { isClosed = false } - var pollOptionsFinalizeLayouts: [(CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollOptionNode)] = [] + var pollOptionsFinalizeLayouts: [(CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode)] = [] if let poll = poll { var optionVoterCount: [Int: Int32] = [:] var maxOptionVoterCount: Int32 = 0 @@ -1186,7 +1269,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { for i in 0 ..< poll.options.count { let option = poll.options[i] - let makeLayout: (_ accountPeerId: PeerId, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollOptionNode))) + let makeLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode))) if let previous = previousOptionNodeLayouts[option.opaqueIdentifier] { makeLayout = previous } else { @@ -1202,7 +1285,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } else if isClosed { optionResult = ChatMessagePollOptionResult(normalized: 0, percent: 0, count: 0) } - let result = makeLayout(item.context.account.peerId, item.presentationData, item.message, poll, option, optionResult, constrainedSize.width - layoutConstants.bubble.borderInset * 2.0) + let result = makeLayout(item.context, item.presentationData, item.message, poll, option, optionResult, constrainedSize.width - layoutConstants.bubble.borderInset * 2.0) boundingSize.width = max(boundingSize.width, result.minimumWidth + layoutConstants.bubble.borderInset * 2.0) pollOptionsFinalizeLayouts.append(result.1) } @@ -1233,7 +1316,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { let typeOptionsSpacing: CGFloat = 3.0 resultSize.height += titleTypeSpacing + typeLayout.size.height + typeOptionsSpacing - var optionNodesSizesAndApply: [(CGSize, (Bool, Bool) -> ChatMessagePollOptionNode)] = [] + var optionNodesSizesAndApply: [(CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode)] = [] for finalizeLayout in pollOptionsFinalizeLayouts { let result = finalizeLayout(boundingWidth - layoutConstants.bubble.borderInset * 2.0) resultSize.width = max(resultSize.width, result.0.width + layoutConstants.bubble.borderInset * 2.0) @@ -1268,28 +1351,13 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.item = item strongSelf.poll = poll - let cachedLayout = strongSelf.textNode.cachedLayout - - if case .System = animation { - if let cachedLayout = cachedLayout { - if cachedLayout != textLayout { - if let textContents = strongSelf.textNode.contents { - let fadeNode = ASDisplayNode() - fadeNode.displaysAsynchronously = false - fadeNode.contents = textContents - fadeNode.frame = strongSelf.textNode.frame - fadeNode.isLayerBacked = true - strongSelf.addSubnode(fadeNode) - fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak fadeNode] _ in - fadeNode?.removeFromSupernode() - }) - strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - } - } - } - } - - let _ = textApply() + let _ = textApply(TextNodeWithEntities.Arguments( + context: item.context, + cache: item.context.animationCache, + renderer: item.context.animationRenderer, + placeholderColor: incoming ? item.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : item.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor, + attemptSynchronous: synchronousLoad) + ) let _ = typeApply() var verticalOffset = textFrame.maxY + titleTypeSpacing + typeLayout.size.height + typeOptionsSpacing @@ -1302,7 +1370,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { isRequesting = inProgressOpaqueIds.contains(poll.options[i].opaqueIdentifier) } } - let optionNode = apply(animation.isAnimated, isRequesting) + let optionNode = apply(animation.isAnimated, isRequesting, synchronousLoad) if optionNode.supernode !== strongSelf { strongSelf.addSubnode(optionNode) let option = optionNode.option @@ -1337,9 +1405,9 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.optionNodes = updatedOptionNodes if textLayout.hasRTL { - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: resultSize.width - textFrame.size.width - textInsets.left - layoutConstants.text.bubbleInsets.right - additionalTextRightInset, y: textFrame.origin.y), size: textFrame.size) + strongSelf.textNode.textNode.frame = CGRect(origin: CGPoint(x: resultSize.width - textFrame.size.width - textInsets.left - layoutConstants.text.bubbleInsets.right - additionalTextRightInset, y: textFrame.origin.y), size: textFrame.size) } else { - strongSelf.textNode.frame = textFrame + strongSelf.textNode.textNode.frame = textFrame } let typeFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: textFrame.maxY + titleTypeSpacing), size: typeLayout.size) strongSelf.typeNode.frame = typeFrame @@ -1599,26 +1667,26 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { - self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } override public func animateAdded(_ currentTimestamp: Double, duration: Double) { - self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { - self.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.textNode.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) self.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { - let textNodeFrame = self.textNode.frame - if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + let textNodeFrame = self.textNode.textNode.frame + if let (index, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { var concealed = true - if let (attributeText, fullText) = self.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { + if let (attributeText, fullText) = self.textNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) } return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed))) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index a58983e228f..f538fe4e492 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -58,6 +58,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { private var replyBackgroundContent: WallpaperBubbleBackgroundNode? private var forwardInfoNode: ChatMessageForwardInfoNode? private var forwardBackgroundContent: WallpaperBubbleBackgroundNode? + private var forwardBackgroundMaskNode: LinkHighlightingNode? private var actionButtonsNode: ChatMessageActionButtonsNode? private var reactionButtonsNode: ChatMessageReactionButtonsNode? @@ -1110,7 +1111,9 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { var forwardBackgroundFrame: CGRect? if let forwardAreaFrame { - forwardBackgroundFrame = forwardAreaFrame.insetBy(dx: -6.0, dy: -3.0) + var forwardBackgroundFrameValue = forwardAreaFrame.insetBy(dx: -6.0, dy: -3.0) + forwardBackgroundFrameValue.size.height += 2.0 + forwardBackgroundFrame = forwardBackgroundFrameValue } var replyBackgroundFrame: CGRect? @@ -1147,7 +1150,17 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { } if let backgroundContent = strongSelf.forwardBackgroundContent, let forwardBackgroundFrame { - backgroundContent.cornerRadius = 4.0 + let forwardBackgroundMaskNode: LinkHighlightingNode + if let current = strongSelf.forwardBackgroundMaskNode { + forwardBackgroundMaskNode = current + } else { + forwardBackgroundMaskNode = LinkHighlightingNode(color: .black) + forwardBackgroundMaskNode.inset = 4.0 + forwardBackgroundMaskNode.outerRadius = 12.0 + strongSelf.forwardBackgroundMaskNode = forwardBackgroundMaskNode + backgroundContent.view.mask = forwardBackgroundMaskNode.view + } + backgroundContent.frame = forwardBackgroundFrame if let (rect, containerSize) = strongSelf.absoluteRect { var backgroundFrame = backgroundContent.frame @@ -1155,6 +1168,28 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { backgroundFrame.origin.y += rect.minY backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) } + + if let forwardInfoNode = strongSelf.forwardInfoNode { + forwardBackgroundMaskNode.frame = backgroundContent.bounds.offsetBy(dx: forwardInfoNode.frame.minX - backgroundContent.frame.minX, dy: forwardInfoNode.frame.minY - backgroundContent.frame.minY) + var backgroundRects = forwardInfoNode.getBoundingRects() + for i in 0 ..< backgroundRects.count { + backgroundRects[i].origin.x -= 2.0 + backgroundRects[i].size.width += 4.0 + } + for i in 0 ..< backgroundRects.count { + if i != 0 { + if abs(backgroundRects[i - 1].maxX - backgroundRects[i].maxX) < 16.0 { + let maxMaxX = max(backgroundRects[i - 1].maxX, backgroundRects[i].maxX) + backgroundRects[i - 1].size.width = max(0.0, maxMaxX - backgroundRects[i - 1].origin.x) + backgroundRects[i].size.width = max(0.0, maxMaxX - backgroundRects[i].origin.x) + } + } + } + forwardBackgroundMaskNode.updateRects(backgroundRects) + } + } else if let forwardBackgroundMaskNode = strongSelf.forwardBackgroundMaskNode { + strongSelf.forwardBackgroundMaskNode = nil + forwardBackgroundMaskNode.view.removeFromSuperview() } let panelsAlpha: CGFloat = item.controllerInteraction.selectionState == nil ? 1.0 : 0.0 @@ -1214,14 +1249,24 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { } } strongSelf.addSubnode(actionButtonsNode) + + if animation.isAnimated { + actionButtonsNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } } else { if case let .System(duration, _) = animation { actionButtonsNode.layer.animateFrame(from: previousFrame, to: actionButtonsFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) } } } else if let actionButtonsNode = strongSelf.actionButtonsNode { - actionButtonsNode.removeFromSupernode() strongSelf.actionButtonsNode = nil + if animation.isAnimated { + actionButtonsNode.layer.animateAlpha(from: actionButtonsNode.alpha, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + actionButtonsNode.removeFromSupernode() + }) + } else { + actionButtonsNode.removeFromSupernode() + } } if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply { @@ -1754,8 +1799,8 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { self.layer.removeAllAnimations() } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - super.animateInsertion(currentTimestamp, duration: duration, short: short) + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + super.animateInsertion(currentTimestamp, duration: duration, options: options) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageThreadInfoNode/Sources/ChatMessageThreadInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageThreadInfoNode/Sources/ChatMessageThreadInfoNode.swift index 1fab74cb24c..f8f4523fd81 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageThreadInfoNode/Sources/ChatMessageThreadInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageThreadInfoNode/Sources/ChatMessageThreadInfoNode.swift @@ -481,8 +481,12 @@ public class ChatMessageThreadInfoNode: ASDisplayNode { } let titleTopicIconContent: EmojiStatusComponent.Content + var containerSize: CGSize = CGSize(width: 22.0, height: 22.0) + var iconX: CGFloat = 0.0 if arguments.threadId == 1 { titleTopicIconContent = .image(image: generalThreadIcon) + containerSize = CGSize(width: 18.0, height: 18.0) + iconX = 3.0 } else if let fileId = topicIconId, fileId != 0 { titleTopicIconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 36.0, height: 36.0), placeholderColor: arguments.presentationData.theme.theme.list.mediaPlaceholderColor, themeColor: arguments.presentationData.theme.theme.list.itemAccentColor, loopMode: .count(1)) } else { @@ -504,7 +508,7 @@ public class ChatMessageThreadInfoNode: ASDisplayNode { transition: .immediate, component: AnyComponent(titleTopicIconComponent), environment: {}, - containerSize: CGSize(width: 22.0, height: 22.0) + containerSize: containerSize ) let iconY: CGFloat @@ -514,7 +518,7 @@ public class ChatMessageThreadInfoNode: ASDisplayNode { iconY = 0.0 } - titleTopicIconView.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top + iconY), size: iconSize) + titleTopicIconView.frame = CGRect(origin: CGPoint(x: insets.left + iconX, y: insets.top + iconY), size: iconSize) } let textFrame = CGRect(origin: CGPoint(x: iconSize.width + 2.0 + insets.left, y: insets.top), size: textLayout.size) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift index cf5f1c7dcc5..e05cc37df0a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -315,7 +315,7 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent var colors: [UInt32] = [] var rotation: Int32? var intensity: Int32? - if let wallpaper = parseWallpaperUrl(webpage.url), case let .slug(_, _, colorsValue, intensityValue, rotationValue) = wallpaper { + if let wallpaper = parseWallpaperUrl(sharedContext: item.context.sharedContext, url: webpage.url), case let .slug(_, _, colorsValue, intensityValue, rotationValue) = wallpaper { colors = colorsValue rotation = rotationValue intensity = intensityValue @@ -353,7 +353,7 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent if type == "telegram_background" { var colors: [UInt32] = [] var rotation: Int32? - if let wallpaper = parseWallpaperUrl(webpage.url) { + if let wallpaper = parseWallpaperUrl(sharedContext: item.context.sharedContext, url: webpage.url) { if case let .color(color) = wallpaper { colors = [color.rgb] } else if case let .gradient(colorsValue, rotationValue) = wallpaper { diff --git a/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/BUILD b/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/BUILD index f7164086390..6596b91582f 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 8bdcac97124..629a1bcea3e 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,8 @@ final class OverscrollContentsComponent: Component { let backgroundColor: UIColor let foregroundColor: UIColor let peer: EnginePeer? + let threadData: ChatOverscrollThreadData? + let isForumThread: Bool let unreadCount: Int let location: TelegramEngine.NextUnreadChannelLocation let expandOffset: CGFloat @@ -679,6 +766,8 @@ final class OverscrollContentsComponent: Component { backgroundColor: UIColor, foregroundColor: UIColor, peer: EnginePeer?, + threadData: ChatOverscrollThreadData?, + isForumThread: Bool, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation, expandOffset: CGFloat, @@ -691,6 +780,8 @@ final class OverscrollContentsComponent: Component { self.backgroundColor = backgroundColor self.foregroundColor = foregroundColor self.peer = peer + self.threadData = threadData + self.isForumThread = isForumThread self.unreadCount = unreadCount self.location = location self.expandOffset = expandOffset @@ -713,6 +804,12 @@ final class OverscrollContentsComponent: Component { if lhs.peer != rhs.peer { return false } + if lhs.threadData != rhs.threadData { + return false + } + if lhs.isForumThread != rhs.isForumThread { + return false + } if lhs.unreadCount != rhs.unreadCount { return false } @@ -870,8 +967,12 @@ 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 if component.isForumThread { + titleText = component.context.sharedContext.currentPresentationData.with({ $0 }).strings.Chat_NavigationNoTopics } else { titleText = component.context.sharedContext.currentPresentationData.with({ $0 }).strings.Chat_NavigationNoChannels } @@ -949,6 +1050,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 +1090,8 @@ public final class ChatOverscrollControl: CombinedComponent { let backgroundColor: UIColor let foregroundColor: UIColor let peer: EnginePeer? + let threadData: ChatOverscrollThreadData? + let isForumThread: Bool let unreadCount: Int let location: TelegramEngine.NextUnreadChannelLocation let context: AccountContext @@ -1001,6 +1105,8 @@ public final class ChatOverscrollControl: CombinedComponent { backgroundColor: UIColor, foregroundColor: UIColor, peer: EnginePeer?, + threadData: ChatOverscrollThreadData?, + isForumThread: Bool, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation, context: AccountContext, @@ -1013,6 +1119,8 @@ public final class ChatOverscrollControl: CombinedComponent { self.backgroundColor = backgroundColor self.foregroundColor = foregroundColor self.peer = peer + self.threadData = threadData + self.isForumThread = isForumThread self.unreadCount = unreadCount self.location = location self.context = context @@ -1033,6 +1141,12 @@ public final class ChatOverscrollControl: CombinedComponent { if lhs.peer != rhs.peer { return false } + if lhs.threadData != rhs.threadData { + return false + } + if lhs.isForumThread != rhs.isForumThread { + return false + } if lhs.unreadCount != rhs.unreadCount { return false } @@ -1070,6 +1184,8 @@ public final class ChatOverscrollControl: CombinedComponent { backgroundColor: context.component.backgroundColor, foregroundColor: context.component.foregroundColor, peer: context.component.peer, + threadData: context.component.threadData, + isForumThread: context.component.isForumThread, unreadCount: context.component.unreadCount, location: context.component.location, expandOffset: context.component.expandDistance, diff --git a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift index 0ce5e17bc6e..7512ef03f89 100644 --- a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift @@ -542,8 +542,8 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - super.animateInsertion(currentTimestamp, duration: duration, short: short) + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + super.animateInsertion(currentTimestamp, duration: duration, options: options) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/BUILD b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/BUILD index 6869a588736..e826f371a24 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/BUILD @@ -17,6 +17,7 @@ swift_library( "//submodules/TelegramUI/Components/ChatControllerInteraction", "//submodules/TelegramUI/Components/Chat/ChatHistoryEntry", "//submodules/TelegramUI/Components/Chat/ChatMessageItemView", + "//submodules/TelegramUI/Components/Chat/ChatMessageItem", "//submodules/TelegramUI/Components/Chat/ChatMessageItemImpl", "//submodules/TelegramUI/Components/Chat/ChatNavigationButton", "//submodules/TelegramUI/Components/Chat/ChatLoadingNode", @@ -48,6 +49,8 @@ swift_library( "//submodules/UndoUI", "//submodules/WallpaperBackgroundNode", "//submodules/TextFormat", + "//submodules/CounterControllerTitleView", + "//submodules/TelegramUI/Components/AdminUserActionsSheet", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift index 89d92a97e90..3f766b0913e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift @@ -11,6 +11,8 @@ import AlertUI import PresentationDataUtils import ChatPresentationInterfaceState import ChatNavigationButton +import CounterControllerTitleView +import AdminUserActionsSheet public final class ChatRecentActionsController: TelegramBaseController { private var controllerNode: ChatRecentActionsControllerNode { @@ -28,10 +30,12 @@ public final class ChatRecentActionsController: TelegramBaseController { private var presentationDataDisposable: Disposable? private var didSetPresentationData = false - private var interaction: ChatRecentActionsInteraction! private var panelInteraction: ChatPanelInterfaceInteraction! - private let titleView: ChatRecentActionsTitleView + private let titleView: CounterControllerTitleView + private var rightBarButton: ChatNavigationButton? + + private var adminsDisposable: Disposable? public init(context: AccountContext, peer: Peer, adminPeerId: PeerId?) { self.context = context @@ -40,7 +44,7 @@ public final class ChatRecentActionsController: TelegramBaseController { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - self.titleView = ChatRecentActionsTitleView(color: self.presentationData.theme.rootController.navigationBar.primaryTextColor) + self.titleView = CounterControllerTitleView(theme: self.presentationData.theme) super.init(context: context, navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData), mediaAccessoryPanelVisibility: .specific(size: .compact), locationBroadcastPanelSource: .none, groupCallPanelSource: .none) @@ -48,18 +52,6 @@ public final class ChatRecentActionsController: TelegramBaseController { self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style - self.interaction = ChatRecentActionsInteraction(displayInfoAlert: { [weak self] in - if let strongSelf = self { - let text: String - if let channel = peer as? TelegramChannel, case .broadcast = channel.info { - text = strongSelf.presentationData.strings.Channel_AdminLog_InfoPanelChannelAlertText - } else { - text = strongSelf.presentationData.strings.Channel_AdminLog_InfoPanelAlertText - } - self?.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: strongSelf.presentationData.strings.Channel_AdminLog_InfoPanelAlertTitle, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - } - }) - // MARK: Nicegram (cloudMessages + copyForwardMessages + copySelectedMessages) self.panelInteraction = ChatPanelInterfaceInteraction(cloudMessages: { _ in }, copyForwardMessages: { _ in }, copySelectedMessages: { }, setupReplyMessage: { _, _ in @@ -183,13 +175,10 @@ public final class ChatRecentActionsController: TelegramBaseController { self.navigationItem.titleView = self.titleView - let rightButton = ChatNavigationButton(action: .search(hasTags: false), buttonItem: UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.activateSearch))) - self.navigationItem.setRightBarButton(rightButton.buttonItem, animated: false) + let rightBarButton = ChatNavigationButton(action: .search(hasTags: false), buttonItem: UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.activateSearch))) + self.rightBarButton = rightBarButton - self.titleView.title = self.presentationData.strings.Channel_AdminLog_TitleAllEvents - self.titleView.pressed = { [weak self] in - self?.openFilterSetup() - } + self.titleView.title = CounterControllerTitle(title: EnginePeer(peer).compactDisplayTitle, counter: self.presentationData.strings.Channel_AdminLog_TitleAllEvents) let themeEmoticon = self.context.account.postbox.peerView(id: peer.id) |> map { view -> String? in @@ -237,10 +226,11 @@ public final class ChatRecentActionsController: TelegramBaseController { deinit { self.presentationDataDisposable?.dispose() + self.adminsDisposable?.dispose() } private func updateThemeAndStrings() { - self.titleView.color = self.presentationData.theme.rootController.navigationBar.primaryTextColor + self.titleView.theme = self.presentationData.theme self.updateTitle() let rightButton = ChatNavigationButton(action: .search(hasTags: false), buttonItem: UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.activateSearch))) @@ -253,14 +243,19 @@ public final class ChatRecentActionsController: TelegramBaseController { } override public func loadDisplayNode() { - self.displayNode = ChatRecentActionsControllerNode(context: self.context, controller: self, peer: self.peer, presentationData: self.presentationData, interaction: self.interaction, pushController: { [weak self] c in + self.displayNode = ChatRecentActionsControllerNode(context: self.context, controller: self, peer: self.peer, presentationData: self.presentationData, pushController: { [weak self] c in (self?.navigationController as? NavigationController)?.pushViewController(c) }, presentController: { [weak self] c, t, a in self?.present(c, in: t, with: a, blockInteraction: true) }, getNavigationController: { [weak self] in return self?.navigationController as? NavigationController }) - + self.controllerNode.isEmptyUpdated = { [weak self] isEmpty in + guard let self, let rightBarButton = self.rightBarButton else { + return + } + self.navigationItem.setRightBarButton(isEmpty ? nil : rightBarButton.buttonItem, animated: true) + } if let adminPeerId = self.initialAdminPeerId { self.controllerNode.updateFilter(events: .all, adminPeerIds: [adminPeerId]) self.updateTitle() @@ -302,18 +297,65 @@ public final class ChatRecentActionsController: TelegramBaseController { self.updateTitle() } - private func openFilterSetup() { - self.present(channelRecentActionsFilterController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: self.peer, events: self.controllerNode.filter.events, adminPeerIds: self.controllerNode.filter.adminPeerIds, apply: { [weak self] events, adminPeerIds in - self?.controllerNode.updateFilter(events: events, adminPeerIds: adminPeerIds) - self?.updateTitle() - }), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + private var adminsPromise: Promise<[RenderedChannelParticipant]?>? + func openFilterSetup() { + if self.adminsPromise == nil { + self.adminsPromise = Promise() + let (disposable, _) = self.context.peerChannelMemberCategoriesContextsManager.admins(engine: self.context.engine, postbox: self.context.account.postbox, network: self.context.account.network, accountPeerId: self.context.account.peerId, peerId: self.peer.id) { membersState in + if case .loading = membersState.loadingState, membersState.list.isEmpty { + self.adminsPromise?.set(.single(nil)) + } else { + self.adminsPromise?.set(.single(membersState.list)) + } + } + self.adminsDisposable = disposable + } + + guard let adminsPromise = self.adminsPromise else { + return + } + + let _ = (adminsPromise.get() + |> filter { $0 != nil } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + var adminPeers: [EnginePeer] = [] + if let result { + for participant in result { + adminPeers.append(EnginePeer(participant.peer)) + } + } + let controller = RecentActionsSettingsSheet( + context: self.context, + peer: EnginePeer(self.peer), + adminPeers: adminPeers, + initialValue: RecentActionsSettingsSheet.Value( + events: self.controllerNode.filter.events, + admins: self.controllerNode.filter.adminPeerIds + ), + completion: { [weak self] result in + guard let self else { + return + } + self.controllerNode.updateFilter(events: result.events, adminPeerIds: result.admins) + self.updateTitle() + } + ) + self.push(controller) + }) } private func updateTitle() { + let title = EnginePeer(self.peer).compactDisplayTitle + let subtitle: String if self.controllerNode.filter.isEmpty { - self.titleView.title = self.presentationData.strings.Channel_AdminLog_TitleAllEvents + subtitle = self.presentationData.strings.Channel_AdminLog_TitleAllEvents } else { - self.titleView.title = self.presentationData.strings.Channel_AdminLog_TitleSelectedEvents + subtitle = self.presentationData.strings.Channel_AdminLog_TitleSelectedEvents } + self.titleView.title = CounterControllerTitle(title: title, counter: subtitle) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index 25f00a74822..8dc6300d157 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -49,8 +49,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { private let pushController: (ViewController) -> Void private let presentController: (ViewController, PresentationContextType, Any?) -> Void private let getNavigationController: () -> NavigationController? + var isEmptyUpdated: (Bool) -> Void = { _ in } - private let interaction: ChatRecentActionsInteraction private var controllerInteraction: ChatControllerInteraction! private let galleryHiddenMesageAndMediaDisposable = MetaDisposable() @@ -68,6 +68,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { private let panelBackgroundNode: NavigationBackgroundNode private let panelSeparatorNode: ASDisplayNode private let panelButtonNode: HighlightableButtonNode + private let panelInfoButtonNode: HighlightableButtonNode fileprivate let listNode: ListView private let loadingNode: ChatLoadingNode @@ -75,6 +76,13 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { private let navigationActionDisposable = MetaDisposable() + private var expandedDeletedMessages = Set() { + didSet { + self.expandedDeletedMessagesPromise.set(self.expandedDeletedMessages) + } + } + private let expandedDeletedMessagesPromise = ValuePromise>(Set()) + private var isLoading: Bool = false { didSet { if self.isLoading != oldValue { @@ -87,6 +95,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { private let eventLogContext: ChannelAdminEventLogContext private var enqueuedTransitions: [(ChatRecentActionsHistoryTransition, Bool)] = [] + private var searchResultsState: (String, [MessageIndex])? private var historyDisposable: Disposable? private let resolvePeerByNameDisposable = MetaDisposable() @@ -99,12 +108,11 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { private weak var controller: ChatRecentActionsController? - init(context: AccountContext, controller: ChatRecentActionsController, peer: Peer, presentationData: PresentationData, interaction: ChatRecentActionsInteraction, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, PresentationContextType, Any?) -> Void, getNavigationController: @escaping () -> NavigationController?) { + init(context: AccountContext, controller: ChatRecentActionsController, peer: Peer, presentationData: PresentationData, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, PresentationContextType, Any?) -> Void, getNavigationController: @escaping () -> NavigationController?) { self.context = context self.controller = controller self.peer = peer self.presentationData = presentationData - self.interaction = interaction self.pushController = pushController self.presentController = presentController self.getNavigationController = getNavigationController @@ -118,7 +126,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { self.panelSeparatorNode = ASDisplayNode() self.panelSeparatorNode.backgroundColor = self.presentationData.theme.chat.inputPanel.panelSeparatorColor self.panelButtonNode = HighlightableButtonNode() - self.panelButtonNode.setTitle(self.presentationData.strings.Channel_AdminLog_InfoPanelTitle, with: Font.regular(17.0), with: self.presentationData.theme.chat.inputPanel.panelControlAccentColor, for: []) + self.panelButtonNode.setTitle(self.presentationData.strings.Channel_AdminLog_Settings, with: Font.regular(17.0), with: self.presentationData.theme.chat.inputPanel.panelControlAccentColor, for: []) + self.panelInfoButtonNode = HighlightableButtonNode() self.listNode = ListView() self.listNode.dynamicBounceEnabled = false @@ -145,8 +154,10 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { self.addSubnode(self.panelBackgroundNode) self.addSubnode(self.panelSeparatorNode) self.addSubnode(self.panelButtonNode) + self.addSubnode(self.panelInfoButtonNode) - self.panelButtonNode.addTarget(self, action: #selector(self.infoButtonPressed), forControlEvents: .touchUpInside) + self.panelButtonNode.addTarget(self, action: #selector(self.settingsButtonPressed), forControlEvents: .touchUpInside) + self.panelInfoButtonNode.addTarget(self, action: #selector(self.infoButtonPressed), forControlEvents: .touchUpInside) let (adminsDisposable, _) = self.context.peerChannelMemberCategoriesContextsManager.admins(engine: self.context.engine, postbox: self.context.account.postbox, network: self.context.account.network, accountPeerId: context.account.peerId, peerId: self.peer.id, searchQuery: nil, updated: { [weak self] state in self?.adminsState = state @@ -159,6 +170,16 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { return false } for entry in state.entries { + if entry.entry.headerStableId == message.stableId { + if case let .deleteMessage(message) = entry.entry.event.action { + if strongSelf.expandedDeletedMessages.contains(message.id) { + strongSelf.expandedDeletedMessages.remove(message.id) + } else { + strongSelf.expandedDeletedMessages.insert(message.id) + } + return true + } + } if entry.entry.stableId == message.stableId { switch entry.entry.event.action { case let .changeStickerPack(_, new): @@ -274,9 +295,28 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, updateMessageReaction: { _, _, _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in - }, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in - }, navigateToThreadMessage: { _, _, _ in - }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { [weak self] url in + }, navigateToMessage: { [weak self] fromId, toId, params in + guard let self else { + return + } + + context.sharedContext.navigateToChat(accountId: self.context.account.id, peerId: toId.peerId, messageId: toId) + }, navigateToMessageStandalone: { _ in + }, navigateToThreadMessage: { [weak self] peerId, threadId, _ in + if let context = self?.context, let navigationController = self?.getNavigationController() { + let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: threadId, messageId: nil, navigationController: navigationController, activateInput: nil, keepStack: .always).startStandalone() + } + }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false + }, requestMessageActionCallback: { [weak self] messageId, _, _, _ in + guard let self else { + return + } + if self.expandedDeletedMessages.contains(messageId) { + self.expandedDeletedMessages.remove(messageId) + } else { + self.expandedDeletedMessages.insert(messageId) + } + }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { [weak self] url in self?.openUrl(url.url) }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message, associatedData in if let strongSelf = self, let navigationController = strongSelf.getNavigationController() { @@ -610,15 +650,43 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { } let previousView = Atomic<[ChatRecentActionsEntry]?>(value: nil) + let previousExpandedDeletedMessages = Atomic>(value: Set()) + let previousDeletedHeaderMessages = Atomic>(value: Set()) let chatThemes = self.context.engine.themes.getChatThemes(accountManager: self.context.sharedContext.accountManager) - let historyViewTransition = combineLatest(historyViewUpdate, self.chatPresentationDataPromise.get(), chatThemes) - |> mapToQueue { update, chatPresentationData, chatThemes -> Signal in - let processedView = chatRecentActionsEntries(entries: update.0, presentationData: chatPresentationData) + let historyViewTransition = combineLatest( + historyViewUpdate, + self.chatPresentationDataPromise.get(), + chatThemes, + self.expandedDeletedMessagesPromise.get() + ) + |> mapToQueue { [weak self] update, chatPresentationData, chatThemes, expandedDeletedMessages -> Signal in + + var deletedHeaderMessages = previousDeletedHeaderMessages.with { $0 } + let processedView = chatRecentActionsEntries(entries: update.0, presentationData: chatPresentationData, expandedDeletedMessages: expandedDeletedMessages, currentDeletedHeaderMessages: &deletedHeaderMessages) + let _ = previousDeletedHeaderMessages.swap(deletedHeaderMessages) + let previous = previousView.swap(processedView) + let previousExpandedDeletedMessages = previousExpandedDeletedMessages.swap(expandedDeletedMessages) + + var updateType = update.2 + if previousExpandedDeletedMessages.count != expandedDeletedMessages.count { + updateType = .generic + } - return .single(chatRecentActionsHistoryPreparedTransition(from: previous ?? [], to: processedView, type: update.2, canLoadEarlier: update.1, displayingResults: update.3, context: context, peer: peer, controllerInteraction: controllerInteraction, chatThemes: chatThemes)) + let toggledDeletedMessageIds = previousExpandedDeletedMessages.symmetricDifference(expandedDeletedMessages) + + var searchResultsState: (String, [MessageIndex])? + if update.3, let query = self?.filter.query { + searchResultsState = (query, processedView.compactMap { entry in + return entry.entry.event.action.messageId.flatMap { MessageIndex(id: $0, timestamp: entry.entry.event.date) } + }) + } else { + searchResultsState = nil + } + + return .single(chatRecentActionsHistoryPreparedTransition(from: previous ?? [], to: processedView, type: updateType, canLoadEarlier: update.1, displayingResults: update.3, context: context, peer: peer, controllerInteraction: controllerInteraction, chatThemes: chatThemes, searchResultsState: searchResultsState, toggledDeletedMessageIds: toggledDeletedMessageIds)) } let appliedTransition = historyViewTransition |> deliverOnMainQueue |> mapToQueue { [weak self] transition -> Signal in @@ -674,7 +742,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { self.panelBackgroundNode.updateColor(color: presentationData.theme.chat.inputPanel.panelBackgroundColor, transition: .immediate) self.panelSeparatorNode.backgroundColor = presentationData.theme.chat.inputPanel.panelSeparatorColor - self.panelButtonNode.setTitle(presentationData.strings.Channel_AdminLog_InfoPanelTitle, with: Font.regular(17.0), with: presentationData.theme.chat.inputPanel.panelControlAccentColor, for: []) + self.panelButtonNode.setTitle(presentationData.strings.Channel_AdminLog_Settings, with: Font.regular(17.0), with: presentationData.theme.chat.inputPanel.panelControlAccentColor, for: []) + self.panelInfoButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Recent Actions/Info"), color: presentationData.theme.chat.inputPanel.panelControlAccentColor), for: .normal) } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { @@ -691,11 +760,20 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { self.backgroundNode.updateLayout(size: self.backgroundNode.bounds.size, displayMode: .aspectFill, transition: transition) let intrinsicPanelHeight: CGFloat = 47.0 - let panelHeight = intrinsicPanelHeight + cleanInsets.bottom - transition.updateFrame(node: self.panelBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - panelHeight), size: CGSize(width: layout.size.width, height: panelHeight))) + var panelHeight = intrinsicPanelHeight + cleanInsets.bottom + var panelOffset: CGFloat = panelHeight + if insets.bottom > cleanInsets.bottom { + panelHeight = intrinsicPanelHeight + panelOffset = insets.bottom + panelHeight + } + transition.updateFrame(node: self.panelBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - panelOffset), size: CGSize(width: layout.size.width, height: panelHeight))) self.panelBackgroundNode.update(size: self.panelBackgroundNode.bounds.size, transition: transition) - transition.updateFrame(node: self.panelSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - panelHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel))) - transition.updateFrame(node: self.panelButtonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - panelHeight), size: CGSize(width: layout.size.width, height: intrinsicPanelHeight))) + transition.updateFrame(node: self.panelSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - panelOffset), size: CGSize(width: layout.size.width, height: UIScreenPixel))) + + let infoButtonSize = CGSize(width: 56.0, height: intrinsicPanelHeight) + transition.updateFrame(node: self.panelButtonNode, frame: CGRect(origin: CGPoint(x: insets.left + infoButtonSize.width, y: layout.size.height - panelOffset), size: CGSize(width: layout.size.width - insets.left - insets.right - infoButtonSize.width * 2.0, height: intrinsicPanelHeight))) + + transition.updateFrame(node: self.panelInfoButtonNode, frame: CGRect(origin: CGPoint(x: layout.size.width - insets.right - infoButtonSize.width, y: layout.size.height - panelOffset), size: infoButtonSize)) self.visibleAreaInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: panelHeight, right: 0.0) @@ -703,14 +781,14 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { transition.updatePosition(node: self.listNode, position: CGRect(origin: CGPoint(), size: layout.size).center) transition.updateFrame(node: self.loadingNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - loadingNode.updateLayout(size: layout.size, insets: insets, transition: transition) + self.loadingNode.updateLayout(size: layout.size, insets: insets, transition: transition) let emptyFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight - panelHeight)) transition.updateFrame(node: self.emptyNode, frame: emptyFrame) self.emptyNode.update(rect: emptyFrame, within: layout.size) self.emptyNode.updateLayout(presentationData: self.chatPresentationData, backgroundNode: self.backgroundNode, size: emptyFrame.size, transition: transition) - let contentBottomInset: CGFloat = panelHeight + 4.0 + let contentBottomInset: CGFloat = panelOffset + 4.0 let listInsets = UIEdgeInsets(top: contentBottomInset, left: layout.safeInsets.right, bottom: insets.top, right: layout.safeInsets.left) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) @@ -748,10 +826,16 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { break } } + if transition.synchronous { + options.insert(.InvertOffsetDirection) + } let displayingResults = transition.displayingResults let isEmpty = transition.isEmpty let displayEmptyNode = isEmpty && displayingResults + + self.searchResultsState = transition.searchResultsState + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: ChatRecentActionsListOpaqueState(entries: transition.filteredEntries, canLoadEarlier: transition.canLoadEarlier), completion: { [weak self] _ in if let strongSelf = self { if displayEmptyNode != strongSelf.listNode.isHidden { @@ -788,6 +872,14 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { strongSelf.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } strongSelf.isLoading = isLoading + + var isEmpty = false + if strongSelf.filter.isEmpty && (transition.isEmpty || isLoading) { + isEmpty = true + } + strongSelf.isEmptyUpdated(isEmpty) + + strongSelf.updateItemNodesSearchTextHighlightStates() } }) } else { @@ -796,13 +888,50 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { } } + @objc func settingsButtonPressed() { + self.controller?.openFilterSetup() + } + @objc func infoButtonPressed() { - self.interaction.displayInfoAlert() + guard let controller = self.controller else { + return + } + let text: String + if let channel = self.peer as? TelegramChannel, case .broadcast = channel.info { + text = self.presentationData.strings.Channel_AdminLog_InfoPanelChannelAlertText + } else { + text = self.presentationData.strings.Channel_AdminLog_InfoPanelAlertText + } + controller.present(textAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, title: self.presentationData.strings.Channel_AdminLog_InfoPanelAlertTitle, text: text, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + } func updateSearchQuery(_ query: String) { self.filter = self.filter.withQuery(query.isEmpty ? nil : query) self.eventLogContext.setFilter(self.filter) + + self.updateItemNodesSearchTextHighlightStates() + } + + func updateItemNodesSearchTextHighlightStates() { + var searchString: String? + var resultsMessageIndices: [MessageIndex]? = nil + if let (query, indices) = self.searchResultsState { + searchString = query + resultsMessageIndices = indices + } + if searchString != self.controllerInteraction?.searchTextHighightState?.0 || resultsMessageIndices != self.controllerInteraction?.searchTextHighightState?.1 { + var searchTextHighightState: (String, [MessageIndex])? + if let searchString = searchString, let resultsMessageIndices = resultsMessageIndices { + searchTextHighightState = (searchString, resultsMessageIndices) + } + self.controllerInteraction?.searchTextHighightState = searchTextHighightState + self.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + itemNode.updateSearchTextHighlightState() + } + } + } } func updateFilter(events: AdminLogEventsFlags, adminPeerIds: [PeerId]?) { @@ -936,6 +1065,40 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { } })) ) + actions.append( + .action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuBanFull, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Ban"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] _, f in + if let strongSelf = self { + f(.default) + strongSelf.banDisposables.set((strongSelf.context.engine.peers.fetchChannelParticipant(peerId: strongSelf.peer.id, participantId: author.id) + |> deliverOnMainQueue).startStrict(next: { participant in + if let strongSelf = self { + let initialUserBannedRights = participant?.banInfo?.rights + strongSelf.banDisposables.set(strongSelf.context.engine.peers.removePeerMember(peerId: strongSelf.peer.id, memberId: author.id).startStandalone(), forKey: author.id) + + strongSelf.presentController(UndoOverlayController( + presentationData: strongSelf.presentationData, + content: .actionSucceeded(title: nil, text: "**\(EnginePeer(author).compactDisplayTitle)** was banned.", cancel: strongSelf.presentationData.strings.Undo_Undo, destructive: false), + elevatedLayout: false, + action: { [weak self] action in + guard let self else { + return true + } + switch action { + case .commit: + break + case .undo: + let _ = self.context.engine.peers.updateChannelMemberBannedRights(peerId: self.peer.id, memberId: author.id, rights: initialUserBannedRights).startStandalone() + default: + break + } + return true + } + ), .current, nil) + } + }), forKey: author.id) + } + })) + ) } } @@ -1229,3 +1392,20 @@ final class ChatMessageContextLocationContentSource: ContextLocationContentSourc return ContextControllerLocationViewInfo(location: self.location, contentAreaInScreenSpace: UIScreen.main.bounds) } } + +extension AdminLogEventAction { + var messageId: MessageId? { + switch self { + case let .editMessage(_, new): + return new.id + case let .deleteMessage(message): + return message.id + case let .pollStopped(message): + return message.id + case let .sendMessage(message): + return message.id + default: + return nil + } + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsEmptyNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsEmptyNode.swift index 65a09f9d998..55b437e6197 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsEmptyNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsEmptyNode.swift @@ -10,14 +10,15 @@ import TelegramPresentationData import WallpaperBackgroundNode import ChatPresentationInterfaceState -private let titleFont = Font.medium(16.0) -private let textFont = Font.regular(15.0) +private let titleFont = Font.semibold(15.0) +private let textFont = Font.regular(13.0) public final class ChatRecentActionsEmptyNode: ASDisplayNode { private var theme: PresentationTheme private var chatWallpaper: TelegramWallpaper private let backgroundNode: NavigationBackgroundNode + private let iconNode: ASImageNode private let titleNode: TextNode private let textNode: TextNode @@ -41,6 +42,9 @@ public final class ChatRecentActionsEmptyNode: ASDisplayNode { self.backgroundNode = NavigationBackgroundNode(color: .clear) + self.iconNode = ASImageNode() + self.iconNode.displaysAsynchronously = false + self.titleNode = TextNode() self.titleNode.isUserInteractionEnabled = false @@ -56,6 +60,7 @@ public final class ChatRecentActionsEmptyNode: ASDisplayNode { self.allowsGroupOpacity = true self.addSubnode(self.backgroundNode) + self.addSubnode(self.iconNode) self.addSubnode(self.titleNode) self.addSubnode(self.textNode) // MARK: Nicegram Unblock @@ -77,14 +82,15 @@ public final class ChatRecentActionsEmptyNode: ASDisplayNode { self.wallpaperBackgroundNode = backgroundNode self.layoutParams = (size, presentationData) + let themeUpdated = self.theme !== presentationData.theme.theme self.theme = presentationData.theme.theme self.chatWallpaper = presentationData.theme.wallpaper self.backgroundNode.updateColor(color: selectDateFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper), transition: .immediate) - let insets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0) + let insets = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 25.0, right: 16.0) - let maxTextWidth = size.width - insets.left - insets.right - 18.0 * 2.0 + let maxTextWidth = min(196.0, size.width - insets.left - insets.right - 18.0 * 2.0) let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTextLayout = TextNode.asyncLayout(self.textNode) @@ -92,16 +98,24 @@ public final class ChatRecentActionsEmptyNode: ASDisplayNode { let serviceColor = serviceMessageColorComponents(theme: self.theme, wallpaper: self.chatWallpaper) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: self.title, font: titleFont, textColor: serviceColor.primaryText), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) - let spacing: CGFloat = titleLayout.size.height.isZero ? 0.0 : 5.0 + let spacing: CGFloat = titleLayout.size.height.isZero ? 0.0 : 7.0 let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: self.text, font: textFont, textColor: serviceColor.primaryText), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) - let contentSize = CGSize(width: max(titleLayout.size.width, textLayout.size.width) + insets.left + insets.right, height: insets.top + insets.bottom + titleLayout.size.height + spacing + textLayout.size.height) + if themeUpdated || self.iconNode.image == nil { + self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Recent Actions/Placeholder"), color: serviceColor.primaryText) + } + let iconSize = self.iconNode.image?.size ?? .zero + + let contentSize = CGSize(width: max(titleLayout.size.width, textLayout.size.width) + insets.left + insets.right, height: 5.0 + insets.bottom + iconSize.height - 2.0 + titleLayout.size.height + spacing + textLayout.size.height) let backgroundFrame = CGRect(origin: CGPoint(x: floor((size.width - contentSize.width) / 2.0), y: floor((size.height - contentSize.height) / 2.0)), size: contentSize) transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) self.backgroundNode.update(size: self.backgroundNode.bounds.size, cornerRadius: min(14.0, self.backgroundNode.bounds.height / 2.0), transition: transition) - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((contentSize.width - titleLayout.size.width) / 2.0), y: backgroundFrame.minY + insets.top), size: titleLayout.size)) - transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((contentSize.width - textLayout.size.width) / 2.0), y: backgroundFrame.minY + insets.top + titleLayout.size.height + spacing), size: textLayout.size)) + let iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((contentSize.width - iconSize.width) / 2.0), y: backgroundFrame.minY + 5.0), size: iconSize) + transition.updateFrame(node: self.iconNode, frame: iconFrame) + + let titleFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((contentSize.width - titleLayout.size.width) / 2.0), y: iconFrame.maxY - 2.0), size: titleLayout.size) + transition.updateFrame(node: self.titleNode, frame: titleFrame) // MARK: Nicegram Unblock let buttonSize = CGSize( @@ -119,6 +133,9 @@ public final class ChatRecentActionsEmptyNode: ASDisplayNode { let _ = self.buttonNode.updateLayout(width: buttonFrame.width, transition: transition) // + let textFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((contentSize.width - textLayout.size.width) / 2.0), y: titleFrame.maxY + spacing), size: textLayout.size) + transition.updateFrame(node: self.textNode, frame: textFrame) + let _ = titleApply() let _ = textApply() diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift index cfc70b04187..29010e6a327 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift @@ -8,6 +8,7 @@ import MergeLists import AccountContext import ChatControllerInteraction import ChatHistoryEntry +import ChatMessageItem import ChatMessageItemImpl import TextFormat @@ -95,6 +96,8 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let id: ChatRecentActionsEntryId let presentationData: ChatPresentationData let entry: ChannelAdminEventLogEntry + let subEntries: [ChannelAdminEventLogEntry] + let isExpanded: Bool? static func ==(lhs: ChatRecentActionsEntry, rhs: ChatRecentActionsEntry) -> Bool { if lhs.id != rhs.id { @@ -106,6 +109,12 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { if lhs.entry != rhs.entry { return false } + if lhs.subEntries != rhs.subEntries { + return false + } + if lhs.isExpanded != rhs.isExpanded { + return false + } return true } @@ -404,6 +413,9 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { if let attribute = attribute as? TextEntitiesMessageAttribute { attributes.append(attribute) } + if let attribute = attribute as? ReplyMessageAttribute { + attributes.append(attribute) + } } for attribute in attributes { for peerId in attribute.associatedPeerIds { @@ -412,7 +424,10 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } } - let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: message.effectiveAuthor, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + if let peer = self.entry.peers[message.id.peerId] { + peers[peer.id] = peer + } + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: message.threadId, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: message.effectiveAuthor, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: message.associatedThreadInfo, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, savedMessageTags: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) } else { var peers = SimpleDictionary() @@ -488,6 +503,9 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { if let attribute = attribute as? TextEntitiesMessageAttribute { attributes.append(attribute) } + if let attribute = attribute as? ReplyMessageAttribute { + attributes.append(attribute) + } } for attribute in attributes { for peerId in attribute.associatedPeerIds { @@ -496,7 +514,10 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } } - let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: message.effectiveAuthor, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + if let peer = self.entry.peers[message.id.peerId] { + peers[peer.id] = peer + } + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: message.threadId, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: message.effectiveAuthor, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: message.associatedMessages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: message.associatedThreadInfo, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, savedMessageTags: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: filterOriginalMessageFlags(message), read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: !prev.text.isEmpty || !message.text.isEmpty ? .eventLogPreviousMessage(filterOriginalMessageFlags(prev)) : nil) } case let .deleteMessage(message): @@ -512,13 +533,48 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { var text: String = "" var entities: [MessageTextEntity] = [] - - appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageDeleted(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? ""), generateEntities: { index in - if index == 0, let author = author { - return [.TextMention(peerId: author.id)] + + let authorName = author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "" + + if !self.subEntries.isEmpty { + var peers: [EnginePeer] = [] + var existingPeerIds = Set() + for entry in self.subEntries { + if case let .deleteMessage(message) = entry.event.action, let author = message.author { + guard !existingPeerIds.contains(author.id) else { + continue + } + peers.append(EnginePeer(author)) + existingPeerIds.insert(author.id) + } } - return [] - }, to: &text, entities: &entities) + let peerNames = peers.map { $0.compactDisplayTitle }.joined(separator: ", ") + let messagesString = self.presentationData.strings.Channel_AdminLog_MessageManyDeleted_Messages(Int32(self.subEntries.count)) + + let fullText: PresentationStrings.FormattedString + if let isExpanded = self.isExpanded { + let moreText = (isExpanded ? self.presentationData.strings.Channel_AdminLog_MessageManyDeleted_HideAll : self.presentationData.strings.Channel_AdminLog_MessageManyDeleted_ShowAll).replacingOccurrences(of: " ", with: "\u{00A0}") + fullText = self.presentationData.strings.Channel_AdminLog_MessageManyDeletedMore(authorName, messagesString, peerNames, moreText) + } else { + fullText = self.presentationData.strings.Channel_AdminLog_MessageManyDeleted(authorName, messagesString, peerNames) + } + + appendAttributedText(text: fullText, generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } else if index == 3 { + return [.Custom(type: ApplicationSpecificEntityType.Button)] + } + return [] + }, to: &text, entities: &entities) + } else { + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageDeleted(authorName), generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } + return [] + }, to: &text, entities: &entities) + } let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) @@ -531,6 +587,9 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { if let attribute = attribute as? TextEntitiesMessageAttribute { attributes.append(attribute) } + if let attribute = attribute as? ReplyMessageAttribute { + attributes.append(attribute) + } } for attribute in attributes { for peerId in attribute.associatedPeerIds { @@ -549,8 +608,27 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { if let peer = self.entry.peers[self.entry.event.peerId] { peers[peer.id] = peer } - let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: message.id.id), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: message.effectiveAuthor, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, savedMessageTags: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + if let peer = self.entry.peers[message.id.peerId] { + peers[peer.id] = peer + } + + var additionalContent: ChatMessageItemAdditionalContent? + if !self.subEntries.isEmpty { + var messages: [Message] = [] + for entry in self.subEntries { + if case let .deleteMessage(message) = entry.event.action { + messages.append(message) + } + } + var hasButton = false + if let isExpanded = self.isExpanded, !isExpanded { + hasButton = true + } + additionalContent = .eventLogGroupedMessages(messages, hasButton) + } + + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: message.id.id), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: message.threadId, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: message.effectiveAuthor, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: message.associatedMessages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: message.associatedThreadInfo, associatedStories: [:]) + return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, savedMessageTags: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: additionalContent) } case .participantJoin, .participantLeave: var peers = SimpleDictionary() @@ -1110,6 +1188,9 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { if let attribute = attribute as? TextEntitiesMessageAttribute { attributes.append(attribute) } + if let attribute = attribute as? ReplyMessageAttribute { + attributes.append(attribute) + } } for attribute in attributes { for peerId in attribute.associatedPeerIds { @@ -1118,7 +1199,10 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } } - let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: message.author, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + if let peer = self.entry.peers[message.id.peerId] { + peers[peer.id] = peer + } + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: message.author, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: message.associatedMessages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: message.associatedThreadInfo, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, savedMessageTags: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: filterOriginalMessageFlags(message), read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: nil) } case let .linkedPeerUpdated(previous, updated): @@ -1706,6 +1790,9 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { if let attribute = attribute as? TextEntitiesMessageAttribute { attributes.append(attribute) } + if let attribute = attribute as? ReplyMessageAttribute { + attributes.append(attribute) + } } for attribute in attributes { for peerId in attribute.associatedPeerIds { @@ -1714,7 +1801,10 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } } - let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: message.effectiveAuthor, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + if let peer = self.entry.peers[message.id.peerId] { + peers[peer.id] = peer + } + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: message.threadId, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: message.effectiveAuthor, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: message.associatedMessages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: message.associatedThreadInfo, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, savedMessageTags: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) } case let .createTopic(info): @@ -2154,16 +2244,63 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } -func chatRecentActionsEntries(entries: [ChannelAdminEventLogEntry], presentationData: ChatPresentationData) -> [ChatRecentActionsEntry] { +private let deletedMessagesDisplayedLimit = 4 + +func chatRecentActionsEntries(entries: [ChannelAdminEventLogEntry], presentationData: ChatPresentationData, expandedDeletedMessages: Set, currentDeletedHeaderMessages: inout Set) -> [ChatRecentActionsEntry] { var result: [ChatRecentActionsEntry] = [] + var deleteMessageEntries: [ChannelAdminEventLogEntry] = [] + + func appendCurrentDeleteEntries() { + if !deleteMessageEntries.isEmpty, let lastEntry = deleteMessageEntries.last, let lastMessageId = lastEntry.event.action.messageId { + let isExpandable = deleteMessageEntries.count >= deletedMessagesDisplayedLimit + let isExpanded = expandedDeletedMessages.contains(lastMessageId) || !isExpandable + let isGroup = deleteMessageEntries.count > 1 + + for i in 0 ..< deleteMessageEntries.count { + let entry = deleteMessageEntries[i] + let isLast = i == deleteMessageEntries.count - 1 + if isExpanded || isLast { + result.append(ChatRecentActionsEntry(id: ChatRecentActionsEntryId(eventId: entry.event.id, contentIndex: .content), presentationData: presentationData, entry: entry, subEntries: isGroup && isExpandable ? deleteMessageEntries : [], isExpanded: isExpandable && isLast ? isExpanded : nil)) + } + } + + currentDeletedHeaderMessages.insert(lastMessageId) + result.append(ChatRecentActionsEntry(id: ChatRecentActionsEntryId(eventId: lastEntry.event.id, contentIndex: .header), presentationData: presentationData, entry: lastEntry, subEntries: isGroup ? deleteMessageEntries : [], isExpanded: isExpandable ? isExpanded : nil)) + + deleteMessageEntries = [] + } + } + for entry in entries.reversed() { - result.append(ChatRecentActionsEntry(id: ChatRecentActionsEntryId(eventId: entry.event.id, contentIndex: .content), presentationData: presentationData, entry: entry)) - if eventNeedsHeader(entry.event) { - result.append(ChatRecentActionsEntry(id: ChatRecentActionsEntryId(eventId: entry.event.id, contentIndex: .header), presentationData: presentationData, entry: entry)) + let currentDeleteMessageEvent = deleteMessageEntries.first?.event + var skipAppendingGeneralEntry = false + if case let .deleteMessage(message) = entry.event.action { + var skipAppendingDeletionEntry = false + if currentDeleteMessageEvent == nil || (currentDeleteMessageEvent!.peerId == entry.event.peerId && abs(currentDeleteMessageEvent!.date - entry.event.date) < 5 && !currentDeletedHeaderMessages.contains(message.id)) { + } else { + if currentDeletedHeaderMessages.contains(message.id) { + deleteMessageEntries.append(entry) + skipAppendingDeletionEntry = true + } + appendCurrentDeleteEntries() + } + if !skipAppendingDeletionEntry { + deleteMessageEntries.append(entry) + } + skipAppendingGeneralEntry = true + } + if !skipAppendingGeneralEntry { + appendCurrentDeleteEntries() + + result.append(ChatRecentActionsEntry(id: ChatRecentActionsEntryId(eventId: entry.event.id, contentIndex: .content), presentationData: presentationData, entry: entry, subEntries: [], isExpanded: nil)) + if eventNeedsHeader(entry.event) { + result.append(ChatRecentActionsEntry(id: ChatRecentActionsEntryId(eventId: entry.event.id, contentIndex: .header), presentationData: presentationData, entry: entry, subEntries: [], isExpanded: nil)) + } } } + appendCurrentDeleteEntries() - assert(result == result.sorted().reversed()) +// assert(result == result.sorted().reversed()) return result } @@ -2175,17 +2312,19 @@ struct ChatRecentActionsHistoryTransition { let updates: [ListViewUpdateItem] let canLoadEarlier: Bool let displayingResults: Bool + let searchResultsState: (String, [MessageIndex])? + var synchronous: Bool let isEmpty: Bool } -func chatRecentActionsHistoryPreparedTransition(from fromEntries: [ChatRecentActionsEntry], to toEntries: [ChatRecentActionsEntry], type: ChannelAdminEventLogUpdateType, canLoadEarlier: Bool, displayingResults: Bool, context: AccountContext, peer: Peer, controllerInteraction: ChatControllerInteraction, chatThemes: [TelegramTheme]) -> ChatRecentActionsHistoryTransition { - let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) +func chatRecentActionsHistoryPreparedTransition(from fromEntries: [ChatRecentActionsEntry], to toEntries: [ChatRecentActionsEntry], type: ChannelAdminEventLogUpdateType, canLoadEarlier: Bool, displayingResults: Bool, context: AccountContext, peer: Peer, controllerInteraction: ChatControllerInteraction, chatThemes: [TelegramTheme], searchResultsState: (String, [MessageIndex])?, toggledDeletedMessageIds: Set) -> ChatRecentActionsHistoryTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdatesReversed(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, peer: peer, controllerInteraction: controllerInteraction, chatThemes: chatThemes), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, peer: peer, controllerInteraction: controllerInteraction, chatThemes: chatThemes), directionHint: nil) } - return ChatRecentActionsHistoryTransition(filteredEntries: toEntries, type: type, deletions: deletions, insertions: insertions, updates: updates, canLoadEarlier: canLoadEarlier, displayingResults: displayingResults, isEmpty: toEntries.isEmpty) + return ChatRecentActionsHistoryTransition(filteredEntries: toEntries, type: type, deletions: deletions, insertions: insertions, updates: updates, canLoadEarlier: canLoadEarlier, displayingResults: displayingResults, searchResultsState: searchResultsState, synchronous: !toggledDeletedMessageIds.isEmpty, isEmpty: toEntries.isEmpty) } private extension ExportedInvitation { diff --git a/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift b/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift index 61ef685756e..5e54ec32151 100644 --- a/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode/Sources/ReplyAccessoryPanelNode.swift @@ -166,7 +166,7 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode { } let textColor = strongSelf.theme.chat.inputPanel.primaryTextColor if entities.count > 0 { - messageText = stringWithAppliedEntities(trimToLineCount(message.text, lineCount: 1), entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message) + messageText = stringWithAppliedEntities(trimToLineCount(message.text, lineCount: 1), entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message) } else { messageText = NSAttributedString(string: text, font: textFont, textColor: isMedia ? strongSelf.theme.chat.inputPanel.secondaryTextColor : strongSelf.theme.chat.inputPanel.primaryTextColor) } diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 0807543a2c4..18698ed0c8b 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -160,7 +160,18 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { return hasPremium } - public static func inputData(context: AccountContext, chatPeerId: PeerId?, areCustomEmojiEnabled: Bool, hasEdit: Bool = false, hasTrending: Bool = true, hasSearch: Bool = true, hideBackground: Bool = false, sendGif: ((FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool)?) -> Signal { + public static func inputData( + context: AccountContext, + chatPeerId: PeerId?, + areCustomEmojiEnabled: Bool, + hasEdit: Bool = false, + hasTrending: Bool = true, + hasSearch: Bool = true, + hasStickers: Bool = true, + hasGifs: Bool = true, + hideBackground: Bool = false, + sendGif: ((FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool)? + ) -> Signal { let animationCache = context.animationCache let animationRenderer = context.animationRenderer @@ -184,7 +195,26 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings - let stickerItems = EmojiPagerContentComponent.stickerInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, stickerNamespaces: stickerNamespaces, stickerOrderedItemListCollectionIds: stickerOrderedItemListCollectionIds, chatPeerId: chatPeerId, hasSearch: hasSearch, hasTrending: hasTrending, forceHasPremium: false, hasEdit: hasEdit, hideBackground: hideBackground) + let stickerItems: Signal + if hasStickers { + stickerItems = EmojiPagerContentComponent.stickerInputData( + context: context, + animationCache: animationCache, + animationRenderer: animationRenderer, + stickerNamespaces: stickerNamespaces, + stickerOrderedItemListCollectionIds: stickerOrderedItemListCollectionIds, + chatPeerId: chatPeerId, + hasSearch: hasSearch, + hasTrending: hasTrending, + forceHasPremium: false, + hasEdit: hasEdit, + subject: .chatStickers, + hideBackground: hideBackground + ) + |> map(Optional.init) + } else { + stickerItems = .single(nil) + } let reactions: Signal<[String], NoError> = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.App()) |> map { appConfiguration -> [String] in @@ -197,10 +227,13 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } |> distinctUntilChanged - let animatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false) - |> map { animatedEmoji -> [String: [StickerPackItem]] in - var animatedEmojiStickers: [String: [StickerPackItem]] = [:] - switch animatedEmoji { + let animatedEmojiStickers: Signal<[String: [StickerPackItem]], NoError> + + if hasGifs { + 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 { @@ -213,8 +246,11 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } default: break + } + return animatedEmojiStickers } - return animatedEmojiStickers + } else { + animatedEmojiStickers = .single([:]) } let gifInputInteraction = GifPagerContentComponent.InputInteraction( @@ -236,22 +272,27 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { ) // We are going to subscribe to the actual data when the view is loaded - let gifItems: Signal = .single(EntityKeyboardGifContent( - hasRecentGifs: true, - component: GifPagerContentComponent( - context: context, - inputInteraction: gifInputInteraction, - subject: .recent, - items: [], - isLoading: false, - loadMoreToken: nil, - displaySearchWithPlaceholder: nil, - searchCategories: nil, - searchInitiallyHidden: true, - searchState: .empty(hasResults: false), - hideBackground: hideBackground - ) - )) + let gifItems: Signal + if hasGifs { + gifItems = .single(EntityKeyboardGifContent( + hasRecentGifs: true, + component: GifPagerContentComponent( + context: context, + inputInteraction: gifInputInteraction, + subject: .recent, + items: [], + isLoading: false, + loadMoreToken: nil, + displaySearchWithPlaceholder: nil, + searchCategories: nil, + searchInitiallyHidden: true, + searchState: .empty(hasResults: false), + hideBackground: hideBackground + ) + )) + } else { + gifItems = .single(nil) + } return combineLatest(queue: .mainQueue(), emojiItems, @@ -321,6 +362,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { private var inputDataDisposable: Disposable? private var hasRecentGifsDisposable: Disposable? private let opaqueTopPanelBackground: Bool + private let useOpaqueTheme: Bool private struct EmojiSearchResult { var groups: [EmojiPagerContentComponent.ItemGroup] @@ -428,7 +470,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } // MARK: Nicegram OpenGifsShortcut, defaultTab param added - public init(defaultTab: EntityKeyboardInputTab? = nil, context: AccountContext, currentInputData: InputData, updatedInputData: Signal, defaultToEmojiTab: Bool, opaqueTopPanelBackground: Bool = false, interaction: ChatEntityKeyboardInputNode.Interaction?, chatPeerId: PeerId?, stateContext: StateContext?) { + public init(defaultTab: EntityKeyboardInputTab? = nil, context: AccountContext, currentInputData: InputData, updatedInputData: Signal, defaultToEmojiTab: Bool, opaqueTopPanelBackground: Bool = false, useOpaqueTheme: Bool = false, interaction: ChatEntityKeyboardInputNode.Interaction?, chatPeerId: PeerId?, stateContext: StateContext?) { // MARK: Nicegram OpenGifsShortcut self.defaultTab = defaultTab // @@ -436,6 +478,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { self.currentInputData = currentInputData self.defaultToEmojiTab = defaultToEmojiTab self.opaqueTopPanelBackground = opaqueTopPanelBackground + self.useOpaqueTheme = useOpaqueTheme self.stateContext = stateContext self.interaction = interaction @@ -662,7 +705,10 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { let presentationData = context.sharedContext.currentPresentationData.with { $0 } premiumToastCounter += 1 - let suggestSavedMessages = premiumToastCounter % 2 == 0 + var suggestSavedMessages = premiumToastCounter % 2 == 0 + if chatPeerId == nil { + suggestSavedMessages = false + } let text: String let actionTitle: String if suggestSavedMessages { @@ -1056,7 +1102,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { })) } case let .category(value): - let resultSignal = self.context.engine.stickers.searchEmoji(emojiString: value) + let resultSignal = self.context.engine.stickers.searchEmoji(category: value) |> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in var items: [EmojiPagerContentComponent.Item] = [] @@ -1070,7 +1116,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { let item = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), - itemFile: itemFile, subgroupId: nil, + itemFile: itemFile, + subgroupId: nil, icon: .none, tintMode: animationData.isTemplate ? .primary : .none ) @@ -1127,10 +1174,10 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { fillWithLoadingPlaceholders: true, items: [] ) - ], id: AnyHashable(value), version: version, isPreset: true), isSearching: false) + ], id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) return } - self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value), version: version, isPreset: true), isSearching: false) + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) version += 1 })) } @@ -1144,7 +1191,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { externalBackground: nil, externalExpansionView: nil, customContentView: nil, - useOpaqueTheme: false, + useOpaqueTheme: self.useOpaqueTheme, hideBackground: false, stateContext: self.stateContext?.emojiState, addImage: nil @@ -1399,7 +1446,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { strongSelf.stickerSearchDisposable.set(nil) strongSelf.stickerSearchStateValue = EmojiSearchState(result: nil, isSearching: false) case let .category(value): - let resultSignal = strongSelf.context.engine.stickers.searchStickers(query: value, scope: [.installed, .remote]) + let resultSignal = strongSelf.context.engine.stickers.searchStickers(category: value, scope: [.installed, .remote]) |> mapToSignal { files -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in var items: [EmojiPagerContentComponent.Item] = [] @@ -1471,10 +1518,10 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { fillWithLoadingPlaceholders: true, items: [] ) - ], id: AnyHashable(value), version: version, isPreset: true), isSearching: false) + ], id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) return } - strongSelf.stickerSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value), version: version, isPreset: true), isSearching: false) + strongSelf.stickerSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) version += 1 })) } @@ -1488,7 +1535,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { externalBackground: nil, externalExpansionView: nil, customContentView: nil, - useOpaqueTheme: false, + useOpaqueTheme: self.useOpaqueTheme, hideBackground: false, stateContext: nil, addImage: nil diff --git a/submodules/TelegramUI/Components/EmojiActionIconComponent/Sources/EmojiActionIconComponent.swift b/submodules/TelegramUI/Components/EmojiActionIconComponent/Sources/EmojiActionIconComponent.swift index 8151591b782..6bee0832add 100644 --- a/submodules/TelegramUI/Components/EmojiActionIconComponent/Sources/EmojiActionIconComponent.swift +++ b/submodules/TelegramUI/Components/EmojiActionIconComponent/Sources/EmojiActionIconComponent.swift @@ -46,8 +46,20 @@ public final class EmojiActionIconComponent: Component { func update(component: EmojiActionIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { let size = CGSize(width: 24.0, height: 24.0) - if let fileId = component.fileId { - var iconSize = size + var iconSize = size + let content: EmojiStatusComponent.AnimationContent? + if let file = component.file { + if let dimensions = file.dimensions { + iconSize = dimensions.cgSize.aspectFitted(size) + } + content = .file(file: file) + } else if let fileId = component.fileId { + content = .customEmoji(fileId: fileId) + } else { + content = nil + } + + if let content { let icon: ComponentView if let current = self.icon { icon = current @@ -55,15 +67,7 @@ public final class EmojiActionIconComponent: Component { icon = ComponentView() self.icon = icon } - let content: EmojiStatusComponent.AnimationContent - if let file = component.file { - if let dimensions = file.dimensions { - iconSize = dimensions.cgSize.aspectFitted(size) - } - content = .file(file: file) - } else { - content = .customEmoji(fileId: fileId) - } + let _ = icon.update( transition: .immediate, component: AnyComponent(EmojiStatusComponent( diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index a668a5f97db..5fca0d36fc5 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -615,7 +615,7 @@ public final class EmojiStatusSelectionController: ViewController { })) } case let .category(value): - let resultSignal = self.context.engine.stickers.searchEmoji(emojiString: value) + let resultSignal = self.context.engine.stickers.searchEmoji(category: value) |> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in var items: [EmojiPagerContentComponent.Item] = [] @@ -687,11 +687,11 @@ public final class EmojiStatusSelectionController: ViewController { fillWithLoadingPlaceholders: true, items: [] ) - ], id: AnyHashable(value), version: version, isPreset: true), isSearching: false) + ], id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) return } - self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value), version: version, isPreset: true), isSearching: false) + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) version += 1 })) } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index f71777f833d..30d4ec87a1e 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -1744,7 +1744,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { private var textField: EmojiSearchTextField? private var tapRecognizer: UITapGestureRecognizer? - private(set) var currentPresetSearchTerm: [String]? + private(set) var currentPresetSearchTerm: EmojiSearchCategories.Group? private var params: Params? @@ -2570,7 +2570,7 @@ public final class EmojiPagerContentComponent: Component { public enum SearchQuery: Equatable { case text(value: String, language: String) - case category(value: [String]) + case category(value: EmojiSearchCategories.Group) } public enum ItemContent: Equatable { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift index e7208d01ca2..d02f26e64f5 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift @@ -1589,6 +1589,13 @@ public extension EmojiPagerContentComponent { return emojiItems } + enum StickersSubject { + case profilePhotoEmojiSelection + case groupPhotoEmojiSelection + case chatStickers + case greetingStickers + } + static func stickerInputData( context: AccountContext, animationCache: AnimationCache, @@ -1601,8 +1608,7 @@ public extension EmojiPagerContentComponent { forceHasPremium: Bool, hasEdit: Bool = false, searchIsPlaceholderOnly: Bool = true, - isProfilePhotoEmojiSelection: Bool = false, - isGroupPhotoEmojiSelection: Bool = false, + subject: StickersSubject = .chatStickers, hideBackground: Bool = false ) -> Signal { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) @@ -1653,11 +1659,41 @@ public extension EmojiPagerContentComponent { let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings let searchCategories: Signal - if isProfilePhotoEmojiSelection || isGroupPhotoEmojiSelection { + switch subject { + case .groupPhotoEmojiSelection, .profilePhotoEmojiSelection: searchCategories = context.engine.stickers.emojiSearchCategories(kind: .avatar) - } else { - searchCategories = context.engine.stickers.emojiSearchCategories(kind: .emoji) + case .chatStickers, .greetingStickers: + searchCategories = context.engine.stickers.emojiSearchCategories(kind: .combinedChatStickers) + |> map { result -> EmojiSearchCategories? in + guard let result else { + return nil + } + + var groups: [EmojiSearchCategories.Group] = [] + groups = result.groups + if case .greetingStickers = subject { + if let index = groups.firstIndex(where: { group in + return group.kind == .greeting + }) { + let group = groups.remove(at: index) + groups.insert(group, at: 0) + } + } else if case .chatStickers = subject { + if let index = groups.firstIndex(where: { group in + return group.kind == .premium + }) { + let group = groups.remove(at: index) + groups.append(group) + } + } + + return EmojiSearchCategories( + hash: result.hash, + groups: groups + ) + } } + return combineLatest( context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: stickerOrderedItemListCollectionIds, namespaces: stickerNamespaces, aroundIndex: nil, count: 10000000), hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: false), @@ -2091,6 +2127,14 @@ public extension EmojiPagerContentComponent { ) } + let warpContentsOnEdges: Bool + switch subject { + case .profilePhotoEmojiSelection, .groupPhotoEmojiSelection: + warpContentsOnEdges = true + default: + warpContentsOnEdges = false + } + return EmojiPagerContentComponent( id: isMasks ? "masks" : "stickers", context: context, @@ -2103,7 +2147,7 @@ public extension EmojiPagerContentComponent { itemLayoutType: .detailed, itemContentUniqueId: nil, searchState: .empty(hasResults: false), - warpContentsOnEdges: isProfilePhotoEmojiSelection || isGroupPhotoEmojiSelection, + warpContentsOnEdges: warpContentsOnEdges, hideBackground: hideBackground, displaySearchWithPlaceholder: hasSearch ? strings.StickersSearch_SearchStickersPlaceholder : nil, searchCategories: searchCategories, diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift index 2ab27b09671..e8fb0bd8c9a 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift @@ -319,7 +319,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode })) } case let .category(value): - let resultSignal = self.context.engine.stickers.searchEmoji(emojiString: value) + let resultSignal = self.context.engine.stickers.searchEmoji(category: value) |> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in var items: [EmojiPagerContentComponent.Item] = [] @@ -333,7 +333,8 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode let item = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), - itemFile: itemFile, subgroupId: nil, + itemFile: itemFile, + subgroupId: nil, icon: .none, tintMode: animationData.isTemplate ? .primary : .none ) @@ -393,11 +394,11 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode fillWithLoadingPlaceholders: true, items: [] ) - ], id: AnyHashable(value), version: version, isPreset: true), isSearching: false) + ], id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) return } - self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value), version: version, isPreset: false), isSearching: false) + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: false), isSearching: false) version += 1 })) } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift index d3b6f069007..93c8f1e1645 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift @@ -104,7 +104,7 @@ final class EmojiSearchSearchBarComponent: Component { let useOpaqueTheme: Bool let textInputState: TextInputState let categories: EmojiSearchCategories? - let searchTermUpdated: ([String]?) -> Void + let searchTermUpdated: (EmojiSearchCategories.Group?) -> Void let activateTextInput: () -> Void init( @@ -115,7 +115,7 @@ final class EmojiSearchSearchBarComponent: Component { useOpaqueTheme: Bool, textInputState: TextInputState, categories: EmojiSearchCategories?, - searchTermUpdated: @escaping ([String]?) -> Void, + searchTermUpdated: @escaping (EmojiSearchCategories.Group?) -> Void, activateTextInput: @escaping () -> Void ) { self.context = context @@ -366,7 +366,7 @@ final class EmojiSearchSearchBarComponent: Component { self.componentState?.updated(transition: .easeInOut(duration: 0.2)) if let _ = self.selectedItem, let categories = component.categories, let group = categories.groups.first(where: { $0.id == itemId }) { - component.searchTermUpdated(group.identifiers) + component.searchTermUpdated(group) if let itemComponentView = itemView.view.view { var offset = self.scrollView.contentOffset.x diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift index c601dbf3080..c4943a46a79 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift @@ -1081,7 +1081,7 @@ public final class GifPagerContentComponent: Component { case .text: break case let .category(value): - component.inputInteraction.updateSearchQuery(value) + component.inputInteraction.updateSearchQuery(value.identifiers) } }) self.visibleSearchHeader = visibleSearchHeader diff --git a/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerPackCurrentItem.swift b/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerPackCurrentItem.swift index cc3e992b8c1..a2548b83686 100644 --- a/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerPackCurrentItem.swift +++ b/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerPackCurrentItem.swift @@ -493,7 +493,7 @@ class GroupStickerPackCurrentItemNode: ItemListRevealOptionsItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/TelegramUI/Components/ItemListDatePickerItem/Sources/ItemListDatePickerItem.swift b/submodules/TelegramUI/Components/ItemListDatePickerItem/Sources/ItemListDatePickerItem.swift index 6951152d8c5..b5e0920f39b 100644 --- a/submodules/TelegramUI/Components/ItemListDatePickerItem/Sources/ItemListDatePickerItem.swift +++ b/submodules/TelegramUI/Components/ItemListDatePickerItem/Sources/ItemListDatePickerItem.swift @@ -301,7 +301,7 @@ public class ItemListDatePickerItemNode: ListViewItemNode, ItemListItemNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/BUILD b/submodules/TelegramUI/Components/ListActionItemComponent/BUILD index ee143c4bab2..e9b7043eb54 100644 --- a/submodules/TelegramUI/Components/ListActionItemComponent/BUILD +++ b/submodules/TelegramUI/Components/ListActionItemComponent/BUILD @@ -15,6 +15,7 @@ swift_library( "//submodules/TelegramPresentationData", "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/SwitchNode", + "//submodules/CheckNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift index 0b0fe45934f..11620ebcf9a 100644 --- a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift +++ b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift @@ -5,11 +5,13 @@ import ComponentFlow import TelegramPresentationData import ListSectionComponent import SwitchNode +import CheckNode public final class ListActionItemComponent: Component { public enum ToggleStyle { case regular case icons + case lock } public struct Toggle: Equatable { @@ -42,10 +44,23 @@ public final class ListActionItemComponent: Component { } } + public struct CustomAccessory: Equatable { + public var component: AnyComponentWithIdentity + public var insets: UIEdgeInsets + public var isInteractive: Bool + + public init(component: AnyComponentWithIdentity, insets: UIEdgeInsets = UIEdgeInsets(), isInteractive: Bool = false) { + self.component = component + self.insets = insets + self.isInteractive = isInteractive + } + } + public enum Accessory: Equatable { case arrow case toggle(Toggle) case activity + case custom(CustomAccessory) } public enum IconInsets: Equatable { @@ -65,22 +80,57 @@ public final class ListActionItemComponent: Component { } } + public enum LeftIcon: Equatable { + public final class Check: Equatable { + public let isSelected: Bool + public let toggle: (() -> Void)? + + public init(isSelected: Bool, toggle: (() -> Void)?) { + self.isSelected = isSelected + self.toggle = toggle + } + + public static func ==(lhs: Check, rhs: Check) -> Bool { + if lhs === rhs { + return true + } + if lhs.isSelected != rhs.isSelected { + return false + } + if (lhs.toggle == nil) != (rhs.toggle == nil) { + return false + } + return true + } + } + + case check(Check) + case custom(AnyComponentWithIdentity) + } + + public enum Highlighting { + case `default` + case disabled + } + public let theme: PresentationTheme public let title: AnyComponent public let contentInsets: UIEdgeInsets - public let leftIcon: AnyComponentWithIdentity? + public let leftIcon: LeftIcon? public let icon: Icon? public let accessory: Accessory? public let action: ((UIView) -> Void)? + public let highlighting: Highlighting public init( theme: PresentationTheme, title: AnyComponent, contentInsets: UIEdgeInsets = UIEdgeInsets(top: 12.0, left: 0.0, bottom: 12.0, right: 0.0), - leftIcon: AnyComponentWithIdentity? = nil, + leftIcon: LeftIcon? = nil, icon: Icon? = nil, accessory: Accessory? = .arrow, - action: ((UIView) -> Void)? + action: ((UIView) -> Void)?, + highlighting: Highlighting = .default ) { self.theme = theme self.title = title @@ -89,6 +139,7 @@ public final class ListActionItemComponent: Component { self.icon = icon self.accessory = accessory self.action = action + self.highlighting = highlighting } public static func ==(lhs: ListActionItemComponent, rhs: ListActionItemComponent) -> Bool { @@ -113,18 +164,96 @@ public final class ListActionItemComponent: Component { if (lhs.action == nil) != (rhs.action == nil) { return false } + if lhs.highlighting != rhs.highlighting { + return false + } return true } + private final class CheckView: HighlightTrackingButton { + private var checkLayer: CheckLayer? + private var theme: PresentationTheme? + + var action: (() -> Void)? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + + self.highligthedChanged = { [weak self] highlighted in + if let self, self.bounds.width > 0.0 { + let animateScale = true + + let topScale: CGFloat = (self.bounds.width - 8.0) / self.bounds.width + let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width + + if highlighted { + self.layer.removeAnimation(forKey: "opacity") + self.layer.removeAnimation(forKey: "transform.scale") + + if animateScale { + let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + transition.setScale(layer: self.layer, scale: topScale) + } + } else { + if animateScale { + let transition = Transition(animation: .none) + transition.setScale(layer: self.layer, scale: 1.0) + + self.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in + guard let self else { + return + } + + self.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue) + }) + } + } + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + self.action?() + } + + func update(size: CGSize, theme: PresentationTheme, isSelected: Bool, transition: Transition) { + let checkLayer: CheckLayer + if let current = self.checkLayer { + checkLayer = current + } else { + checkLayer = CheckLayer(theme: CheckNodeTheme(theme: theme, style: .plain), content: .check) + self.checkLayer = checkLayer + self.layer.addSublayer(checkLayer) + } + + if self.theme !== theme { + self.theme = theme + + checkLayer.theme = CheckNodeTheme(theme: theme, style: .plain) + } + + checkLayer.frame = CGRect(origin: CGPoint(), size: size) + checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate) + } + } + public final class View: HighlightTrackingButton, ListSectionComponent.ChildView { private let title = ComponentView() private var leftIcon: ComponentView? + private var leftCheckView: CheckView? private var icon: ComponentView? private var arrowView: UIImageView? private var switchNode: SwitchNode? private var iconSwitchNode: IconSwitchNode? private var activityIndicatorView: UIActivityIndicatorView? + private var customAccessoryView: ComponentView? private var component: ListActionItemComponent? @@ -147,6 +276,9 @@ public final class ListActionItemComponent: Component { guard let self, let component = self.component, component.action != nil else { return } + if isHighlighted, component.highlighting == .disabled { + return + } if case .toggle = component.accessory, component.action == nil { return } @@ -180,6 +312,9 @@ public final class ListActionItemComponent: Component { let themeUpdated = component.theme !== previousComponent?.theme + var customAccessorySize: CGSize? + var customAccessoryTransition: Transition = transition + var contentLeftInset: CGFloat = 16.0 let contentRightInset: CGFloat switch component.accessory { @@ -195,13 +330,41 @@ public final class ListActionItemComponent: Component { contentRightInset = 76.0 case .activity: contentRightInset = 76.0 + case let .custom(customAccessory): + if case let .custom(previousCustomAccessory) = previousComponent?.accessory, previousCustomAccessory.component.id != customAccessory.component.id { + self.customAccessoryView?.view?.removeFromSuperview() + self.customAccessoryView = nil + } + + let customAccessoryView: ComponentView + if let current = self.customAccessoryView { + customAccessoryView = current + } else { + customAccessoryTransition = customAccessoryTransition.withAnimation(.none) + customAccessoryView = ComponentView() + self.customAccessoryView = customAccessoryView + } + + let customAccessorySizeValue = customAccessoryView.update( + transition: customAccessoryTransition, + component: customAccessory.component.component, + environment: {}, + containerSize: availableSize + ) + customAccessorySize = customAccessorySizeValue + contentRightInset = customAccessorySizeValue.width + customAccessory.insets.left + customAccessory.insets.right } var contentHeight: CGFloat = 0.0 contentHeight += component.contentInsets.top - if component.leftIcon != nil { - contentLeftInset += 46.0 + if let leftIcon = component.leftIcon { + switch leftIcon { + case .check: + contentLeftInset += 46.0 + case .custom: + contentLeftInset += 46.0 + } } let titleSize = self.title.update( @@ -275,39 +438,103 @@ public final class ListActionItemComponent: Component { } if let leftIconValue = component.leftIcon { - if previousComponent?.leftIcon?.id != leftIconValue.id, let leftIcon = self.leftIcon { - self.leftIcon = nil - if let iconView = leftIcon.view { - transition.setAlpha(view: iconView, alpha: 0.0, completion: { [weak iconView] _ in - iconView?.removeFromSuperview() + switch leftIconValue { + case let .check(check): + if let leftIcon = self.leftIcon { + self.leftIcon = nil + if let iconView = leftIcon.view { + transition.setAlpha(view: iconView, alpha: 0.0, completion: { [weak iconView] _ in + iconView?.removeFromSuperview() + }) + } + } + + let leftCheckView: CheckView + var animateIn = false + if let current = self.leftCheckView { + leftCheckView = current + } else { + animateIn = true + leftCheckView = CheckView() + self.leftCheckView = leftCheckView + self.addSubview(leftCheckView) + + leftCheckView.action = { [weak self] in + guard let self, let component = self.component else { + return + } + if case let .check(check) = component.leftIcon { + check.toggle?() + } + } + } + + leftCheckView.isUserInteractionEnabled = check.toggle != nil + + let checkSize = CGSize(width: 22.0, height: 22.0) + let checkFrame = CGRect(origin: CGPoint(x: floor((contentLeftInset - checkSize.width) * 0.5), y: floor((contentHeight - checkSize.height) * 0.5)), size: checkSize) + + if animateIn { + leftCheckView.frame = CGRect(origin: CGPoint(x: -checkSize.width, y: self.bounds.height == 0.0 ? checkFrame.minY : floor((self.bounds.height - checkSize.height) * 0.5)), size: checkFrame.size) + transition.setPosition(view: leftCheckView, position: checkFrame.center) + transition.setBounds(view: leftCheckView, bounds: CGRect(origin: CGPoint(), size: checkFrame.size)) + leftCheckView.update(size: checkFrame.size, theme: component.theme, isSelected: check.isSelected, transition: .immediate) + } else { + transition.setPosition(view: leftCheckView, position: checkFrame.center) + transition.setBounds(view: leftCheckView, bounds: CGRect(origin: CGPoint(), size: checkFrame.size)) + leftCheckView.update(size: checkFrame.size, theme: component.theme, isSelected: check.isSelected, transition: transition) + } + case let .custom(customLeftIcon): + var resetLeftIcon = false + if case let .custom(previousCustomLeftIcon) = previousComponent?.leftIcon { + if previousCustomLeftIcon.id != customLeftIcon.id { + resetLeftIcon = true + } + } else { + resetLeftIcon = true + } + if resetLeftIcon { + if let leftIcon = self.leftIcon { + self.leftIcon = nil + if let iconView = leftIcon.view { + transition.setAlpha(view: iconView, alpha: 0.0, completion: { [weak iconView] _ in + iconView?.removeFromSuperview() + }) + } + } + } + if let leftCheckView = self.leftCheckView { + self.leftCheckView = nil + transition.setAlpha(view: leftCheckView, alpha: 0.0, completion: { [weak leftCheckView] _ in + leftCheckView?.removeFromSuperview() }) } - } - - var leftIconTransition = transition - let leftIcon: ComponentView - if let current = self.leftIcon { - leftIcon = current - } else { - leftIconTransition = leftIconTransition.withAnimation(.none) - leftIcon = ComponentView() - self.leftIcon = leftIcon - } - - let leftIconSize = leftIcon.update( - transition: leftIconTransition, - component: leftIconValue.component, - environment: {}, - containerSize: CGSize(width: availableSize.width, height: availableSize.height) - ) - let leftIconFrame = CGRect(origin: CGPoint(x: floor((contentLeftInset - leftIconSize.width) * 0.5), y: floor((min(60.0, contentHeight) - leftIconSize.height) * 0.5)), size: leftIconSize) - if let leftIconView = leftIcon.view { - if leftIconView.superview == nil { - leftIconView.isUserInteractionEnabled = false - self.addSubview(leftIconView) - transition.animateAlpha(view: leftIconView, from: 0.0, to: 1.0) + + var leftIconTransition = transition + let leftIcon: ComponentView + if let current = self.leftIcon { + leftIcon = current + } else { + leftIconTransition = leftIconTransition.withAnimation(.none) + leftIcon = ComponentView() + self.leftIcon = leftIcon + } + + let leftIconSize = leftIcon.update( + transition: leftIconTransition, + component: customLeftIcon.component, + environment: {}, + containerSize: CGSize(width: availableSize.width, height: availableSize.height) + ) + let leftIconFrame = CGRect(origin: CGPoint(x: floor((contentLeftInset - leftIconSize.width) * 0.5), y: floor((min(60.0, contentHeight) - leftIconSize.height) * 0.5)), size: leftIconSize) + if let leftIconView = leftIcon.view { + if leftIconView.superview == nil { + leftIconView.isUserInteractionEnabled = false + self.addSubview(leftIconView) + transition.animateAlpha(view: leftIconView, from: 0.0, to: 1.0) + } + leftIconTransition.setFrame(view: leftIconView, frame: leftIconFrame) } - leftIconTransition.setFrame(view: leftIconView, frame: leftIconFrame) } } else { if let leftIcon = self.leftIcon { @@ -318,6 +545,12 @@ public final class ListActionItemComponent: Component { }) } } + if let leftCheckView = self.leftCheckView { + self.leftCheckView = nil + transition.setAlpha(view: leftCheckView, alpha: 0.0, completion: { [weak leftCheckView] _ in + leftCheckView?.removeFromSuperview() + }) + } } if case .arrow = component.accessory { @@ -374,6 +607,7 @@ public final class ListActionItemComponent: Component { } } } + switchNode.isUserInteractionEnabled = toggle.isInteractive if updateSwitchTheme { @@ -385,17 +619,21 @@ public final class ListActionItemComponent: Component { let switchSize = CGSize(width: 51.0, height: 31.0) let switchFrame = CGRect(origin: CGPoint(x: availableSize.width - 16.0 - switchSize.width, y: floor((min(60.0, contentHeight) - switchSize.height) * 0.5)), size: switchSize) switchTransition.setFrame(view: switchNode.view, frame: switchFrame) - case .icons: + case .icons, .lock: let switchNode: IconSwitchNode var switchTransition = transition var updateSwitchTheme = themeUpdated if let current = self.iconSwitchNode { switchNode = current - switchNode.setOn(toggle.isOn, animated: !transition.animation.isImmediate) + switchNode.updateIsLocked(toggle.style == .lock) + if switchNode.isOn != toggle.isOn { + switchNode.setOn(toggle.isOn, animated: !transition.animation.isImmediate) + } } else { switchTransition = switchTransition.withAnimation(.none) updateSwitchTheme = true switchNode = IconSwitchNode() + switchNode.updateIsLocked(toggle.style == .lock) switchNode.setOn(toggle.isOn, animated: false) self.iconSwitchNode = switchNode self.addSubview(switchNode.view) @@ -466,6 +704,24 @@ public final class ListActionItemComponent: Component { } } + if case let .custom(customAccessory) = component.accessory, let customAccessoryView = self.customAccessoryView, let customAccessorySize { + let activityAccessoryFrame = CGRect(origin: CGPoint(x: availableSize.width - customAccessory.insets.right - customAccessorySize.width, y: floor((contentHeight - customAccessorySize.height) * 0.5)), size: customAccessorySize) + if let customAccessoryComponentView = customAccessoryView.view { + if customAccessoryComponentView.superview == nil { + customAccessoryComponentView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0) + self.addSubview(customAccessoryComponentView) + } + customAccessoryComponentView.isUserInteractionEnabled = customAccessory.isInteractive + customAccessoryTransition.setPosition(view: customAccessoryComponentView, position: CGPoint(x: activityAccessoryFrame.maxX, y: activityAccessoryFrame.minY)) + customAccessoryTransition.setBounds(view: customAccessoryComponentView, bounds: CGRect(origin: CGPoint(), size: activityAccessoryFrame.size)) + } + } else { + if let customAccessoryView = self.customAccessoryView { + self.customAccessoryView = nil + customAccessoryView.view?.removeFromSuperview() + } + } + self.separatorInset = contentLeftInset return CGSize(width: availableSize.width, height: contentHeight) diff --git a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift index 4be6ef70f60..64e26e0594a 100644 --- a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift +++ b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift @@ -51,6 +51,7 @@ public final class ListMultilineTextFieldItemComponent: Component { public let emptyLineHandling: EmptyLineHandling public let updated: ((String) -> Void)? public let returnKeyAction: (() -> Void)? + public let backspaceKeyAction: (() -> Void)? public let textUpdateTransition: Transition public let tag: AnyObject? @@ -68,8 +69,9 @@ public final class ListMultilineTextFieldItemComponent: Component { characterLimit: Int? = nil, displayCharacterLimit: Bool = false, emptyLineHandling: EmptyLineHandling = .allowed, - updated: ((String) -> Void)?, + updated: ((String) -> Void)? = nil, returnKeyAction: (() -> Void)? = nil, + backspaceKeyAction: (() -> Void)? = nil, textUpdateTransition: Transition = .immediate, tag: AnyObject? = nil ) { @@ -88,6 +90,7 @@ public final class ListMultilineTextFieldItemComponent: Component { self.emptyLineHandling = emptyLineHandling self.updated = updated self.returnKeyAction = returnKeyAction + self.backspaceKeyAction = backspaceKeyAction self.textUpdateTransition = textUpdateTransition self.tag = tag } @@ -138,23 +141,12 @@ public final class ListMultilineTextFieldItemComponent: Component { return true } - private final class TextField: UITextField { - var sideInset: CGFloat = 0.0 - - override func textRect(forBounds bounds: CGRect) -> CGRect { - return CGRect(origin: CGPoint(x: self.sideInset, y: 0.0), size: CGSize(width: bounds.width - self.sideInset * 2.0, height: bounds.height)) - } - - override func editingRect(forBounds bounds: CGRect) -> CGRect { - return CGRect(origin: CGPoint(x: self.sideInset, y: 0.0), size: CGSize(width: bounds.width - self.sideInset * 2.0, height: bounds.height)) - } - } - public final class View: UIView, ListSectionComponent.ChildView, ComponentTaggedView { private let textField = ComponentView() private let textFieldExternalState = TextFieldComponent.ExternalState() private let placeholder = ComponentView() + private var customPlaceholder: ComponentView? private var measureTextLimitLabel: ComponentView? private var textLimitLabel: ComponentView? @@ -287,6 +279,12 @@ public final class ListMultilineTextFieldItemComponent: Component { return } component.returnKeyAction?() + }, + backspaceKeyAction: { [weak self] in + guard let self, let component = self.component else { + return + } + component.backspaceKeyAction?() } )), environment: {}, @@ -377,6 +375,51 @@ public final class ListMultilineTextFieldItemComponent: Component { return size } + + public func updateCustomPlaceholder(value: String, size: CGSize, transition: Transition) { + guard let component = self.component else { + return + } + + let verticalInset: CGFloat = 12.0 + let sideInset: CGFloat = 16.0 + + if !value.isEmpty { + let customPlaceholder: ComponentView + var customPlaceholderTransition = transition + if let current = self.customPlaceholder { + customPlaceholder = current + } else { + customPlaceholderTransition = customPlaceholderTransition.withAnimation(.none) + customPlaceholder = ComponentView() + self.customPlaceholder = customPlaceholder + } + + let placeholderSize = customPlaceholder.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: value.isEmpty ? " " : value, font: Font.regular(17.0), textColor: component.theme.list.itemPlaceholderTextColor)) + )), + environment: {}, + containerSize: CGSize(width: size.width - sideInset * 2.0, height: 100.0) + ) + let placeholderFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: placeholderSize) + if let placeholderView = customPlaceholder.view { + if placeholderView.superview == nil { + placeholderView.layer.anchorPoint = CGPoint() + placeholderView.isUserInteractionEnabled = false + self.insertSubview(placeholderView, at: 0) + } + transition.setPosition(view: placeholderView, position: placeholderFrame.origin) + placeholderView.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size) + + placeholderView.isHidden = self.textFieldExternalState.hasText + } + } else if let customPlaceholder = self.customPlaceholder { + self.customPlaceholder = nil + customPlaceholder.view?.removeFromSuperview() + } + } } public func makeView() -> View { diff --git a/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift index 9071b79f609..e2727290a06 100644 --- a/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift +++ b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift @@ -10,6 +10,287 @@ public protocol ListSectionComponentChildView: AnyObject { var separatorInset: CGFloat { get } } +public final class ListSectionContentView: UIView { + public final class ItemView: UIView { + public let contents = ComponentView() + public let separatorLayer = SimpleLayer() + public let highlightLayer = SimpleLayer() + + override public init(frame: CGRect) { + super.init(frame: frame) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + } + + public final class ReadyItem { + public let id: AnyHashable + public let itemView: ItemView + public let size: CGSize + public let transition: Transition + + public init(id: AnyHashable, itemView: ItemView, size: CGSize, transition: Transition) { + self.id = id + self.itemView = itemView + self.size = size + self.transition = transition + } + } + + public final class Configuration { + public let theme: PresentationTheme + public let displaySeparators: Bool + public let extendsItemHighlightToSection: Bool + public let background: ListSectionComponent.Background + + public init( + theme: PresentationTheme, + displaySeparators: Bool, + extendsItemHighlightToSection: Bool, + background: ListSectionComponent.Background + ) { + self.theme = theme + self.displaySeparators = displaySeparators + self.extendsItemHighlightToSection = extendsItemHighlightToSection + self.background = background + } + } + + public struct UpdateResult { + public var size: CGSize + public var backgroundFrame: CGRect + + public init(size: CGSize, backgroundFrame: CGRect) { + self.size = size + self.backgroundFrame = backgroundFrame + } + } + + private let contentSeparatorContainerLayer: SimpleLayer + private let contentHighlightContainerLayer: SimpleLayer + private let contentItemContainerView: UIView + + public let externalContentBackgroundView: DynamicCornerRadiusView + + public var itemViews: [AnyHashable: ItemView] = [:] + private var highlightedItemId: AnyHashable? + + private var configuration: Configuration? + + public override init(frame: CGRect) { + self.contentSeparatorContainerLayer = SimpleLayer() + self.contentHighlightContainerLayer = SimpleLayer() + self.contentItemContainerView = UIView() + + self.externalContentBackgroundView = DynamicCornerRadiusView() + + super.init(frame: CGRect()) + + self.layer.addSublayer(self.contentSeparatorContainerLayer) + self.layer.addSublayer(self.contentHighlightContainerLayer) + self.addSubview(self.contentItemContainerView) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func updateHighlightedItem(itemId: AnyHashable?) { + guard let configuration = self.configuration else { + return + } + + if self.highlightedItemId == itemId { + return + } + let previousHighlightedItemId = self.highlightedItemId + self.highlightedItemId = itemId + + if configuration.extendsItemHighlightToSection { + let transition: Transition + let backgroundColor: UIColor + if itemId != nil { + transition = .immediate + backgroundColor = configuration.theme.list.itemHighlightedBackgroundColor + } else { + transition = .easeInOut(duration: 0.2) + backgroundColor = configuration.theme.list.itemBlocksBackgroundColor + } + + self.externalContentBackgroundView.updateColor(color: backgroundColor, transition: transition) + } else { + if let previousHighlightedItemId, let previousItemView = self.itemViews[previousHighlightedItemId] { + Transition.easeInOut(duration: 0.2).setBackgroundColor(layer: previousItemView.highlightLayer, color: .clear) + } + if let itemId, let itemView = self.itemViews[itemId] { + Transition.immediate.setBackgroundColor(layer: itemView.highlightLayer, color: configuration.theme.list.itemHighlightedBackgroundColor) + } + } + } + + public func update(configuration: Configuration, width: CGFloat, leftInset: CGFloat, readyItems: [ReadyItem], transition: Transition) -> UpdateResult { + self.configuration = configuration + + switch configuration.background { + case .all, .range: + self.clipsToBounds = true + case let .none(clipped): + self.clipsToBounds = clipped + } + + let backgroundColor: UIColor + if self.highlightedItemId != nil && configuration.extendsItemHighlightToSection { + backgroundColor = configuration.theme.list.itemHighlightedBackgroundColor + } else { + backgroundColor = configuration.theme.list.itemBlocksBackgroundColor + } + self.externalContentBackgroundView.updateColor(color: backgroundColor, transition: transition) + + var innerContentHeight: CGFloat = 0.0 + var validItemIds: [AnyHashable] = [] + for index in 0 ..< readyItems.count { + let readyItem = readyItems[index] + validItemIds.append(readyItem.id) + + let itemFrame = CGRect(origin: CGPoint(x: leftInset, y: innerContentHeight), size: readyItem.size) + if let itemComponentView = readyItem.itemView.contents.view { + var isAdded = false + if itemComponentView.superview == nil { + isAdded = true + readyItem.itemView.addSubview(itemComponentView) + self.contentItemContainerView.addSubview(readyItem.itemView) + self.contentSeparatorContainerLayer.addSublayer(readyItem.itemView.separatorLayer) + self.contentHighlightContainerLayer.addSublayer(readyItem.itemView.highlightLayer) + transition.animateAlpha(view: readyItem.itemView, from: 0.0, to: 1.0) + transition.animateAlpha(layer: readyItem.itemView.separatorLayer, from: 0.0, to: 1.0) + transition.animateAlpha(layer: readyItem.itemView.highlightLayer, from: 0.0, to: 1.0) + + let itemId = readyItem.id + if let itemComponentView = itemComponentView as? ListSectionComponentChildView { + itemComponentView.customUpdateIsHighlighted = { [weak self] isHighlighted in + guard let self else { + return + } + self.updateHighlightedItem(itemId: isHighlighted ? itemId : nil) + } + } + } + var separatorInset: CGFloat = 0.0 + if let itemComponentView = itemComponentView as? ListSectionComponentChildView { + separatorInset = itemComponentView.separatorInset + } + + let itemSeparatorFrame = CGRect(origin: CGPoint(x: separatorInset, y: itemFrame.maxY - UIScreenPixel), size: CGSize(width: width - separatorInset, height: UIScreenPixel)) + + if isAdded && itemComponentView is ListSubSectionComponent.View { + readyItem.itemView.frame = itemFrame + readyItem.itemView.clipsToBounds = true + readyItem.itemView.frame = CGRect(origin: CGPoint(x: itemFrame.minX, y: itemFrame.minY), size: CGSize(width: itemFrame.width, height: 0.0)) + let itemView = readyItem.itemView + transition.setFrame(view: readyItem.itemView, frame: itemFrame, completion: { [weak itemView] completed in + if completed { + itemView?.clipsToBounds = false + } + }) + + readyItem.itemView.separatorLayer.frame = CGRect(origin: CGPoint(x: itemSeparatorFrame.minX, y: itemFrame.minY), size: CGSize(width: itemSeparatorFrame.width, height: 0.0)) + transition.setFrame(layer: readyItem.itemView.separatorLayer, frame: itemSeparatorFrame) + } else { + readyItem.transition.setFrame(view: readyItem.itemView, frame: itemFrame) + readyItem.transition.setFrame(layer: readyItem.itemView.separatorLayer, frame: itemSeparatorFrame) + } + + let itemSeparatorTopOffset: CGFloat = index == 0 ? 0.0 : -UIScreenPixel + let itemHighlightFrame = CGRect(origin: CGPoint(x: itemFrame.minX, y: itemFrame.minY + itemSeparatorTopOffset), size: CGSize(width: itemFrame.width, height: itemFrame.height - itemSeparatorTopOffset)) + readyItem.transition.setFrame(layer: readyItem.itemView.highlightLayer, frame: itemHighlightFrame) + + readyItem.transition.setFrame(view: itemComponentView, frame: CGRect(origin: CGPoint(), size: itemFrame.size)) + + let separatorAlpha: CGFloat + if configuration.displaySeparators { + if index != readyItems.count - 1 { + separatorAlpha = 1.0 + } else { + separatorAlpha = 0.0 + } + } else { + separatorAlpha = 0.0 + } + readyItem.transition.setAlpha(layer: readyItem.itemView.separatorLayer, alpha: separatorAlpha) + readyItem.itemView.separatorLayer.backgroundColor = configuration.theme.list.itemBlocksSeparatorColor.cgColor + } + innerContentHeight += readyItem.size.height + } + + var removedItemIds: [AnyHashable] = [] + for (id, itemView) in self.itemViews { + if !validItemIds.contains(id) { + removedItemIds.append(id) + + if let itemComponentView = itemView.contents.view, itemComponentView is ListSubSectionComponent.View { + itemView.clipsToBounds = true + transition.setFrame(view: itemView, frame: CGRect(origin: itemView.frame.origin, size: CGSize(width: itemView.bounds.width, height: 0.0))) + transition.setFrame(layer: itemView.separatorLayer, frame: CGRect(origin: CGPoint(x: itemView.separatorLayer.frame.minX, y: itemView.frame.minY), size: itemView.separatorLayer.bounds.size)) + } + + transition.setAlpha(view: itemView, alpha: 0.0, completion: { [weak itemView] _ in + itemView?.removeFromSuperview() + }) + let separatorLayer = itemView.separatorLayer + transition.setAlpha(layer: separatorLayer, alpha: 0.0, completion: { [weak separatorLayer] _ in + separatorLayer?.removeFromSuperlayer() + }) + let highlightLayer = itemView.highlightLayer + transition.setAlpha(layer: highlightLayer, alpha: 0.0, completion: { [weak highlightLayer] _ in + highlightLayer?.removeFromSuperlayer() + }) + } + } + for id in removedItemIds { + self.itemViews.removeValue(forKey: id) + } + + let size = CGSize(width: width, height: innerContentHeight) + + transition.setFrame(view: self.contentItemContainerView, frame: CGRect(origin: CGPoint(), size: size)) + transition.setFrame(layer: self.contentSeparatorContainerLayer, frame: CGRect(origin: CGPoint(), size: size)) + transition.setFrame(layer: self.contentHighlightContainerLayer, frame: CGRect(origin: CGPoint(), size: size)) + + let backgroundFrame: CGRect + var backgroundAlpha: CGFloat = 1.0 + var contentCornerRadius: CGFloat = 11.0 + switch configuration.background { + case let .none(clipped): + backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size) + backgroundAlpha = 0.0 + self.externalContentBackgroundView.update(size: backgroundFrame.size, corners: DynamicCornerRadiusView.Corners(minXMinY: 11.0, maxXMinY: 11.0, minXMaxY: 11.0, maxXMaxY: 11.0), transition: transition) + if !clipped { + contentCornerRadius = 0.0 + } + case .all: + backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size) + self.externalContentBackgroundView.update(size: backgroundFrame.size, corners: DynamicCornerRadiusView.Corners(minXMinY: 11.0, maxXMinY: 11.0, minXMaxY: 11.0, maxXMaxY: 11.0), transition: transition) + case let .range(from, corners): + if let itemView = self.itemViews[from], itemView.frame.minY < size.height { + backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: itemView.frame.minY), size: CGSize(width: size.width, height: size.height - itemView.frame.minY)) + } else { + backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height), size: CGSize(width: size.width, height: 0.0)) + } + self.externalContentBackgroundView.update(size: backgroundFrame.size, corners: corners, transition: transition) + } + transition.setFrame(view: self.externalContentBackgroundView, frame: backgroundFrame) + transition.setAlpha(view: self.externalContentBackgroundView, alpha: backgroundAlpha) + transition.setCornerRadius(layer: self.layer, cornerRadius: contentCornerRadius) + + return UpdateResult( + size: size, + backgroundFrame: backgroundFrame + ) + } +} + public final class ListSectionComponent: Component { public typealias ChildView = ListSectionComponentChildView @@ -24,7 +305,6 @@ public final class ListSectionComponent: Component { public let header: AnyComponent? public let footer: AnyComponent? public let items: [AnyComponentWithIdentity] - public let itemUpdateOrder: [AnyHashable]? public let displaySeparators: Bool public let extendsItemHighlightToSection: Bool @@ -34,7 +314,6 @@ public final class ListSectionComponent: Component { header: AnyComponent?, footer: AnyComponent?, items: [AnyComponentWithIdentity], - itemUpdateOrder: [AnyHashable]? = nil, displaySeparators: Bool = true, extendsItemHighlightToSection: Bool = false ) { @@ -43,7 +322,6 @@ public final class ListSectionComponent: Component { self.header = header self.footer = footer self.items = items - self.itemUpdateOrder = itemUpdateOrder self.displaySeparators = displaySeparators self.extendsItemHighlightToSection = extendsItemHighlightToSection } @@ -64,9 +342,6 @@ public final class ListSectionComponent: Component { if lhs.items != rhs.items { return false } - if lhs.itemUpdateOrder != rhs.itemUpdateOrder { - return false - } if lhs.displaySeparators != rhs.displaySeparators { return false } @@ -76,103 +351,34 @@ public final class ListSectionComponent: Component { return true } - private final class ItemView: UIView { - let contents = ComponentView() - let separatorLayer = SimpleLayer() - let highlightLayer = SimpleLayer() - - override init(frame: CGRect) { - super.init(frame: frame) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - } - public final class View: UIView { - private let contentView: UIView - private let contentSeparatorContainerLayer: SimpleLayer - private let contentHighlightContainerLayer: SimpleLayer - private let contentItemContainerView: UIView - private let contentBackgroundView: DynamicCornerRadiusView + private let contentView: ListSectionContentView private var header: ComponentView? private var footer: ComponentView? - private var itemViews: [AnyHashable: ItemView] = [:] - - private var highlightedItemId: AnyHashable? private var component: ListSectionComponent? public override init(frame: CGRect) { - self.contentView = UIView() - self.contentView.clipsToBounds = true - - self.contentSeparatorContainerLayer = SimpleLayer() - self.contentHighlightContainerLayer = SimpleLayer() - self.contentItemContainerView = UIView() - - self.contentBackgroundView = DynamicCornerRadiusView() + self.contentView = ListSectionContentView() super.init(frame: CGRect()) - self.addSubview(self.contentBackgroundView) + self.addSubview(self.contentView.externalContentBackgroundView) self.addSubview(self.contentView) - - self.contentView.layer.addSublayer(self.contentSeparatorContainerLayer) - self.contentView.layer.addSublayer(self.contentHighlightContainerLayer) - self.contentView.addSubview(self.contentItemContainerView) } required public init?(coder: NSCoder) { preconditionFailure() } - private func updateHighlightedItem(itemId: AnyHashable?) { - if self.highlightedItemId == itemId { - return - } - let previousHighlightedItemId = self.highlightedItemId - self.highlightedItemId = itemId - - guard let component = self.component else { - return - } - - if component.extendsItemHighlightToSection { - let transition: Transition - let backgroundColor: UIColor - if itemId != nil { - transition = .immediate - backgroundColor = component.theme.list.itemHighlightedBackgroundColor - } else { - transition = .easeInOut(duration: 0.2) - backgroundColor = component.theme.list.itemBlocksBackgroundColor - } - - self.contentBackgroundView.updateColor(color: backgroundColor, transition: transition) - } else { - if let previousHighlightedItemId, let previousItemView = self.itemViews[previousHighlightedItemId] { - Transition.easeInOut(duration: 0.2).setBackgroundColor(layer: previousItemView.highlightLayer, color: .clear) - } - if let itemId, let itemView = self.itemViews[itemId] { - Transition.immediate.setBackgroundColor(layer: itemView.highlightLayer, color: component.theme.list.itemHighlightedBackgroundColor) - } - } + public func itemView(id: AnyHashable) -> UIView? { + return self.contentView.itemViews[id]?.contents.view } func update(component: ListSectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.component = component - let backgroundColor: UIColor - if self.highlightedItemId != nil && component.extendsItemHighlightToSection { - backgroundColor = component.theme.list.itemHighlightedBackgroundColor - } else { - backgroundColor = component.theme.list.itemBlocksBackgroundColor - } - self.contentBackgroundView.updateColor(color: backgroundColor, transition: transition) - let headerSideInset: CGFloat = 16.0 var contentHeight: CGFloat = 0.0 @@ -208,55 +414,19 @@ public final class ListSectionComponent: Component { } } - var innerContentHeight: CGFloat = 0.0 - var validItemIds: [AnyHashable] = [] - - struct ReadyItem { - var index: Int - var itemId: AnyHashable - var itemView: ItemView - var itemTransition: Transition - var itemSize: CGSize - - init(index: Int, itemId: AnyHashable, itemView: ItemView, itemTransition: Transition, itemSize: CGSize) { - self.index = index - self.itemId = itemId - self.itemView = itemView - self.itemTransition = itemTransition - self.itemSize = itemSize - } - } - - var readyItems: [ReadyItem] = [] - var itemUpdateOrder: [Int] = [] - if let itemUpdateOrderValue = component.itemUpdateOrder { - for id in itemUpdateOrderValue { - if let index = component.items.firstIndex(where: { $0.id == id }) { - if !itemUpdateOrder.contains(index) { - itemUpdateOrder.append(index) - } - } - } - } + var readyItems: [ListSectionContentView.ReadyItem] = [] for i in 0 ..< component.items.count { - if !itemUpdateOrder.contains(i) { - itemUpdateOrder.append(i) - } - } - - for i in itemUpdateOrder { let item = component.items[i] let itemId = item.id - validItemIds.append(itemId) - let itemView: ItemView + let itemView: ListSectionContentView.ItemView var itemTransition = transition - if let current = self.itemViews[itemId] { + if let current = self.contentView.itemViews[itemId] { itemView = current } else { itemTransition = itemTransition.withAnimation(.none) - itemView = ItemView() - self.itemViews[itemId] = itemView + itemView = ListSectionContentView.ItemView() + self.contentView.itemViews[itemId] = itemView itemView.contents.parentState = state } @@ -267,89 +437,27 @@ public final class ListSectionComponent: Component { containerSize: CGSize(width: availableSize.width, height: availableSize.height) ) - readyItems.append(ReadyItem( - index: i, - itemId: itemId, + readyItems.append(ListSectionContentView.ReadyItem( + id: itemId, itemView: itemView, - itemTransition: itemTransition, - itemSize: itemSize + size: itemSize, + transition: itemTransition )) } - for readyItem in readyItems.sorted(by: { $0.index < $1.index }) { - let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: innerContentHeight), size: readyItem.itemSize) - if let itemComponentView = readyItem.itemView.contents.view { - if itemComponentView.superview == nil { - readyItem.itemView.addSubview(itemComponentView) - self.contentItemContainerView.addSubview(readyItem.itemView) - self.contentSeparatorContainerLayer.addSublayer(readyItem.itemView.separatorLayer) - self.contentHighlightContainerLayer.addSublayer(readyItem.itemView.highlightLayer) - transition.animateAlpha(view: readyItem.itemView, from: 0.0, to: 1.0) - transition.animateAlpha(layer: readyItem.itemView.separatorLayer, from: 0.0, to: 1.0) - transition.animateAlpha(layer: readyItem.itemView.highlightLayer, from: 0.0, to: 1.0) - - let itemId = readyItem.itemId - if let itemComponentView = itemComponentView as? ChildView { - itemComponentView.customUpdateIsHighlighted = { [weak self] isHighlighted in - guard let self else { - return - } - self.updateHighlightedItem(itemId: isHighlighted ? itemId : nil) - } - } - } - var separatorInset: CGFloat = 0.0 - if let itemComponentView = itemComponentView as? ChildView { - separatorInset = itemComponentView.separatorInset - } - readyItem.itemTransition.setFrame(view: readyItem.itemView, frame: itemFrame) - - let itemSeparatorTopOffset: CGFloat = readyItem.index == 0 ? 0.0 : -UIScreenPixel - let itemHighlightFrame = CGRect(origin: CGPoint(x: itemFrame.minX, y: itemFrame.minY + itemSeparatorTopOffset), size: CGSize(width: itemFrame.width, height: itemFrame.height - itemSeparatorTopOffset)) - readyItem.itemTransition.setFrame(layer: readyItem.itemView.highlightLayer, frame: itemHighlightFrame) - - readyItem.itemTransition.setFrame(view: itemComponentView, frame: CGRect(origin: CGPoint(), size: itemFrame.size)) - - let itemSeparatorFrame = CGRect(origin: CGPoint(x: separatorInset, y: itemFrame.maxY - UIScreenPixel), size: CGSize(width: availableSize.width - separatorInset, height: UIScreenPixel)) - readyItem.itemTransition.setFrame(layer: readyItem.itemView.separatorLayer, frame: itemSeparatorFrame) - - let separatorAlpha: CGFloat - if component.displaySeparators { - if readyItem.index != component.items.count - 1 { - separatorAlpha = 1.0 - } else { - separatorAlpha = 0.0 - } - } else { - separatorAlpha = 0.0 - } - readyItem.itemTransition.setAlpha(layer: readyItem.itemView.separatorLayer, alpha: separatorAlpha) - readyItem.itemView.separatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor - } - innerContentHeight += readyItem.itemSize.height - } - - var removedItemIds: [AnyHashable] = [] - for (id, itemView) in self.itemViews { - if !validItemIds.contains(id) { - removedItemIds.append(id) - - transition.setAlpha(view: itemView, alpha: 0.0, completion: { [weak itemView] _ in - itemView?.removeFromSuperview() - }) - let separatorLayer = itemView.separatorLayer - transition.setAlpha(layer: separatorLayer, alpha: 0.0, completion: { [weak separatorLayer] _ in - separatorLayer?.removeFromSuperlayer() - }) - let highlightLayer = itemView.highlightLayer - transition.setAlpha(layer: highlightLayer, alpha: 0.0, completion: { [weak highlightLayer] _ in - highlightLayer?.removeFromSuperlayer() - }) - } - } - for id in removedItemIds { - self.itemViews.removeValue(forKey: id) - } + let contentResult = self.contentView.update( + configuration: ListSectionContentView.Configuration( + theme: component.theme, + displaySeparators: component.displaySeparators, + extendsItemHighlightToSection: component.extendsItemHighlightToSection, + background: component.background + ), + width: availableSize.width, + leftInset: 0.0, + readyItems: readyItems, + transition: transition + ) + let innerContentHeight = contentResult.size.height if innerContentHeight != 0.0 && contentHeight != 0.0 { contentHeight += 7.0 @@ -357,36 +465,7 @@ public final class ListSectionComponent: Component { let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: availableSize.width, height: innerContentHeight)) transition.setFrame(view: self.contentView, frame: contentFrame) - - transition.setFrame(view: self.contentItemContainerView, frame: CGRect(origin: CGPoint(), size: contentFrame.size)) - transition.setFrame(layer: self.contentSeparatorContainerLayer, frame: CGRect(origin: CGPoint(), size: contentFrame.size)) - transition.setFrame(layer: self.contentHighlightContainerLayer, frame: CGRect(origin: CGPoint(), size: contentFrame.size)) - - let backgroundFrame: CGRect - var backgroundAlpha: CGFloat = 1.0 - var contentCornerRadius: CGFloat = 11.0 - switch component.background { - case let .none(clipped): - backgroundFrame = contentFrame - backgroundAlpha = 0.0 - self.contentBackgroundView.update(size: backgroundFrame.size, corners: DynamicCornerRadiusView.Corners(minXMinY: 11.0, maxXMinY: 11.0, minXMaxY: 11.0, maxXMaxY: 11.0), transition: transition) - if !clipped { - contentCornerRadius = 0.0 - } - case .all: - backgroundFrame = contentFrame - self.contentBackgroundView.update(size: backgroundFrame.size, corners: DynamicCornerRadiusView.Corners(minXMinY: 11.0, maxXMinY: 11.0, minXMaxY: 11.0, maxXMaxY: 11.0), transition: transition) - case let .range(from, corners): - if let itemView = self.itemViews[from], itemView.frame.minY < contentFrame.height { - backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: contentFrame.minY + itemView.frame.minY), size: CGSize(width: contentFrame.width, height: contentFrame.height - itemView.frame.minY)) - } else { - backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minY, y: contentFrame.height), size: CGSize(width: contentFrame.width, height: 0.0)) - } - self.contentBackgroundView.update(size: backgroundFrame.size, corners: corners, transition: transition) - } - transition.setFrame(view: self.contentBackgroundView, frame: backgroundFrame) - transition.setAlpha(view: self.contentBackgroundView, alpha: backgroundAlpha) - transition.setCornerRadius(layer: self.contentView.layer, cornerRadius: contentCornerRadius) + transition.setFrame(view: self.contentView.externalContentBackgroundView, frame: contentResult.backgroundFrame.offsetBy(dx: contentFrame.minX, dy: contentFrame.minY)) contentHeight += innerContentHeight @@ -436,3 +515,135 @@ public final class ListSectionComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + +public final class ListSubSectionComponent: Component { + public typealias ChildView = ListSectionComponentChildView + + public let theme: PresentationTheme + public let leftInset: CGFloat + public let items: [AnyComponentWithIdentity] + public let displaySeparators: Bool + + public init( + theme: PresentationTheme, + leftInset: CGFloat, + items: [AnyComponentWithIdentity], + displaySeparators: Bool = true + ) { + self.theme = theme + self.leftInset = leftInset + self.items = items + self.displaySeparators = displaySeparators + } + + public static func ==(lhs: ListSubSectionComponent, rhs: ListSubSectionComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.leftInset != rhs.leftInset { + return false + } + if lhs.items != rhs.items { + return false + } + if lhs.displaySeparators != rhs.displaySeparators { + return false + } + return true + } + + public final class View: UIView, ListSectionComponent.ChildView { + private let contentView: ListSectionContentView + + private var component: ListSubSectionComponent? + + public var customUpdateIsHighlighted: ((Bool) -> Void)? + public var separatorInset: CGFloat = 0.0 + + public override init(frame: CGRect) { + self.contentView = ListSectionContentView() + + super.init(frame: CGRect()) + + self.addSubview(self.contentView) + } + + required public init?(coder: NSCoder) { + preconditionFailure() + } + + public func itemView(id: AnyHashable) -> UIView? { + return self.contentView.itemViews[id]?.contents.view + } + + func update(component: ListSubSectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + var contentHeight: CGFloat = 0.0 + + var readyItems: [ListSectionContentView.ReadyItem] = [] + for i in 0 ..< component.items.count { + let item = component.items[i] + let itemId = item.id + + let itemView: ListSectionContentView.ItemView + var itemTransition = transition + if let current = self.contentView.itemViews[itemId] { + itemView = current + } else { + itemTransition = itemTransition.withAnimation(.none) + itemView = ListSectionContentView.ItemView() + self.contentView.itemViews[itemId] = itemView + itemView.contents.parentState = state + } + + let itemSize = itemView.contents.update( + transition: itemTransition, + component: item.component, + environment: {}, + containerSize: CGSize(width: availableSize.width - component.leftInset, height: availableSize.height) + ) + + readyItems.append(ListSectionContentView.ReadyItem( + id: itemId, + itemView: itemView, + size: itemSize, + transition: itemTransition + )) + } + + let contentResult = self.contentView.update( + configuration: ListSectionContentView.Configuration( + theme: component.theme, + displaySeparators: component.displaySeparators, + extendsItemHighlightToSection: false, + background: .none(clipped: false) + ), + width: availableSize.width - component.leftInset, + leftInset: 0.0, + readyItems: readyItems, + transition: transition + ) + let innerContentHeight = contentResult.size.height + + let contentFrame = CGRect(origin: CGPoint(x: component.leftInset, y: contentHeight), size: CGSize(width: availableSize.width - component.leftInset, height: innerContentHeight)) + transition.setFrame(view: self.contentView, frame: contentFrame) + transition.setFrame(view: self.contentView.externalContentBackgroundView, frame: contentResult.backgroundFrame.offsetBy(dx: contentFrame.minX, dy: contentFrame.minY)) + + contentHeight += innerContentHeight + + self.separatorInset = component.leftInset + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index f23092d8bfd..37d5717a945 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -207,10 +207,14 @@ public final class MediaEditor { } public var resultIsVideo: Bool { + if self.values.entities.contains(where: { $0.entity.isAnimated }) { + return true + } if case let .sticker(file) = self.subject { return file.isAnimatedSticker || file.isVideoSticker + } else { + return self.player != nil || self.audioPlayer != nil || self.additionalPlayer != nil } - return self.player != nil || self.audioPlayer != nil || self.additionalPlayer != nil || self.values.entities.contains(where: { $0.entity.isAnimated }) } public var resultImage: UIImage? { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoFFMpegWriter.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoFFMpegWriter.swift index a7c5fd134f7..9f9557616c8 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoFFMpegWriter.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoFFMpegWriter.swift @@ -46,7 +46,7 @@ final class MediaEditorVideoFFMpegWriter: MediaEditorVideoExportWriter { } self.pool = pool - if !self.ffmpegWriter.setup(withOutputPath: outputPath, width: width, height: height, bitrate: 200 * 1000, framerate: 30) { + if !self.ffmpegWriter.setup(withOutputPath: outputPath, width: width, height: height, bitrate: 240 * 1000, framerate: 30) { self.status = .failed } } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift index 3c90c168f12..0562d0fb63b 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift @@ -112,6 +112,10 @@ private final class MediaCutoutScreenComponent: Component { x: location.x / controller.drawingView.bounds.width, y: location.y / controller.drawingView.bounds.height ) + let validRange: Range = 0.0 ..< 1.0 + guard validRange.contains(point.x) && validRange.contains(point.y) else { + return + } component.mediaEditor.processImage { [weak self] originalImage, _ in cutoutImage(from: originalImage, values: nil, target: .point(point), includeExtracted: false, completion: { [weak self] results in diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 7e90ee8700b..712bd98407f 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -468,7 +468,7 @@ final class MediaEditorScreenComponent: Component { self.currentInputMode = .text if hasFirstResponder(self) { if let view = self.inputPanel.view as? MessageInputPanelComponent.View { - self.nextTransitionUserData = TextFieldComponent.AnimationHint(view: nil, kind: .textFocusChanged) + self.nextTransitionUserData = TextFieldComponent.AnimationHint(view: nil, kind: .textFocusChanged(isFocused: false)) if view.isActive { view.deactivateInput(force: true) } else { @@ -476,7 +476,7 @@ final class MediaEditorScreenComponent: Component { } } } else { - self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(view: nil, kind: .textFocusChanged))) + self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(view: nil, kind: .textFocusChanged(isFocused: false)))) } } @@ -1177,8 +1177,10 @@ final class MediaEditorScreenComponent: Component { let authorName = forwardAuthor.displayTitle(strings: environment.strings, displayOrder: .firstLast) header = AnyComponent( ForwardInfoPanelComponent( + context: component.context, authorName: authorName, text: forwardStory.text, + entities: forwardStory.entities, isChannel: forwardAuthor.id.isGroupOrChannel, isVibrant: true, fillsWidth: true @@ -6742,54 +6744,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate dismissImpl?() completion() - self.updateEditProgress(0.0, cancel: { [weak self] in - self?.stickerUploadDisposable.set(nil) - }) - self.stickerUploadDisposable.set((self.context.engine.stickers.createStickerSet( - title: title ?? "", - shortName: "", - stickers: [ - ImportSticker( - resource: .standalone(resource: file.resource), - thumbnailResource: file.previewRepresentations.first.flatMap { .standalone(resource: $0.resource) }, - emojis: self.effectiveStickerEmoji(), - dimensions: file.dimensions ?? PixelDimensions(width: 512, height: 512), - duration: file.duration, - mimeType: file.mimeType, - keywords: "" - ) - ], - thumbnail: nil, - type: .stickers(content: .image), - software: nil - ) |> deliverOnMainQueue).startStandalone(next: { [weak self] status in - guard let self else { - return - } - switch status { - case let .progress(progress, _, _): - self.updateEditProgress(progress, cancel: { [weak self] in - self?.stickerUploadDisposable.set(nil) - }) - case let .complete(info, items): - self.completion(MediaEditorScreen.Result(), { [weak self] finished in - self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in - guard let self else { - return - } - let navigationController = self.navigationController as? NavigationController - self.dismiss() - if let navigationController { - Queue.mainQueue().after(0.2) { - let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) - let controller = self.context.sharedContext.makeStickerPackScreen(context: self.context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [.result(info: info, items: items, installed: true)], isEditing: false, expandIfNeeded: true, parentNavigationController: navigationController, sendSticker: self.sendSticker) - (navigationController.viewControllers.last as? ViewController)?.present(controller, in: .window(.root)) - } - } - }) - }) - } - })) + if let title { + self.uploadSticker(file, action: .createStickerPack(title: title)) + } }, cancel: {}) dismissImpl = { [weak controller] in controller?.dismiss() @@ -6853,38 +6810,38 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let signal = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> castError(UploadStickerError.self) - |> mapToSignal { peer -> Signal in + |> mapToSignal { peer -> Signal<(UploadStickerStatus, (StickerPackReference, String)?), UploadStickerError> in guard let peer else { return .complete() } return resourceSignal - |> mapToSignal { result -> Signal in + |> mapToSignal { result -> Signal<(UploadStickerStatus, (StickerPackReference, String)?), UploadStickerError> in switch result { case .failed: return .fail(.generic) case let .progress(progress): - return .single(.progress(progress * 0.5)) + return .single((.progress(progress * 0.5), nil)) case let .complete(resource): if let resource = resource as? CloudDocumentMediaResource { - return .single(.progress(1.0)) |> then(.single(.complete(resource, mimeType))) + return .single((.progress(1.0), nil)) |> then(.single((.complete(resource, mimeType), nil))) } else { return context.engine.stickers.uploadSticker(peer: peer._asPeer(), resource: resource, thumbnail: file.previewRepresentations.first?.resource, alt: "", dimensions: dimensions, duration: duration, mimeType: mimeType) - |> mapToSignal { status -> Signal in + |> mapToSignal { status -> Signal<(UploadStickerStatus, (StickerPackReference, String)?), UploadStickerError> in switch status { case let .progress(progress): - return .single(.progress(isVideo ? 0.5 + progress * 0.5 : progress)) + return .single((.progress(isVideo ? 0.5 + progress * 0.5 : progress), nil)) case let .complete(resource, _): let file = stickerFile(resource: resource, thumbnailResource: file.previewRepresentations.first?.resource, size: file.size ?? 0, dimensions: dimensions, duration: file.duration, isVideo: isVideo) switch action { case .send: - return .single(status) + return .single((status, nil)) case .addToFavorites: return context.engine.stickers.toggleStickerSaved(file: file, saved: true) |> `catch` { _ -> Signal in return .fail(.generic) } |> map { _ in - return status + return (status, nil) } case let .createStickerPack(title): let sticker = ImportSticker( @@ -6900,13 +6857,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return .fail(.generic) } |> mapToSignal { innerStatus in - if case .complete = innerStatus { - return .single(status) + if case let .complete(info, _) = innerStatus { + return .single((status, (.id(id: info.id.id, accessHash: info.accessHash), title))) } else { return .complete() } } - case let .addToStickerPack(pack, _): + case let .addToStickerPack(pack, title): let sticker = ImportSticker( resource: .standalone(resource: resource), emojis: emojis, @@ -6920,10 +6877,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return .fail(.generic) } |> map { _ in - return status + return (status, (pack, title)) } case .upload, .update: - return .single(status) + return .single((status, nil)) } } } @@ -6932,7 +6889,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } self.stickerUploadDisposable.set((signal - |> deliverOnMainQueue).startStandalone(next: { [weak self] status in + |> deliverOnMainQueue).startStandalone(next: { [weak self] (status, packReferenceAndTitle) in guard let self else { return } @@ -6974,15 +6931,14 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let parentController = navigationController?.viewControllers.last as? ViewController { parentController.present(UndoOverlayController(presentationData: presentationData, content: .sticker(context: self.context, file: file, loop: true, title: nil, text: presentationData.strings.Conversation_StickerAddedToFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), in: .current) } - case let .addToStickerPack(packReference, title): - let navigationController = self.navigationController as? NavigationController - if let navigationController { + case .addToStickerPack, .createStickerPack: + if let (packReference, packTitle) = packReferenceAndTitle, let navigationController = self.navigationController as? NavigationController { Queue.mainQueue().after(0.2) { let controller = self.context.sharedContext.makeStickerPackScreen(context: self.context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], isEditing: false, expandIfNeeded: true, parentNavigationController: navigationController, sendSticker: self.sendSticker) (navigationController.viewControllers.last as? ViewController)?.present(controller, in: .window(.root)) Queue.mainQueue().after(0.1) { - controller.present(UndoOverlayController(presentationData: presentationData, content: .sticker(context: self.context, file: file, loop: true, title: nil, text: presentationData.strings.StickerPack_StickerAdded(title).string, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), in: .current) + controller.present(UndoOverlayController(presentationData: presentationData, content: .sticker(context: self.context, file: file, loop: true, title: nil, text: presentationData.strings.StickerPack_StickerAdded(packTitle).string, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), in: .current) } } } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift index 05ff53d07f0..df261499171 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift @@ -166,7 +166,7 @@ private func getPathFromMaskImage(_ image: CIImage, size: CGSize, values: MediaE let contourImageSize = image.extent.size.aspectFilled(CGSize(width: 256.0, height: 256.0)) var contour = findEdgePoints(in: pixelBuffer) - guard !contour.isEmpty else { + guard contour.count > 1 else { return nil } @@ -228,20 +228,48 @@ func findEdgePoints(in pixelBuffer: CVPixelBuffer) -> [CGPoint] { return pixel >= 235 } - let directions = [(1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1)] - var lastDirectionIndex = 0 - var startPoint: Point? = nil -outerLoop: for y in 0..() + var componentSize = 0 + + func floodFill(from point: Point) -> Int { + var stack = [point] + var size = 0 + + while let current = stack.popLast() { + let x = Int(current.x) + let y = Int(current.y) + + if x < 0 || x >= width || y < 0 || y >= height || visited.contains(current) || !isPixelWhiteAt(x: x, y: y) { + continue + } + + visited.insert(current) + size += 1 + stack.append(contentsOf: [Point(x: x+1, y: y), Point(x: x-1, y: y), Point(x: x, y: y+1), Point(x: x, y: y-1)]) + } + + return size + } + + for y in 0.. componentSize { + componentSize = size + startPoint = point + } } } } - guard let startingPoint = startPoint else { return [] } + let directions = [(1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1)] + var lastDirectionIndex = 0 + + guard let startingPoint = startPoint, componentSize > 60 else { return [] } + edgePoints.insert(startingPoint) edgePath.append(startingPoint) var currentPoint = startingPoint @@ -326,7 +354,7 @@ private func getEdgesBitmap(_ ciImage: CIImage) -> CVPixelBuffer? { context.fill(CGRect(origin: .zero, size: size)) image.draw(in: CGRect(origin: .zero, size: size)) UIGraphicsPopContext() - + return buffer } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD index 628ab160fa8..12b514ecbbd 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD @@ -155,6 +155,7 @@ swift_library( "//submodules/TelegramUI/Components/Settings/BirthdayPickerScreen", "//submodules/TelegramUI/Components/Settings/PeerSelectionScreen", "//submodules/ConfettiEffect", + "//submodules/ContactsPeerItem", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift index cc18a4165d2..b4cf661eabb 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift @@ -14,6 +14,7 @@ import MergeLists import ItemListUI import PeerInfoVisualMediaPaneNode import PeerInfoPaneNode +import ContactsPeerItem private struct PeerMembersListTransaction { let deletions: [ListViewDeleteItem] @@ -84,7 +85,7 @@ private enum PeerMembersListEntry: Comparable, Identifiable { } } - func item(context: AccountContext, presentationData: PresentationData, enclosingPeer: Peer, addMemberAction: @escaping () -> Void, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void) -> ListViewItem { + func item(context: AccountContext, presentationData: PresentationData, enclosingPeer: Peer, addMemberAction: @escaping () -> Void, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, contextAction: ((PeerInfoMember, ASDisplayNode, ContextGesture?) -> Void)?) -> ListViewItem { switch self { case let .addMember(_, text): return ItemListPeerActionItem(presentationData: ItemListPresentationData(presentationData), icon: PresentationResourcesItemList.addPersonIcon(presentationData.theme), title: text, alwaysPlain: true, sectionId: 0, height: .compactPeerList, color: .accent, editing: false, action: { @@ -123,24 +124,104 @@ private enum PeerMembersListEntry: Comparable, Identifiable { action(member, .remove) })) } + + let presence: EnginePeer.Presence + if member.peer.id == context.account.peerId { + presence = EnginePeer.Presence(status: .present(until: Int32.max), lastActivity: 0) + } else if let value = member.presence { + presence = EnginePeer.Presence(value) + } else { + presence = EnginePeer.Presence(status: .longTimeAgo, lastActivity: 0) + } + + return ContactsPeerItem( + presentationData: ItemListPresentationData(presentationData), + style: .plain, + sectionId: 0, + sortOrder: presentationData.nameSortOrder, + displayOrder: presentationData.nameDisplayOrder, + context: context, + peerMode: .peer, + peer: .peer(peer: EnginePeer(member.peer), chatPeer: EnginePeer(member.peer)), + status: .presence(presence, presentationData.dateTimeFormat), + rightLabelText: label, + enabled: true, + selection: .none, + editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), + options: options, + additionalActions: [], + actionIcon: .none, + index: nil, + header: nil, + action: { _ in + action(member, .open) + }, + disabledAction: nil, + setPeerIdWithRevealedOptions: { _, _ in + }, + deletePeer: nil, + itemHighlighting: nil, + contextAction: contextAction == nil ? nil : { node, gesture, _ in + contextAction?(member, node, gesture) + }, + animationCache: context.animationCache, + animationRenderer: context.animationRenderer, + storyStats: member.storyStats.flatMap { storyStats in + return ( + total: storyStats.totalCount, + unseen: storyStats.unseenCount, + hasUnseenCloseFriends: storyStats.hasUnseenCloseFriends + ) + }, + openStories: { _, sourceNode in + action(member, .openStories(sourceView: sourceNode.view)) + } + ) - return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: EnginePeer(member.peer), presence: member.presence.flatMap(EnginePeer.Presence.init), text: .presence, label: label == nil ? .none : .text(label!, .standard), editing: ItemListPeerItemEditing(editable: !options.isEmpty, editing: false, revealed: false), revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: true, selectable: member.id != context.account.peerId, sectionId: 0, action: { - action(member, .open) - }, setPeerIdWithRevealedOptions: { _, _ in - }, removePeer: { _ in - }, contextAction: nil, hasTopStripe: false, noInsets: true, noCorners: true, disableInteractiveTransitionIfNecessary: true, storyStats: member.storyStats, openStories: { sourceView in - action(member, .openStories(sourceView: sourceView)) - }) + /*return ItemListPeerItem( + presentationData: ItemListPresentationData(presentationData), + dateTimeFormat: presentationData.dateTimeFormat, + nameDisplayOrder: presentationData.nameDisplayOrder, + context: context, + peer: EnginePeer(member.peer), + presence: member.presence.flatMap(EnginePeer.Presence.init), + text: .presence, + label: label == nil ? .none : .text(label!, .standard), + editing: ItemListPeerItemEditing(editable: !options.isEmpty, editing: false, revealed: false), + revealOptions: ItemListPeerItemRevealOptions(options: options), + switchValue: nil, + enabled: true, + selectable: member.id != context.account.peerId, + sectionId: 0, + action: { + action(member, .open) + }, + setPeerIdWithRevealedOptions: { _, _ in + }, + removePeer: { _ in + }, + contextAction: contextAction == nil ? nil : { node, gesture in + contextAction?(member, node, gesture) + }, + hasTopStripe: false, + noInsets: true, + noCorners: true, + disableInteractiveTransitionIfNecessary: true, + storyStats: member.storyStats, + openStories: { sourceView in + action(member, .openStories(sourceView: sourceView)) + } + )*/ } } } -private func preparedTransition(from fromEntries: [PeerMembersListEntry], to toEntries: [PeerMembersListEntry], context: AccountContext, presentationData: PresentationData, enclosingPeer: Peer, addMemberAction: @escaping () -> Void, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void) -> PeerMembersListTransaction { +private func preparedTransition(from fromEntries: [PeerMembersListEntry], to toEntries: [PeerMembersListEntry], context: AccountContext, presentationData: PresentationData, enclosingPeer: Peer, addMemberAction: @escaping () -> Void, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, contextAction: ((PeerInfoMember, ASDisplayNode, ContextGesture?) -> Void)?) -> PeerMembersListTransaction { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enclosingPeer: enclosingPeer, addMemberAction: addMemberAction, action: action), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enclosingPeer: enclosingPeer, addMemberAction: addMemberAction, action: action), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enclosingPeer: enclosingPeer, addMemberAction: addMemberAction, action: action, contextAction: contextAction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enclosingPeer: enclosingPeer, addMemberAction: addMemberAction, action: action, contextAction: contextAction), directionHint: nil) } return PeerMembersListTransaction(deletions: deletions, insertions: insertions, updates: updates, animated: toEntries.count < fromEntries.count) } @@ -276,7 +357,79 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { let transaction = preparedTransition(from: self.currentEntries, to: entries, context: self.context, presentationData: presentationData, enclosingPeer: enclosingPeer, addMemberAction: { [weak self] in self?.addMemberAction() }, action: { [weak self] member, action in - self?.action(member, action) + guard let self else { + return + } + + if case .open = action { + self.listNode.clearHighlightAnimated(true) + } + + self.action(member, action) + }, contextAction: { [weak self] member, sourceNode, gesture in + guard let self else { + return + } + var node: ContextExtractedContentContainingNode? + if let sourceNode = sourceNode as? ContextExtractedContentContainingNode { + node = sourceNode + } else { + for subnode in sourceNode.subnodes ?? [] { + if let subnode = subnode as? ContextExtractedContentContainingNode { + node = subnode + break + } + } + } + guard let node else { + gesture?.cancel() + return + } + + let actions = availableActionsForMemberOfPeer(accountPeerId: self.context.account.peerId, peer: enclosingPeer, member: member) + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + var items: [ContextMenuItem] = [] + let action = self.action + + if actions.contains(.promote) && enclosingPeer is TelegramChannel { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.GroupInfo_ActionPromote, icon: { _ in + return nil + }, action: { c, _ in + c.dismiss(completion: { + action(member, .promote) + }) + }))) + } + if actions.contains(.restrict) { + if enclosingPeer is TelegramChannel { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.GroupInfo_ActionRestrict, icon: { _ in + return nil + }, action: { c, _ in + c.dismiss(completion: { + action(member, .restrict) + }) + }))) + } + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Delete, textColor: .destructive, icon: { _ in + return nil + }, action: { c, _ in + c.dismiss(completion: { + action(member, .remove) + }) + }))) + } + + if items.isEmpty { + gesture?.cancel() + return + } + + let dismissPromise = ValuePromise(false) + let source = PeerInfoMemberExtractedContentSource(sourceNode: node, keepInPlace: false, blurBackground: true, centerVertically: false, shouldBeDismissed: dismissPromise.get()) + + let contextController = ContextController(presentationData: presentationData, source: .extracted(source), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + self.parentController?.presentInGlobalOverlay(contextController) }) self.enclosingPeer = enclosingPeer self.currentEntries = entries @@ -335,3 +488,30 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { func updateSelectedMessages(animated: Bool) { } } + +final class PeerInfoMemberExtractedContentSource: ContextExtractedContentSource { + var keepInPlace: Bool + let ignoreContentTouches: Bool = false + let blurBackground: Bool + + private let sourceNode: ContextExtractedContentContainingNode + + var centerVertically: Bool + var shouldBeDismissed: Signal + + init(sourceNode: ContextExtractedContentContainingNode, keepInPlace: Bool, blurBackground: Bool, centerVertically: Bool, shouldBeDismissed: Signal) { + self.sourceNode = sourceNode + self.keepInPlace = keepInPlace + self.blurBackground = blurBackground + self.centerVertically = centerVertically + self.shouldBeDismissed = shouldBeDismissed + } + + func takeView() -> ContextControllerTakeViewInfo? { + return ContextControllerTakeViewInfo(containingItem: .node(self.sourceNode), contentAreaInScreenSpace: UIScreen.main.bounds) + } + + func putBack() -> ContextControllerPutBackViewInfo? { + return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift index 9e57b3cc6dc..ca3183dc1e6 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift @@ -648,6 +648,12 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat } return [.leftCenter, .rightCenter] } + if case .members = currentPaneKey { + if index == 0 { + return .leftCenter + } + return [.leftCenter, .rightCenter] + } if strongSelf.currentPane?.node.navigationContentNode != nil { return [] } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index ef20a65b4be..9f2aac0a7d5 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -1472,7 +1472,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese interaction.openChat(nil) })) - items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 4, text: presentationData.strings.ReportPeer_ReportReaction, color: .destructive, action: { + items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 4, text: presentationData.strings.ReportPeer_BanAndReport, color: .destructive, action: { interaction.openReport(.reaction(reactionSourceMessageId)) })) } else if let _ = nearbyPeerDistance { @@ -2116,7 +2116,6 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL let ItemInviteLinks = 102 let ItemLinkedChannel = 103 let ItemPreHistory = 104 - let ItemStickerPack = 105 let ItemMembers = 106 let ItemPermissions = 107 let ItemAdmins = 108 @@ -2285,13 +2284,7 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL interaction.editingOpenPreHistorySetup() })) } - - if cachedData.flags.contains(.canSetStickerSet) && canEditPeerInfo(context: context, peer: channel, chatLocation: chatLocation, threadData: data.threadData) { - items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemStickerPack, label: .text(cachedData.stickerPack?.title ?? presentationData.strings.GroupInfo_SharedMediaNone), text: presentationData.strings.Stickers_GroupStickers, icon: UIImage(bundleImageName: "Settings/Menu/Stickers"), action: { - interaction.editingOpenStickerPackSetup() - })) - } - + if isCreator, let appConfiguration = data.appConfiguration { var minParticipants = 200 if let data = appConfiguration.data, let value = data["forum_upgrade_participants_min"] as? Double { @@ -7259,7 +7252,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } UIPasteboard.general.string = bioText - self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: self.presentationData.strings.MyProfile_ToastBioCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + let toastText: String + if let _ = self.data?.peer as? TelegramUser { + toastText = self.presentationData.strings.MyProfile_ToastBioCopied + } else { + toastText = self.presentationData.strings.ChannelProfile_ToastAboutCopied + } + + self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: toastText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) } var items: [ContextMenuItem] = [] @@ -7286,7 +7286,13 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }))) } - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.MyProfile_BioActionCopy, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { c, _ in + let copyText: String + if let _ = self.data?.peer as? TelegramUser { + copyText = self.presentationData.strings.MyProfile_BioActionCopy + } else { + copyText = self.presentationData.strings.ChannelProfile_AboutActionCopy + } + items.append(.action(ContextMenuActionItem(text: copyText, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { c, _ in c.dismiss { copyAction() } @@ -8250,13 +8256,39 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro switch type { case let .reaction(sourceMessageId): - let _ = (self.context.engine.peers.reportPeerReaction(authorId: self.peerId, messageId: sourceMessageId) - |> deliverOnMainQueue).startStandalone(completed: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .emoji(name: "PoliceCar", text: strongSelf.presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return false }), in: .current) - }) + let presentationData = self.presentationData + let actionSheet = ActionSheetController(presentationData: presentationData) + let dismissAction: () -> Void = { [weak actionSheet] in + actionSheet?.dismissAnimated() + } + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: presentationData.strings.ReportPeer_ReportReaction_Text), + ActionSheetButtonItem(title: presentationData.strings.ReportPeer_ReportReaction_BanAndReport, color: .destructive, action: { [weak self] in + dismissAction() + guard let self else { + return + } + self.activeActionDisposable.set(self.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: self.peerId, isBlocked: true).startStrict()) + self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: .emoji(name: "PoliceCar", text: self.presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return false }), in: .current) + }), + ActionSheetButtonItem(title: presentationData.strings.ReportPeer_ReportReaction_Report, action: { [weak self] in + dismissAction() + guard let self else { + return + } + let _ = (self.context.engine.peers.reportPeerReaction(authorId: self.peerId, messageId: sourceMessageId) + |> deliverOnMainQueue).startStandalone(completed: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .emoji(name: "PoliceCar", text: strongSelf.presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return false }), in: .current) + }) + }) + ]), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + self.controller?.present(actionSheet, in: .window(.root)) default: let options: [PeerReportOption] if case .user = type { @@ -13379,7 +13411,7 @@ public func presentAddMembersImpl(context: AccountContext, updatedPresentationDa }, clearHighlightAutomatically: true)) } - let contactsController = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, updatedPresentationData: updatedPresentationData, mode: .peerSelection(searchChatList: false, searchGroups: false, searchChannels: false), options: options, filters: [.excludeSelf, .disable(recentIds)], onlyWriteable: true, isGroupInvitation: true)) + let contactsController = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, updatedPresentationData: updatedPresentationData, mode: .peerSelection(searchChatList: false, searchGroups: false, searchChannels: false), options: .single(options), filters: [.excludeSelf, .disable(recentIds)], onlyWriteable: true, isGroupInvitation: true)) contactsController.navigationPresentation = .modal confirmationImpl = { [weak contactsController] peerId in diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index b36e4667297..2b8f8e1d17a 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -1367,7 +1367,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { contactListNode.activateSearch = { [weak self] in self?.requestActivateSearch?() } - contactListNode.openPeer = { [weak self] peer, _ in + contactListNode.openPeer = { [weak self] peer, _, _, _ in if case let .peer(peer, _, _) = peer { self?.contactListNode?.listNode.clearHighlightAnimated(true) self?.requestOpenPeer?(EnginePeer(peer), nil) diff --git a/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift b/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift index f91488ec89c..74a091362b8 100644 --- a/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift +++ b/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift @@ -168,6 +168,10 @@ public final class PlainButtonComponent: Component { } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.isHidden || self.alpha == 0.0 { + return nil + } + let result = super.hitTest(point, with: event) if result != nil { return result diff --git a/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/Sources/PremiumGiftAttachmentScreen.swift b/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/Sources/PremiumGiftAttachmentScreen.swift index fbcd865a8f6..6232f869d9a 100644 --- a/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/Sources/PremiumGiftAttachmentScreen.swift +++ b/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/Sources/PremiumGiftAttachmentScreen.swift @@ -11,6 +11,9 @@ import AttachmentUI public class PremiumGiftAttachmentScreen: PremiumGiftScreen, AttachmentContainable { public var requestAttachmentMenuExpansion: () -> Void = {} public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } + public var parentController: () -> ViewController? = { + return nil + } public var cancelPanGesture: () -> Void = { } public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift index c7eda3161a6..2db55b4b3b1 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift @@ -384,7 +384,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories), chatListFilters: nil, onlyUsers: true - )), options: [], filters: [], alwaysEnabled: true, limit: 100, reachedLimit: { _ in + )), filters: [], alwaysEnabled: true, limit: 100, reachedLimit: { _ in })) controller.navigationPresentation = .modal @@ -853,10 +853,10 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { maximumNumberOfLines: 1 ))), ], alignment: .left, spacing: 2.0)), - leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( + leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( name: "Chat List/ComposeIcon", tintColor: environment.theme.list.itemAccentColor - ))), + )))), accessory: nil, action: { [weak self] _ in guard let self else { @@ -929,11 +929,11 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { maximumNumberOfLines: 1 ))), ], alignment: .left, spacing: 2.0)), - leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image( + leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(Image( image: checkIcon, tintColor: !isSelected ? .clear : environment.theme.list.itemAccentColor, contentMode: .center - ))), + )))), accessory: nil, action: { [weak self] _ in guard let self else { @@ -1199,11 +1199,11 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { maximumNumberOfLines: 1 ))), ], alignment: .left, spacing: 2.0)), - leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image( + leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(Image( image: checkIcon, tintColor: !self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor, contentMode: .center - ))), + )))), accessory: nil, action: { [weak self] _ in guard let self else { @@ -1229,11 +1229,11 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { maximumNumberOfLines: 1 ))), ], alignment: .left, spacing: 2.0)), - leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image( + leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(Image( image: checkIcon, tintColor: self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor, contentMode: .center - ))), + )))), accessory: nil, action: { [weak self] _ in guard let self else { @@ -1277,10 +1277,10 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { maximumNumberOfLines: 1 ))), ], alignment: .left, spacing: 2.0)), - leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( + leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( name: "Chat List/AddIcon", tintColor: environment.theme.list.itemAccentColor - ))), + )))), accessory: nil, action: { [weak self] _ in guard let self else { diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift index 66c7fd76f80..fc531baa095 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift @@ -413,10 +413,10 @@ final class BusinessLinksSetupScreenComponent: Component { maximumNumberOfLines: 1 ))), ], alignment: .left, spacing: 2.0)), - leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( + leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( name: "Item List/AddLinkIcon", tintColor: environment.theme.list.itemAccentColor - ))), + )))), accessory: nil, action: { [weak self] _ in guard let self else { diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift index 27beabbb900..110d7b16714 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift @@ -1303,8 +1303,13 @@ public final class QuickReplySetupScreen: ViewControllerComponentContainer, Atta } public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } + public var parentController: () -> ViewController? = { + return nil + } public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in } + public var updateTabBarVisibility: (Bool, ContainedViewLayoutTransition) -> Void = { _, _ in + } public var cancelPanGesture: () -> Void = { } public var isContainerPanning: () -> Bool = { diff --git a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift index e663082df39..c258c60fb4b 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift @@ -528,10 +528,10 @@ final class BusinessDaySetupScreenComponent: Component { maximumNumberOfLines: 1 ))), ], alignment: .left, spacing: 2.0)), - leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( + leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( name: "Item List/AddTimeIcon", tintColor: environment.theme.list.itemAccentColor - ))), + )))), accessory: nil, action: { [weak self] _ in guard let self else { diff --git a/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/BusinessIntroSetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/BusinessIntroSetupScreen.swift index c9a0f9fc935..b06da137cc0 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/BusinessIntroSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/BusinessIntroSetupScreen.swift @@ -229,7 +229,8 @@ final class BusinessIntroSetupScreenComponent: Component { hasSearch: true, hasTrending: false, forceHasPremium: true, - searchIsPlaceholderOnly: false + searchIsPlaceholderOnly: false, + subject: .greetingStickers ) self.stickerContentDisposable = (stickerContent |> deliverOnMainQueue).start(next: { [weak self] stickerContent in @@ -517,7 +518,7 @@ final class BusinessIntroSetupScreenComponent: Component { })) } case let .category(value): - let resultSignal = component.context.engine.stickers.searchStickers(query: value, scope: [.installed, .remote]) + let resultSignal = component.context.engine.stickers.searchStickers(category: value, scope: [.installed, .remote]) |> mapToSignal { files -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in var items: [EmojiPagerContentComponent.Item] = [] @@ -588,13 +589,13 @@ final class BusinessIntroSetupScreenComponent: Component { fillWithLoadingPlaceholders: true, items: [] ) - ], id: AnyHashable(value), version: version, isPreset: true), isSearching: false) + ], id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) if !self.isUpdating { self.state?.updated(transition: .immediate) } return } - self.stickerSearchState = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value), version: version, isPreset: true), isSearching: false) + self.stickerSearchState = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) version += 1 if !self.isUpdating { self.state?.updated(transition: .immediate) @@ -821,8 +822,7 @@ final class BusinessIntroSetupScreenComponent: Component { )), maximumNumberOfLines: 0 )), - items: introSectionItems, - itemUpdateOrder: introSectionItems.map(\.id).reversed() + items: introSectionItems )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/BusinessRecipientListScreen.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/BusinessRecipientListScreen.swift index 722de3591a0..cd442605996 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/BusinessRecipientListScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/BusinessRecipientListScreen.swift @@ -242,7 +242,7 @@ final class BusinessRecipientListScreenComponent: Component { additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories), chatListFilters: nil, onlyUsers: true - )), options: [], filters: [], alwaysEnabled: true, limit: 100, reachedLimit: { _ in + )), filters: [], alwaysEnabled: true, limit: 100, reachedLimit: { _ in })) controller.navigationPresentation = .modal @@ -422,10 +422,10 @@ final class BusinessRecipientListScreenComponent: Component { maximumNumberOfLines: 1 ))), ], alignment: .left, spacing: 2.0)), - leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( + leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( name: "Chat List/AddIcon", tintColor: environment.theme.list.itemAccentColor - ))), + )))), accessory: nil, action: { [weak self] _ in guard let self else { diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift index d022792bd4d..7709c950483 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift @@ -770,11 +770,11 @@ final class ChatbotSetupScreenComponent: Component { maximumNumberOfLines: 1 ))), ], alignment: .left, spacing: 2.0)), - leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image( + leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(Image( image: checkIcon, tintColor: !self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor, contentMode: .center - ))), + )))), accessory: nil, action: { [weak self] _ in guard let self else { @@ -800,11 +800,11 @@ final class ChatbotSetupScreenComponent: Component { maximumNumberOfLines: 1 ))), ], alignment: .left, spacing: 2.0)), - leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image( + leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(Image( image: checkIcon, tintColor: self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor, contentMode: .center - ))), + )))), accessory: nil, action: { [weak self] _ in guard let self else { diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorItem/Sources/PeerNameColorItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorItem/Sources/PeerNameColorItem.swift index 1ee373526bc..f25416016e6 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorItem/Sources/PeerNameColorItem.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorItem/Sources/PeerNameColorItem.swift @@ -566,7 +566,7 @@ public final class PeerNameColorItemNode: ListViewItemNode, ItemListItemNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift index e98d2b47b16..c7204300d08 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift @@ -72,13 +72,17 @@ final class ChannelAppearanceScreenComponent: Component { let peer: EnginePeer? let peerWallpaper: TelegramWallpaper? let peerEmojiPack: StickerPackCollectionInfo? + let canSetStickerPack: Bool + let peerStickerPack: StickerPackCollectionInfo? let subscriberCount: Int? let availableThemes: [TelegramTheme] - init(peer: EnginePeer?, peerWallpaper: TelegramWallpaper?, peerEmojiPack: StickerPackCollectionInfo?, subscriberCount: Int?, availableThemes: [TelegramTheme]) { + init(peer: EnginePeer?, peerWallpaper: TelegramWallpaper?, peerEmojiPack: StickerPackCollectionInfo?, canSetStickerPack: Bool, peerStickerPack: StickerPackCollectionInfo?, subscriberCount: Int?, availableThemes: [TelegramTheme]) { self.peer = peer self.peerWallpaper = peerWallpaper self.peerEmojiPack = peerEmojiPack + self.canSetStickerPack = canSetStickerPack + self.peerStickerPack = peerStickerPack self.subscriberCount = subscriberCount self.availableThemes = availableThemes } @@ -89,16 +93,20 @@ final class ChannelAppearanceScreenComponent: Component { TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), TelegramEngine.EngineData.Item.Peer.ParticipantCount(id: peerId), TelegramEngine.EngineData.Item.Peer.EmojiPack(id: peerId), + TelegramEngine.EngineData.Item.Peer.CanSetStickerPack(id: peerId), + TelegramEngine.EngineData.Item.Peer.StickerPack(id: peerId), TelegramEngine.EngineData.Item.Peer.Wallpaper(id: peerId) ), telegramThemes(postbox: context.account.postbox, network: context.account.network, accountManager: context.sharedContext.accountManager) ) |> map { peerData, cloudThemes -> ContentsData in - let (peer, subscriberCount, emojiPack, wallpaper) = peerData + let (peer, subscriberCount, emojiPack, canSetStickerPack, stickerPack, wallpaper) = peerData return ContentsData( peer: peer, peerWallpaper: wallpaper, peerEmojiPack: emojiPack, + canSetStickerPack: canSetStickerPack, + peerStickerPack: stickerPack, subscriberCount: subscriberCount, availableThemes: cloudThemes ) @@ -178,6 +186,7 @@ final class ChannelAppearanceScreenComponent: Component { private let resetColorSection = ComponentView() private let emojiStatusSection = ComponentView() private let emojiPackSection = ComponentView() + private let stickerPackSection = ComponentView() private var chatPreviewItemNode: PeerNameColorChatPreviewItemNode? @@ -652,6 +661,15 @@ final class ChannelAppearanceScreenComponent: Component { self.environment?.controller()?.push(controller) } + private func openStickerPackSetup() { + guard let component = self.component, let environment = self.environment, let contentsData = self.contentsData else { + return + } + + let controller = groupStickerPackSetupController(context: component.context, peerId: component.peerId, currentPackInfo: contentsData.peerStickerPack) + environment.controller()?.push(controller) + } + private func openEmojiPackSetup() { guard let component = self.component, let environment = self.environment, let resolvedState = self.resolveState() else { return @@ -1290,7 +1308,6 @@ final class ChannelAppearanceScreenComponent: Component { )))) } - var emojiPackFile: TelegramMediaFile? if let thumbnail = emojiPack?.thumbnail { emojiPackFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: thumbnail.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: thumbnail.immediateThumbnailData, mimeType: "", size: nil, attributes: []) @@ -1408,6 +1425,70 @@ final class ChannelAppearanceScreenComponent: Component { contentHeight += emojiStatusSectionSize.height contentHeight += sectionSpacing + if isGroup && contentsData.canSetStickerPack { + var stickerPackContents: [AnyComponentWithIdentity] = [] + stickerPackContents.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Stickers_GroupStickers, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 0 + )))) + + var stickerPackFile: TelegramMediaFile? + if let peerStickerPack = contentsData.peerStickerPack, let thumbnail = peerStickerPack.thumbnail { + stickerPackFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: peerStickerPack.id.id), partialReference: nil, resource: thumbnail.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: thumbnail.immediateThumbnailData, mimeType: "", size: nil, attributes: []) + } + + let stickerPackSectionSize = self.stickerPackSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Stickers_GroupStickersHelp, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(HStack(stickerPackContents, spacing: 6.0)), + icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent( + context: component.context, + color: environment.theme.list.itemAccentColor, + fileId: nil, + file: stickerPackFile + )))), + action: { [weak self] view in + guard let self else { + return + } + self.openStickerPackSetup() + } + ))) + ], + displaySeparators: false, + extendsItemHighlightToSection: true + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let stickerPackSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: stickerPackSectionSize) + if let stickerPackSectionView = self.stickerPackSection.view { + if stickerPackSectionView.superview == nil { + self.scrollView.addSubview(stickerPackSectionView) + } + transition.setFrame(view: stickerPackSectionView, frame: stickerPackSectionFrame) + } + contentHeight += stickerPackSectionSize.height + contentHeight += sectionSpacing + } + var chatPreviewTheme: PresentationTheme = environment.theme var chatPreviewWallpaper: TelegramWallpaper = presentationData.chatWallpaper if let updatedWallpaper = self.updatedPeerWallpaper, case .remove = updatedWallpaper { diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/EmojiPickerItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/EmojiPickerItem.swift index c858d11c1a8..a795370b00f 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/EmojiPickerItem.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/EmojiPickerItem.swift @@ -212,7 +212,7 @@ final class EmojiPickerItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift index 5e6bbfb2c41..c96c73c3a56 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorChatPreviewItem.swift @@ -437,7 +437,7 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift index 8ae84d1763e..1ce8589d645 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift @@ -428,7 +428,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift index c2334260274..3740225ec6f 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift @@ -726,10 +726,10 @@ public func PeerNameColorScreen( action: { action in if case .info = action { var replaceImpl: ((ViewController) -> Void)? - let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .colors, action: { + let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .colors, forceDark: false, action: { let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil) replaceImpl?(controller) - }) + }, dismissed: nil) replaceImpl = { [weak controller] c in controller?.replace(with: c) } diff --git a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ItemListReactionItem.swift b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ItemListReactionItem.swift index 4e244e9371e..d7c97d8c048 100644 --- a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ItemListReactionItem.swift +++ b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ItemListReactionItem.swift @@ -415,7 +415,7 @@ public class ItemListReactionItemNode: ListViewItemNode, ItemListItemNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift index 706ec770ffe..c1b99534e14 100644 --- a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift +++ b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift @@ -444,7 +444,7 @@ class ReactionChatPreviewItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/TelegramUI/Components/Settings/ThemeCarouselItem/Sources/ThemeCarouselItem.swift b/submodules/TelegramUI/Components/Settings/ThemeCarouselItem/Sources/ThemeCarouselItem.swift index acb2e064e8d..68e12157f40 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeCarouselItem/Sources/ThemeCarouselItem.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeCarouselItem/Sources/ThemeCarouselItem.swift @@ -520,8 +520,8 @@ private final class ThemeCarouselThemeItemIconNode : ListViewItemNode { }) } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - super.animateInsertion(currentTimestamp, duration: duration, short: short) + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + super.animateInsertion(currentTimestamp, duration: duration, options: options) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } @@ -906,7 +906,7 @@ public class ThemeCarouselThemeItemNode: ListViewItemNode, ItemListItemNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/Sources/ThemeSettingsThemeItem.swift b/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/Sources/ThemeSettingsThemeItem.swift index 64b0b365d35..7fd014433a9 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/Sources/ThemeSettingsThemeItem.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/Sources/ThemeSettingsThemeItem.swift @@ -352,8 +352,8 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { }) } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - super.animateInsertion(currentTimestamp, duration: duration, short: short) + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + super.animateInsertion(currentTimestamp, duration: duration, options: options) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } @@ -741,7 +741,7 @@ public class ThemeSettingsThemeItemNode: ListViewItemNode, ItemListItemNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/BUILD b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/BUILD index 882bfd1377a..45b7b88b4e1 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/BUILD @@ -28,7 +28,7 @@ swift_library( "//submodules/MergeLists", "//submodules/ShareController", "//submodules/GalleryUI", - "//submodules/CounterContollerTitleView", + "//submodules/CounterControllerTitleView", "//submodules/LegacyMediaPickerUI", "//submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode", "//submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent", diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryController.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryController.swift index c306b30e67d..18a158a3d45 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryController.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryController.swift @@ -14,7 +14,7 @@ import AccountContext import ShareController import GalleryUI import HexColor -import CounterContollerTitleView +import CounterControllerTitleView import UndoUI import LegacyComponents import LegacyMediaPickerUI @@ -350,8 +350,8 @@ public class WallpaperGalleryController: ViewController { self.centralItemAttributesDisposable.add(self.centralItemSubtitle.get().start(next: { [weak self] subtitle in if let strongSelf = self { if let subtitle = subtitle { - let titleView = CounterContollerTitleView(theme: strongSelf.presentationData.theme) - titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.WallpaperPreview_Title, counter: subtitle) + let titleView = CounterControllerTitleView(theme: strongSelf.presentationData.theme) + titleView.title = CounterControllerTitle(title: strongSelf.presentationData.strings.WallpaperPreview_Title, counter: subtitle) strongSelf.navigationItem.titleView = titleView strongSelf.title = nil } else { @@ -527,10 +527,10 @@ public class WallpaperGalleryController: ViewController { if forBoth && !strongSelf.context.isPremium { let context = strongSelf.context var replaceImpl: ((ViewController) -> Void)? - let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .wallpapers, action: { + let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .wallpapers, forceDark: false, action: { let controller = context.sharedContext.makePremiumIntroController(context: context, source: .wallpapers, forceDark: false, dismissed: nil) replaceImpl?(controller) - }) + }, dismissed: nil) replaceImpl = { [weak controller] c in controller?.replace(with: c) } diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperPatternPanelNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperPatternPanelNode.swift index 508c0bb11b3..c767e89908a 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperPatternPanelNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperPatternPanelNode.swift @@ -154,8 +154,8 @@ private final class WallpaperPatternItemNode : ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - super.animateInsertion(currentTimestamp, duration: duration, short: short) + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + super.animateInsertion(currentTimestamp, duration: duration, options: options) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridController.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridController.swift index d7a193baded..5892427c044 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridController.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridController.swift @@ -337,7 +337,11 @@ public final class ThemeColorsGridController: ViewController, AttachmentContaina public var requestAttachmentMenuExpansion: () -> Void = {} public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } + public var parentController: () -> ViewController? = { + return nil + } public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in } + public var updateTabBarVisibility: (Bool, ContainedViewLayoutTransition) -> Void = { _, _ in } public var cancelPanGesture: () -> Void = { } public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchColorsItem.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchColorsItem.swift index d02bdf36a42..2056b25cb9f 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchColorsItem.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchColorsItem.swift @@ -240,7 +240,7 @@ class ThemeGridSearchColorsItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } diff --git a/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift b/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift index f110a6643d8..851c0342708 100644 --- a/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift +++ b/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift @@ -1156,7 +1156,7 @@ public class StickerPickerScreen: ViewController { })) } case let .category(value): - let resultSignal = context.engine.stickers.searchEmoji(emojiString: value) + let resultSignal = context.engine.stickers.searchEmoji(category: value) |> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in var items: [EmojiPagerContentComponent.Item] = [] @@ -1227,10 +1227,10 @@ public class StickerPickerScreen: ViewController { fillWithLoadingPlaceholders: true, items: [] ) - ], id: AnyHashable(value), version: version, isPreset: true), isSearching: false) + ], id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) return } - self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value), version: version, isPreset: true), isSearching: false) + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) version += 1 })) } @@ -1442,7 +1442,7 @@ public class StickerPickerScreen: ViewController { strongSelf.stickerSearchDisposable.set(nil) strongSelf.stickerSearchStateValue = EmojiSearchState(result: nil, isSearching: false) case let .category(value): - let resultSignal = context.engine.stickers.searchStickers(query: value, scope: [.installed, .remote]) + let resultSignal = context.engine.stickers.searchStickers(category: value, scope: [.installed, .remote]) |> mapToSignal { files -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in var items: [EmojiPagerContentComponent.Item] = [] @@ -1513,10 +1513,10 @@ public class StickerPickerScreen: ViewController { fillWithLoadingPlaceholders: true, items: [] ) - ], id: AnyHashable(value), version: version, isPreset: true), isSearching: false) + ], id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) return } - strongSelf.stickerSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value), version: version, isPreset: true), isSearching: false) + strongSelf.stickerSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) version += 1 })) } diff --git a/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/BUILD b/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/BUILD index f17f6090055..ae6df8bf440 100644 --- a/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/BUILD +++ b/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/BUILD @@ -12,9 +12,13 @@ swift_library( deps = [ "//submodules/Display", "//submodules/ComponentFlow", + "//submodules/TelegramCore", "//submodules/TelegramPresentationData", "//submodules/Components/MultilineTextComponent", + "//submodules/Components/MultilineTextWithEntitiesComponent", "//submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView", + "//submodules/AccountContext", + "//submodules/TextFormat", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/Sources/ForwardInfoPanelComponent.swift b/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/Sources/ForwardInfoPanelComponent.swift index c1f065c7f12..170788ca402 100644 --- a/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/Sources/ForwardInfoPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/Sources/ForwardInfoPanelComponent.swift @@ -2,25 +2,35 @@ import Foundation import UIKit import Display import ComponentFlow +import TelegramCore import MultilineTextComponent +import MultilineTextWithEntitiesComponent import MessageInlineBlockBackgroundView +import AccountContext +import TextFormat public final class ForwardInfoPanelComponent: Component { + public let context: AccountContext public let authorName: String public let text: String + public let entities: [MessageTextEntity] public let isChannel: Bool public let isVibrant: Bool public let fillsWidth: Bool public init( + context: AccountContext, authorName: String, text: String, + entities: [MessageTextEntity], isChannel: Bool, isVibrant: Bool, fillsWidth: Bool ) { + self.context = context self.authorName = authorName self.text = text + self.entities = entities self.isChannel = isChannel self.isVibrant = isVibrant self.fillsWidth = fillsWidth @@ -33,6 +43,9 @@ public final class ForwardInfoPanelComponent: Component { if lhs.text != rhs.text { return false } + if lhs.entities != rhs.entities { + return false + } if lhs.isChannel != rhs.isChannel { return false } @@ -47,7 +60,6 @@ public final class ForwardInfoPanelComponent: Component { public final class View: UIView { public let backgroundView: UIImageView -// private let blurBackgroundView: BlurredBackgroundView private let blurBackgroundView: UIVisualEffectView private let blockView: MessageInlineBlockBackgroundView private var iconView: UIImageView? @@ -58,8 +70,6 @@ public final class ForwardInfoPanelComponent: Component { private weak var state: EmptyComponentState? override init(frame: CGRect) { -// self.blurBackgroundView = BlurredBackgroundView(color: UIColor(rgb: 0x000000, alpha: 0.4)) - if #available(iOS 13.0, *) { self.blurBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterialDark)) } else { @@ -102,7 +112,7 @@ public final class ForwardInfoPanelComponent: Component { iconView.frame = CGRect(origin: CGPoint(x: sideInset + UIScreenPixel, y: 5.0), size: image.size) } titleOffset += 13.0 - + let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( @@ -124,14 +134,20 @@ public final class ForwardInfoPanelComponent: Component { view.frame = titleFrame } + let textFont = Font.regular(14.0) + let textColor = UIColor.white + let attributedText = stringWithAppliedEntities(component.text, entities: component.entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: nil) + let textSize = self.text.update( transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: component.text, - font: Font.regular(14.0), - textColor: .white - )), + component: AnyComponent(MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: UIColor(rgb: 0xffffff, alpha: 0.4), + text: .plain(attributedText), + horizontalAlignment: .natural, + truncationType: .end, maximumNumberOfLines: 1 )), environment: {}, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift index f32358946b9..c8e338546a7 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift @@ -170,7 +170,8 @@ public extension StoryContainerScreen { transitionIn: @escaping () -> StoryContainerScreen.TransitionIn?, transitionOut: @escaping (EnginePeer.Id) -> StoryContainerScreen.TransitionOut?, setFocusedItem: @escaping (Signal) -> Void, - setProgress: @escaping (Signal) -> Void + setProgress: @escaping (Signal) -> Void, + completion: @escaping (StoryContainerScreen) -> Void = { _ in } ) { let storyContent = StoryContentContextImpl(context: context, isHidden: isHidden, focusedPeerId: peerId, singlePeer: singlePeer, fixedOrder: initialOrder) let signal = storyContent.state @@ -211,6 +212,7 @@ public extension StoryContainerScreen { ) setFocusedItem(storyContainerScreen.focusedItem) parentController?.push(storyContainerScreen) + completion(storyContainerScreen) } |> ignoreValues diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 0fd99473826..9d200b7a992 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -1223,6 +1223,16 @@ private final class StoryContainerScreenComponent: Component { } } + func presentExternalTooltip(_ tooltipScreen: UndoOverlayController) { + guard let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else { + return + } + itemSetComponentView.sendMessageContext.tooltipScreen = tooltipScreen + itemSetComponentView.updateIsProgressPaused() + + self.environment?.controller()?.present(tooltipScreen, in: .current) + } + func update(component: StoryContainerScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { if self.didAnimateOut { return availableSize @@ -2057,6 +2067,12 @@ public class StoryContainerScreen: ViewControllerComponentContainer { } } + public func presentExternalTooltip(_ tooltipScreen: UndoOverlayController) { + if let componentView = self.node.hostView.componentView as? StoryContainerScreenComponent.View { + componentView.presentExternalTooltip(tooltipScreen) + } + } + func dismissWithoutTransitionOut() { self.focusedItemPromise.set(.single(nil)) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift index 7bc8d4a842d..5c8132efaa2 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift @@ -698,6 +698,7 @@ final class StoryContentCaptionComponent: Component { let authorName: String let isChannel: Bool let text: String? + let entities: [MessageTextEntity] switch forwardInfo { case let .known(peer, _, _): @@ -706,6 +707,7 @@ final class StoryContentCaptionComponent: Component { if let story = self.forwardInfoStory { text = story.text + entities = story.entities } else if self.forwardInfoDisposable == nil, let forwardInfoStory = component.forwardInfoStory { self.forwardInfoDisposable = (forwardInfoStory |> deliverOnMainQueue).start(next: { story in @@ -717,13 +719,16 @@ final class StoryContentCaptionComponent: Component { } }) text = "" + entities = [] } else { text = "" + entities = [] } case let .unknown(name, _): authorName = name isChannel = false text = "" + entities = [] } if let text { @@ -741,8 +746,10 @@ final class StoryContentCaptionComponent: Component { PlainButtonComponent( content: AnyComponent( ForwardInfoPanelComponent( + context: component.context, authorName: authorName, text: text, + entities: entities, isChannel: isChannel, isVibrant: false, fillsWidth: false diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 1a59d41eb53..6f931b1bca6 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -905,7 +905,7 @@ public final class StoryItemSetContainerComponent: Component { if hasFirstResponder(self) { view.deactivateInput() } else { - self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(view: nil, kind: .textFocusChanged))) + self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(view: nil, kind: .textFocusChanged(isFocused: false)))) } } } @@ -5757,22 +5757,27 @@ public final class StoryItemSetContainerComponent: Component { }) } - private func presentStoriesUpgradeScreen(source: PremiumSource) { + private func presentStoriesUpgradeScreen(source: PremiumIntroSource) { guard let component = self.component else { return } let context = component.context var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitsListScreen(context: context, subject: .stories, source: .other, order: [.stories], buttonText: component.strings.Story_PremiumUpgradeStoriesButton, isPremium: false, forceDark: true) - controller.action = { [weak self] in + var dismissedImpl: (() -> Void)? + let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .stories, forceDark: true, action: { [weak self] in guard let self else { return } - let controller = PremiumIntroScreen(context: context, source: source, forceDark: true) + var dismissedImpl: (() -> Void)? + let controller = context.sharedContext.makePremiumIntroController(context: context, source: source, forceDark: true, dismissed: { + dismissedImpl?() + }) self.sendMessageContext.actionSheet = controller - controller.wasDismissed = { [weak self, weak controller]in + replaceImpl?(controller) + + dismissedImpl = { [weak self, weak controller] in guard let self else { return } @@ -5782,10 +5787,10 @@ public final class StoryItemSetContainerComponent: Component { } self.updateIsProgressPaused() } - - replaceImpl?(controller) - } - controller.disposed = { [weak self, weak controller] in + }, dismissed: { + dismissedImpl?() + }) + dismissedImpl = { [weak self, weak controller] in guard let self else { return } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 2c724d58823..0f5ea43c4b6 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -757,15 +757,11 @@ final class StoryItemSetContainerSendMessage { let size = image.size.aspectFitted(CGSize(width: 512.0, height: 512.0)) func scaleImage(_ image: UIImage, size: CGSize, boundiingSize: CGSize) -> UIImage? { - if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { - let format = UIGraphicsImageRendererFormat() - format.scale = 1.0 - let renderer = UIGraphicsImageRenderer(size: size, format: format) - return renderer.image { _ in - image.draw(in: CGRect(origin: .zero, size: size)) - } - } else { - return TGScaleImageToPixelSize(image, size) + let format = UIGraphicsImageRendererFormat() + format.scale = 1.0 + let renderer = UIGraphicsImageRenderer(size: size, format: format) + return renderer.image { _ in + image.draw(in: CGRect(origin: .zero, size: size)) } } @@ -3190,7 +3186,8 @@ final class StoryItemSetContainerSendMessage { let sheet = StoryStealthModeSheetScreen( context: component.context, - mode: .control(cooldownUntilTimestamp: config.stealthModeState.actualizedNow().cooldownUntilTimestamp), + mode: .control(external: false, cooldownUntilTimestamp: config.stealthModeState.actualizedNow().cooldownUntilTimestamp), + forceDark: true, backwardDuration: pastPeriod, forwardDuration: futurePeriod, buttonAction: { [weak self, weak view] in @@ -3265,6 +3262,7 @@ final class StoryItemSetContainerSendMessage { let sheet = StoryStealthModeSheetScreen( context: component.context, mode: .upgrade, + forceDark: true, backwardDuration: pastPeriod, forwardDuration: futurePeriod, buttonAction: { diff --git a/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeSheetScreen.swift b/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeSheetScreen.swift index 81033083930..61ea6561a80 100644 --- a/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeSheetScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeSheetScreen.swift @@ -100,7 +100,7 @@ private final class StoryStealthModeSheetContentComponent: Component { self.state = state var remainingCooldownSeconds: Int32 = 0 - if case let .control(cooldownUntilTimestamp) = component.mode { + if case let .control(_, cooldownUntilTimestamp) = component.mode { if let cooldownUntilTimestamp { remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) remainingCooldownSeconds = max(0, remainingCooldownSeconds) @@ -243,9 +243,9 @@ private final class StoryStealthModeSheetContentComponent: Component { let buttonText: String let content: AnyComponentWithIdentity switch component.mode { - case .control: + case let .control(external, _): if remainingCooldownSeconds <= 0 { - buttonText = environment.strings.Story_StealthMode_EnableAction + buttonText = external ? environment.strings.Story_StealthMode_EnableAndOpenAction : environment.strings.Story_StealthMode_EnableAction } else { buttonText = environment.strings.Story_StealthMode_CooldownAction(stringForDuration(remainingCooldownSeconds)).string } @@ -283,7 +283,7 @@ private final class StoryStealthModeSheetContentComponent: Component { } switch component.mode { - case let .control(cooldownUntilTimestamp): + case let .control(_, cooldownUntilTimestamp): var remainingCooldownSeconds: Int32 = 0 if let cooldownUntilTimestamp { remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) @@ -468,13 +468,14 @@ private final class StoryStealthModeSheetScreenComponent: Component { public class StoryStealthModeSheetScreen: ViewControllerComponentContainer { public enum Mode: Equatable { - case control(cooldownUntilTimestamp: Int32?) + case control(external: Bool, cooldownUntilTimestamp: Int32?) case upgrade } public init( context: AccountContext, mode: Mode, + forceDark: Bool, backwardDuration: Int32, forwardDuration: Int32, buttonAction: (() -> Void)? = nil @@ -485,7 +486,7 @@ public class StoryStealthModeSheetScreen: ViewControllerComponentContainer { backwardDuration: backwardDuration, forwardDuration: forwardDuration, buttonAction: buttonAction - ), navigationBarAppearance: .none, theme: .dark) + ), navigationBarAppearance: .none, theme: forceDark ? .dark : .default) self.statusBar.statusBarStyle = .Ignore self.navigationPresentation = .flatModal diff --git a/submodules/TelegramUI/Components/TelegramAccountAuxiliaryMethods/Sources/TelegramAccountAuxiliaryMethods.swift b/submodules/TelegramUI/Components/TelegramAccountAuxiliaryMethods/Sources/TelegramAccountAuxiliaryMethods.swift index c5612d39c12..d0b50ef37dd 100644 --- a/submodules/TelegramUI/Components/TelegramAccountAuxiliaryMethods/Sources/TelegramAccountAuxiliaryMethods.swift +++ b/submodules/TelegramUI/Components/TelegramAccountAuxiliaryMethods/Sources/TelegramAccountAuxiliaryMethods.swift @@ -41,12 +41,23 @@ public func makeTelegramAccountAuxiliaryMethods(uploadInBackground: ((Postbox, M } |> castError(MediaResourceDataFetchError.self) |> mapToSignal { useModernPipeline -> Signal in - fetchLocalFileVideoMediaResource(postbox: postbox, resource: resource, alwaysUseModernPipeline: useModernPipeline) + return fetchLocalFileVideoMediaResource(postbox: postbox, resource: resource, alwaysUseModernPipeline: useModernPipeline) } } else if let resource = resource as? LocalFileGifMediaResource { return fetchLocalFileGifMediaResource(resource: resource) } else if let photoLibraryResource = resource as? PhotoLibraryMediaResource { - return fetchPhotoLibraryResource(localIdentifier: photoLibraryResource.localIdentifier, width: photoLibraryResource.width, height: photoLibraryResource.height, format: photoLibraryResource.format, quality: photoLibraryResource.quality) + return postbox.transaction { transaction -> Bool in + var useExif = true + let appConfig = currentAppConfiguration(transaction: transaction) + if let data = appConfig.data, let _ = data["ios_killswitch_disable_use_photo_exif"] { + useExif = false + } + return useExif + } + |> castError(MediaResourceDataFetchError.self) + |> mapToSignal { useExif -> Signal in + return fetchPhotoLibraryResource(localIdentifier: photoLibraryResource.localIdentifier, width: photoLibraryResource.width, height: photoLibraryResource.height, format: photoLibraryResource.format, quality: photoLibraryResource.quality, useExif: useExif) + } } else if let resource = resource as? ICloudFileResource { return fetchICloudFileResource(resource: resource) } else if let resource = resource as? SecureIdLocalImageResource { diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index f0c41d9058d..2005164bf6a 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -69,9 +69,9 @@ public final class TextFieldComponent: Component { public final class AnimationHint { - public enum Kind { + public enum Kind: Equatable { case textChanged - case textFocusChanged + case textFocusChanged(isFocused: Bool) } public weak var view: View? @@ -105,6 +105,7 @@ public final class TextFieldComponent: Component { public let hideKeyboard: Bool public let customInputView: UIView? public let resetText: NSAttributedString? + public let assumeIsEditing: Bool public let isOneLineWhenUnfocused: Bool public let characterLimit: Int? public let emptyLineHandling: EmptyLineHandling @@ -114,6 +115,7 @@ public final class TextFieldComponent: Component { public let present: (ViewController) -> Void public let paste: (PasteData) -> Void public let returnKeyAction: (() -> Void)? + public let backspaceKeyAction: (() -> Void)? public init( context: AccountContext, @@ -126,6 +128,7 @@ public final class TextFieldComponent: Component { hideKeyboard: Bool, customInputView: UIView?, resetText: NSAttributedString?, + assumeIsEditing: Bool = false, isOneLineWhenUnfocused: Bool, characterLimit: Int? = nil, emptyLineHandling: EmptyLineHandling = .allowed, @@ -134,7 +137,8 @@ public final class TextFieldComponent: Component { lockedFormatAction: @escaping () -> Void, present: @escaping (ViewController) -> Void, paste: @escaping (PasteData) -> Void, - returnKeyAction: (() -> Void)? = nil + returnKeyAction: (() -> Void)? = nil, + backspaceKeyAction: (() -> Void)? = nil ) { self.context = context self.theme = theme @@ -146,6 +150,7 @@ public final class TextFieldComponent: Component { self.hideKeyboard = hideKeyboard self.customInputView = customInputView self.resetText = resetText + self.assumeIsEditing = assumeIsEditing self.isOneLineWhenUnfocused = isOneLineWhenUnfocused self.characterLimit = characterLimit self.emptyLineHandling = emptyLineHandling @@ -155,6 +160,7 @@ public final class TextFieldComponent: Component { self.present = present self.paste = paste self.returnKeyAction = returnKeyAction + self.backspaceKeyAction = backspaceKeyAction } public static func ==(lhs: TextFieldComponent, rhs: TextFieldComponent) -> Bool { @@ -188,6 +194,9 @@ public final class TextFieldComponent: Component { if lhs.resetText != rhs.resetText { return false } + if lhs.assumeIsEditing != rhs.assumeIsEditing { + return false + } if lhs.isOneLineWhenUnfocused != rhs.isOneLineWhenUnfocused { return false } @@ -296,7 +305,14 @@ public final class TextFieldComponent: Component { } public func insertText(_ text: NSAttributedString) { + guard let component = self.component else { + return + } + self.updateInputState { state in + if let characterLimit = component.characterLimit, state.inputText.length + text.length > characterLimit { + return state + } return state.insertText(text) } if !self.isUpdating { @@ -447,7 +463,7 @@ public final class TextFieldComponent: Component { return } if !self.isUpdating { - self.state?.updated(transition: Transition(animation: .curve(duration: 0.5, curve: .spring)).withUserData(AnimationHint(view: self, kind: .textFocusChanged))) + self.state?.updated(transition: Transition(animation: .curve(duration: 0.5, curve: .spring)).withUserData(AnimationHint(view: self, kind: .textFocusChanged(isFocused: true)))) } if component.isOneLineWhenUnfocused { Queue.mainQueue().justDispatch { @@ -458,11 +474,15 @@ public final class TextFieldComponent: Component { public func chatInputTextNodeDidFinishEditing() { if !self.isUpdating { - self.state?.updated(transition: Transition(animation: .curve(duration: 0.5, curve: .spring)).withUserData(AnimationHint(view: self, kind: .textFocusChanged))) + self.state?.updated(transition: Transition(animation: .curve(duration: 0.5, curve: .spring)).withUserData(AnimationHint(view: self, kind: .textFocusChanged(isFocused: false)))) } } public func chatInputTextNodeBackspaceWhileEmpty() { + guard let component = self.component else { + return + } + component.backspaceKeyAction?() } @available(iOS 13.0, *) @@ -1040,7 +1060,6 @@ public final class TextFieldComponent: Component { if let current = component.externalState.currentEmojiSuggestion, current.position.value == emojiSuggestionPosition.value { emojiSuggestion = current } else { - emojiSuggestion = EmojiSuggestion(localPosition: trackingPosition, position: emojiSuggestionPosition) component.externalState.currentEmojiSuggestion = emojiSuggestion } @@ -1135,7 +1154,7 @@ public final class TextFieldComponent: Component { } let wasEditing = component.externalState.isEditing - let isEditing = self.textView.isFirstResponder + let isEditing = self.textView.isFirstResponder || component.assumeIsEditing var innerTextInsets = component.insets innerTextInsets.left = 0.0 diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Ban.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Ban.imageset/Contents.json new file mode 100644 index 00000000000..4aeca4c67f8 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Ban.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ban_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Ban.imageset/ban_24.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Ban.imageset/ban_24.pdf new file mode 100644 index 00000000000..5fd4cbbd92d Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Ban.imageset/ban_24.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Recent Actions/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Recent Actions/Contents.json new file mode 100644 index 00000000000..6e965652df6 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Recent Actions/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Recent Actions/Info.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Recent Actions/Info.imageset/Contents.json new file mode 100644 index 00000000000..cb1f2a6fcb1 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Recent Actions/Info.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "question.circle.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Recent Actions/Info.imageset/question.circle.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Recent Actions/Info.imageset/question.circle.pdf new file mode 100644 index 00000000000..071d47854fb Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Recent Actions/Info.imageset/question.circle.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Recent Actions/Placeholder.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Recent Actions/Placeholder.imageset/Contents.json new file mode 100644 index 00000000000..36e91193f05 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Recent Actions/Placeholder.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "recentactions.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Recent Actions/Placeholder.imageset/recentactions.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Recent Actions/Placeholder.imageset/recentactions.pdf new file mode 100644 index 00000000000..d4a44d77bf1 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Recent Actions/Placeholder.imageset/recentactions.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Contact List/AddBirthdayIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Contact List/AddBirthdayIcon.imageset/Contents.json new file mode 100644 index 00000000000..8ddd25baad2 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Contact List/AddBirthdayIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "addbirthday_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Contact List/AddBirthdayIcon.imageset/addbirthday_30.pdf b/submodules/TelegramUI/Images.xcassets/Contact List/AddBirthdayIcon.imageset/addbirthday_30.pdf new file mode 100644 index 00000000000..e871e0e803c Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Contact List/AddBirthdayIcon.imageset/addbirthday_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Item List/ExpandingItemVerticalRegularArrow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Item List/ExpandingItemVerticalRegularArrow.imageset/Contents.json new file mode 100644 index 00000000000..ccce3ad789a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/ExpandingItemVerticalRegularArrow.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "delmsgarrow_16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Item List/ExpandingItemVerticalRegularArrow.imageset/delmsgarrow_16.pdf b/submodules/TelegramUI/Images.xcassets/Item List/ExpandingItemVerticalRegularArrow.imageset/delmsgarrow_16.pdf new file mode 100644 index 00000000000..f0fb36302f2 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Item List/ExpandingItemVerticalRegularArrow.imageset/delmsgarrow_16.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Item List/ExpandingItemVerticalSmallArrow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Item List/ExpandingItemVerticalSmallArrow.imageset/Contents.json new file mode 100644 index 00000000000..7d404648910 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/ExpandingItemVerticalSmallArrow.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "arrow.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Item List/ExpandingItemVerticalSmallArrow.imageset/arrow.pdf b/submodules/TelegramUI/Images.xcassets/Item List/ExpandingItemVerticalSmallArrow.imageset/arrow.pdf new file mode 100644 index 00000000000..1cf0950fbb5 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Item List/ExpandingItemVerticalSmallArrow.imageset/arrow.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Item List/InlineIconUsers.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Item List/InlineIconUsers.imageset/Contents.json new file mode 100644 index 00000000000..2b79531409a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/InlineIconUsers.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "delmsgusers_16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Item List/InlineIconUsers.imageset/delmsgusers_16.pdf b/submodules/TelegramUI/Images.xcassets/Item List/InlineIconUsers.imageset/delmsgusers_16.pdf new file mode 100644 index 00000000000..e42ada8d59a Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Item List/InlineIconUsers.imageset/delmsgusers_16.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Item List/SwitchLockIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Item List/SwitchLockIcon.imageset/Contents.json new file mode 100644 index 00000000000..c6b56e00237 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/SwitchLockIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "lock.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Item List/SwitchLockIcon.imageset/lock.pdf b/submodules/TelegramUI/Images.xcassets/Item List/SwitchLockIcon.imageset/lock.pdf new file mode 100644 index 00000000000..db9260ea737 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Item List/SwitchLockIcon.imageset/lock.pdf differ diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index c00430c1f8d..a9d55013cbd 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -2494,13 +2494,13 @@ private class UserInterfaceStyleObserverWindow: UIWindow { return (sharedApplicationContext.sharedContext, context, authContext) } } - |> deliverOnMainQueue).start(next: { _, context, authContext in - if let authContext = authContext, let confirmationCode = parseConfirmationCodeUrl(url) { + |> deliverOnMainQueue).start(next: { sharedContext, context, authContext in + if let authContext = authContext, let confirmationCode = parseConfirmationCodeUrl(sharedContext: sharedContext, url: url) { authContext.rootController.applyConfirmationCode(confirmationCode) } else if let context = context { context.openUrl(url) } else if let authContext = authContext { - if let proxyData = parseProxyUrl(url) { + if let proxyData = parseProxyUrl(sharedContext: sharedContext, url: url) { authContext.rootController.view.endEditing(true) let presentationData = authContext.sharedContext.currentPresentationData.with { $0 } let controller = ProxyServerActionSheetController(presentationData: presentationData, accountManager: authContext.sharedContext.accountManager, postbox: authContext.account.postbox, network: authContext.account.network, server: proxyData, updatedPresentationData: nil) @@ -2708,6 +2708,8 @@ private class UserInterfaceStyleObserverWindow: UIWindow { self.openChatWhenReady(accountId: nil, peerId: context.context.account.peerId, threadId: nil, storyId: nil) case .account: context.switchAccount() + case .appIcon: + context.openAppIcon() } } } diff --git a/submodules/TelegramUI/Sources/ApplicationContext.swift b/submodules/TelegramUI/Sources/ApplicationContext.swift index 52f0de5dd3b..b206a10e12f 100644 --- a/submodules/TelegramUI/Sources/ApplicationContext.swift +++ b/submodules/TelegramUI/Sources/ApplicationContext.swift @@ -947,6 +947,10 @@ final class AuthorizedApplicationContext { self.rootController.openRootCamera() } + func openAppIcon() { + self.rootController.openAppIcon() + } + func switchAccount() { let _ = (activeAccountsAndPeers(context: self.context) |> take(1) diff --git a/submodules/TelegramUI/Sources/ApplicationShortcutItem.swift b/submodules/TelegramUI/Sources/ApplicationShortcutItem.swift index 6579fe691b2..cbaac75d105 100644 --- a/submodules/TelegramUI/Sources/ApplicationShortcutItem.swift +++ b/submodules/TelegramUI/Sources/ApplicationShortcutItem.swift @@ -9,6 +9,7 @@ enum ApplicationShortcutItemType: String { case camera case savedMessages case account + case appIcon } struct ApplicationShortcutItem: Equatable { @@ -32,6 +33,8 @@ extension ApplicationShortcutItem { icon = UIApplicationShortcutIcon(templateImageName: "Shortcuts/SavedMessages") case .account: icon = UIApplicationShortcutIcon(templateImageName: "Shortcuts/Account") + case .appIcon: + icon = UIApplicationShortcutIcon(templateImageName: "Shortcuts/AppIcon") } return UIApplicationShortcutItem(type: self.type.rawValue, localizedTitle: self.title, localizedSubtitle: self.subtitle, icon: icon, userInfo: nil) } @@ -45,18 +48,12 @@ func applicationShortcutItems(strings: PresentationStrings, otherAccountName: St ApplicationShortcutItem(type: .savedMessages, title: strings.Conversation_SavedMessages, subtitle: nil), ApplicationShortcutItem(type: .account, title: strings.Shortcut_SwitchAccount, subtitle: otherAccountName) ] - } else if DeviceAccess.isCameraAccessAuthorized() { - return [ - ApplicationShortcutItem(type: .search, title: strings.Common_Search, subtitle: nil), - ApplicationShortcutItem(type: .compose, title: strings.Compose_NewMessage, subtitle: nil), - ApplicationShortcutItem(type: .camera, title: strings.Camera_Title, subtitle: nil), - ApplicationShortcutItem(type: .savedMessages, title: strings.Conversation_SavedMessages, subtitle: nil) - ] } else { return [ ApplicationShortcutItem(type: .search, title: strings.Common_Search, subtitle: nil), ApplicationShortcutItem(type: .compose, title: strings.Compose_NewMessage, subtitle: nil), - ApplicationShortcutItem(type: .savedMessages, title: strings.Conversation_SavedMessages, subtitle: nil) + ApplicationShortcutItem(type: .savedMessages, title: strings.Conversation_SavedMessages, subtitle: nil), + ApplicationShortcutItem(type: .appIcon, title: strings.Shortcut_AppIcon, subtitle: nil) ] } } diff --git a/submodules/TelegramUI/Sources/AttachmentFileController.swift b/submodules/TelegramUI/Sources/AttachmentFileController.swift index 456dde1f032..c1c1126a1c8 100644 --- a/submodules/TelegramUI/Sources/AttachmentFileController.swift +++ b/submodules/TelegramUI/Sources/AttachmentFileController.swift @@ -196,7 +196,11 @@ private final class AttachmentFileContext: AttachmentMediaPickerContext { class AttachmentFileControllerImpl: ItemListController, AttachmentFileController, AttachmentContainable { public var requestAttachmentMenuExpansion: () -> Void = {} public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } + public var parentController: () -> ViewController? = { + return nil + } public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in } + public var updateTabBarVisibility: (Bool, ContainedViewLayoutTransition) -> Void = { _, _ in } public var cancelPanGesture: () -> Void = { } public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift new file mode 100644 index 00000000000..ff1bfab1713 --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -0,0 +1,4861 @@ +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) + } + + // MARK: Nicegram, additonal ChatPanelInterfaceInteraction closures + let interfaceInteraction = ChatPanelInterfaceInteraction(cloudMessages: { [weak self] messages in + guard let strongSelf = self, strongSelf.isNodeLoaded else { + return + } + if let messages = messages { + // Multiple + if !messages.isEmpty { + strongSelf.commitPurposefulAction() + let forwardMessageIds = messages.map { $0.id }.sorted() + strongSelf.forwardMessages(messageIds: forwardMessageIds, cloud: true) + } + } else { + // One + strongSelf.commitPurposefulAction() + if let forwardMessageIdsSet = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds { + let forwardMessageIds = Array(forwardMessageIdsSet).sorted() + strongSelf.forwardMessages(messageIds: forwardMessageIds, cloud: true) + } + } + }, copyForwardMessages: { [weak self] messages in + guard let strongSelf = self, strongSelf.isNodeLoaded else { + return + } + if let messages = messages { + // Multiple + if !messages.isEmpty { + strongSelf.commitPurposefulAction() + let forwardMessageIds = messages.map { $0.id }.sorted() + strongSelf.forwardMessages(messageIds: forwardMessageIds, asCopy: true) + } + } else { + // One + strongSelf.commitPurposefulAction() + if let forwardMessageIdsSet = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds { + let forwardMessageIds = Array(forwardMessageIdsSet).sorted() + strongSelf.forwardMessages(messageIds: forwardMessageIds, asCopy: true) + } + } + }, copySelectedMessages: { [weak self] in + // MARK: Nicegram CopySelectedMessages + guard let strongSelf = self else { + return + } + + strongSelf.commitPurposefulAction() + guard let forwardMessageIdsSet = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds else { + return + } + let forwardMessageIds = Array(forwardMessageIdsSet) + let _ = (strongSelf.context.engine.data.get(EngineDataMap( + forwardMessageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init) + )) + |> map { messages -> String in + return messages.values + .compactMap{$0} + .sorted(by: { $0.timestamp < $1.timestamp }) + .map(\.text) + .filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } + .joined(separator: "\n") + } + |> deliverOnMainQueue).start(next: { [weak self] text in + UIPasteboard.general.string = text + if let self = self { + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) + Queue.mainQueue().after(0.2, { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied) + self.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in + return true + }), in: .current) + }) + } + }) + }, openGifs: { [weak self] in + // MARK: Nicegram OpenGifsShortcut + guard let self = self else { return } + self.chatDisplayNode.openStickers(defaultTab: .gif, beginWithEmoji: false) + self.mediaRecordingModeTooltipController?.dismissImmediately() + // + }, replyPrivately: { [weak self] message in + // MARK: Nicegram ReplyPrivately + self?.replyPrivately(message: message) + // + }, 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 + } + // MARK: Nicegram AiChat + strongSelf.updateAiOverlayVisibility() + // + } + }) + } 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 + } + // MARK: Nicegram AiChat + strongSelf.updateAiOverlayVisibility() + // + } + }) + } + + 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 00000000000..611e1b98967 --- /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 00000000000..cecc39ee852 --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift @@ -0,0 +1,653 @@ +// MARK: Nicegram Imports +import NGWebUtils +// +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 + // MARK: - Nicegram (open restricted peer info) + let isAllowed: Bool + if let strongSelf = self, let peer = peerView.peers[peerView.peerId] { + let contentSettings = strongSelf.context.currentContentSettings.with { $0 } + isAllowed = isAllowedPeerInfo(peer: peer, contentSettings: contentSettings) + } else { + isAllowed = false + } + // + + // MARK: - Nicegram, isAllowed check added + if let strongSelf = self, let peer = peerView.peers[peerView.peerId], (peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil || isAllowed) && !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 eacaa102761..94f05aae8dd 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift @@ -454,3 +454,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 00000000000..ae57377d9c1 --- /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 00000000000..77353f1fad7 --- /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 00000000000..ada64473b48 --- /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 00000000000..496c26e8f9b --- /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 00000000000..d21126fadb8 --- /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 00000000000..138e33de1cb --- /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 2eef61c4be6..b527be081d6 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -3261,7 +3261,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } for media in message.media { if let poll = media as? TelegramMediaPoll, poll.pollId == pollId { - strongSelf.push(pollResultsController(context: strongSelf.context, messageId: messageId, poll: poll)) + strongSelf.push(pollResultsController(context: strongSelf.context, messageId: messageId, message: message, poll: poll)) break } } @@ -3556,7 +3556,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } for media in message.media { if let poll = media as? TelegramMediaPoll, poll.pollId.namespace == Namespaces.Media.CloudPoll { - strongSelf.push(pollResultsController(context: strongSelf.context, messageId: messageId, poll: poll, focusOnOptionWithOpaqueIdentifier: optionOpaqueIdentifier)) + strongSelf.push(pollResultsController(context: strongSelf.context, messageId: messageId, message: message, poll: poll, focusOnOptionWithOpaqueIdentifier: optionOpaqueIdentifier)) break } } @@ -4180,7 +4180,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if case .user = peerType, maxQuantity > 1 { let presentationData = self.presentationData var reachedLimitImpl: ((Int32) -> Void)? - let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .requestedUsersSelection, options: [], isPeerEnabled: { peer in + let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .requestedUsersSelection, isPeerEnabled: { peer in if case let .user(user) = peer, user.botInfo == nil { return true } else { @@ -4433,11 +4433,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) } @@ -5740,8 +5741,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 @@ -5778,8 +5779,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 @@ -6343,6 +6344,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 @@ -7358,6167 +7380,939 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return .single(nil) } } + + var storedAnimateFromSnapshotState: ChatControllerNode.SnapshotState? + + func animateFromPreviousController(snapshotState: ChatControllerNode.SnapshotState) { + self.storedAnimateFromSnapshotState = snapshotState + } 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.loadDisplayNodeImpl() + } - 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))") + override public func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if self.willAppear { + self.chatDisplayNode.historyNode.refreshPollActionsForVisibleMessages() + } else { + self.willAppear = true - if offset > 0.0 { - if var scrolledToMessageIdValue = strongSelf.scrolledToMessageIdValue { - scrolledToMessageIdValue.allowedReplacementDirection.insert(.up) - strongSelf.scrolledToMessageIdValue = scrolledToMessageIdValue - } - } else if offset < 0.0 { - strongSelf.scrolledToMessageIdValue = nil + // Limit this to reply threads just to be safe now + if case .replyThread = self.chatLocation { + self.chatDisplayNode.historyNode.refocusOnUnreadMessagesIfNeeded() } - - if let currentPinchSourceItemNode = strongSelf.currentPinchSourceItemNode { - if let itemNode = itemNode { - if itemNode === currentPinchSourceItemNode { - strongSelf.currentPinchController?.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition) - } + } + + if case let .replyThread(message) = self.chatLocation, message.isForumPost { + if self.keepMessageCountersSyncrhonizedDisposable == nil { + self.keepMessageCountersSyncrhonizedDisposable = self.context.engine.messages.keepMessageCountersSyncrhonized(peerId: message.peerId, threadId: message.threadId).startStrict() + } + } else if self.chatLocation.peerId == self.context.account.peerId { + if self.keepMessageCountersSyncrhonizedDisposable == nil { + if let threadId = self.chatLocation.threadId { + self.keepMessageCountersSyncrhonizedDisposable = self.context.engine.messages.keepMessageCountersSyncrhonized(peerId: self.context.account.peerId, threadId: threadId).startStrict() } else { - strongSelf.currentPinchController?.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition) + self.keepMessageCountersSyncrhonizedDisposable = self.context.engine.messages.keepMessageCountersSyncrhonized(peerId: self.context.account.peerId).startStrict() } } - - if isTracking { - strongSelf.chatDisplayNode.loadingPlaceholderNode?.addContentOffset(offset: offset, transition: transition) + if self.keepSavedMessagesSyncrhonizedDisposable == nil { + self.keepSavedMessagesSyncrhonizedDisposable = self.context.engine.stickers.refreshSavedMessageTags(subPeerId: self.chatLocation.threadId.flatMap(PeerId.init)).startStrict() } - 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 let scheduledActivateInput = scheduledActivateInput, case .text = scheduledActivateInput { + self.scheduledActivateInput = nil + + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in + return state.updatedInputMode({ _ in + switch scheduledActivateInput { + case .text: + return .text + case .entityInput: + return .media(mode: .other, expanded: nil, focused: false) + } + }) + }) } - if case .peer(self.context.account.peerId) = self.chatLocation { - var didDisplayTooltip = false - if "".isEmpty { - didDisplayTooltip = true + + var chatNavigationStack: [ChatNavigationStackItem] = self.chatNavigationStack + if let peerId = self.chatLocation.peerId { + if let summary = self.customNavigationDataSummary as? ChatControllerNavigationDataSummary { + chatNavigationStack.removeAll() + chatNavigationStack = summary.peerNavigationItems.filter({ $0 != ChatNavigationStackItem(peerId: peerId, threadId: self.chatLocation.threadId) }) } - self.chatDisplayNode.historyNode.hasLotsOfMessagesUpdated = { [weak self] hasLotsOfMessages in - guard let self, hasLotsOfMessages else { - return + if let _ = self.chatLocation.threadId { + if !chatNavigationStack.contains(ChatNavigationStackItem(peerId: peerId, threadId: nil)) { + chatNavigationStack.append(ChatNavigationStackItem(peerId: peerId, threadId: nil)) } - if didDisplayTooltip { + } + } + + if !chatNavigationStack.isEmpty { + self.chatDisplayNode.navigationBar?.backButtonNode.isGestureEnabled = true + self.chatDisplayNode.navigationBar?.backButtonNode.activated = { [weak self] gesture, _ in + guard let strongSelf = self, let backButtonNode = strongSelf.chatDisplayNode.navigationBar?.backButtonNode, let navigationController = strongSelf.effectiveNavigationController else { + gesture.cancel() 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 nextFolderId: Int32? = strongSelf.currentChatListFilter + PeerInfoScreenImpl.displayChatNavigationMenu( + context: strongSelf.context, + chatNavigationStack: chatNavigationStack, + nextFolderId: nextFolderId, + parentController: strongSelf, + backButtonView: backButtonNode.view, + navigationController: navigationController, + gesture: gesture + ) + } + } + + // MARK: Nicegram TranslateEnteredMessage (prefetch interlocator language) + let _ = getLanguageCode(forChatWith: chatLocation.peerId, context: context).start() + // + } + + var returnInputViewFocus = false + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + // MARK: Nicegram NGCard + if #available(iOS 13.0, *) { + if !self.didAppear { + if let peerId = self.chatLocation.peerId { + let int64Id = peerId.id._internalGetInt64Value() - 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 getCardOfferDataUseCase = CardContainer.shared.getCardOfferDataUseCase() + let cardBotId = getCardOfferDataUseCase()?.botId - let _ = ApplicationSpecificNotice.incrementSavedMessagesChatsSuggestion(accountManager: self.context.sharedContext.accountManager).startStandalone() - }) + if int64Id == cardBotId { + CardTgHelper.trackCardBotView() + } + } } } - - self.chatDisplayNode.historyNode.addContentOffset = { [weak self] offset, itemNode in - guard let strongSelf = self else { - return + // + + // MARK: Nicegram NGStats + if #available(iOS 13.0, *) { + if !self.didAppear { + if let peerId = self.chatLocation.peerId { + sharePeerData(peerId: peerId, context: self.context) + } } - 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 + // MARK: Nicegram GroupAnalytics + if !self.didAppear { + if let peerId = self.chatLocation.peerId { + trackChatOpen( + peerId: peerId, + context: self.context + ) } } + // - 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() + // MARK: Nicegram Unblock + if let peer = self.presentationInterfaceState.renderedPeer?.peer { + let isLastSeenBlockedChat = (peer.id.id._internalGetInt64Value() == AppCache.lastSeenBlockedChatId) + if isLastSeenBlockedChat, + isAllowedChat(peer: peer, contentSettings: context.currentContentSettings.with { $0 }) { + AppCache.lastSeenBlockedChatId = nil + if #available(iOS 14.0, *) { + if let windowScene = self.view.window?.windowScene { + SKStoreReviewController.requestReview(in: windowScene) } + } else { + SKStoreReviewController.requestReview() } - }) + } } + // + + self.didAppear = true - self.chatDisplayNode.overlayTitle = self.overlayTitle + self.chatDisplayNode.historyNode.experimentalSnapScrollToItem = false + self.chatDisplayNode.historyNode.canReadHistory.set(combineLatest(context.sharedContext.applicationBindings.applicationInForeground, self.canReadHistory.get()) |> map { a, b in + return a && b + }) - let currentAccountPeer = self.context.account.postbox.loadedPeerWithId(self.context.account.peerId) - |> map { peer in - return SendAsPeer(peer: peer, subscribers: nil, isPremiumRequired: false) - } + self.chatDisplayNode.loadInputPanels(theme: self.presentationInterfaceState.theme, strings: self.presentationInterfaceState.strings, fontSize: self.presentationInterfaceState.fontSize) - 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) - }) + if self.recentlyUsedInlineBotsDisposable == nil { + self.recentlyUsedInlineBotsDisposable = (self.context.engine.peers.recentlyUsedInlineBots() |> deliverOnMainQueue).startStrict(next: { [weak self] peers in + self?.recentlyUsedInlineBotsValue = peers.filter({ $0.1 >= 0.14 }).map({ $0.0._asPeer() }) }) } - 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 + if case .standard(.default) = self.presentationInterfaceState.mode, self.raiseToListen == nil { + self.raiseToListen = RaiseToListenManager(shouldActivate: { [weak self] in + if let strongSelf = self, strongSelf.isNodeLoaded && strongSelf.canReadHistoryValue, strongSelf.presentationInterfaceState.interfaceState.editMessage == nil, strongSelf.playlistStateAndType == nil { + if !strongSelf.context.sharedContext.currentMediaInputSettings.with({ $0.enableRaiseToSpeak }) { + return false + } - var canBypassRestrictions = false - if let boostsToUnrestrict = cachedData.boostsToUnrestrict, let appliedBoosts = cachedData.appliedBoosts, appliedBoosts >= boostsToUnrestrict { - canBypassRestrictions = true + if strongSelf.effectiveNavigationController?.topViewController !== strongSelf { + return false } - 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 strongSelf.presentationInterfaceState.inputTextPanelState.mediaRecordingState != nil { + return false } - if let activeCall = cachedData.activeCall { - activeGroupCallInfo = ChatActiveGroupCallInfo(activeCall: activeCall) + + if !strongSelf.traceVisibility() { + return false } - } 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) + if strongSelf.currentContextController != nil { + return false } - } 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) - } + if !isTopmostChatController(strongSelf) { + return false } - } 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 strongSelf.firstLoadedMessageToListen() != nil || strongSelf.chatDisplayNode.isTextInputPanelActive { + if strongSelf.context.sharedContext.immediateHasOngoingCall { + return false } + + if case .media = strongSelf.presentationInterfaceState.inputMode { + return false + } + return true } } - - 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 - } + return false + }, activate: { [weak self] in + self?.activateRaiseGesture() + }, deactivate: { [weak self] in + self?.deactivateRaiseGesture() + }) + self.raiseToListen?.enabled = self.canReadHistoryValue + self.tempVoicePlaylistEnded = { [weak self] in + guard let strongSelf = self else { + return } - - 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) - } - } + if !canSendMessagesToChat(strongSelf.presentationInterfaceState) { + return } - var buttonKeyboardMessage = combinedInitialData.buttonKeyboardMessage - if let buttonKeyboardMessageValue = buttonKeyboardMessage, buttonKeyboardMessageValue.isRestricted(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with({ $0 })) { - buttonKeyboardMessage = nil + if let raiseToListen = strongSelf.raiseToListen { + strongSelf.voicePlaylistDidEndTimestamp = CACurrentMediaTime() + raiseToListen.activateBasedOnProximity(delay: 0.0) } - 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) }) + if strongSelf.returnInputViewFocus { + strongSelf.returnInputViewFocus = false + strongSelf.chatDisplayNode.ensureInputViewFocused() } } - }) - - 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()) + self.tempVoicePlaylistItemChanged = { [weak self] previousItem, currentItem in + guard let strongSelf = self else { + return } + + strongSelf.chatDisplayNode.historyNode.voicePlaylistItemChanged(previousItem, currentItem) } - |> 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 arguments = self.presentationArguments as? ChatControllerOverlayPresentationData { + //TODO clear arguments + self.chatDisplayNode.animateInAsOverlay(from: arguments.expandData.0, completion: { + arguments.expandData.1() + }) } - 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 !self.didSetup3dTouch { + self.didSetup3dTouch = true + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + let dropInteraction = UIDropInteraction(delegate: self) + self.chatDisplayNode.view.addInteraction(dropInteraction) + } } - 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 + if !self.checkedPeerChatServiceActions { + self.checkedPeerChatServiceActions = true + + if case let .peer(peerId) = self.chatLocation, self.screenCaptureManager == nil { + if peerId.namespace == Namespaces.Peer.SecretChat { + self.screenCaptureManager = ScreenCaptureDetectionManager(check: { [weak self] in + if let strongSelf = self, strongSelf.traceVisibility() { + if strongSelf.canReadHistoryValue { + let _ = strongSelf.context.engine.messages.addSecretChatMessageScreenshot(peerId: peerId).startStandalone() + } + return true + } else { + return false + } + }) + } else if peerId.namespace == Namespaces.Peer.CloudUser && peerId.id._internalGetInt64Value() == 777000 { + self.screenCaptureManager = ScreenCaptureDetectionManager(check: { [weak self] in + if let strongSelf = self, strongSelf.traceVisibility() { + let loginCodeRegex = try? NSRegularExpression(pattern: "[\\d\\-]{5,7}", options: []) + var loginCodesToInvalidate: [String] = [] + strongSelf.chatDisplayNode.historyNode.forEachVisibleMessageItemNode({ itemNode in + if let text = itemNode.item?.message.text, let matches = loginCodeRegex?.matches(in: text, options: [], range: NSMakeRange(0, (text as NSString).length)), let match = matches.first { + loginCodesToInvalidate.append((text as NSString).substring(with: match.range)) + } + }) + if !loginCodesToInvalidate.isEmpty { + let _ = strongSelf.context.engine.auth.invalidateLoginCodes(codes: loginCodesToInvalidate).startStandalone() + } + return true + } else { + return false + } + }) + } else if peerId.namespace == Namespaces.Peer.CloudUser { + self.screenCaptureManager = ScreenCaptureDetectionManager(check: { [weak self] in + guard let self else { + return false + } + + let _ = (self.context.sharedContext.mediaManager.globalMediaPlayerState + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak self] playlistStateAndType in + if let self, let (_, playbackState, _) = playlistStateAndType, case let .state(state) = playbackState { + if let source = state.item.playbackData?.source, case let .telegramFile(_, _, isViewOnce) = source, isViewOnce { + self.context.sharedContext.mediaManager.setPlaylist(nil, type: .voice, control: .playback(.pause)) + } + } + }) + 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 - } + if case let .peer(peerId) = self.chatLocation { + let _ = self.context.engine.peers.checkPeerChatServiceActions(peerId: peerId).startStandalone() } - |> 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 + if self.chatLocation.peerId != nil && self.chatDisplayNode.frameForInputActionButton() != nil { + let inputText = self.presentationInterfaceState.interfaceState.effectiveInputState.inputText.string + if !inputText.isEmpty { + if inputText.count > 4 { + let _ = (ApplicationSpecificNotice.getChatMessageOptionsTip(accountManager: self.context.sharedContext.accountManager) + |> deliverOnMainQueue).startStandalone(next: { [weak self] counter in + if let strongSelf = self, counter < 3 { + let _ = ApplicationSpecificNotice.incrementChatMessageOptionsTip(accountManager: strongSelf.context.sharedContext.accountManager).startStandalone() + strongSelf.displaySendingOptionsTooltip() } - return ChatPresentationInterfaceState.ThreadData(title: data.info.title, icon: data.info.icon, iconColor: data.info.iconColor, isOwnedByMe: data.isOwnedByMe, isClosed: data.isClosed) + }) + } + } else if self.presentationInterfaceState.interfaceState.mediaRecordingMode == .audio { + var canSendMedia = false + if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel { + if channel.hasBannedPermission(.banSendMedia) == nil && channel.hasBannedPermission(.banSendVoice) == nil { + canSendMedia = true + } + } else if let group = self.presentationInterfaceState.renderedPeer?.peer as? TelegramGroup { + if !group.hasBannedPermission(.banSendMedia) && !group.hasBannedPermission(.banSendVoice) { + canSendMedia = true } - |> distinctUntilChanged } else { - return .single(nil) + canSendMedia = true } - } - 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 + if canSendMedia && self.presentationInterfaceState.voiceMessagesAvailable { + let _ = (ApplicationSpecificNotice.getChatMediaMediaRecordingTips(accountManager: self.context.sharedContext.accountManager) + |> deliverOnMainQueue).startStandalone(next: { [weak self] counter in + guard let strongSelf = self else { + return + } + var displayTip = false + if counter == 0 { + displayTip = true + } else if counter < 3 && arc4random_uniform(4) == 1 { + displayTip = true + } + if displayTip { + let _ = ApplicationSpecificNotice.incrementChatMediaMediaRecordingTips(accountManager: strongSelf.context.sharedContext.accountManager).startStandalone() + strongSelf.displayMediaRecordingTooltip() } - return state.updatedSuggestPremiumGift(suggest) }) } - }) + } + } + + self.editMessageErrorsDisposable.set((self.context.account.pendingUpdateMessageManager.errors + |> deliverOnMainQueue).startStrict(next: { [weak self] (_, error) in + guard let strongSelf = self else { + return + } - 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 text: String + switch error { + case .generic, .textTooLong, .invalidGrouping: + text = strongSelf.presentationData.strings.Channel_EditMessageErrorGeneric + case .restricted: + text = strongSelf.presentationData.strings.Group_ErrorSendRestrictedMedia + } + strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { + })]), in: .window(.root)) + })) + + if case let .peer(peerId) = self.chatLocation { + let context = self.context + self.keepPeerInfoScreenDataHotDisposable.set(keepPeerInfoScreenDataHot(context: context, peerId: peerId, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder).startStrict()) - 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 + if peerId.namespace == Namespaces.Peer.CloudUser { + self.preloadAvatarDisposable.set((peerInfoProfilePhotosWithCache(context: context, peerId: peerId) + |> mapToSignal { (complete, result) -> Signal in + var signals: [Signal] = [.complete()] + for i in 0 ..< min(1, result.count) { + if let video = result[i].videoRepresentations.first { + let duration: Double = (video.representation.startTimestamp ?? 0.0) + (i == 0 ? 4.0 : 2.0) + signals.append(preloadVideoResource(postbox: context.account.postbox, userLocation: .other, userContentType: .video, resourceReference: video.reference, duration: duration)) } } - |> distinctUntilChanged - } else { - return .single(nil) - } + return combineLatest(signals) |> mapToSignal { _ in + return .never() + } + }).startStrict()) } - |> 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.preloadAttachBotIconsDisposables = AttachmentController.preloadAttachBotIcons(context: self.context) } - 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) - } - } + if let _ = self.focusOnSearchAfterAppearance { + self.focusOnSearchAfterAppearance = nil + if let searchNode = self.navigationBar?.contentNode as? ChatSearchNavigationContentNode { + searchNode.activate() } - }) - - 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 - } - } + if let peekData = self.peekData, case let .peer(peerId) = self.chatLocation { + let timestamp = Int32(Date().timeIntervalSince1970) + let remainingTime = max(1, peekData.deadline - timestamp) + self.peekTimerDisposable.set(( + combineLatest( + self.context.account.postbox.peerView(id: peerId), + Signal.single(true) + |> suspendAwareDelay(Double(remainingTime), granularity: 2.0, queue: .mainQueue()) + ) + |> deliverOnMainQueue + ).startStrict(next: { [weak self] peerView, _ in + guard let strongSelf = self, let peer = peerViewMainPeer(peerView) else { + return } - 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 peer = peer as? TelegramChannel { + switch peer.participationStatus { + case .member: + return + default: + break } - - 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))) - } + } + strongSelf.present(textAlertController( + context: strongSelf.context, + title: strongSelf.presentationData.strings.Conversation_PrivateChannelTimeLimitedAlertTitle, + text: strongSelf.presentationData.strings.Conversation_PrivateChannelTimeLimitedAlertText, + actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Conversation_PrivateChannelTimeLimitedAlertJoin, action: { + guard let strongSelf = self else { + return } - } 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))) + strongSelf.peekTimerDisposable.set( + (strongSelf.context.engine.peers.joinChatInteractively(with: peekData.linkData) + |> deliverOnMainQueue).startStrict(next: { peerId in + guard let strongSelf = self else { + return + } + if peerId == nil { + strongSelf.dismiss() + } + }, error: { _ in + guard let strongSelf = self else { + return + } + strongSelf.dismiss() + }) + ) + }), + TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + guard let strongSelf = self else { + return } - } - } - } - } + strongSelf.dismiss() + }) + ], + actionLayout: .vertical, + dismissOnOutsideTap: false + ), in: .window(.root)) + })) } - self.chatDisplayNode.historyNode.scrolledToSomeIndex = { [weak self] in - guard let strongSelf = self else { + self.checksTooltipDisposable.set((self.context.engine.notices.getServerProvidedSuggestions() + |> deliverOnMainQueue).startStrict(next: { [weak self] values in + guard let strongSelf = self, strongSelf.chatLocation.peerId != strongSelf.context.account.peerId 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 { + if !values.contains(.newcomerTicks) { return } - - var correlationIds: [Int64] = [] - for message in messages { - switch message { - case let .message(_, _, _, _, _, _, _, _, correlationId, _): - if let correlationId = correlationId { - correlationIds.append(correlationId) - } - default: - break + strongSelf.shouldDisplayChecksTooltip = true + })) + + if case let .peer(peerId) = self.chatLocation { + self.peerSuggestionsDisposable.set((self.context.engine.notices.getPeerSpecificServerProvidedSuggestions(peerId: peerId) + |> deliverOnMainQueue).startStrict(next: { [weak self] values in + guard let strongSelf = self else { + return } - } - strongSelf.commitPurposefulAction() - - if let peerId = strongSelf.chatLocation.peerId { - var hasDisabledContent = false - if "".isEmpty { - hasDisabledContent = false + + if !strongSelf.traceVisibility() || strongSelf.navigationController?.topViewController != strongSelf { + return } - 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 - } - }) + if values.contains(.convertToGigagroup) && !strongSelf.displayedConvertToGigagroupSuggestion { + strongSelf.displayedConvertToGigagroupSuggestion = true - 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() - } + let attributedTitle = NSAttributedString(string: strongSelf.presentationData.strings.BroadcastGroups_LimitAlert_Title, font: Font.semibold(strongSelf.presentationData.listsFontSize.baseDisplaySize), textColor: strongSelf.presentationData.theme.actionSheet.primaryTextColor, paragraphAlignment: .center) + let body = MarkdownAttributeSet(font: Font.regular(strongSelf.presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: strongSelf.presentationData.theme.actionSheet.primaryTextColor) + let bold = MarkdownAttributeSet(font: Font.semibold(strongSelf.presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: strongSelf.presentationData.theme.actionSheet.primaryTextColor) - 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 - } + let participantsLimit = strongSelf.context.currentLimitsConfiguration.with { $0 }.maxSupergroupMemberCount + let text = strongSelf.presentationData.strings.BroadcastGroups_LimitAlert_Text(presentationStringsFormattedNumber(participantsLimit, strongSelf.presentationData.dateTimeFormat.groupingSeparator)).string + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center) - 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 controller = richTextAlertController(context: strongSelf.context, title: attributedTitle, text: attributedText, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.BroadcastGroups_LimitAlert_SettingsTip, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false }), in: .current) + }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.BroadcastGroups_LimitAlert_LearnMore, action: { + + let context = strongSelf.context + let presentationData = strongSelf.presentationData + let controller = PermissionController(context: context, splashScreen: true) + controller.navigationPresentation = .modal + controller.setState(.custom(icon: .animation("BroadcastGroup"), title: presentationData.strings.BroadcastGroups_IntroTitle, subtitle: nil, text: presentationData.strings.BroadcastGroups_IntroText, buttonTitle: presentationData.strings.BroadcastGroups_Convert, secondaryButtonTitle: presentationData.strings.BroadcastGroups_Cancel, footerText: nil), animated: false) + controller.proceed = { [weak controller] result in + let attributedTitle = NSAttributedString(string: presentationData.strings.BroadcastGroups_ConfirmationAlert_Title, font: Font.semibold(presentationData.listsFontSize.baseDisplaySize), textColor: presentationData.theme.actionSheet.primaryTextColor, paragraphAlignment: .center) + let body = MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: presentationData.theme.actionSheet.primaryTextColor) + let bold = MarkdownAttributeSet(font: Font.semibold(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: presentationData.theme.actionSheet.primaryTextColor) + let attributedText = parseMarkdownIntoAttributedString(presentationData.strings.BroadcastGroups_ConfirmationAlert_Text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center) + + let alertController = richTextAlertController(context: context, title: attributedTitle, text: attributedText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + let _ = context.engine.notices.dismissPeerSpecificServerProvidedSuggestion(peerId: peerId, suggestion: .convertToGigagroup).startStandalone() + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.BroadcastGroups_ConfirmationAlert_Convert, action: { [weak controller] in + controller?.dismiss() + + let _ = context.engine.notices.dismissPeerSpecificServerProvidedSuggestion(peerId: peerId, suggestion: .convertToGigagroup).startStandalone() + + let _ = (convertGroupToGigagroup(account: context.account, peerId: peerId) + |> deliverOnMainQueue).startStandalone(completed: { + let participantsLimit = context.currentLimitsConfiguration.with { $0 }.maxSupergroupMemberCount + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .gigagroupConversion(text: presentationData.strings.BroadcastGroups_Success(presentationStringsFormattedNumber(participantsLimit, presentationData.dateTimeFormat.decimalSeparator)).string), elevatedLayout: false, action: { _ in return false }), in: .current) + }) + })]) + controller?.present(alertController, in: .window(.root)) } - } - - 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.push(controller) + })]) + strongSelf.present(controller, in: .window(.root)) } - } - - 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) + if let scheduledActivateInput = self.scheduledActivateInput { + self.scheduledActivateInput = nil + + switch scheduledActivateInput { + case .text: + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in + return state.updatedInputMode({ _ in + return .text + }) }) - } else { - strongSelf.presentAttachmentMenu(subject: .default) + case .entityInput: + self.chatDisplayNode.openStickers(beginWithEmoji: true) } } - 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) + + if let snapshotState = self.storedAnimateFromSnapshotState { + self.storedAnimateFromSnapshotState = nil + + if let titleViewSnapshotState = snapshotState.titleViewSnapshotState { + self.chatTitleView?.animateFromSnapshot(titleViewSnapshotState) } - } - 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)) - } + if let avatarSnapshotState = snapshotState.avatarSnapshotState { + (self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.animateFromSnapshot(avatarSnapshotState) } + self.chatDisplayNode.animateFromSnapshot(snapshotState, completion: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.chatDisplayNode.historyNode.preloadPages = true + }) + } else { + self.chatDisplayNode.historyNode.preloadPages = true } - 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) - } - }) - } - } - } + if let attachBotStart = self.attachBotStart { + self.attachBotStart = nil + self.presentAttachmentBot(botId: attachBotStart.botId, payload: attachBotStart.payload, justInstalled: attachBotStart.justInstalled) } - self.chatDisplayNode.navigateButtons.downPressed = { [weak self] in - guard let self else { - return + if self.powerSavingMonitoringDisposable == nil { + self.powerSavingMonitoringDisposable = (self.context.sharedContext.automaticMediaDownloadSettings + |> mapToSignal { settings -> Signal in + return automaticEnergyUsageShouldBeOn(settings: settings) } - - 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() + |> distinctUntilChanged).startStrict(next: { [weak self] isPowerSavingEnabled in + guard let self else { + return } - } 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() + var previousValueValue: Bool? + + previousValueValue = ChatListControllerImpl.sharedPreviousPowerSavingEnabled + ChatListControllerImpl.sharedPreviousPowerSavingEnabled = isPowerSavingEnabled + + /*#if DEBUG + previousValueValue = false + #endif*/ + + if isPowerSavingEnabled != previousValueValue && previousValueValue != nil && isPowerSavingEnabled { + let batteryLevel = UIDevice.current.batteryLevel + if batteryLevel > 0.0 && self.view.window != nil { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let batteryPercentage = Int(batteryLevel * 100.0) + + self.dismissAllUndoControllers() + self.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "lowbattery_30", scale: 1.0, colors: [:], title: presentationData.strings.PowerSaving_AlertEnabledTitle, text: presentationData.strings.PowerSaving_AlertEnabledText("\(batteryPercentage)").string, customUndoText: presentationData.strings.PowerSaving_AlertEnabledAction, timeout: 5.0), elevatedLayout: false, action: { [weak self] action in + if case .undo = action, let self { + let _ = updateMediaDownloadSettingsInteractively(accountManager: self.context.sharedContext.accountManager, { settings in + var settings = settings + settings.energyUsageSettings.activationThreshold = 4 + return settings + }).startStandalone() + } + return false + }), in: .current) } } - } - } - self.chatDisplayNode.navigateButtons.upPressed = { [weak self] in - guard let self else { - return - } - - if self.presentationInterfaceState.search?.resultsState != nil { - self.interfaceInteraction?.navigateMessageSearch(.earlier) - } + }) } + } + + override public func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) - 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 - } - } - })) - } + UIView.performWithoutAnimation { + self.view.endEditing(true) } - 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.historyNode.canReadHistory.set(.single(false)) + self.saveInterfaceState() - 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.dismissAllTooltips() + + self.sendMessageActionsController?.dismiss() + self.themeScreen?.dismiss() + + self.attachmentController?.dismiss() + + self.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + + if let _ = self.peekData { + self.peekTimerDisposable.set(nil) + } + } + + func saveInterfaceState(includeScrollState: Bool = true) { + if case .messageOptions = self.subject { + return } - self.chatDisplayNode.navigateButtons.reactionsButton.activated = { [weak self] gesture, _ in - guard let strongSelf = self else { - gesture.cancel() - return + var includeScrollState = includeScrollState + + var peerId: PeerId + var threadId: Int64? + switch self.chatLocation { + case let .peer(peerIdValue): + peerId = peerIdValue + case let .replyThread(replyThreadMessage): + if replyThreadMessage.peerId == self.context.account.peerId && replyThreadMessage.threadId == self.context.account.peerId.toInt64() { + peerId = replyThreadMessage.peerId + threadId = nil + includeScrollState = true + + let scrollState = self.chatDisplayNode.historyNode.immediateScrollState() + let _ = ChatInterfaceState.update(engine: self.context.engine, peerId: peerId, threadId: replyThreadMessage.threadId, { current in + return current.withUpdatedHistoryScrollState(scrollState) + }).startStandalone() + } else { + peerId = replyThreadMessage.peerId + threadId = replyThreadMessage.threadId } - - 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) + case .customChatContents: + return } - // MARK: Nicegram (cloudMessages + copyForwardMessages) - let interfaceInteraction = ChatPanelInterfaceInteraction(cloudMessages: { [weak self] messages in - guard let strongSelf = self, strongSelf.isNodeLoaded else { - return + let timestamp = Int32(Date().timeIntervalSince1970) + var interfaceState = self.presentationInterfaceState.interfaceState.withUpdatedTimestamp(timestamp) + if includeScrollState { + let scrollState = self.chatDisplayNode.historyNode.immediateScrollState() + interfaceState = interfaceState.withUpdatedHistoryScrollState(scrollState) + } + interfaceState = interfaceState.withUpdatedInputLanguage(self.chatDisplayNode.currentTextInputLanguage) + if case .peer = self.chatLocation, let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum) { + interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState()).withUpdatedReplyMessageSubject(nil) + } + let _ = ChatInterfaceState.update(engine: self.context.engine, peerId: peerId, threadId: threadId, { _ in + return interfaceState + }).startStandalone() + } + + override public func viewWillLeaveNavigation() { + self.chatDisplayNode.willNavigateAway() + } + + override public func inFocusUpdated(isInFocus: Bool) { + self.disableStickerAnimationsPromise.set(!isInFocus) + self.chatDisplayNode.inFocusUpdated(isInFocus: isInFocus) + } + + func canManagePin() -> Bool { + guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { + return false + } + + var canManagePin = false + if let channel = peer as? TelegramChannel { + canManagePin = channel.hasPermission(.pinMessages) + } else if let group = peer as? TelegramGroup { + switch group.role { + case .creator, .admin: + canManagePin = true + default: + if let defaultBannedRights = group.defaultBannedRights { + canManagePin = !defaultBannedRights.flags.contains(.banPinMessages) + } else { + canManagePin = true + } } - if let messages = messages { - // Multiple - if !messages.isEmpty { - strongSelf.commitPurposefulAction() - let forwardMessageIds = messages.map { $0.id }.sorted() - strongSelf.forwardMessages(messageIds: forwardMessageIds, cloud: true) - } + } else if let _ = peer as? TelegramUser, self.presentationInterfaceState.explicitelyCanPinMessages { + canManagePin = true + } + + return canManagePin + } + + var suspendNavigationBarLayout: Bool = false + var suspendedNavigationBarLayout: ContainerViewLayout? + var additionalNavigationBarBackgroundHeight: CGFloat = 0.0 + var additionalNavigationBarHitTestSlop: CGFloat = 0.0 + + override public func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + if self.suspendNavigationBarLayout { + self.suspendedNavigationBarLayout = layout + return + } + self.applyNavigationBarLayout(layout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: transition) + } + + override public func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? { + return nil + } + + public func updateIsScrollingLockedAtTop(isScrollingLockedAtTop: Bool) { + self.chatDisplayNode.isScrollingLockedAtTop = isScrollingLockedAtTop + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.suspendNavigationBarLayout = true + super.containerLayoutUpdated(layout, transition: transition) + + self.validLayout = layout + self.chatTitleView?.layout = layout + + switch self.presentationInterfaceState.mode { + case .standard, .inline: + break + case .overlay: + if case .Ignore = self.statusBar.statusBarStyle { + } else if layout.safeInsets.top.isZero { + self.statusBar.statusBarStyle = .Hide } else { - // One - strongSelf.commitPurposefulAction() - if let forwardMessageIdsSet = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds { - let forwardMessageIds = Array(forwardMessageIdsSet).sorted() - strongSelf.forwardMessages(messageIds: forwardMessageIds, cloud: true) - } - } - }, copyForwardMessages: { [weak self] messages in - guard let strongSelf = self, strongSelf.isNodeLoaded else { - return + self.statusBar.statusBarStyle = .Ignore } - if let messages = messages { - // Multiple - if !messages.isEmpty { - strongSelf.commitPurposefulAction() - let forwardMessageIds = messages.map { $0.id }.sorted() - strongSelf.forwardMessages(messageIds: forwardMessageIds, asCopy: true) - } - } else { - // One - strongSelf.commitPurposefulAction() - if let forwardMessageIdsSet = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds { - let forwardMessageIds = Array(forwardMessageIdsSet).sorted() - strongSelf.forwardMessages(messageIds: forwardMessageIds, asCopy: true) + } + + var layout = layout + if case .compact = layout.metrics.widthClass, let attachmentController = self.attachmentController, attachmentController.window != nil { + layout = layout.withUpdatedInputHeight(nil) + } + + var navigationBarTransition = transition + self.chatDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition, listViewTransaction: { updateSizeAndInsets, additionalScrollDistance, scrollToTop, completion in + self.chatDisplayNode.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, additionalScrollDistance: additionalScrollDistance, scrollToTop: scrollToTop, completion: completion) + }, updateExtraNavigationBarBackgroundHeight: { value, hitTestSlop, extraNavigationTransition in + navigationBarTransition = extraNavigationTransition + self.additionalNavigationBarBackgroundHeight = value + self.additionalNavigationBarHitTestSlop = hitTestSlop + }) + + if case .compact = layout.metrics.widthClass { + let hasOverlayNodes = self.context.sharedContext.mediaManager.overlayMediaManager.controller?.hasNodes ?? false + if self.validLayout != nil && layout.size.width > layout.size.height && !hasOverlayNodes && self.traceVisibility() && isTopmostChatController(self) { + var completed = false + self.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in + if !completed, let itemNode = itemNode as? ChatMessageItemView, let message = itemNode.item?.message, let (_, soundEnabled, _, _, _) = itemNode.playMediaWithSound(), soundEnabled { + let _ = self.controllerInteraction?.openMessage(message, OpenMessageParams(mode: .landscape)) + completed = true + } } } - }, copySelectedMessages: { [weak self] in - // MARK: Nicegram CopySelectedMessages - guard let strongSelf = self else { - return - } - - strongSelf.commitPurposefulAction() - guard let forwardMessageIdsSet = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds else { - return + } + + self.suspendNavigationBarLayout = false + if let suspendedNavigationBarLayout = self.suspendedNavigationBarLayout { + self.suspendedNavigationBarLayout = suspendedNavigationBarLayout + self.applyNavigationBarLayout(suspendedNavigationBarLayout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: navigationBarTransition) + } + self.navigationBar?.additionalContentNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: 0.0, bottom: self.additionalNavigationBarHitTestSlop, right: 0.0) + } + + func updateChatPresentationInterfaceState(animated: Bool = true, interactive: Bool, saveInterfaceState: Bool = false, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) { + self.updateChatPresentationInterfaceState(transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate, interactive: interactive, saveInterfaceState: saveInterfaceState, f, completion: completion) + } + + func updateChatPresentationInterfaceState(transition: ContainedViewLayoutTransition, interactive: Bool, saveInterfaceState: Bool = false, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) { + updateChatPresentationInterfaceStateImpl( + selfController: self, + transition: transition, + interactive: interactive, + saveInterfaceState: saveInterfaceState, + f, + completion: completion + ) + } + + func updateItemNodesSelectionStates(animated: Bool) { + self.chatDisplayNode.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + itemNode.updateSelectionState(animated: animated) } - let forwardMessageIds = Array(forwardMessageIdsSet) - let _ = (strongSelf.context.engine.data.get(EngineDataMap( - forwardMessageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init) - )) - |> map { messages -> String in - return messages.values - .compactMap{$0} - .sorted(by: { $0.timestamp < $1.timestamp }) - .map(\.text) - .filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } - .joined(separator: "\n") - } - |> deliverOnMainQueue).start(next: { [weak self] text in - UIPasteboard.general.string = text - if let self = self { - self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) - Queue.mainQueue().after(0.2, { - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied) - self.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in - return true - }), in: .current) - }) - } - }) - }, openGifs: { [weak self] in - // MARK: Nicegram OpenGifsShortcut - guard let self = self else { return } - self.chatDisplayNode.openStickers(defaultTab: .gif, beginWithEmoji: false) - self.mediaRecordingModeTooltipController?.dismissImmediately() - // - }, replyPrivately: { [weak self] message in - // MARK: Nicegram ReplyPrivately - self?.replyPrivately(message: message) - // - }, 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.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 }) + } + + self.chatDisplayNode.historyNode.forEachItemHeaderNode{ itemHeaderNode in + if let avatarNode = itemHeaderNode as? ChatMessageAvatarHeaderNode { + avatarNode.updateSelectionState(animated: animated) } - }, 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) - } - } - } + } + } + + func updatePollTooltipMessageState(animated: Bool) { + self.chatDisplayNode.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageBubbleItemNode { + for contentNode in itemNode.contentNodes { + if let contentNode = contentNode as? ChatMessagePollBubbleContentNode { + contentNode.updatePollTooltipMessageState(animated: animated) } - })) - } - }, 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) } + itemNode.updatePsaTooltipMessageState(animated: animated) } - }, 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)) - } - }) + } + } + + func updateItemNodesSearchTextHighlightStates() { + var searchString: String? + var resultsMessageIndices: [MessageIndex]? + if let search = self.presentationInterfaceState.search, let resultsState = search.resultsState, !resultsState.messageIndices.isEmpty { + searchString = search.query + resultsMessageIndices = resultsState.messageIndices + } + if searchString != self.controllerInteraction?.searchTextHighightState?.0 || resultsMessageIndices?.count != self.controllerInteraction?.searchTextHighightState?.1.count { + var searchTextHighightState: (String, [MessageIndex])? + if let searchString = searchString, let resultsMessageIndices = resultsMessageIndices { + searchTextHighightState = (searchString, resultsMessageIndices) } - }, 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() + self.controllerInteraction?.searchTextHighightState = searchTextHighightState + self.chatDisplayNode.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + itemNode.updateSearchTextHighlightState() } } - }, 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 + } + } + + func updateItemNodesHighlightedStates(animated: Bool) { + self.chatDisplayNode.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + itemNode.updateHighlightedState(animated: animated) } - - 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 - } - // MARK: Nicegram AiChat - strongSelf.updateAiOverlayVisibility() - // - } - }) - } 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 - } - // MARK: Nicegram AiChat - strongSelf.updateAiOverlayVisibility() - // - } - }) - } - - 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, action: { - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .fasterDownload, forceDark: false, dismissed: nil) - replaceImpl?(controller) - }) - 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? - - func animateFromPreviousController(snapshotState: ChatControllerNode.SnapshotState) { - self.storedAnimateFromSnapshotState = snapshotState - } - - override public func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - if self.willAppear { - self.chatDisplayNode.historyNode.refreshPollActionsForVisibleMessages() - } else { - self.willAppear = true - - // Limit this to reply threads just to be safe now - if case .replyThread = self.chatLocation { - self.chatDisplayNode.historyNode.refocusOnUnreadMessagesIfNeeded() - } - } - - if case let .replyThread(message) = self.chatLocation, message.isForumPost { - if self.keepMessageCountersSyncrhonizedDisposable == nil { - self.keepMessageCountersSyncrhonizedDisposable = self.context.engine.messages.keepMessageCountersSyncrhonized(peerId: message.peerId, threadId: message.threadId).startStrict() - } - } else if self.chatLocation.peerId == self.context.account.peerId { - if self.keepMessageCountersSyncrhonizedDisposable == nil { - if let threadId = self.chatLocation.threadId { - self.keepMessageCountersSyncrhonizedDisposable = self.context.engine.messages.keepMessageCountersSyncrhonized(peerId: self.context.account.peerId, threadId: threadId).startStrict() - } else { - self.keepMessageCountersSyncrhonizedDisposable = self.context.engine.messages.keepMessageCountersSyncrhonized(peerId: self.context.account.peerId).startStrict() - } - } - if self.keepSavedMessagesSyncrhonizedDisposable == nil { - self.keepSavedMessagesSyncrhonizedDisposable = self.context.engine.stickers.refreshSavedMessageTags(subPeerId: self.chatLocation.threadId.flatMap(PeerId.init)).startStrict() - } - } - - if let scheduledActivateInput = scheduledActivateInput, case .text = scheduledActivateInput { - self.scheduledActivateInput = nil - - self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in - return state.updatedInputMode({ _ in - switch scheduledActivateInput { - case .text: - return .text - case .entityInput: - return .media(mode: .other, expanded: nil, focused: false) - } - }) - }) - } - - var chatNavigationStack: [ChatNavigationStackItem] = self.chatNavigationStack - if let peerId = self.chatLocation.peerId { - if let summary = self.customNavigationDataSummary as? ChatControllerNavigationDataSummary { - chatNavigationStack.removeAll() - chatNavigationStack = summary.peerNavigationItems.filter({ $0 != ChatNavigationStackItem(peerId: peerId, threadId: self.chatLocation.threadId) }) - } - if let _ = self.chatLocation.threadId { - if !chatNavigationStack.contains(ChatNavigationStackItem(peerId: peerId, threadId: nil)) { - chatNavigationStack.append(ChatNavigationStackItem(peerId: peerId, threadId: nil)) - } - } - } - - if !chatNavigationStack.isEmpty { - self.chatDisplayNode.navigationBar?.backButtonNode.isGestureEnabled = true - self.chatDisplayNode.navigationBar?.backButtonNode.activated = { [weak self] gesture, _ in - guard let strongSelf = self, let backButtonNode = strongSelf.chatDisplayNode.navigationBar?.backButtonNode, let navigationController = strongSelf.effectiveNavigationController else { - gesture.cancel() - return - } - let nextFolderId: Int32? = strongSelf.currentChatListFilter - PeerInfoScreenImpl.displayChatNavigationMenu( - context: strongSelf.context, - chatNavigationStack: chatNavigationStack, - nextFolderId: nextFolderId, - parentController: strongSelf, - backButtonView: backButtonNode.view, - navigationController: navigationController, - gesture: gesture - ) - } - } - - // MARK: Nicegram TranslateEnteredMessage (prefetch interlocator language) - let _ = getLanguageCode(forChatWith: chatLocation.peerId, context: context).start() - // - } - - var returnInputViewFocus = false - - override public func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - // MARK: Nicegram NGCard - if #available(iOS 13.0, *) { - if !self.didAppear { - if let peerId = self.chatLocation.peerId { - let int64Id = peerId.id._internalGetInt64Value() - - let getCardOfferDataUseCase = CardContainer.shared.getCardOfferDataUseCase() - let cardBotId = getCardOfferDataUseCase()?.botId - - if int64Id == cardBotId { - CardTgHelper.trackCardBotView() - } - } - } - } - // - - // MARK: Nicegram NGStats - if #available(iOS 13.0, *) { - if !self.didAppear { - if let peerId = self.chatLocation.peerId { - sharePeerData(peerId: peerId, context: self.context) - } - } - } - // - - // MARK: Nicegram GroupAnalytics - if !self.didAppear { - if let peerId = self.chatLocation.peerId { - trackChatOpen( - peerId: peerId, - context: self.context - ) - } - } - // - - // MARK: Nicegram Unblock - if let peer = self.presentationInterfaceState.renderedPeer?.peer { - let isLastSeenBlockedChat = (peer.id.id._internalGetInt64Value() == AppCache.lastSeenBlockedChatId) - if isLastSeenBlockedChat, - isAllowedChat(peer: peer, contentSettings: context.currentContentSettings.with { $0 }) { - AppCache.lastSeenBlockedChatId = nil - if #available(iOS 14.0, *) { - if let windowScene = self.view.window?.windowScene { - SKStoreReviewController.requestReview(in: windowScene) - } - } else { - SKStoreReviewController.requestReview() - } - } - } - // - - self.didAppear = true - - self.chatDisplayNode.historyNode.experimentalSnapScrollToItem = false - self.chatDisplayNode.historyNode.canReadHistory.set(combineLatest(context.sharedContext.applicationBindings.applicationInForeground, self.canReadHistory.get()) |> map { a, b in - return a && b - }) - - self.chatDisplayNode.loadInputPanels(theme: self.presentationInterfaceState.theme, strings: self.presentationInterfaceState.strings, fontSize: self.presentationInterfaceState.fontSize) - - if self.recentlyUsedInlineBotsDisposable == nil { - self.recentlyUsedInlineBotsDisposable = (self.context.engine.peers.recentlyUsedInlineBots() |> deliverOnMainQueue).startStrict(next: { [weak self] peers in - self?.recentlyUsedInlineBotsValue = peers.filter({ $0.1 >= 0.14 }).map({ $0.0._asPeer() }) - }) - } - - if case .standard(.default) = self.presentationInterfaceState.mode, self.raiseToListen == nil { - self.raiseToListen = RaiseToListenManager(shouldActivate: { [weak self] in - if let strongSelf = self, strongSelf.isNodeLoaded && strongSelf.canReadHistoryValue, strongSelf.presentationInterfaceState.interfaceState.editMessage == nil, strongSelf.playlistStateAndType == nil { - if !strongSelf.context.sharedContext.currentMediaInputSettings.with({ $0.enableRaiseToSpeak }) { - return false - } - - if strongSelf.effectiveNavigationController?.topViewController !== strongSelf { - return false - } - - if strongSelf.presentationInterfaceState.inputTextPanelState.mediaRecordingState != nil { - return false - } - - if !strongSelf.traceVisibility() { - return false - } - if strongSelf.currentContextController != nil { - return false - } - if !isTopmostChatController(strongSelf) { - return false - } - - if strongSelf.firstLoadedMessageToListen() != nil || strongSelf.chatDisplayNode.isTextInputPanelActive { - if strongSelf.context.sharedContext.immediateHasOngoingCall { - return false - } - - if case .media = strongSelf.presentationInterfaceState.inputMode { - return false - } - return true - } - } - return false - }, activate: { [weak self] in - self?.activateRaiseGesture() - }, deactivate: { [weak self] in - self?.deactivateRaiseGesture() - }) - self.raiseToListen?.enabled = self.canReadHistoryValue - self.tempVoicePlaylistEnded = { [weak self] in - guard let strongSelf = self else { - return - } - if !canSendMessagesToChat(strongSelf.presentationInterfaceState) { - return - } - - if let raiseToListen = strongSelf.raiseToListen { - strongSelf.voicePlaylistDidEndTimestamp = CACurrentMediaTime() - raiseToListen.activateBasedOnProximity(delay: 0.0) - } - - if strongSelf.returnInputViewFocus { - strongSelf.returnInputViewFocus = false - strongSelf.chatDisplayNode.ensureInputViewFocused() - } - } - self.tempVoicePlaylistItemChanged = { [weak self] previousItem, currentItem in - guard let strongSelf = self else { - return - } - - strongSelf.chatDisplayNode.historyNode.voicePlaylistItemChanged(previousItem, currentItem) - } - } - - if let arguments = self.presentationArguments as? ChatControllerOverlayPresentationData { - //TODO clear arguments - self.chatDisplayNode.animateInAsOverlay(from: arguments.expandData.0, completion: { - arguments.expandData.1() - }) - } - - if !self.didSetup3dTouch { - self.didSetup3dTouch = true - if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { - let dropInteraction = UIDropInteraction(delegate: self) - self.chatDisplayNode.view.addInteraction(dropInteraction) - } - } - - if !self.checkedPeerChatServiceActions { - self.checkedPeerChatServiceActions = true - - if case let .peer(peerId) = self.chatLocation, self.screenCaptureManager == nil { - if peerId.namespace == Namespaces.Peer.SecretChat { - self.screenCaptureManager = ScreenCaptureDetectionManager(check: { [weak self] in - if let strongSelf = self, strongSelf.traceVisibility() { - if strongSelf.canReadHistoryValue { - let _ = strongSelf.context.engine.messages.addSecretChatMessageScreenshot(peerId: peerId).startStandalone() - } - return true - } else { - return false - } - }) - } else if peerId.namespace == Namespaces.Peer.CloudUser && peerId.id._internalGetInt64Value() == 777000 { - self.screenCaptureManager = ScreenCaptureDetectionManager(check: { [weak self] in - if let strongSelf = self, strongSelf.traceVisibility() { - let loginCodeRegex = try? NSRegularExpression(pattern: "[\\d\\-]{5,7}", options: []) - var loginCodesToInvalidate: [String] = [] - strongSelf.chatDisplayNode.historyNode.forEachVisibleMessageItemNode({ itemNode in - if let text = itemNode.item?.message.text, let matches = loginCodeRegex?.matches(in: text, options: [], range: NSMakeRange(0, (text as NSString).length)), let match = matches.first { - loginCodesToInvalidate.append((text as NSString).substring(with: match.range)) - } - }) - if !loginCodesToInvalidate.isEmpty { - let _ = strongSelf.context.engine.auth.invalidateLoginCodes(codes: loginCodesToInvalidate).startStandalone() - } - return true - } else { - return false - } - }) - } else if peerId.namespace == Namespaces.Peer.CloudUser { - self.screenCaptureManager = ScreenCaptureDetectionManager(check: { [weak self] in - guard let self else { - return false - } - - let _ = (self.context.sharedContext.mediaManager.globalMediaPlayerState - |> take(1) - |> deliverOnMainQueue).startStandalone(next: { [weak self] playlistStateAndType in - if let self, let (_, playbackState, _) = playlistStateAndType, case let .state(state) = playbackState { - if let source = state.item.playbackData?.source, case let .telegramFile(_, _, isViewOnce) = source, isViewOnce { - self.context.sharedContext.mediaManager.setPlaylist(nil, type: .voice, control: .playback(.pause)) - } - } - }) - return true - }) - } - } - - if case let .peer(peerId) = self.chatLocation { - let _ = self.context.engine.peers.checkPeerChatServiceActions(peerId: peerId).startStandalone() - } - - if self.chatLocation.peerId != nil && self.chatDisplayNode.frameForInputActionButton() != nil { - let inputText = self.presentationInterfaceState.interfaceState.effectiveInputState.inputText.string - if !inputText.isEmpty { - if inputText.count > 4 { - let _ = (ApplicationSpecificNotice.getChatMessageOptionsTip(accountManager: self.context.sharedContext.accountManager) - |> deliverOnMainQueue).startStandalone(next: { [weak self] counter in - if let strongSelf = self, counter < 3 { - let _ = ApplicationSpecificNotice.incrementChatMessageOptionsTip(accountManager: strongSelf.context.sharedContext.accountManager).startStandalone() - strongSelf.displaySendingOptionsTooltip() - } - }) - } - } else if self.presentationInterfaceState.interfaceState.mediaRecordingMode == .audio { - var canSendMedia = false - if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel { - if channel.hasBannedPermission(.banSendMedia) == nil && channel.hasBannedPermission(.banSendVoice) == nil { - canSendMedia = true - } - } else if let group = self.presentationInterfaceState.renderedPeer?.peer as? TelegramGroup { - if !group.hasBannedPermission(.banSendMedia) && !group.hasBannedPermission(.banSendVoice) { - canSendMedia = true - } - } else { - canSendMedia = true - } - if canSendMedia && self.presentationInterfaceState.voiceMessagesAvailable { - let _ = (ApplicationSpecificNotice.getChatMediaMediaRecordingTips(accountManager: self.context.sharedContext.accountManager) - |> deliverOnMainQueue).startStandalone(next: { [weak self] counter in - guard let strongSelf = self else { - return - } - var displayTip = false - if counter == 0 { - displayTip = true - } else if counter < 3 && arc4random_uniform(4) == 1 { - displayTip = true - } - if displayTip { - let _ = ApplicationSpecificNotice.incrementChatMediaMediaRecordingTips(accountManager: strongSelf.context.sharedContext.accountManager).startStandalone() - strongSelf.displayMediaRecordingTooltip() - } - }) - } - } - } - - self.editMessageErrorsDisposable.set((self.context.account.pendingUpdateMessageManager.errors - |> deliverOnMainQueue).startStrict(next: { [weak self] (_, error) in - guard let strongSelf = self else { - return - } - - let text: String - switch error { - case .generic, .textTooLong, .invalidGrouping: - text = strongSelf.presentationData.strings.Channel_EditMessageErrorGeneric - case .restricted: - text = strongSelf.presentationData.strings.Group_ErrorSendRestrictedMedia - } - strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { - })]), in: .window(.root)) - })) - - if case let .peer(peerId) = self.chatLocation { - let context = self.context - self.keepPeerInfoScreenDataHotDisposable.set(keepPeerInfoScreenDataHot(context: context, peerId: peerId, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder).startStrict()) - - if peerId.namespace == Namespaces.Peer.CloudUser { - self.preloadAvatarDisposable.set((peerInfoProfilePhotosWithCache(context: context, peerId: peerId) - |> mapToSignal { (complete, result) -> Signal in - var signals: [Signal] = [.complete()] - for i in 0 ..< min(1, result.count) { - if let video = result[i].videoRepresentations.first { - let duration: Double = (video.representation.startTimestamp ?? 0.0) + (i == 0 ? 4.0 : 2.0) - signals.append(preloadVideoResource(postbox: context.account.postbox, userLocation: .other, userContentType: .video, resourceReference: video.reference, duration: duration)) - } - } - return combineLatest(signals) |> mapToSignal { _ in - return .never() - } - }).startStrict()) - } - } - - self.preloadAttachBotIconsDisposables = AttachmentController.preloadAttachBotIcons(context: self.context) - } - - if let _ = self.focusOnSearchAfterAppearance { - self.focusOnSearchAfterAppearance = nil - if let searchNode = self.navigationBar?.contentNode as? ChatSearchNavigationContentNode { - searchNode.activate() - } - } - - if let peekData = self.peekData, case let .peer(peerId) = self.chatLocation { - let timestamp = Int32(Date().timeIntervalSince1970) - let remainingTime = max(1, peekData.deadline - timestamp) - self.peekTimerDisposable.set(( - combineLatest( - self.context.account.postbox.peerView(id: peerId), - Signal.single(true) - |> suspendAwareDelay(Double(remainingTime), granularity: 2.0, queue: .mainQueue()) - ) - |> deliverOnMainQueue - ).startStrict(next: { [weak self] peerView, _ in - guard let strongSelf = self, let peer = peerViewMainPeer(peerView) else { - return - } - if let peer = peer as? TelegramChannel { - switch peer.participationStatus { - case .member: - return - default: - break - } - } - strongSelf.present(textAlertController( - context: strongSelf.context, - title: strongSelf.presentationData.strings.Conversation_PrivateChannelTimeLimitedAlertTitle, - text: strongSelf.presentationData.strings.Conversation_PrivateChannelTimeLimitedAlertText, - actions: [ - TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Conversation_PrivateChannelTimeLimitedAlertJoin, action: { - guard let strongSelf = self else { - return - } - strongSelf.peekTimerDisposable.set( - (strongSelf.context.engine.peers.joinChatInteractively(with: peekData.linkData) - |> deliverOnMainQueue).startStrict(next: { peerId in - guard let strongSelf = self else { - return - } - if peerId == nil { - strongSelf.dismiss() - } - }, error: { _ in - guard let strongSelf = self else { - return - } - strongSelf.dismiss() - }) - ) - }), - TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { - guard let strongSelf = self else { - return - } - strongSelf.dismiss() - }) - ], - actionLayout: .vertical, - dismissOnOutsideTap: false - ), in: .window(.root)) - })) - } - - self.checksTooltipDisposable.set((self.context.engine.notices.getServerProvidedSuggestions() - |> deliverOnMainQueue).startStrict(next: { [weak self] values in - guard let strongSelf = self, strongSelf.chatLocation.peerId != strongSelf.context.account.peerId else { - return - } - if !values.contains(.newcomerTicks) { - return - } - strongSelf.shouldDisplayChecksTooltip = true - })) - - if case let .peer(peerId) = self.chatLocation { - self.peerSuggestionsDisposable.set((self.context.engine.notices.getPeerSpecificServerProvidedSuggestions(peerId: peerId) - |> deliverOnMainQueue).startStrict(next: { [weak self] values in - guard let strongSelf = self else { - return - } - - if !strongSelf.traceVisibility() || strongSelf.navigationController?.topViewController != strongSelf { - return - } - - if values.contains(.convertToGigagroup) && !strongSelf.displayedConvertToGigagroupSuggestion { - strongSelf.displayedConvertToGigagroupSuggestion = true - - let attributedTitle = NSAttributedString(string: strongSelf.presentationData.strings.BroadcastGroups_LimitAlert_Title, font: Font.semibold(strongSelf.presentationData.listsFontSize.baseDisplaySize), textColor: strongSelf.presentationData.theme.actionSheet.primaryTextColor, paragraphAlignment: .center) - let body = MarkdownAttributeSet(font: Font.regular(strongSelf.presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: strongSelf.presentationData.theme.actionSheet.primaryTextColor) - let bold = MarkdownAttributeSet(font: Font.semibold(strongSelf.presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: strongSelf.presentationData.theme.actionSheet.primaryTextColor) - - let participantsLimit = strongSelf.context.currentLimitsConfiguration.with { $0 }.maxSupergroupMemberCount - let text = strongSelf.presentationData.strings.BroadcastGroups_LimitAlert_Text(presentationStringsFormattedNumber(participantsLimit, strongSelf.presentationData.dateTimeFormat.groupingSeparator)).string - let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center) - - let controller = richTextAlertController(context: strongSelf.context, title: attributedTitle, text: attributedText, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { - strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.BroadcastGroups_LimitAlert_SettingsTip, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false }), in: .current) - }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.BroadcastGroups_LimitAlert_LearnMore, action: { - - let context = strongSelf.context - let presentationData = strongSelf.presentationData - let controller = PermissionController(context: context, splashScreen: true) - controller.navigationPresentation = .modal - controller.setState(.custom(icon: .animation("BroadcastGroup"), title: presentationData.strings.BroadcastGroups_IntroTitle, subtitle: nil, text: presentationData.strings.BroadcastGroups_IntroText, buttonTitle: presentationData.strings.BroadcastGroups_Convert, secondaryButtonTitle: presentationData.strings.BroadcastGroups_Cancel, footerText: nil), animated: false) - controller.proceed = { [weak controller] result in - let attributedTitle = NSAttributedString(string: presentationData.strings.BroadcastGroups_ConfirmationAlert_Title, font: Font.semibold(presentationData.listsFontSize.baseDisplaySize), textColor: presentationData.theme.actionSheet.primaryTextColor, paragraphAlignment: .center) - let body = MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: presentationData.theme.actionSheet.primaryTextColor) - let bold = MarkdownAttributeSet(font: Font.semibold(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), textColor: presentationData.theme.actionSheet.primaryTextColor) - let attributedText = parseMarkdownIntoAttributedString(presentationData.strings.BroadcastGroups_ConfirmationAlert_Text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center) - - let alertController = richTextAlertController(context: context, title: attributedTitle, text: attributedText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - let _ = context.engine.notices.dismissPeerSpecificServerProvidedSuggestion(peerId: peerId, suggestion: .convertToGigagroup).startStandalone() - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.BroadcastGroups_ConfirmationAlert_Convert, action: { [weak controller] in - controller?.dismiss() - - let _ = context.engine.notices.dismissPeerSpecificServerProvidedSuggestion(peerId: peerId, suggestion: .convertToGigagroup).startStandalone() - - let _ = (convertGroupToGigagroup(account: context.account, peerId: peerId) - |> deliverOnMainQueue).startStandalone(completed: { - let participantsLimit = context.currentLimitsConfiguration.with { $0 }.maxSupergroupMemberCount - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .gigagroupConversion(text: presentationData.strings.BroadcastGroups_Success(presentationStringsFormattedNumber(participantsLimit, presentationData.dateTimeFormat.decimalSeparator)).string), elevatedLayout: false, action: { _ in return false }), in: .current) - }) - })]) - controller?.present(alertController, in: .window(.root)) - } - strongSelf.push(controller) - })]) - strongSelf.present(controller, in: .window(.root)) - } - })) - } - - if let scheduledActivateInput = self.scheduledActivateInput { - self.scheduledActivateInput = nil - - switch scheduledActivateInput { - case .text: - self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in - return state.updatedInputMode({ _ in - return .text - }) - }) - case .entityInput: - self.chatDisplayNode.openStickers(beginWithEmoji: true) - } - } - - if let snapshotState = self.storedAnimateFromSnapshotState { - self.storedAnimateFromSnapshotState = nil - - if let titleViewSnapshotState = snapshotState.titleViewSnapshotState { - self.chatTitleView?.animateFromSnapshot(titleViewSnapshotState) - } - if let avatarSnapshotState = snapshotState.avatarSnapshotState { - (self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.animateFromSnapshot(avatarSnapshotState) - } - self.chatDisplayNode.animateFromSnapshot(snapshotState, completion: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.chatDisplayNode.historyNode.preloadPages = true - }) - } else { - self.chatDisplayNode.historyNode.preloadPages = true - } - - if let attachBotStart = self.attachBotStart { - self.attachBotStart = nil - self.presentAttachmentBot(botId: attachBotStart.botId, payload: attachBotStart.payload, justInstalled: attachBotStart.justInstalled) - } - - if self.powerSavingMonitoringDisposable == nil { - self.powerSavingMonitoringDisposable = (self.context.sharedContext.automaticMediaDownloadSettings - |> mapToSignal { settings -> Signal in - return automaticEnergyUsageShouldBeOn(settings: settings) - } - |> distinctUntilChanged).startStrict(next: { [weak self] isPowerSavingEnabled in - guard let self else { - return - } - var previousValueValue: Bool? - - previousValueValue = ChatListControllerImpl.sharedPreviousPowerSavingEnabled - ChatListControllerImpl.sharedPreviousPowerSavingEnabled = isPowerSavingEnabled - - /*#if DEBUG - previousValueValue = false - #endif*/ - - if isPowerSavingEnabled != previousValueValue && previousValueValue != nil && isPowerSavingEnabled { - let batteryLevel = UIDevice.current.batteryLevel - if batteryLevel > 0.0 && self.view.window != nil { - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let batteryPercentage = Int(batteryLevel * 100.0) - - self.dismissAllUndoControllers() - self.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "lowbattery_30", scale: 1.0, colors: [:], title: presentationData.strings.PowerSaving_AlertEnabledTitle, text: presentationData.strings.PowerSaving_AlertEnabledText("\(batteryPercentage)").string, customUndoText: presentationData.strings.PowerSaving_AlertEnabledAction, timeout: 5.0), elevatedLayout: false, action: { [weak self] action in - if case .undo = action, let self { - let _ = updateMediaDownloadSettingsInteractively(accountManager: self.context.sharedContext.accountManager, { settings in - var settings = settings - settings.energyUsageSettings.activationThreshold = 4 - return settings - }).startStandalone() - } - return false - }), in: .current) - } - } - }) - } - } - - override public func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - UIView.performWithoutAnimation { - self.view.endEditing(true) - } - - self.chatDisplayNode.historyNode.canReadHistory.set(.single(false)) - self.saveInterfaceState() - - self.dismissAllTooltips() - - self.sendMessageActionsController?.dismiss() - self.themeScreen?.dismiss() - - self.attachmentController?.dismiss() - - self.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() - - if let _ = self.peekData { - self.peekTimerDisposable.set(nil) - } - } - - func saveInterfaceState(includeScrollState: Bool = true) { - if case .messageOptions = self.subject { - return - } - - var includeScrollState = includeScrollState - - var peerId: PeerId - var threadId: Int64? - switch self.chatLocation { - case let .peer(peerIdValue): - peerId = peerIdValue - case let .replyThread(replyThreadMessage): - if replyThreadMessage.peerId == self.context.account.peerId && replyThreadMessage.threadId == self.context.account.peerId.toInt64() { - peerId = replyThreadMessage.peerId - threadId = nil - includeScrollState = true - - let scrollState = self.chatDisplayNode.historyNode.immediateScrollState() - let _ = ChatInterfaceState.update(engine: self.context.engine, peerId: peerId, threadId: replyThreadMessage.threadId, { current in - return current.withUpdatedHistoryScrollState(scrollState) - }).startStandalone() - } else { - peerId = replyThreadMessage.peerId - threadId = replyThreadMessage.threadId - } - case .customChatContents: - return - } - - let timestamp = Int32(Date().timeIntervalSince1970) - var interfaceState = self.presentationInterfaceState.interfaceState.withUpdatedTimestamp(timestamp) - if includeScrollState { - let scrollState = self.chatDisplayNode.historyNode.immediateScrollState() - interfaceState = interfaceState.withUpdatedHistoryScrollState(scrollState) - } - interfaceState = interfaceState.withUpdatedInputLanguage(self.chatDisplayNode.currentTextInputLanguage) - if case .peer = self.chatLocation, let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum) { - interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState()).withUpdatedReplyMessageSubject(nil) - } - let _ = ChatInterfaceState.update(engine: self.context.engine, peerId: peerId, threadId: threadId, { _ in - return interfaceState - }).startStandalone() - } - - override public func viewWillLeaveNavigation() { - self.chatDisplayNode.willNavigateAway() - } - - override public func inFocusUpdated(isInFocus: Bool) { - self.disableStickerAnimationsPromise.set(!isInFocus) - self.chatDisplayNode.inFocusUpdated(isInFocus: isInFocus) - } - - func canManagePin() -> Bool { - guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { - return false - } - - var canManagePin = false - if let channel = peer as? TelegramChannel { - canManagePin = channel.hasPermission(.pinMessages) - } else if let group = peer as? TelegramGroup { - switch group.role { - case .creator, .admin: - canManagePin = true - default: - if let defaultBannedRights = group.defaultBannedRights { - canManagePin = !defaultBannedRights.flags.contains(.banPinMessages) - } else { - canManagePin = true - } - } - } else if let _ = peer as? TelegramUser, self.presentationInterfaceState.explicitelyCanPinMessages { - canManagePin = true - } - - return canManagePin - } - - var suspendNavigationBarLayout: Bool = false - var suspendedNavigationBarLayout: ContainerViewLayout? - var additionalNavigationBarBackgroundHeight: CGFloat = 0.0 - var additionalNavigationBarHitTestSlop: CGFloat = 0.0 - - override public func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - if self.suspendNavigationBarLayout { - self.suspendedNavigationBarLayout = layout - return - } - self.applyNavigationBarLayout(layout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: transition) - } - - override public func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? { - return nil - } - - public func updateIsScrollingLockedAtTop(isScrollingLockedAtTop: Bool) { - self.chatDisplayNode.isScrollingLockedAtTop = isScrollingLockedAtTop - } - - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - self.suspendNavigationBarLayout = true - super.containerLayoutUpdated(layout, transition: transition) - - self.validLayout = layout - self.chatTitleView?.layout = layout - - switch self.presentationInterfaceState.mode { - case .standard, .inline: - break - case .overlay: - if case .Ignore = self.statusBar.statusBarStyle { - } else if layout.safeInsets.top.isZero { - self.statusBar.statusBarStyle = .Hide - } else { - self.statusBar.statusBarStyle = .Ignore - } - } - - var layout = layout - if case .compact = layout.metrics.widthClass, let attachmentController = self.attachmentController, attachmentController.window != nil { - layout = layout.withUpdatedInputHeight(nil) - } - - var navigationBarTransition = transition - self.chatDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition, listViewTransaction: { updateSizeAndInsets, additionalScrollDistance, scrollToTop, completion in - self.chatDisplayNode.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, additionalScrollDistance: additionalScrollDistance, scrollToTop: scrollToTop, completion: completion) - }, updateExtraNavigationBarBackgroundHeight: { value, hitTestSlop, extraNavigationTransition in - navigationBarTransition = extraNavigationTransition - self.additionalNavigationBarBackgroundHeight = value - self.additionalNavigationBarHitTestSlop = hitTestSlop - }) - - if case .compact = layout.metrics.widthClass { - let hasOverlayNodes = self.context.sharedContext.mediaManager.overlayMediaManager.controller?.hasNodes ?? false - if self.validLayout != nil && layout.size.width > layout.size.height && !hasOverlayNodes && self.traceVisibility() && isTopmostChatController(self) { - var completed = false - self.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in - if !completed, let itemNode = itemNode as? ChatMessageItemView, let message = itemNode.item?.message, let (_, soundEnabled, _, _, _) = itemNode.playMediaWithSound(), soundEnabled { - let _ = self.controllerInteraction?.openMessage(message, OpenMessageParams(mode: .landscape)) - completed = true - } - } - } - } - - self.suspendNavigationBarLayout = false - if let suspendedNavigationBarLayout = self.suspendedNavigationBarLayout { - self.suspendedNavigationBarLayout = suspendedNavigationBarLayout - self.applyNavigationBarLayout(suspendedNavigationBarLayout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: navigationBarTransition) - } - self.navigationBar?.additionalContentNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: 0.0, bottom: self.additionalNavigationBarHitTestSlop, right: 0.0) - } - - func updateChatPresentationInterfaceState(animated: Bool = true, interactive: Bool, saveInterfaceState: Bool = false, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) { - self.updateChatPresentationInterfaceState(transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate, interactive: interactive, saveInterfaceState: saveInterfaceState, f, completion: completion) - } - - func updateChatPresentationInterfaceState(transition: ContainedViewLayoutTransition, interactive: Bool, saveInterfaceState: Bool = false, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) { - updateChatPresentationInterfaceStateImpl( - selfController: self, - transition: transition, - interactive: interactive, - saveInterfaceState: saveInterfaceState, - f, - completion: completion - ) - } - - func updateItemNodesSelectionStates(animated: Bool) { - self.chatDisplayNode.historyNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? ChatMessageItemView { - itemNode.updateSelectionState(animated: animated) - } - } - - self.chatDisplayNode.historyNode.forEachItemHeaderNode{ itemHeaderNode in - if let avatarNode = itemHeaderNode as? ChatMessageAvatarHeaderNode { - avatarNode.updateSelectionState(animated: animated) - } - } - } - - func updatePollTooltipMessageState(animated: Bool) { - self.chatDisplayNode.historyNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? ChatMessageBubbleItemNode { - for contentNode in itemNode.contentNodes { - if let contentNode = contentNode as? ChatMessagePollBubbleContentNode { - contentNode.updatePollTooltipMessageState(animated: animated) - } - } - itemNode.updatePsaTooltipMessageState(animated: animated) - } - } - } - - func updateItemNodesSearchTextHighlightStates() { - var searchString: String? - var resultsMessageIndices: [MessageIndex]? - if let search = self.presentationInterfaceState.search, let resultsState = search.resultsState, !resultsState.messageIndices.isEmpty { - searchString = search.query - resultsMessageIndices = resultsState.messageIndices - } - if searchString != self.controllerInteraction?.searchTextHighightState?.0 || resultsMessageIndices?.count != self.controllerInteraction?.searchTextHighightState?.1.count { - var searchTextHighightState: (String, [MessageIndex])? - if let searchString = searchString, let resultsMessageIndices = resultsMessageIndices { - searchTextHighightState = (searchString, resultsMessageIndices) - } - self.controllerInteraction?.searchTextHighightState = searchTextHighightState - self.chatDisplayNode.historyNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? ChatMessageItemView { - itemNode.updateSearchTextHighlightState() - } - } - } - } - - func updateItemNodesHighlightedStates(animated: Bool) { - self.chatDisplayNode.historyNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? ChatMessageItemView { - itemNode.updateHighlightedState(animated: animated) - } - } - } - - @objc func leftNavigationButtonAction() { - if let button = self.leftNavigationButton { - self.navigationButtonAction(button.action) - } - } - - @objc func rightNavigationButtonAction() { - if let button = self.rightNavigationButton { - if case let .peer(peerId) = self.chatLocation, case .openChatInfo(expandAvatar: true, _) = button.action, let storyStats = self.storyStats, storyStats.unseenCount != 0, let avatarNode = self.avatarNode { - self.openStories(peerId: peerId, avatarHeaderNode: nil, avatarNode: avatarNode.avatarNode) - } else { - self.navigationButtonAction(button.action) - } - } - } - - @objc func secondaryRightNavigationButtonAction() { - if let button = self.secondaryRightNavigationButton { - self.navigationButtonAction(button.action) - } - } - - @objc func moreButtonPressed() { - self.moreBarButton.play() - self.moreBarButton.contextAction?(self.moreBarButton.containerNode, nil) - } - - public func beginClearHistory(type: InteractiveHistoryClearingType) { - guard case let .peer(peerId) = self.chatLocation else { - return - } - self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - self.chatDisplayNode.historyNode.historyAppearsCleared = true - - let statusText: String - if case .scheduledMessages = self.presentationInterfaceState.subject { - statusText = self.presentationData.strings.Undo_ScheduledMessagesCleared - } else if case .forEveryone = type { - if peerId.namespace == Namespaces.Peer.CloudUser { - statusText = self.presentationData.strings.Undo_ChatClearedForBothSides - } else { - statusText = self.presentationData.strings.Undo_ChatClearedForEveryone - } - } else { - statusText = self.presentationData.strings.Undo_ChatCleared } - - self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: statusText, text: nil), elevatedLayout: false, action: { [weak self] value in - guard let strongSelf = self else { - return false - } - if value == .commit { - let _ = strongSelf.context.engine.messages.clearHistoryInteractively(peerId: peerId, threadId: nil, type: type).startStandalone(completed: { - self?.chatDisplayNode.historyNode.historyAppearsCleared = false - }) - return true - } else if value == .undo { - strongSelf.chatDisplayNode.historyNode.historyAppearsCleared = false - return true - } - return false - }), in: .current) - } - - public func cancelSelectingMessages() { - 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 - // MARK: - Nicegram (open restricted peer info) - let isAllowed: Bool - if let strongSelf = self, let peer = peerView.peers[peerView.peerId] { - let contentSettings = strongSelf.context.currentContentSettings.with { $0 } - isAllowed = isAllowedPeerInfo(peer: peer, contentSettings: contentSettings) - } else { - isAllowed = false - } - - if let strongSelf = self, let peer = peerView.peers[peerView.peerId], (peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil || isAllowed) && !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 + @objc func leftNavigationButtonAction() { + if let button = self.leftNavigationButton { + self.navigationButtonAction(button.action) + } + } + + @objc func rightNavigationButtonAction() { + if let button = self.rightNavigationButton { + if case let .peer(peerId) = self.chatLocation, case .openChatInfo(expandAvatar: true, _) = button.action, let storyStats = self.storyStats, storyStats.unseenCount != 0, let avatarNode = self.avatarNode { + self.openStories(peerId: peerId, avatarHeaderNode: nil, avatarNode: avatarNode.avatarNode) + } else { + self.navigationButtonAction(button.action) + } + } + } + + @objc func secondaryRightNavigationButtonAction() { + if let button = self.secondaryRightNavigationButton { + self.navigationButtonAction(button.action) + } + } + + @objc func moreButtonPressed() { + self.moreBarButton.play() + self.moreBarButton.contextAction?(self.moreBarButton.containerNode, nil) + } + + public func beginClearHistory(type: InteractiveHistoryClearingType) { + guard case let .peer(peerId) = self.chatLocation else { + return + } + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + self.chatDisplayNode.historyNode.historyAppearsCleared = true + + let statusText: String + if case .scheduledMessages = self.presentationInterfaceState.subject { + statusText = self.presentationData.strings.Undo_ScheduledMessagesCleared + } else if case .forEveryone = type { + if peerId.namespace == Namespaces.Peer.CloudUser { + statusText = self.presentationData.strings.Undo_ChatClearedForBothSides + } else { + statusText = self.presentationData.strings.Undo_ChatClearedForEveryone } - case .edit: - self.editChat() + } else { + statusText = self.presentationData.strings.Undo_ChatCleared } + + self.present(UndoOverlayController(presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, content: .removedChat(title: statusText, text: nil), elevatedLayout: false, action: { [weak self] value in + guard let strongSelf = self else { + return false + } + if value == .commit { + let _ = strongSelf.context.engine.messages.clearHistoryInteractively(peerId: peerId, threadId: nil, type: type).startStandalone(completed: { + self?.chatDisplayNode.historyNode.historyAppearsCleared = false + }) + return true + } else if value == .undo { + strongSelf.chatDisplayNode.historyNode.historyAppearsCleared = false + return true + } + return false + }), in: .current) + } + + public func cancelSelectingMessages() { + self.navigationButtonAction(.cancelMessageSelection) } func editMessageMediaWithMessages(_ messages: [EnqueueMessage]) { @@ -13749,7 +8543,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - let tooltipScreen = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .entities(text: solution.text, entities: solution.entities), icon: .animation(name: "anim_infotip", delay: 0.2, tintColor: nil), location: .top, shouldDismissOnTouch: { point, _ in + let tooltipScreen = TooltipScreen(context: self.context, account: self.context.account, sharedContext: self.context.sharedContext, text: .entities(text: solution.text, entities: solution.entities), icon: .animation(name: "anim_infotip", delay: 0.2, tintColor: nil), location: .top, shouldDismissOnTouch: { point, _ in return .ignore }, openActiveTextItem: { [weak self] item, action in guard let strongSelf = self else { @@ -14029,7 +8823,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.present(tooltipScreen, in: .current) } - func configurePollCreation(isQuiz: Bool? = nil) -> CreatePollControllerImpl? { + func configurePollCreation(isQuiz: Bool? = nil) -> ViewController? { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return nil } @@ -14055,7 +8849,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G pollId: MediaId(namespace: Namespaces.Media.LocalPoll, id: Int64.random(in: Int64.min ... Int64.max)), publicity: poll.publicity, kind: poll.kind, - text: poll.text, + text: poll.text.string, + textEntities: poll.text.entities, options: poll.options, correctAnswers: poll.correctAnswers, results: poll.results, @@ -14661,439 +9456,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else { let timeout = (self.voicePlaylistDidEndTimestamp + 1.0) - CACurrentMediaTime() self.raiseToListenActivateRecordingTimer = SwiftSignalKit.Timer(timeout: max(0.0, timeout), repeat: false, completion: { [weak self] in - self?.requestAudioRecorder(beginWithTone: true) - }, queue: .mainQueue()) - self.raiseToListenActivateRecordingTimer?.start() - } - } - - func deactivateRaiseGesture() { - self.raiseToListenActivateRecordingTimer?.invalidate() - self.raiseToListenActivateRecordingTimer = nil - 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) + self?.requestAudioRecorder(beginWithTone: true) + }, queue: .mainQueue()) + self.raiseToListenActivateRecordingTimer?.start() } } + func deactivateRaiseGesture() { + self.raiseToListenActivateRecordingTimer?.invalidate() + self.raiseToListenActivateRecordingTimer = nil + self.dismissMediaRecorder(.pause) + } + func updateDownButtonVisibility() { let recordingMediaMessage = self.audioRecorderValue != nil || self.videoRecorderValue != nil || self.presentationInterfaceState.interfaceState.mediaDraftState != nil @@ -15146,7 +9520,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } // MARK: Nicegram ReplyPrivately - private func replyPrivately(message: Message) { + func replyPrivately(message: Message) { guard let author = message.author else { return } let messageId = message.id let peerId = author.id @@ -15180,183 +9554,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) @@ -15496,144 +9693,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 @@ -15750,12 +9809,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - let dismissWebAppContollers: () -> Void = { + let dismissWebAppControllers: () -> Void = { } switch navigation { case let .chat(textInputState, subject, peekData): - dismissWebAppContollers() + dismissWebAppControllers() if case .peer(peerId.id) = strongSelf.chatLocation { if let subject = subject, case let .message(messageSubject, _, timecode) = subject { if case let .id(messageId) = messageSubject { @@ -15773,7 +9832,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } commit() case .info: - dismissWebAppContollers() + dismissWebAppControllers() strongSelf.navigationActionDisposable.set((strongSelf.context.account.postbox.loadedPeerWithId(peerId.id) |> take(1) |> deliverOnMainQueue).startStrict(next: { [weak self] peer in @@ -15793,7 +9852,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G )) commit() case let .withBotStartPayload(startPayload): - dismissWebAppContollers() + dismissWebAppControllers() if case .peer(peerId.id) = strongSelf.chatLocation { strongSelf.startBot(startPayload.payload) } else if let navigationController = strongSelf.effectiveNavigationController { @@ -15801,7 +9860,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } commit() case let .withAttachBot(attachBotStart): - dismissWebAppContollers() + dismissWebAppControllers() if let navigationController = strongSelf.effectiveNavigationController { strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peerId), attachBotStart: attachBotStart)) } @@ -15811,7 +9870,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in if let strongSelf = self, let peer { strongSelf.presentBotApp(botApp: botAppStart.botApp, botPeer: peer, payload: botAppStart.payload, concealed: concealed, commit: { - dismissWebAppContollers() + dismissWebAppControllers() commit() }) } @@ -15850,334 +9909,42 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G default: progress?.set(.single(false)) self.context.sharedContext.openChatInstantPage(context: self.context, message: message, sourcePeerType: nil, navigationController: navigationController) - return - } - } - } else if content.file == nil, (content.image == nil || content.isMediaLargeByDefault == true || content.isMediaLargeByDefault == nil), let embedUrl = content.embedUrl, !embedUrl.isEmpty { - progress?.set(.single(false)) - if let controllerInteraction = self.controllerInteraction { - if controllerInteraction.openMessage(message, OpenMessageParams(mode: .default)) { - return - } - } - } - } - - let _ = self.presentVoiceMessageDiscardAlert(action: { [weak self] in - guard let self else { - return - } - let disposable = openUserGeneratedUrl(context: self.context, peerId: self.peerView?.peerId, url: url, concealed: concealed, skipUrlAuth: skipUrlAuth, skipConcealedAlert: skipConcealedAlert, present: { [weak self] c in - self?.present(c, in: .window(.root)) - }, openResolved: { [weak self] resolved in - self?.openResolved(result: resolved, sourceMessageId: message?.id, forceExternal: forceExternal, concealed: concealed, commit: commit) - }, progress: progress) - self.navigationActionDisposable.set(disposable) - }, performAction: true) - } - - func openUrlIn(_ url: String) { - let actionSheet = OpenInActionSheetController(context: self.context, updatedPresentationData: self.updatedPresentationData, item: .url(url: url), openUrl: { [weak self] url in - if let strongSelf = self, let navigationController = strongSelf.effectiveNavigationController { - strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: strongSelf.presentationData, navigationController: navigationController, dismissInput: { - self?.chatDisplayNode.dismissInput() - }) - } - }) - self.chatDisplayNode.dismissInput() - self.present(actionSheet, in: .window(.root)) - } - - func presentBanMessageOptions(accountPeerId: PeerId, author: Peer, messageIds: Set, options: ChatAvailableMessageActionOptions) { - guard let peerId = self.chatLocation.peerId else { - return - } - do { - self.navigationActionDisposable.set((self.context.engine.peers.fetchChannelParticipant(peerId: peerId, participantId: author.id) - |> deliverOnMainQueue).startStrict(next: { [weak self] participant in - if let strongSelf = self { - let canBan = participant?.canBeBannedBy(peerId: accountPeerId) ?? true - - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) - var items: [ActionSheetItem] = [] - - var actions = Set([0]) - - let toggleCheck: (Int, Int) -> Void = { [weak actionSheet] category, itemIndex in - if actions.contains(category) { - actions.remove(category) - } else { - actions.insert(category) - } - actionSheet?.updateItem(groupIndex: 0, itemIndex: itemIndex, { item in - if let item = item as? ActionSheetCheckboxItem { - return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action) - } - return item - }) - } - - var itemIndex = 0 - var categories: [Int] = [0] - if canBan { - categories.append(1) - } - categories.append(contentsOf: [2, 3]) - - for categoryId in categories as [Int] { - var title = "" - if categoryId == 0 { - title = strongSelf.presentationData.strings.Conversation_Moderate_Delete - } else if categoryId == 1 { - title = strongSelf.presentationData.strings.Conversation_Moderate_Ban - } else if categoryId == 2 { - title = strongSelf.presentationData.strings.Conversation_Moderate_Report - } else if categoryId == 3 { - title = strongSelf.presentationData.strings.Conversation_Moderate_DeleteAllMessages(EnginePeer(author).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string - } - let index = itemIndex - items.append(ActionSheetCheckboxItem(title: title, label: "", value: actions.contains(categoryId), action: { value in - toggleCheck(categoryId, index) - })) - itemIndex += 1 - } - - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Done, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - if actions.contains(3) { - let _ = strongSelf.context.engine.messages.deleteAllMessagesWithAuthor(peerId: peerId, authorId: author.id, namespace: Namespaces.Message.Cloud).startStandalone() - let _ = strongSelf.context.engine.messages.clearAuthorHistory(peerId: peerId, memberId: author.id).startStandalone() - } else if actions.contains(0) { - let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() - } - if actions.contains(1) { - let _ = strongSelf.context.engine.peers.removePeerMember(peerId: peerId, memberId: author.id).startStandalone() - } - } - })) - - 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)) - } - })) - } - } - - func presentDeleteMessageOptions(messageIds: Set, options: ChatAvailableMessageActionOptions, contextController: ContextControllerProtocol?, completion: @escaping (ContextMenuActionResult) -> Void) { - let _ = (self.context.engine.data.get( - EngineDataMap(messageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init(id:))) - ) - |> deliverOnMainQueue).start(next: { [weak self] messages in - guard let self else { - return - } - - let actionSheet = ActionSheetController(presentationData: self.presentationData) - var items: [ActionSheetItem] = [] - var personalPeerName: String? - var isChannel = false - if let user = self.presentationInterfaceState.renderedPeer?.peer as? TelegramUser { - personalPeerName = EnginePeer(user).compactDisplayTitle - } else if let peer = self.presentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat, let associatedPeerId = peer.associatedPeerId, let user = self.presentationInterfaceState.renderedPeer?.peers[associatedPeerId] as? TelegramUser { - personalPeerName = EnginePeer(user).compactDisplayTitle - } else if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, case .broadcast = channel.info { - isChannel = true - } - - if options.contains(.cancelSending) { - items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ContextMenuCancelSending, color: .destructive, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() - } - })) - } - - var contextItems: [ContextMenuItem] = [] - var canDisplayContextMenu = true - - var unsendPersonalMessages = false - if options.contains(.unsendPersonal) { - canDisplayContextMenu = false - items.append(ActionSheetTextItem(title: self.presentationData.strings.Chat_UnsendMyMessagesAlertTitle(personalPeerName ?? "").string)) - items.append(ActionSheetSwitchItem(title: self.presentationData.strings.Chat_UnsendMyMessages, isOn: false, action: { value in - unsendPersonalMessages = value - })) - } else if options.contains(.deleteGlobally) { - let globalTitle: String - if isChannel { - globalTitle = self.presentationData.strings.Conversation_DeleteMessagesForEveryone - } else if let personalPeerName = personalPeerName { - globalTitle = self.presentationData.strings.Conversation_DeleteMessagesFor(personalPeerName).string - } else { - globalTitle = self.presentationData.strings.Conversation_DeleteMessagesForEveryone - } - contextItems.append(.action(ContextMenuActionItem(text: globalTitle, textColor: .destructive, icon: { _ in nil }, action: { [weak self] c, f in - if let strongSelf = self { - var giveaway: TelegramMediaGiveaway? - for messageId in messageIds { - if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { - if let media = message.media.first(where: { $0 is TelegramMediaGiveaway }) as? TelegramMediaGiveaway { - giveaway = media - break - } - } - } - let commit = { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() - } - if let giveaway { - let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) - if currentTime < giveaway.untilDate { - Queue.mainQueue().after(0.2) { - let dateString = stringForDate(timestamp: giveaway.untilDate, timeZone: .current, strings: strongSelf.presentationData.strings) - strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: strongSelf.presentationData.strings.Chat_Giveaway_DeleteConfirmation_Title, text: strongSelf.presentationData.strings.Chat_Giveaway_DeleteConfirmation_Text(dateString).string, actions: [TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Common_Delete, action: { - commit() - }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { - })], parseMarkdown: true), in: .window(.root)) - } - f(.default) - } else { - f(.dismissWithoutContent) - commit() - } - } else { - if "".isEmpty { - f(.dismissWithoutContent) - commit() - } else { - c.dismiss(completion: { - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { - commit() - }) - }) - } - } - } - }))) - items.append(ActionSheetButtonItem(title: globalTitle, color: .destructive, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() - } - })) - } - if options.contains(.deleteLocally) { - var localOptionText = self.presentationData.strings.Conversation_DeleteMessagesForMe - if self.chatLocation.peerId == self.context.account.peerId { - if case .peer(self.context.account.peerId) = self.chatLocation, messages.values.allSatisfy({ message in message?._asMessage().effectivelyIncoming(self.context.account.peerId) ?? false }) { - localOptionText = self.presentationData.strings.Chat_ConfirmationRemoveFromSavedMessages - } else { - localOptionText = self.presentationData.strings.Chat_ConfirmationDeleteFromSavedMessages - } - } else if case .scheduledMessages = self.presentationInterfaceState.subject { - localOptionText = messageIds.count > 1 ? self.presentationData.strings.ScheduledMessages_DeleteMany : self.presentationData.strings.ScheduledMessages_Delete - } else { - if options.contains(.unsendPersonal) { - localOptionText = self.presentationData.strings.Chat_DeleteMessagesConfirmation(Int32(messageIds.count)) - } else if case .peer(self.context.account.peerId) = self.chatLocation { - if messageIds.count == 1 { - localOptionText = self.presentationData.strings.Conversation_Moderate_Delete - } else { - localOptionText = self.presentationData.strings.Conversation_DeleteManyMessages - } + return } } - contextItems.append(.action(ContextMenuActionItem(text: localOptionText, textColor: .destructive, icon: { _ in nil }, action: { [weak self] c, f in - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - - let commit: () -> Void = { - guard let strongSelf = self else { - return - } - let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).startStandalone() - } - - if "".isEmpty { - f(.dismissWithoutContent) - commit() - } else { - c.dismiss(completion: { - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { - commit() - }) - }) - } - } - }))) - items.append(ActionSheetButtonItem(title: localOptionText, color: .destructive, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) - let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).startStandalone() - + } else if content.file == nil, (content.image == nil || content.isMediaLargeByDefault == true || content.isMediaLargeByDefault == nil), let embedUrl = content.embedUrl, !embedUrl.isEmpty { + progress?.set(.single(false)) + if let controllerInteraction = self.controllerInteraction { + if controllerInteraction.openMessage(message, OpenMessageParams(mode: .default)) { + return } - })) - } - - if canDisplayContextMenu, let contextController = contextController { - contextController.setItems(.single(ContextController.Items(content: .list(contextItems))), minHeight: nil, animated: true) - } else { - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - - if let contextController = contextController { - contextController.dismiss(completion: { [weak self] in - self?.present(actionSheet, in: .window(.root)) - }) - } else { - self.chatDisplayNode.dismissInput() - self.present(actionSheet, in: .window(.root)) - completion(.default) } } - }) - } - - func presentClearCacheSuggestion() { - guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { - return } - self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) - let actionSheet = ActionSheetController(presentationData: self.presentationData) - var items: [ActionSheetItem] = [] - - items.append(DeleteChatPeerActionSheetItem(context: self.context, peer: EnginePeer(peer), chatPeer: EnginePeer(peer), action: .clearCacheSuggestion, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder)) - - var presented = false - items.append(ActionSheetButtonItem(title: self.presentationData.strings.ClearCache_FreeSpace, color: .accent, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self, !presented { - presented = true - let context = strongSelf.context - strongSelf.push(StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { category in - return storageUsageExceptionsScreen(context: context, category: category) - })) - } - })) + let _ = self.presentVoiceMessageDiscardAlert(action: { [weak self] in + guard let self else { + return + } + let disposable = openUserGeneratedUrl(context: self.context, peerId: self.peerView?.peerId, url: url, concealed: concealed, skipUrlAuth: skipUrlAuth, skipConcealedAlert: skipConcealedAlert, present: { [weak self] c in + self?.present(c, in: .window(.root)) + }, openResolved: { [weak self] resolved in + self?.openResolved(result: resolved, sourceMessageId: message?.id, forceExternal: forceExternal, concealed: concealed, commit: commit) + }, progress: progress) + self.navigationActionDisposable.set(disposable) + }, performAction: 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() - }) - ])]) + func openUrlIn(_ url: String) { + let actionSheet = OpenInActionSheetController(context: self.context, updatedPresentationData: self.updatedPresentationData, item: .url(url: url), openUrl: { [weak self] url in + if let strongSelf = self, let navigationController = strongSelf.effectiveNavigationController { + strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: strongSelf.presentationData, navigationController: navigationController, dismissInput: { + self?.chatDisplayNode.dismissInput() + }) + } + }) self.chatDisplayNode.dismissInput() - self.presentInGlobalOverlay(actionSheet) + self.present(actionSheet, in: .window(.root)) } @available(iOSApplicationExtension 11.0, iOS 11.0, *) @@ -17007,241 +10774,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 } @@ -17395,129 +10927,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) } @@ -17533,92 +10942,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/ChatControllerAdminBanUsers.swift b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift new file mode 100644 index 00000000000..0f361cc0ec6 --- /dev/null +++ b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift @@ -0,0 +1,615 @@ +import Foundation +import TelegramPresentationData +import AccountContext +import Postbox +import TelegramCore +import SwiftSignalKit +import Display +import TelegramPresentationData +import PresentationDataUtils +import UndoUI +import AdminUserActionsSheet +import ContextUI +import TelegramStringFormatting +import StorageUsageScreen +import SettingsUI +import DeleteChatPeerActionSheetItem +import OverlayStatusController + +fileprivate struct InitialBannedRights { + var value: TelegramChatBannedRights? +} + +extension ChatControllerImpl { + fileprivate func applyAdminUserActionsResult(messageIds: Set, result: AdminUserActionsSheet.Result, initialUserBannedRights: [EnginePeer.Id: InitialBannedRights]) { + guard let peerId = self.chatLocation.peerId else { + return + } + + var title: String? = messageIds.count == 1 ? self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTitleSingle : self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTitleMultiple + if !result.deleteAllFromPeers.isEmpty { + title = self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTitleMultiple + } + var text: String = "" + var undoRights: [EnginePeer.Id: InitialBannedRights] = [:] + + if !result.reportSpamPeers.isEmpty { + if !text.isEmpty { + text.append("\n") + } + text.append(self.presentationData.strings.Chat_AdminAction_ToastReportedSpamText(Int32(result.reportSpamPeers.count))) + } + if !result.banPeers.isEmpty { + if !text.isEmpty { + text.append("\n") + } + text.append(self.presentationData.strings.Chat_AdminAction_ToastBannedText(Int32(result.banPeers.count))) + for id in result.banPeers { + if let value = initialUserBannedRights[id] { + undoRights[id] = value + } + } + } + if !result.updateBannedRights.isEmpty { + if !text.isEmpty { + text.append("\n") + } + text.append(self.presentationData.strings.Chat_AdminAction_ToastRestrictedText(Int32(result.updateBannedRights.count))) + for (id, _) in result.updateBannedRights { + if let value = initialUserBannedRights[id] { + undoRights[id] = value + } + } + } + + do { + let _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() + + for authorId in result.deleteAllFromPeers { + let _ = self.context.engine.messages.deleteAllMessagesWithAuthor(peerId: peerId, authorId: authorId, namespace: Namespaces.Message.Cloud).startStandalone() + let _ = self.context.engine.messages.clearAuthorHistory(peerId: peerId, memberId: authorId).startStandalone() + } + + for authorId in result.reportSpamPeers { + let _ = self.context.engine.peers.reportPeer(peerId: authorId, reason: .spam, message: "").startStandalone() + } + + for authorId in result.banPeers { + let _ = self.context.engine.peers.removePeerMember(peerId: peerId, memberId: authorId).startStandalone() + } + + for (authorId, rights) in result.updateBannedRights { + let _ = self.context.engine.peers.updateChannelMemberBannedRights(peerId: peerId, memberId: authorId, rights: rights).startStandalone() + } + } + + if text.isEmpty { + text = messageIds.count == 1 ? self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTextSingle : self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTextMultiple + if !result.deleteAllFromPeers.isEmpty { + text = self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTextMultiple + } + title = nil + } + + self.present( + UndoOverlayController( + presentationData: self.presentationData, + content: undoRights.isEmpty ? .actionSucceeded(title: title, text: text, cancel: nil, destructive: false) : .removedChat(title: title ?? text, text: title == nil ? nil : text), + elevatedLayout: false, + action: { [weak self] action in + guard let self else { + return true + } + + switch action { + case .commit: + break + case .undo: + for (authorId, rights) in initialUserBannedRights { + let _ = self.context.engine.peers.updateChannelMemberBannedRights(peerId: peerId, memberId: authorId, rights: rights.value).startStandalone() + } + default: + break + } + return true + } + ), + in: .current + ) + + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + } + + func presentMultiBanMessageOptions(accountPeerId: PeerId, authors: [Peer], messageIds: Set, options: ChatAvailableMessageActionOptions) { + guard let peerId = self.chatLocation.peerId else { + return + } + + var signal = combineLatest(authors.map { author in + self.context.engine.peers.fetchChannelParticipant(peerId: peerId, participantId: author.id) + |> map { result -> (Peer, ChannelParticipant?) in + return (author, result) + } + }) + let disposables = MetaDisposable() + self.navigationActionDisposable.set(disposables) + + var cancelImpl: (() -> Void)? + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let progressSignal = Signal { [weak self] subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + self?.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) + } + + disposables.set((signal + |> deliverOnMainQueue).startStrict(next: { [weak self] authorsAndParticipants in + guard let self else { + return + } + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] chatPeer in + guard let self, let chatPeer else { + return + } + var renderedParticipants: [RenderedChannelParticipant] = [] + var initialUserBannedRights: [EnginePeer.Id: InitialBannedRights] = [:] + for (author, maybeParticipant) in authorsAndParticipants { + let participant: ChannelParticipant + if let maybeParticipant { + participant = maybeParticipant + } else { + participant = .member(id: author.id, invitedAt: 0, adminInfo: nil, banInfo: ChannelParticipantBannedInfo( + rights: TelegramChatBannedRights( + flags: [.banReadMessages], + untilDate: Int32.max + ), + restrictedBy: self.context.account.peerId, + timestamp: 0, + isMember: false + ), rank: nil) + } + + let peer = author + renderedParticipants.append(RenderedChannelParticipant( + participant: participant, + peer: peer + )) + switch participant { + case .creator: + break + case let .member(_, _, _, banInfo, _): + if let banInfo { + initialUserBannedRights[participant.peerId] = InitialBannedRights(value: banInfo.rights) + } else { + initialUserBannedRights[participant.peerId] = InitialBannedRights(value: nil) + } + } + } + self.push(AdminUserActionsSheet( + context: self.context, + chatPeer: chatPeer, + peers: renderedParticipants, + messageCount: messageIds.count, + completion: { [weak self] result in + guard let self else { + return + } + self.applyAdminUserActionsResult(messageIds: messageIds, result: result, initialUserBannedRights: initialUserBannedRights) + } + )) + }) + })) + } + + func presentBanMessageOptions(accountPeerId: PeerId, author: Peer, messageIds: Set, options: ChatAvailableMessageActionOptions) { + guard let peerId = self.chatLocation.peerId else { + return + } + + var signal = self.context.engine.peers.fetchChannelParticipant(peerId: peerId, participantId: author.id) + let disposables = MetaDisposable() + self.navigationActionDisposable.set(disposables) + + var cancelImpl: (() -> Void)? + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let progressSignal = Signal { [weak self] subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + self?.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) + } + + disposables.set((signal + |> deliverOnMainQueue).startStrict(next: { [weak self] maybeParticipant in + guard let self else { + return + } + + let participant: ChannelParticipant + if let maybeParticipant { + participant = maybeParticipant + } else { + participant = .member(id: author.id, invitedAt: 0, adminInfo: nil, banInfo: ChannelParticipantBannedInfo( + rights: TelegramChatBannedRights( + flags: [.banReadMessages], + untilDate: Int32.max + ), + restrictedBy: self.context.account.peerId, + timestamp: 0, + isMember: false + ), rank: nil) + } + + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), + TelegramEngine.EngineData.Item.Peer.Peer(id: author.id) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] chatPeer, authorPeer in + guard let self, let chatPeer else { + return + } + guard let authorPeer else { + return + } + var initialUserBannedRights: [EnginePeer.Id: InitialBannedRights] = [:] + switch participant { + case .creator: + break + case let .member(_, _, _, banInfo, _): + if let banInfo { + initialUserBannedRights[participant.peerId] = InitialBannedRights(value: banInfo.rights) + } else { + initialUserBannedRights[participant.peerId] = InitialBannedRights(value: nil) + } + } + self.push(AdminUserActionsSheet( + context: self.context, + chatPeer: chatPeer, + peers: [RenderedChannelParticipant( + participant: participant, + peer: authorPeer._asPeer() + )], + messageCount: messageIds.count, + completion: { [weak self] result in + guard let self else { + return + } + self.applyAdminUserActionsResult(messageIds: messageIds, result: result, initialUserBannedRights: initialUserBannedRights) + } + )) + }) + })) + + /*do { + self.navigationActionDisposable.set((self.context.engine.peers.fetchChannelParticipant(peerId: peerId, participantId: author.id) + |> deliverOnMainQueue).startStrict(next: { + if let strongSelf = self { + if "".isEmpty { + + return + } + + let canBan = participant?.canBeBannedBy(peerId: accountPeerId) ?? true + + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) + var items: [ActionSheetItem] = [] + + var actions = Set([0]) + + let toggleCheck: (Int, Int) -> Void = { [weak actionSheet] category, itemIndex in + if actions.contains(category) { + actions.remove(category) + } else { + actions.insert(category) + } + actionSheet?.updateItem(groupIndex: 0, itemIndex: itemIndex, { item in + if let item = item as? ActionSheetCheckboxItem { + return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action) + } + return item + }) + } + + var itemIndex = 0 + var categories: [Int] = [0] + if canBan { + categories.append(1) + } + categories.append(contentsOf: [2, 3]) + + for categoryId in categories as [Int] { + var title = "" + if categoryId == 0 { + title = strongSelf.presentationData.strings.Conversation_Moderate_Delete + } else if categoryId == 1 { + title = strongSelf.presentationData.strings.Conversation_Moderate_Ban + } else if categoryId == 2 { + title = strongSelf.presentationData.strings.Conversation_Moderate_Report + } else if categoryId == 3 { + title = strongSelf.presentationData.strings.Conversation_Moderate_DeleteAllMessages(EnginePeer(author).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string + } + let index = itemIndex + items.append(ActionSheetCheckboxItem(title: title, label: "", value: actions.contains(categoryId), action: { value in + toggleCheck(categoryId, index) + })) + itemIndex += 1 + } + + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Done, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + if actions.contains(3) { + let _ = strongSelf.context.engine.messages.deleteAllMessagesWithAuthor(peerId: peerId, authorId: author.id, namespace: Namespaces.Message.Cloud).startStandalone() + let _ = strongSelf.context.engine.messages.clearAuthorHistory(peerId: peerId, memberId: author.id).startStandalone() + } else if actions.contains(0) { + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() + } + if actions.contains(1) { + let _ = strongSelf.context.engine.peers.removePeerMember(peerId: peerId, memberId: author.id).startStandalone() + } + } + })) + + 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)) + } + })) + }*/ + } + + func presentDeleteMessageOptions(messageIds: Set, options: ChatAvailableMessageActionOptions, contextController: ContextControllerProtocol?, completion: @escaping (ContextMenuActionResult) -> Void) { + let _ = (self.context.engine.data.get( + EngineDataMap(messageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init(id:))) + ) + |> deliverOnMainQueue).start(next: { [weak self] messages in + guard let self else { + return + } + + let actionSheet = ActionSheetController(presentationData: self.presentationData) + var items: [ActionSheetItem] = [] + var personalPeerName: String? + var isChannel = false + if let user = self.presentationInterfaceState.renderedPeer?.peer as? TelegramUser { + personalPeerName = EnginePeer(user).compactDisplayTitle + } else if let peer = self.presentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat, let associatedPeerId = peer.associatedPeerId, let user = self.presentationInterfaceState.renderedPeer?.peers[associatedPeerId] as? TelegramUser { + personalPeerName = EnginePeer(user).compactDisplayTitle + } else if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, case .broadcast = channel.info { + isChannel = true + } + + if options.contains(.cancelSending) { + items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ContextMenuCancelSending, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() + } + })) + } + + var contextItems: [ContextMenuItem] = [] + var canDisplayContextMenu = true + + var unsendPersonalMessages = false + if options.contains(.unsendPersonal) { + canDisplayContextMenu = false + items.append(ActionSheetTextItem(title: self.presentationData.strings.Chat_UnsendMyMessagesAlertTitle(personalPeerName ?? "").string)) + items.append(ActionSheetSwitchItem(title: self.presentationData.strings.Chat_UnsendMyMessages, isOn: false, action: { value in + unsendPersonalMessages = value + })) + } else if options.contains(.deleteGlobally) { + let globalTitle: String + if isChannel { + globalTitle = self.presentationData.strings.Conversation_DeleteMessagesForEveryone + } else if let personalPeerName = personalPeerName { + globalTitle = self.presentationData.strings.Conversation_DeleteMessagesFor(personalPeerName).string + } else { + globalTitle = self.presentationData.strings.Conversation_DeleteMessagesForEveryone + } + contextItems.append(.action(ContextMenuActionItem(text: globalTitle, textColor: .destructive, icon: { _ in nil }, action: { [weak self] c, f in + if let strongSelf = self { + var giveaway: TelegramMediaGiveaway? + for messageId in messageIds { + if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { + if let media = message.media.first(where: { $0 is TelegramMediaGiveaway }) as? TelegramMediaGiveaway { + giveaway = media + break + } + } + } + let commit = { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() + } + if let giveaway { + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + if currentTime < giveaway.untilDate { + Queue.mainQueue().after(0.2) { + let dateString = stringForDate(timestamp: giveaway.untilDate, timeZone: .current, strings: strongSelf.presentationData.strings) + strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: strongSelf.presentationData.strings.Chat_Giveaway_DeleteConfirmation_Title, text: strongSelf.presentationData.strings.Chat_Giveaway_DeleteConfirmation_Text(dateString).string, actions: [TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Common_Delete, action: { + commit() + }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + })], parseMarkdown: true), in: .window(.root)) + } + f(.default) + } else { + f(.dismissWithoutContent) + commit() + } + } else { + if "".isEmpty { + f(.dismissWithoutContent) + commit() + } else { + c.dismiss(completion: { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { + commit() + }) + }) + } + } + } + }))) + items.append(ActionSheetButtonItem(title: globalTitle, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() + } + })) + } + if options.contains(.deleteLocally) { + var localOptionText = self.presentationData.strings.Conversation_DeleteMessagesForMe + if self.chatLocation.peerId == self.context.account.peerId { + if case .peer(self.context.account.peerId) = self.chatLocation, messages.values.allSatisfy({ message in message?._asMessage().effectivelyIncoming(self.context.account.peerId) ?? false }) { + localOptionText = self.presentationData.strings.Chat_ConfirmationRemoveFromSavedMessages + } else { + localOptionText = self.presentationData.strings.Chat_ConfirmationDeleteFromSavedMessages + } + } else if case .scheduledMessages = self.presentationInterfaceState.subject { + localOptionText = messageIds.count > 1 ? self.presentationData.strings.ScheduledMessages_DeleteMany : self.presentationData.strings.ScheduledMessages_Delete + } else { + if options.contains(.unsendPersonal) { + localOptionText = self.presentationData.strings.Chat_DeleteMessagesConfirmation(Int32(messageIds.count)) + } else if case .peer(self.context.account.peerId) = self.chatLocation { + if messageIds.count == 1 { + localOptionText = self.presentationData.strings.Conversation_Moderate_Delete + } else { + localOptionText = self.presentationData.strings.Conversation_DeleteManyMessages + } + } + } + contextItems.append(.action(ContextMenuActionItem(text: localOptionText, textColor: .destructive, icon: { _ in nil }, action: { [weak self] c, f in + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + + let commit: () -> Void = { + guard let strongSelf = self else { + return + } + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).startStandalone() + } + + if "".isEmpty { + f(.dismissWithoutContent) + commit() + } else { + c.dismiss(completion: { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { + commit() + }) + }) + } + } + }))) + items.append(ActionSheetButtonItem(title: localOptionText, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).startStandalone() + + } + })) + } + + if canDisplayContextMenu, let contextController = contextController { + contextController.setItems(.single(ContextController.Items(content: .list(contextItems))), minHeight: nil, animated: true) + } else { + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + + if let contextController = contextController { + contextController.dismiss(completion: { [weak self] in + self?.present(actionSheet, in: .window(.root)) + }) + } else { + self.chatDisplayNode.dismissInput() + self.present(actionSheet, in: .window(.root)) + completion(.default) + } + } + }) + } + + func presentClearCacheSuggestion() { + guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { + return + } + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) + + let actionSheet = ActionSheetController(presentationData: self.presentationData) + var items: [ActionSheetItem] = [] + + items.append(DeleteChatPeerActionSheetItem(context: self.context, peer: EnginePeer(peer), chatPeer: EnginePeer(peer), action: .clearCacheSuggestion, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder)) + + var presented = false + items.append(ActionSheetButtonItem(title: self.presentationData.strings.ClearCache_FreeSpace, color: .accent, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self, !presented { + presented = true + let context = strongSelf.context + strongSelf.push(StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { category in + return storageUsageExceptionsScreen(context: context, category: category) + })) + } + })) + + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + self.chatDisplayNode.dismissInput() + self.presentInGlobalOverlay(actionSheet) + } +} diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index df8443507a8..99027c8412f 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -527,7 +527,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } if let poll = media as? TelegramMediaPoll { var updatedMedia = message.media.filter { !($0 is TelegramMediaPoll) } - updatedMedia.append(TelegramMediaPoll(pollId: poll.pollId, publicity: poll.publicity, kind: poll.kind, text: poll.text, options: poll.options, correctAnswers: poll.correctAnswers, results: TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [], solution: nil), isClosed: false, deadlineTimeout: nil)) + updatedMedia.append(TelegramMediaPoll(pollId: poll.pollId, publicity: poll.publicity, kind: poll.kind, text: poll.text, textEntities: poll.textEntities, options: poll.options, correctAnswers: poll.correctAnswers, results: TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [], solution: nil), isClosed: false, deadlineTimeout: nil)) messageMedia = updatedMedia } if let _ = media as? TelegramMediaDice { diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index 6bf4da54af1..8f6a31dd57f 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -554,9 +554,10 @@ extension ChatControllerImpl { } })) case .poll: - let controller = strongSelf.configurePollCreation() - completion(controller, controller?.mediaPickerContext) - strongSelf.controllerNavigationDisposable.set(nil) + if let controller = strongSelf.configurePollCreation() as? AttachmentContainable { + completion(controller, controller.mediaPickerContext) + strongSelf.controllerNavigationDisposable.set(nil) + } case .gift: if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions @@ -855,7 +856,7 @@ extension ChatControllerImpl { } }, recognizedQRCode: { [weak self] code in if let strongSelf = self { - if let (host, port, username, password, secret) = parseProxyUrl(code) { + if let (host, port, username, password, secret) = parseProxyUrl(sharedContext: strongSelf.context.sharedContext, url: code) { strongSelf.openResolved(result: ResolvedUrl.proxy(host: host, port: port, username: username, password: password, secret: secret), sourceMessageId: nil) } } @@ -1163,6 +1164,8 @@ extension ChatControllerImpl { controller.openCamera = { [weak self] cameraView in if let cameraView = cameraView as? TGAttachmentCameraView { self?.openCamera(cameraView: cameraView) + } else { + self?.openCamera(cameraView: nil) } } controller.presentWebSearch = { [weak self, weak controller] mediaGroups, activateOnDisplay in @@ -1694,7 +1697,7 @@ extension ChatControllerImpl { } }, recognizedQRCode: { [weak self] code in if let strongSelf = self { - if let (host, port, username, password, secret) = parseProxyUrl(code) { + if let (host, port, username, password, secret) = parseProxyUrl(sharedContext: strongSelf.context.sharedContext, url: code) { strongSelf.openResolved(result: ResolvedUrl.proxy(host: host, port: port, username: username, password: password, secret: secret), sourceMessageId: nil) } } diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 89f140c6bd5..2f541681be7 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -41,6 +41,7 @@ import ChatMessageBubbleItemNode import ChatMessageTransitionNode import ChatControllerInteraction import DustEffect +import UrlHandling // MARK: Nicegram AiChat private extension ListViewUpdateSizeAndInsets { @@ -689,19 +690,19 @@ 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? private var adMessagesDisposable: Disposable? - private var preloadAdPeerId: PeerId? + private var preloadAdPeerName: String? private let preloadAdPeerDisposable = MetaDisposable() private var didSetupRecommendedChannelsPreload = false private let preloadRecommendedChannelsDisposable = MetaDisposable() @@ -1048,7 +1049,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) @@ -1147,16 +1148,23 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto self.allAdMessages = (messages.first, [], 0) } else { - var adPeerId: PeerId? - adPeerId = messages.first?.author?.id + var adPeerName: String? + if let adAttribute = messages.first?.adAttribute, let parsedUrl = parseAdUrl(sharedContext: self.context.sharedContext, url: adAttribute.url), case let .peer(reference, _) = parsedUrl, case let .name(peerName) = reference { + adPeerName = peerName + } - if self.preloadAdPeerId != adPeerId { - self.preloadAdPeerId = adPeerId - if let adPeerId = adPeerId { + if self.preloadAdPeerName != adPeerName { + self.preloadAdPeerName = adPeerName + if let adPeerName { + let context = self.context let combinedDisposable = DisposableSet() self.preloadAdPeerDisposable.set(combinedDisposable) - combinedDisposable.add(self.context.account.viewTracker.polledChannel(peerId: adPeerId).startStrict()) - combinedDisposable.add(self.context.account.addAdditionalPreloadHistoryPeerId(peerId: adPeerId)) + combinedDisposable.add(context.engine.peers.resolvePeerByName(name: adPeerName).startStrict(next: { result in + if case let .result(maybePeer) = result, let peer = maybePeer { + combinedDisposable.add(context.account.viewTracker.polledChannel(peerId: peer.id).startStrict()) + combinedDisposable.add(context.account.addAdditionalPreloadHistoryPeerId(peerId: peer.id)) + } + })) } else { self.preloadAdPeerDisposable.set(nil) } @@ -2267,8 +2275,11 @@ 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 { - swipeText = ("Pull up to go to the next channel", []) - releaseText = ("Release to go to the next channel", []) + swipeText = (self.currentPresentationData.strings.Chat_NextSuggestedChannelSwipeProgress, []) + releaseText = (self.currentPresentationData.strings.Chat_NextSuggestedChannelSwipeAction, []) + } else if nextChannelToRead.threadData != nil { + swipeText = (self.currentPresentationData.strings.Chat_NextUnreadTopicSwipeProgress, []) + releaseText = (self.currentPresentationData.strings.Chat_NextUnreadTopicSwipeAction, []) } else { swipeText = (self.currentPresentationData.strings.Chat_NextChannelSameLocationSwipeProgress, []) releaseText = (self.currentPresentationData.strings.Chat_NextChannelSameLocationSwipeAction, []) @@ -2312,6 +2323,13 @@ 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 + ) + }, + isForumThread: self.chatLocation.threadId != nil, unreadCount: self.nextChannelToRead?.unreadCount ?? 0, location: self.nextChannelToRead?.location ?? .same, context: self.context, @@ -2485,6 +2503,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto for i in (wideIndexRange.0 ... wideIndexRange.1) { switch historyView.filteredEntries[i] { case let .MessageEntry(message, _, _, _, _, _): + guard message.adAttribute == nil && message.id.namespace == Namespaces.Message.Cloud else { + continue + } if !message.text.isEmpty && message.author?.id != self.context.account.peerId { if let translation = message.attributes.first(where: { $0 is TranslationMessageAttribute }) as? TranslationMessageAttribute, translation.toLang == translateToLanguage { } else { @@ -2493,6 +2514,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } case let .MessageGroupEntry(_, messages, _): for (message, _, _, _, _) in messages { + guard message.adAttribute == nil && message.id.namespace == Namespaces.Message.Cloud else { + continue + } if !message.text.isEmpty && message.author?.id != self.context.account.peerId { if let translation = message.attributes.first(where: { $0 is TranslationMessageAttribute }) as? TranslationMessageAttribute, translation.toLang == translateToLanguage { } else { @@ -2579,7 +2603,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto contentRequiredValidation = true } else if message.flags.contains(.Incoming), let media = media as? TelegramMediaMap, let liveBroadcastingTimeout = media.liveBroadcastingTimeout { let timestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) - if message.timestamp + liveBroadcastingTimeout > timestamp { + if liveBroadcastingTimeout == liveLocationIndefinitePeriod || message.timestamp + liveBroadcastingTimeout > timestamp { messageIdsWithLiveLocation.append(message.id) } } else if let telegramFile = media as? TelegramMediaFile { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 498b94cc494..861cf5e9e4f 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -595,15 +595,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.primaryTextColor) }, iconSource: nil, action: { c, _ in c.dismiss(completion: { - var replaceImpl: ((ViewController) -> Void)? - let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .noAds, action: { - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: false, dismissed: nil) - replaceImpl?(controller) - }) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) - } - controllerInteraction.navigationController()?.pushViewController(controller) + controllerInteraction.openNoAdsDemo() }) }))) } else { @@ -621,10 +613,10 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState }, iconSource: nil, action: { c, _ in c.dismiss(completion: { var replaceImpl: ((ViewController) -> Void)? - let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .noAds, action: { + 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) } @@ -700,10 +692,8 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState if messages.count == 1 { for media in messages[0].media { if let file = media as? TelegramMediaFile { - for attribute in file.attributes { - if case let .Sticker(_, packInfo, _) = attribute, packInfo != nil { - loadStickerSaveStatus = file.fileId - } + if file.isSticker { + loadStickerSaveStatus = file.fileId } if loadStickerSaveStatus == nil { loadCopyMediaResource = file.resource @@ -913,6 +903,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState messageActions = ChatAvailableMessageActions( options: messageActions.options.intersection([.deleteLocally, .deleteGlobally, .forward]), banAuthor: nil, + banAuthors: [], disableDelete: true, isCopyProtected: messageActions.isCopyProtected, setTag: false, @@ -1119,10 +1110,10 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState }, action: { _, f in let context = context var replaceImpl: ((ViewController) -> Void)? - let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .fasterDownload, action: { + 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) } @@ -1228,16 +1219,6 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } - for attribute in message.attributes { - if hasExpandedAudioTranscription, let attribute = attribute as? AudioTranscriptionMessageAttribute { - if !messageText.isEmpty { - messageText.append("\n") - } - messageText.append(attribute.text) - break - } - } - var isPoll = false if messageText.isEmpty { for media in message.media { @@ -2452,6 +2433,7 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer var optionsMap: [MessageId: ChatAvailableMessageActionOptions] = [:] var banPeer: Peer? + var banPeers: [Peer] = [] var hadPersonalIncoming = false var hadBanPeerId = false var disableDelete = false @@ -2566,25 +2548,35 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer if message.flags.contains(.Incoming) { optionsMap[id]!.insert(.report) } - if channel.hasPermission(.banMembers), case .group = channel.info { + if (channel.hasPermission(.banMembers) || channel.hasPermission(.deleteAllMessages)), case .group = channel.info { if message.flags.contains(.Incoming) { - if message.author is TelegramUser { - if !hadBanPeerId { - hadBanPeerId = true - banPeer = message.author - } else if banPeer?.id != message.author?.id { - banPeer = nil - } - } else if message.author is TelegramChannel { - if !hadBanPeerId { + if let author = message.author { + if author is TelegramUser { + if !hadBanPeerId { + hadBanPeerId = true + banPeer = author + } else if banPeer?.id != message.author?.id { + banPeer = nil + } + + if !banPeers.contains(where: { $0.id == author.id }) { + banPeers.append(author) + } + } else if author is TelegramChannel { + if !hadBanPeerId { + hadBanPeerId = true + banPeer = author + } else if banPeer?.id != message.author?.id { + banPeer = nil + } + + if !banPeers.contains(where: { $0.id == author.id }) { + banPeers.append(author) + } + } else { hadBanPeerId = true - banPeer = message.author - } else if banPeer?.id != message.author?.id { banPeer = nil } - } else { - hadBanPeerId = true - banPeer = nil } } else { hadBanPeerId = true @@ -2724,9 +2716,9 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer commonTags = nil } - return ChatAvailableMessageActions(options: reducedOptions, banAuthor: banPeer, disableDelete: disableDelete, isCopyProtected: isCopyProtected, setTag: setTag, editTags: commonTags ?? Set()) + return ChatAvailableMessageActions(options: reducedOptions, banAuthor: banPeer, banAuthors: banPeers, disableDelete: disableDelete, isCopyProtected: isCopyProtected, setTag: setTag, editTags: commonTags ?? Set()) } else { - return ChatAvailableMessageActions(options: [], banAuthor: nil, disableDelete: false, isCopyProtected: isCopyProtected, setTag: false, editTags: Set()) + return ChatAvailableMessageActions(options: [], banAuthor: nil, banAuthors: [], disableDelete: false, isCopyProtected: isCopyProtected, setTag: false, editTags: Set()) } } } diff --git a/submodules/TelegramUI/Sources/ChatThemeScreen.swift b/submodules/TelegramUI/Sources/ChatThemeScreen.swift index 454a84050f3..78043bd9aef 100644 --- a/submodules/TelegramUI/Sources/ChatThemeScreen.swift +++ b/submodules/TelegramUI/Sources/ChatThemeScreen.swift @@ -502,8 +502,8 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - super.animateInsertion(currentTimestamp, duration: duration, short: short) + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + super.animateInsertion(currentTimestamp, duration: duration, options: options) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } diff --git a/submodules/TelegramUI/Sources/ComposeController.swift b/submodules/TelegramUI/Sources/ComposeController.swift index 881560b309b..d62b23f8683 100644 --- a/submodules/TelegramUI/Sources/ComposeController.swift +++ b/submodules/TelegramUI/Sources/ComposeController.swift @@ -116,7 +116,7 @@ public class ComposeControllerImpl: ViewController, ComposeController { self?.activateSearch() } - self.contactsNode.contactListNode.openPeer = { [weak self] peer, _ in + self.contactsNode.contactListNode.openPeer = { [weak self] peer, _, _, _ in if case let .peer(peer, _, _) = peer { self?.openPeer(peerId: peer.id) } @@ -124,7 +124,7 @@ public class ComposeControllerImpl: ViewController, ComposeController { self.contactsNode.openCreateNewGroup = { [weak self] in if let strongSelf = self { - let controller = strongSelf.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: strongSelf.context, mode: .groupCreation, options: [], onlyWriteable: true)) + let controller = strongSelf.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: strongSelf.context, mode: .groupCreation, onlyWriteable: true)) (strongSelf.navigationController as? NavigationController)?.pushViewController(controller, completion: { [weak self] in if let strongSelf = self { strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true) diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift index a21c3faef0f..5b1f86bbe67 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift @@ -12,10 +12,11 @@ import AccountContext import AlertUI import PresentationDataUtils import ContactListUI -import CounterContollerTitleView +import CounterControllerTitleView import EditableTokenListNode import PremiumUI import UndoUI +import ContextUI private func peerTokenTitle(accountPeerId: PeerId, peer: Peer, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) -> String { if peer.id == accountPeerId { @@ -34,7 +35,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection private let isPeerEnabled: ((EnginePeer) -> Bool)? private let attemptDisabledItemSelection: ((EnginePeer, ChatListDisabledPeerReason) -> Void)? - private let titleView: CounterContollerTitleView + private let titleView: CounterControllerTitleView private var contactsNode: ContactMultiselectionControllerNode { return self.displayNode as! ContactMultiselectionControllerNode @@ -80,7 +81,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection private var limitsConfiguration: LimitsConfiguration? private var limitsConfigurationDisposable: Disposable? private var initialPeersDisposable: Disposable? - private let options: [ContactListAdditionalOption] + private let options: Signal<[ContactListAdditionalOption], NoError> private let filters: [ContactListFilter] private let onlyWriteable: Bool private let isGroupInvitation: Bool @@ -99,7 +100,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection self.limit = params.limit self.presentationData = params.updatedPresentationData?.initial ?? params.context.sharedContext.currentPresentationData.with { $0 } - self.titleView = CounterContollerTitleView(theme: self.presentationData.theme) + self.titleView = CounterControllerTitleView(theme: self.presentationData.theme) super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) @@ -249,7 +250,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection case let .chats(chatsNode): count = chatsNode.currentState.selectedPeerIds.count } - self.titleView.title = CounterContollerTitle(title: self.presentationData.strings.Compose_NewGroupTitle, counter: "\(count)/\(maxCount)") + self.titleView.title = CounterControllerTitle(title: self.presentationData.strings.Compose_NewGroupTitle, counter: "\(count)/\(maxCount)") if self.rightNavigationButton == nil { let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed)) self.rightNavigationButton = rightNavigationButton @@ -261,23 +262,23 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection if case let .contacts(contactsNode) = self.contactsNode.contentNode { count = contactsNode.selectionState?.selectedPeerIndices.count ?? 0 } - self.titleView.title = CounterContollerTitle(title: self.presentationData.strings.Premium_Gift_ContactSelection_Title, counter: "\(count)/\(maxCount)") + self.titleView.title = CounterControllerTitle(title: self.presentationData.strings.Premium_Gift_ContactSelection_Title, counter: "\(count)/\(maxCount)") case .requestedUsersSelection: let maxCount: Int32 = self.limit ?? 10 var count = 0 if case let .contacts(contactsNode) = self.contactsNode.contentNode { count = contactsNode.selectionState?.selectedPeerIndices.count ?? 0 } - self.titleView.title = CounterContollerTitle(title: self.presentationData.strings.RequestPeer_SelectUsers, counter: "\(count)/\(maxCount)") + self.titleView.title = CounterControllerTitle(title: self.presentationData.strings.RequestPeer_SelectUsers, counter: "\(count)/\(maxCount)") case .channelCreation: - self.titleView.title = CounterContollerTitle(title: self.presentationData.strings.GroupInfo_AddParticipantTitle, counter: "") + self.titleView.title = CounterControllerTitle(title: self.presentationData.strings.GroupInfo_AddParticipantTitle, counter: "") if self.rightNavigationButton == nil { let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed)) self.rightNavigationButton = rightNavigationButton self.navigationItem.rightBarButtonItem = self.rightNavigationButton } case .peerSelection: - self.titleView.title = CounterContollerTitle(title: self.presentationData.strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder, counter: "") + self.titleView.title = CounterControllerTitle(title: self.presentationData.strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder, counter: "") if self.rightNavigationButton == nil { let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed)) self.rightNavigationButton = rightNavigationButton @@ -285,7 +286,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection self.navigationItem.rightBarButtonItem = self.rightNavigationButton } case let .chatSelection(chatSelection): - self.titleView.title = CounterContollerTitle(title: chatSelection.title, counter: "") + self.titleView.title = CounterControllerTitle(title: chatSelection.title, counter: "") if self.rightNavigationButton == nil { let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed)) self.rightNavigationButton = rightNavigationButton @@ -415,6 +416,38 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection } } + self.contactsNode.openPeerMore = { [weak self] peer, node, gesture in + guard let self, case let .peer(peer, _, _) = peer, let node = node as? ContextReferenceContentNode else { + return + } + + let presentationData = self.presentationData + + var items: [ContextMenuItem] = [] + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Premium_Gift_ContactSelection_SendMessage, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MessageBubble"), color: theme.contextMenu.primaryColor) + }, iconPosition: .left, action: { [weak self] _, a in + a(.default) + + if let self { + self.params.sendMessage?(EnginePeer(peer)) + } + }))) + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Premium_Gift_ContactSelection_OpenProfile, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor) + }, iconPosition: .left, action: { [weak self] _, a in + a(.default) + + if let self { + self.params.openProfile?(EnginePeer(peer)) + } + }))) + + let contextController = ContextController(presentationData: presentationData, source: .reference(ContactContextReferenceContentSource(controller: self, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + self.present(contextController, in: .window(.root)) + } + self.contactsNode.openDisabledPeer = { [weak self] peer, reason in guard let self else { return @@ -507,13 +540,13 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection switch strongSelf.mode { case .groupCreation: let maxCount: Int32 = strongSelf.limitsConfiguration?.maxSupergroupMemberCount ?? 5000 - strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.Compose_NewGroupTitle, counter: "\(updatedCount)/\(maxCount)") + strongSelf.titleView.title = CounterControllerTitle(title: strongSelf.presentationData.strings.Compose_NewGroupTitle, counter: "\(updatedCount)/\(maxCount)") case .premiumGifting: let maxCount: Int32 = strongSelf.limit ?? 10 - strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.Premium_Gift_ContactSelection_Title, counter: "\(updatedCount)/\(maxCount)") + strongSelf.titleView.title = CounterControllerTitle(title: strongSelf.presentationData.strings.Premium_Gift_ContactSelection_Title, counter: "\(updatedCount)/\(maxCount)") case .requestedUsersSelection: let maxCount: Int32 = strongSelf.limit ?? 10 - strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.RequestPeer_SelectUsers, counter: "\(updatedCount)/\(maxCount)") + strongSelf.titleView.title = CounterControllerTitle(title: strongSelf.presentationData.strings.RequestPeer_SelectUsers, counter: "\(updatedCount)/\(maxCount)") case .peerSelection, .channelCreation, .chatSelection: break } @@ -765,3 +798,17 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection self._result.set(.single(.result(peerIds: peerIds, additionalOptionIds: additionalOptionIds))) } } + +private final class ContactContextReferenceContentSource: ContextReferenceContentSource { + private let controller: ViewController + private let sourceNode: ContextReferenceContentNode + + init(controller: ViewController, sourceNode: ContextReferenceContentNode) { + self.controller = controller + self.sourceNode = sourceNode + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds) + } +} diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift index 4929a3a7379..93863b99889 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift @@ -13,6 +13,7 @@ import AnimationCache import MultiAnimationRenderer import EditableTokenListNode import SolidRoundedButtonNode +import ContextUI private struct SearchResultEntry: Identifiable { let index: Int @@ -58,6 +59,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { var requestDeactivateSearch: (() -> Void)? var requestOpenPeerFromSearch: ((ContactListPeerId) -> Void)? var openPeer: ((ContactListPeer) -> Void)? + var openPeerMore: ((ContactListPeer, ASDisplayNode?, ContextGesture?) -> Void)? var openDisabledPeer: ((EnginePeer, ChatListDisabledPeerReason) -> Void)? var removeSelectedPeer: ((ContactListPeerId) -> Void)? var removeSelectedCategory: ((Int) -> Void)? @@ -80,7 +82,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { private let onlyWriteable: Bool private let isGroupInvitation: Bool - init(navigationBar: NavigationBar?, context: AccountContext, presentationData: PresentationData, mode: ContactMultiselectionControllerMode, isPeerEnabled: ((EnginePeer) -> Bool)?, attemptDisabledItemSelection: ((EnginePeer, ChatListDisabledPeerReason) -> Void)?, options: [ContactListAdditionalOption], filters: [ContactListFilter], onlyWriteable: Bool, isGroupInvitation: Bool, limit: Int32?, reachedSelectionLimit: ((Int32) -> Void)?, present: @escaping (ViewController, Any?) -> Void) { + init(navigationBar: NavigationBar?, context: AccountContext, presentationData: PresentationData, mode: ContactMultiselectionControllerMode, isPeerEnabled: ((EnginePeer) -> Bool)?, attemptDisabledItemSelection: ((EnginePeer, ChatListDisabledPeerReason) -> Void)?, options: Signal<[ContactListAdditionalOption], NoError>, filters: [ContactListFilter], onlyWriteable: Bool, isGroupInvitation: Bool, limit: Int32?, reachedSelectionLimit: ((Int32) -> Void)?, present: @escaping (ViewController, Any?) -> Void) { self.navigationBar = navigationBar self.context = context @@ -233,7 +235,13 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { } else { displayTopPeers = .none } - let contactListNode = ContactListNode(context: context, presentation: .single(.natural(options: options, includeChatList: includeChatList, topPeers: displayTopPeers)), filters: filters, onlyWriteable: onlyWriteable, isGroupInvitation: isGroupInvitation, selectionState: ContactListNodeGroupSelectionState()) + + let presentation: Signal = options + |> map { options in + return .natural(options: options, includeChatList: includeChatList, topPeers: displayTopPeers) + } + + let contactListNode = ContactListNode(context: context, presentation: presentation, filters: filters, onlyWriteable: onlyWriteable, isGroupInvitation: isGroupInvitation, selectionState: ContactListNodeGroupSelectionState()) self.contentNode = .contacts(contactListNode) if !selectedPeers.isEmpty { @@ -262,8 +270,12 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { switch self.contentNode { case let .contacts(contactsNode): - contactsNode.openPeer = { [weak self] peer, _ in - self?.openPeer?(peer) + contactsNode.openPeer = { [weak self] peer, action, sourceNode, gesture in + if case .more = action { + self?.openPeerMore?(peer, sourceNode, gesture) + } else { + self?.openPeer?(peer) + } } contactsNode.openDisabledPeer = { [weak self] peer, reason in guard let self else { @@ -362,7 +374,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { globalSearch: globalSearch, displaySavedMessages: displaySavedMessages ))), filters: filters, onlyWriteable: strongSelf.onlyWriteable, isGroupInvitation: strongSelf.isGroupInvitation, isPeerEnabled: strongSelf.isPeerEnabled, selectionState: selectionState, isSearch: true) - searchResultsNode.openPeer = { peer, _ in + searchResultsNode.openPeer = { peer, _, _, _ in self?.tokenListNode.setText("") self?.openPeer?(peer) } diff --git a/submodules/TelegramUI/Sources/ContactSelectionController.swift b/submodules/TelegramUI/Sources/ContactSelectionController.swift index 8fee5a2f58b..65cf9921a35 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionController.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionController.swift @@ -78,7 +78,11 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController var requestAttachmentMenuExpansion: () -> Void = {} var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } + public var parentController: () -> ViewController? = { + return nil + } var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in } + var updateTabBarVisibility: (Bool, ContainedViewLayoutTransition) -> Void = { _, _ in } var cancelPanGesture: () -> Void = { } var isContainerPanning: () -> Bool = { return false } var isContainerExpanded: () -> Bool = { return false } @@ -197,7 +201,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController self?.activateSearch() } - self.contactsNode.contactListNode.openPeer = { [weak self] peer, action in + self.contactsNode.contactListNode.openPeer = { [weak self] peer, action, _, _ in self?.openPeer(peer: peer, action: action) } diff --git a/submodules/TelegramUI/Sources/ConvertToWebP.swift b/submodules/TelegramUI/Sources/ConvertToWebP.swift index e90d5687601..c0b0fd95980 100644 --- a/submodules/TelegramUI/Sources/ConvertToWebP.swift +++ b/submodules/TelegramUI/Sources/ConvertToWebP.swift @@ -1,19 +1,14 @@ import UIKit import SwiftSignalKit -import LegacyComponents import Display import WebPBinding private func scaleImage(_ image: UIImage, size: CGSize, boundiingSize: CGSize) -> UIImage? { - if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { - let format = UIGraphicsImageRendererFormat() - format.scale = 1.0 - let renderer = UIGraphicsImageRenderer(size: size, format: format) - return renderer.image { _ in - image.draw(in: CGRect(origin: .zero, size: size)) - } - } else { - return TGScaleImageToPixelSize(image, size) + let format = UIGraphicsImageRendererFormat() + format.scale = 1.0 + let renderer = UIGraphicsImageRenderer(size: size, format: format) + return renderer.image { _ in + image.draw(in: CGRect(origin: .zero, size: size)) } } diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index 84124959287..6dfd49f5047 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -25,8 +25,8 @@ public struct ParsedSecureIdUrl { public let opaqueNonce: Data } -public func parseProxyUrl(_ url: URL) -> ProxyServerSettings? { - guard let proxy = parseProxyUrl(url.absoluteString) else { +public func parseProxyUrl(sharedContext: SharedAccountContext, url: URL) -> ProxyServerSettings? { + guard let proxy = parseProxyUrl(sharedContext: sharedContext, url: url.absoluteString) else { return nil } if let secret = proxy.secret, let _ = MTProxySecret.parseData(secret) { @@ -107,14 +107,14 @@ public func parseSecureIdUrl(_ url: URL) -> ParsedSecureIdUrl? { return nil } -public func parseConfirmationCodeUrl(_ url: URL) -> Int? { +public func parseConfirmationCodeUrl(sharedContext: SharedAccountContext, url: URL) -> Int? { if url.pathComponents.count == 3 && url.pathComponents[1].lowercased() == "login" { if let code = Int(url.pathComponents[2]) { return code } } if url.scheme == "tg" { - if let host = url.host, let query = url.query, let parsedUrl = parseInternalUrl(query: host + "?" + query) { + if let host = url.host, let query = url.query, let parsedUrl = parseInternalUrl(sharedContext: sharedContext, query: host + "?" + query) { switch parsedUrl { case let .confirmationCode(code): return code @@ -165,7 +165,7 @@ private func extractNicegramDeeplink(from link: String) -> String? { func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, url: String, forceExternal: Bool, presentationData: PresentationData, navigationController: NavigationController?, dismissInput: @escaping () -> Void) { // MARK: Nicegram if let nicegramDeeplink = extractNicegramDeeplink(from: url) { - openExternalUrlImpl(context: context, urlContext: urlContext, url: nicegramDeeplink, forceExternal: forceExternal, presentationData: presentationData, navigationController: navigationController, dismissInput: dismissInput) + openExternalUrlImpl(context: context, urlContext: urlContext, url: nicegramDeeplink, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: dismissInput) return } @@ -206,7 +206,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur return } - guard let parsedUrl = parsedUrlValue else { + guard var parsedUrl = parsedUrlValue else { return } @@ -287,6 +287,20 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur |> deliverOnMainQueue).startStandalone(next: handleResolvedUrl) } + if context.sharedContext.immediateExperimentalUISettings.browserExperiment { + if let scheme = parsedUrl.scheme, (scheme == "tg" || scheme == context.sharedContext.applicationBindings.appSpecificScheme) { + if parsedUrl.host == "ipfs" { + if let value = URL(string: "ipfs:/" + parsedUrl.path) { + parsedUrl = value + } + } + } else if let scheme = parsedUrl.scheme, scheme == "https", parsedUrl.host == "t.me", parsedUrl.path.hasPrefix("/ipfs/") { + if let value = URL(string: "ipfs://" + String(parsedUrl.path[parsedUrl.path.index(parsedUrl.path.startIndex, offsetBy: "/ipfs/".count)...])) { + parsedUrl = value + } + } + } + if let scheme = parsedUrl.scheme, (scheme == "tg" || scheme == context.sharedContext.applicationBindings.appSpecificScheme) { var convertedUrl: String? if let query = parsedUrl.query { diff --git a/submodules/TelegramUI/Sources/PollResultsController.swift b/submodules/TelegramUI/Sources/PollResultsController.swift index a634c29e409..7b35b3fd0fc 100644 --- a/submodules/TelegramUI/Sources/PollResultsController.swift +++ b/submodules/TelegramUI/Sources/PollResultsController.swift @@ -14,13 +14,15 @@ private let collapsedInitialLimit: Int = 10 private final class PollResultsControllerArguments { let context: AccountContext + let message: EngineMessage let collapseOption: (Data) -> Void let expandOption: (Data) -> Void let openPeer: (EngineRenderedPeer) -> Void let expandSolution: () -> Void - init(context: AccountContext, collapseOption: @escaping (Data) -> Void, expandOption: @escaping (Data) -> Void, openPeer: @escaping (EngineRenderedPeer) -> Void, expandSolution: @escaping () -> Void) { + init(context: AccountContext, message: EngineMessage, collapseOption: @escaping (Data) -> Void, expandOption: @escaping (Data) -> Void, openPeer: @escaping (EngineRenderedPeer) -> Void, expandSolution: @escaping () -> Void) { self.context = context + self.message = message self.collapseOption = collapseOption self.expandOption = expandOption self.openPeer = openPeer @@ -66,17 +68,17 @@ private enum PollResultsItemTag: ItemListItemTag, Equatable { } private enum PollResultsEntry: ItemListNodeEntry { - case text(String) - case optionPeer(optionId: Int, index: Int, peer: EngineRenderedPeer, optionText: String, optionAdditionalText: String, optionCount: Int32, optionExpanded: Bool, opaqueIdentifier: Data, shimmeringAlternation: Int?, isFirstInOption: Bool) + case text(String, [MessageTextEntity]) + case optionPeer(optionId: Int, index: Int, peer: EngineRenderedPeer, optionText: String, optionTextEntities: [MessageTextEntity], optionAdditionalText: String, optionCount: Int32, optionExpanded: Bool, opaqueIdentifier: Data, shimmeringAlternation: Int?, isFirstInOption: Bool) case optionExpand(optionId: Int, opaqueIdentifier: Data, text: String, enabled: Bool) case solutionHeader(String) - case solutionText(String) + case solutionText(String, [MessageTextEntity]) var section: ItemListSectionId { switch self { case .text: return PollResultsSection.text.rawValue - case let .optionPeer(optionId, _, _, _, _, _, _, _, _, _): + case let .optionPeer(optionId, _, _, _, _, _, _, _, _, _, _): return PollResultsSection.option(optionId).rawValue case let .optionExpand(optionId, _, _, _): return PollResultsSection.option(optionId).rawValue @@ -89,7 +91,7 @@ private enum PollResultsEntry: ItemListNodeEntry { switch self { case .text: return .text - case let .optionPeer(optionId, index, _, _, _, _, _, _, _, _): + case let .optionPeer(optionId, index, _, _, _, _, _, _, _, _, _): return .optionPeer(optionId, index) case let .optionExpand(optionId, _, _, _): return .optionExpand(optionId) @@ -129,7 +131,7 @@ private enum PollResultsEntry: ItemListNodeEntry { default: return true } - case let .optionPeer(lhsOptionId, lhsIndex, _, _, _, _, _, _, _, _): + case let .optionPeer(lhsOptionId, lhsIndex, _, _, _, _, _, _, _, _, _): switch rhs { case .text: return false @@ -137,7 +139,7 @@ private enum PollResultsEntry: ItemListNodeEntry { return false case .solutionText: return false - case let .optionPeer(rhsOptionId, rhsIndex, _, _, _, _, _, _, _, _): + case let .optionPeer(rhsOptionId, rhsIndex, _, _, _, _, _, _, _, _, _): if lhsOptionId == rhsOptionId { return lhsIndex < rhsIndex } else { @@ -158,7 +160,7 @@ private enum PollResultsEntry: ItemListNodeEntry { return false case .solutionText: return false - case let .optionPeer(rhsOptionId, _, _, _, _, _, _, _, _, _): + case let .optionPeer(rhsOptionId, _, _, _, _, _, _, _, _, _, _): if lhsOptionId == rhsOptionId { return false } else { @@ -177,14 +179,93 @@ private enum PollResultsEntry: ItemListNodeEntry { func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! PollResultsControllerArguments switch self { - case let .text(text): - return ItemListTextItem(presentationData: presentationData, text: .large(text), sectionId: self.section) + case let .text(text, entities): + let font = Font.semibold(presentationData.fontSize.itemListBaseFontSize) + var entityFiles: [EngineMedia.Id: TelegramMediaFile] = [:] + for (id, media) in arguments.message.associatedMedia { + if let file = media as? TelegramMediaFile { + entityFiles[id] = file + } + } + let attributedText = stringWithAppliedEntities( + text, + entities: entities.filter { entity in + if case .CustomEmoji = entity.type { + return true + } else { + return false + } + }, + baseColor: presentationData.theme.list.freeTextColor, + linkColor: presentationData.theme.list.freeTextColor, + baseQuoteTintColor: nil, + baseQuoteSecondaryTintColor: nil, + baseQuoteTertiaryTintColor: nil, + codeBlockTitleColor: nil, + codeBlockAccentColor: nil, + codeBlockBackgroundColor: nil, + baseFont: font, + linkFont: font, + boldFont: font, + italicFont: font, + boldItalicFont: font, + fixedFont: font, + blockQuoteFont: font, + underlineLinks: false, + external: false, + message: arguments.message._asMessage(), + entityFiles: entityFiles, + adjustQuoteFontSize: false, + cachedMessageSyntaxHighlight: nil + ) + return ItemListTextItem(presentationData: presentationData, text: .custom(context: arguments.context, string: attributedText), sectionId: self.section) case let .solutionHeader(text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .solutionText(text): + case let .solutionText(text, entities): + let _ = entities + //TODO:localize return ItemListMultilineTextItem(presentationData: presentationData, text: text, enabledEntityTypes: [], sectionId: self.section, style: .blocks) - case let .optionPeer(optionId, _, peer, optionText, optionAdditionalText, optionCount, optionExpanded, opaqueIdentifier, shimmeringAlternation, isFirstInOption): - let header = ItemListPeerItemHeader(theme: presentationData.theme, strings: presentationData.strings, text: optionText, additionalText: optionAdditionalText, actionTitle: optionExpanded ? presentationData.strings.PollResults_Collapse : presentationData.strings.MessagePoll_VotedCount(optionCount), id: Int64(optionId), action: optionExpanded ? { + case let .optionPeer(optionId, _, peer, optionText, optionTextEntities, optionAdditionalText, optionCount, optionExpanded, opaqueIdentifier, shimmeringAlternation, isFirstInOption): + let font = Font.regular(13.0) + var entityFiles: [EngineMedia.Id: TelegramMediaFile] = [:] + for (id, media) in arguments.message.associatedMedia { + if let file = media as? TelegramMediaFile { + entityFiles[id] = file + } + } + let attributedText = stringWithAppliedEntities( + optionText, + entities: optionTextEntities.filter { entity in + if case .CustomEmoji = entity.type { + return true + } else { + return false + } + }, + baseColor: presentationData.theme.list.freeTextColor, + linkColor: presentationData.theme.list.freeTextColor, + baseQuoteTintColor: nil, + baseQuoteSecondaryTintColor: nil, + baseQuoteTertiaryTintColor: nil, + codeBlockTitleColor: nil, + codeBlockAccentColor: nil, + codeBlockBackgroundColor: nil, + baseFont: font, + linkFont: font, + boldFont: font, + italicFont: font, + boldItalicFont: font, + fixedFont: font, + blockQuoteFont: font, + underlineLinks: false, + external: false, + message: arguments.message._asMessage(), + entityFiles: entityFiles, + adjustQuoteFontSize: false, + cachedMessageSyntaxHighlight: nil + ) + + let header = ItemListPeerItemHeader(theme: presentationData.theme, strings: presentationData.strings, context: arguments.context, text: attributedText, additionalText: optionAdditionalText, actionTitle: optionExpanded ? presentationData.strings.PollResults_Collapse : presentationData.strings.MessagePoll_VotedCount(optionCount), id: Int64(optionId), action: optionExpanded ? { arguments.collapseOption(opaqueIdentifier) } : nil) return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: .firstLast, context: arguments.context, peer: peer.peers[peer.peerId]!, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: shimmeringAlternation == nil, sectionId: self.section, action: { @@ -205,7 +286,7 @@ private struct PollResultsControllerState: Equatable { var isSolutionExpanded: Bool = false } -private func pollResultsControllerEntries(presentationData: PresentationData, poll: TelegramMediaPoll, state: PollResultsControllerState, resultsState: PollResultsState) -> [PollResultsEntry] { +private func pollResultsControllerEntries(presentationData: PresentationData, message: EngineMessage, poll: TelegramMediaPoll, state: PollResultsControllerState, resultsState: PollResultsState) -> [PollResultsEntry] { var entries: [PollResultsEntry] = [] var isEmpty = false @@ -216,7 +297,7 @@ private func pollResultsControllerEntries(presentationData: PresentationData, po } } - entries.append(.text(poll.text)) + entries.append(.text(poll.text, poll.textEntities)) var optionVoterCount: [Int: Int32] = [:] let totalVoterCount = poll.results.totalVoters ?? 0 @@ -241,6 +322,7 @@ private func pollResultsControllerEntries(presentationData: PresentationData, po let percentage = optionPercentage.count > i ? optionPercentage[i] : 0 let option = poll.options[i] let optionTextHeader = option.text.uppercased() + let optionTextHeaderEntities = option.entities let optionAdditionalTextHeader = " — \(percentage)%" if isEmpty { if let voterCount = optionVoterCount[i], voterCount != 0 { @@ -253,7 +335,7 @@ private func pollResultsControllerEntries(presentationData: PresentationData, po for peerIndex in 0 ..< displayCount { let fakeUser = TelegramUser(id: EnginePeer.Id(namespace: .max, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) let peer = EngineRenderedPeer(peer: EnginePeer(fakeUser)) - entries.append(.optionPeer(optionId: i, index: peerIndex, peer: peer, optionText: optionTextHeader, optionAdditionalText: optionAdditionalTextHeader, optionCount: voterCount, optionExpanded: false, opaqueIdentifier: option.opaqueIdentifier, shimmeringAlternation: peerIndex % 2, isFirstInOption: peerIndex == 0)) + entries.append(.optionPeer(optionId: i, index: peerIndex, peer: peer, optionText: optionTextHeader, optionTextEntities: optionTextHeaderEntities, optionAdditionalText: optionAdditionalTextHeader, optionCount: voterCount, optionExpanded: false, opaqueIdentifier: option.opaqueIdentifier, shimmeringAlternation: peerIndex % 2, isFirstInOption: peerIndex == 0)) } if displayCount < Int(voterCount) { let remainingCount = Int(voterCount) - displayCount @@ -295,7 +377,7 @@ private func pollResultsControllerEntries(presentationData: PresentationData, po if peerIndex >= displayCount { break inner } - entries.append(.optionPeer(optionId: i, index: peerIndex, peer: EngineRenderedPeer(peer), optionText: optionTextHeader, optionAdditionalText: optionAdditionalTextHeader, optionCount: Int32(count), optionExpanded: optionExpandedAtCount != nil, opaqueIdentifier: option.opaqueIdentifier, shimmeringAlternation: nil, isFirstInOption: peerIndex == 0)) + entries.append(.optionPeer(optionId: i, index: peerIndex, peer: EngineRenderedPeer(peer), optionText: optionTextHeader, optionTextEntities: optionTextHeaderEntities, optionAdditionalText: optionAdditionalTextHeader, optionCount: Int32(count), optionExpanded: optionExpandedAtCount != nil, opaqueIdentifier: option.opaqueIdentifier, shimmeringAlternation: nil, isFirstInOption: peerIndex == 0)) peerIndex += 1 } @@ -310,7 +392,7 @@ private func pollResultsControllerEntries(presentationData: PresentationData, po return entries } -public func pollResultsController(context: AccountContext, messageId: EngineMessage.Id, poll: TelegramMediaPoll, focusOnOptionWithOpaqueIdentifier: Data? = nil) -> ViewController { +public func pollResultsController(context: AccountContext, messageId: EngineMessage.Id, message: EngineMessage, poll: TelegramMediaPoll, focusOnOptionWithOpaqueIdentifier: Data? = nil) -> ViewController { let statePromise = ValuePromise(PollResultsControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: PollResultsControllerState()) let updateState: ((PollResultsControllerState) -> PollResultsControllerState) -> Void = { f in @@ -324,7 +406,7 @@ public func pollResultsController(context: AccountContext, messageId: EngineMess let resultsContext = context.engine.messages.pollResults(messageId: messageId, poll: poll) - let arguments = PollResultsControllerArguments(context: context, + let arguments = PollResultsControllerArguments(context: context, message: message, collapseOption: { optionId in updateState { state in var state = state @@ -384,14 +466,14 @@ public func pollResultsController(context: AccountContext, messageId: EngineMess totalVoters = totalVotersValue } - let entries = pollResultsControllerEntries(presentationData: presentationData, poll: poll, state: state, resultsState: resultsState) + let entries = pollResultsControllerEntries(presentationData: presentationData, message: message, poll: poll, state: state, resultsState: resultsState) var initialScrollToItem: ListViewScrollToItem? if let focusOnOptionWithOpaqueIdentifier = focusOnOptionWithOpaqueIdentifier, previousWasEmptyValue == nil { var isFirstOption = true loop: for i in 0 ..< entries.count { switch entries[i] { - case let .optionPeer(_, _, _, _, _, _, _, opaqueIdentifier, _, _): + case let .optionPeer(_, _, _, _, _, _, _, _, opaqueIdentifier, _, _): if opaqueIdentifier == focusOnOptionWithOpaqueIdentifier { if !isFirstOption { initialScrollToItem = ListViewScrollToItem(index: i, position: .top(0.0), animated: false, curve: .Default(duration: nil), directionHint: .Down) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 514209a88bf..488d4e27490 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -64,6 +64,7 @@ import BusinessIntroSetupScreen import TelegramNotices import BotSettingsScreen import CameraScreen +import BirthdayPickerScreen import NGCore import NGData @@ -2188,13 +2189,17 @@ 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 return controller } - public func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, action: @escaping () -> Void) -> ViewController { + public func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, forceDark: Bool, action: @escaping () -> Void, dismissed: (() -> Void)?) -> ViewController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + var buttonText: String = presentationData.strings.Common_OK let mappedSubject: PremiumDemoScreen.Subject switch subject { case .doubleLimits: @@ -2227,6 +2232,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSubject = .translation case .stories: mappedSubject = .stories + buttonText = presentationData.strings.Story_PremiumUpgradeStoriesButton case .colors: mappedSubject = .colors case .wallpapers: @@ -2239,10 +2245,24 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSubject = .messagePrivacy case .folderTags: mappedSubject = .folderTags + case .business: + mappedSubject = .business + buttonText = presentationData.strings.Chat_EmptyStateIntroFooterPremiumActionButton default: mappedSubject = .doubleLimits } - return PremiumDemoScreen(context: context, subject: mappedSubject, action: action) + + switch mappedSubject { + case .stories, .business, .doubleLimits: + let controller = PremiumLimitsListScreen(context: context, subject: mappedSubject, source: .other, order: [mappedSubject.perk], buttonText: buttonText, isPremium: false, forceDark: forceDark) + controller.action = action + if let dismissed { + controller.disposed = dismissed + } + return controller + default: + return PremiumDemoScreen(context: context, subject: mappedSubject, action: action) + } } public func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, forceDark: Bool, cancel: @escaping () -> Void, action: @escaping () -> Bool) -> ViewController { @@ -2284,7 +2304,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { let limit: Int32 = 10 var reachedLimitImpl: ((Int32) -> Void)? - + var presentBirthdayPickerImpl: (() -> Void)? let mode: ContactMultiselectionControllerMode var currentBirthdays: [EnginePeer.Id: TelegramBirthday]? if case let .chatList(birthdays) = source, let birthdays, !birthdays.isEmpty { @@ -2297,15 +2317,55 @@ public final class SharedAccountContextImpl: SharedAccountContext { mode = .premiumGifting(birthdays: nil, selectToday: false) } - let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: mode, options: [], isPeerEnabled: { peer in - if case let .user(user) = peer, user.botInfo == nil && !peer.isService && !user.flags.contains(.isSupport) { - return true - } else { - return false + let contactOptions: Signal<[ContactListAdditionalOption], NoError> + if currentBirthdays != nil || "".isEmpty { + contactOptions = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Birthday(id: context.account.peerId)) + |> map { birthday in + if birthday == nil { + return [ContactListAdditionalOption( + title: presentationData.strings.Premium_Gift_ContactSelection_AddBirthday, + icon: .generic(UIImage(bundleImageName: "Contact List/AddBirthdayIcon")!), + action: { + presentBirthdayPickerImpl?() + }, + clearHighlightAutomatically: true + )] + } else { + return [] + } } - }, limit: limit, reachedLimit: { limit in - reachedLimitImpl?(limit) - })) + |> deliverOnMainQueue + } else { + contactOptions = .single([]) + } + + var openProfileImpl: ((EnginePeer) -> Void)? + var sendMessageImpl: ((EnginePeer) -> Void)? + + let controller = context.sharedContext.makeContactMultiselectionController( + ContactMultiselectionControllerParams( + context: context, + mode: mode, + options: contactOptions, + isPeerEnabled: { peer in + if case let .user(user) = peer, user.botInfo == nil && !peer.isService && !user.flags.contains(.isSupport) { + return true + } else { + return false + } + }, + limit: limit, + reachedLimit: { limit in + reachedLimitImpl?(limit) + }, + openProfile: { peer in + openProfileImpl?(peer) + }, + sendMessage: { peer in + sendMessageImpl?(peer) + } + ) + ) reachedLimitImpl = { [weak controller] limit in guard let controller else { @@ -2360,6 +2420,63 @@ public final class SharedAccountContextImpl: SharedAccountContext { controller.push(giftController) }) + sendMessageImpl = { [weak self, weak controller] peer in + guard let self, let controller, let navigationController = controller.navigationController as? NavigationController else { + return + } + self.navigateToChatController( + NavigateToChatControllerParams( + navigationController: navigationController, + context: context, + chatLocation: .peer(peer) + ) + ) + } + + openProfileImpl = { [weak self, weak controller] peer in + guard let self, let controller else { + return + } + if let infoController = self.makePeerInfoController( + context: context, + updatedPresentationData: nil, + peer: peer._asPeer(), + mode: .generic, + avatarInitiallyExpanded: true, + fromChat: false, + requestsContext: nil + ) { + controller.replace(with: infoController) + } + } + + presentBirthdayPickerImpl = { [weak controller] in + guard let controller else { + return + } + let _ = context.engine.notices.dismissServerProvidedSuggestion(suggestion: .setupBirthday).startStandalone() + + let settingsPromise: Promise + if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface, let current = rootController.getPrivacySettings() { + settingsPromise = current + } else { + settingsPromise = Promise() + settingsPromise.set(.single(nil) |> then(context.engine.privacy.requestAccountPrivacySettings() |> map(Optional.init))) + } + let birthdayController = BirthdayPickerScreen(context: context, settings: settingsPromise.get(), openSettings: { + context.sharedContext.makeBirthdayPrivacyController(context: context, settings: settingsPromise, openedFromBirthdayScreen: true, present: { [weak controller] c in + controller?.push(c) + }) + }, completion: { [weak controller] value in + let _ = context.engine.accountData.updateBirthday(birthday: value).startStandalone() + + controller?.present(UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: nil, text: presentationData.strings.Birthday_Added, cancel: nil, destructive: false), elevatedLayout: false, action: { _ in + return true + }), in: .current) + }) + controller.push(birthdayController) + } + return controller } @@ -2589,7 +2706,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { } public func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController { - return proxySettingsController(accountManager: sharedContext.accountManager, postbox: account.postbox, network: account.network, mode: .modal, presentationData: sharedContext.currentPresentationData.with { $0 }, updatedPresentationData: sharedContext.presentationData) + return proxySettingsController(accountManager: sharedContext.accountManager, sharedContext: sharedContext, postbox: account.postbox, network: account.network, mode: .modal, presentationData: sharedContext.currentPresentationData.with { $0 }, updatedPresentationData: sharedContext.presentationData) } public func makeInstalledStickerPacksController(context: AccountContext, mode: InstalledStickerPacksControllerMode, forceTheme: PresentationTheme?) -> ViewController { diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index ae3d01d8f29..0a78bbd80b2 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -427,6 +427,23 @@ public final class TelegramRootController: NavigationController, TelegramRootCon presentedLegacyShortcutCamera(context: self.context, saveCapturedMedia: false, saveEditedPhotos: false, mediaGrouping: true, parentController: controller) } + public func openAppIcon() { + guard let rootTabController = self.rootTabController else { + return + } + + self.popToRoot(animated: false) + + if let index = rootTabController.controllers.firstIndex(where: { $0 is PeerInfoScreenImpl }) { + rootTabController.selectedIndex = index + } + + let themeController = themeSettingsController(context: self.context, focusOnItemTag: .icon) + var controllers: [UIViewController] = Array(self.viewControllers.prefix(1)) + controllers.append(themeController) + self.setViewControllers(controllers, animated: true) + } + @discardableResult public func openStoryCamera(customTarget: EnginePeer.Id?, transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? { guard let controller = self.viewControllers.last as? ViewController else { diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index 75383808cf2..540098b578b 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -56,6 +56,7 @@ public struct ExperimentalUISettings: Codable, Equatable { public var dustEffect: Bool public var callV2: Bool public var allowWebViewInspection: Bool + public var disableReloginTokens: Bool public static var defaultSettings: ExperimentalUISettings { return ExperimentalUISettings( @@ -90,7 +91,8 @@ public struct ExperimentalUISettings: Codable, Equatable { crashOnMemoryPressure: false, dustEffect: false, callV2: false, - allowWebViewInspection: false + allowWebViewInspection: false, + disableReloginTokens: false ) } @@ -125,7 +127,8 @@ public struct ExperimentalUISettings: Codable, Equatable { crashOnMemoryPressure: Bool, dustEffect: Bool, callV2: Bool, - allowWebViewInspection: Bool + allowWebViewInspection: Bool, + disableReloginTokens: Bool ) { self.keepChatNavigationStack = keepChatNavigationStack self.skipReadHistory = skipReadHistory @@ -158,6 +161,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.dustEffect = dustEffect self.callV2 = callV2 self.allowWebViewInspection = allowWebViewInspection + self.disableReloginTokens = disableReloginTokens } public init(from decoder: Decoder) throws { @@ -194,6 +198,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.dustEffect = try container.decodeIfPresent(Bool.self, forKey: "dustEffect") ?? false self.callV2 = try container.decodeIfPresent(Bool.self, forKey: "callV2") ?? false self.allowWebViewInspection = try container.decodeIfPresent(Bool.self, forKey: "allowWebViewInspection") ?? false + self.disableReloginTokens = try container.decodeIfPresent(Bool.self, forKey: "disableReloginTokens") ?? false } public func encode(to encoder: Encoder) throws { @@ -230,6 +235,7 @@ public struct ExperimentalUISettings: Codable, Equatable { try container.encode(self.dustEffect, forKey: "dustEffect") try container.encode(self.callV2, forKey: "callV2") try container.encode(self.allowWebViewInspection, forKey: "allowWebViewInspection") + try container.encode(self.disableReloginTokens, forKey: "disableReloginTokens") } } diff --git a/submodules/TelegramUpdateUI/Sources/UpdateInfoItem.swift b/submodules/TelegramUpdateUI/Sources/UpdateInfoItem.swift index 8482927fc72..ff3135a8ad3 100644 --- a/submodules/TelegramUpdateUI/Sources/UpdateInfoItem.swift +++ b/submodules/TelegramUpdateUI/Sources/UpdateInfoItem.swift @@ -362,7 +362,7 @@ class UpdateInfoItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index 5fa784ff84a..b4036b80078 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -80,6 +80,7 @@ public struct ChatTextFontAttributes: OptionSet, Hashable, Sequence { public static let italic = ChatTextFontAttributes(rawValue: 1 << 1) public static let monospace = ChatTextFontAttributes(rawValue: 1 << 2) public static let blockQuote = ChatTextFontAttributes(rawValue: 1 << 3) + public static let smaller = ChatTextFontAttributes(rawValue: 1 << 4) public func makeIterator() -> AnyIterator { var index = 0 diff --git a/submodules/TextFormat/Sources/GenerateTextEntities.swift b/submodules/TextFormat/Sources/GenerateTextEntities.swift index a97df3aef1c..bb7f39b3a63 100644 --- a/submodules/TextFormat/Sources/GenerateTextEntities.swift +++ b/submodules/TextFormat/Sources/GenerateTextEntities.swift @@ -59,6 +59,7 @@ private let validTimecodePreviousSet: CharacterSet = { public struct ApplicationSpecificEntityType { public static let Timecode: Int32 = 1 + public static let Button: Int32 = 2 } private enum CurrentEntityType { diff --git a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift index a3949917e95..6cf40ed8185 100644 --- a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift +++ b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift @@ -253,6 +253,9 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti if let time = parseTimecodeString(text) { string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Timecode), value: TelegramTimecode(time: time, text: text), range: range) } + } else if type == ApplicationSpecificEntityType.Button { + string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Button), value: true as NSNumber, range: range) + addFontAttributes(range, .smaller) } case let .CustomEmoji(_, fileId): let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId) @@ -284,6 +287,8 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti font = italicFont } else if fontAttributes == [.monospace] { font = fixedFont + } else if fontAttributes == [.smaller] { + font = baseFont.withSize(floor(baseFont.pointSize * 0.9)) } else { font = baseFont } diff --git a/submodules/TextFormat/Sources/TelegramAttributes.swift b/submodules/TextFormat/Sources/TelegramAttributes.swift index 1f75c378d3a..babf5c99314 100644 --- a/submodules/TextFormat/Sources/TelegramAttributes.swift +++ b/submodules/TextFormat/Sources/TelegramAttributes.swift @@ -43,4 +43,5 @@ public struct TelegramTextAttributes { public static let Pre = "TelegramPre" public static let Spoiler = "TelegramSpoiler" public static let Code = "TelegramCode" + public static let Button = "TelegramButton" } diff --git a/submodules/TooltipUI/Sources/TooltipScreen.swift b/submodules/TooltipUI/Sources/TooltipScreen.swift index 7d0975080ac..330aa3ea3b8 100644 --- a/submodules/TooltipUI/Sources/TooltipScreen.swift +++ b/submodules/TooltipUI/Sources/TooltipScreen.swift @@ -601,7 +601,16 @@ private final class TooltipScreenNode: ViewControllerTracingNode { } let textSize: CGSize - if case .attributedString = self.text, let context = self.context { + + var isTextWithEntities = false + switch self.text { + case .attributedString, .entities: + isTextWithEntities = true + default: + break + } + + if isTextWithEntities, let context = self.context { textSize = self.textView.update( transition: .immediate, component: AnyComponent(MultilineTextWithEntitiesComponent( diff --git a/submodules/TranslateUI/Sources/LocalizationListItem.swift b/submodules/TranslateUI/Sources/LocalizationListItem.swift index 4ea85cc9d7e..1ee42bf8e3d 100644 --- a/submodules/TranslateUI/Sources/LocalizationListItem.swift +++ b/submodules/TranslateUI/Sources/LocalizationListItem.swift @@ -453,7 +453,7 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index a98c33ba185..e55b397eecc 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -103,6 +103,7 @@ public enum ParsedInternalUrl { case chatFolder(slug: String) case premiumGiftCode(slug: String) case messageLink(slug: String) + case externalUrl(url: String) } private enum ParsedUrl { @@ -110,7 +111,7 @@ private enum ParsedUrl { case internalUrl(ParsedInternalUrl) } -public func parseInternalUrl(query: String) -> ParsedInternalUrl? { +public func parseInternalUrl(sharedContext: SharedAccountContext, query: String) -> ParsedInternalUrl? { var query = query if query.hasPrefix("s/") { query = String(query[query.index(query.startIndex, offsetBy: 2)...]) @@ -129,6 +130,12 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { if !pathComponents.isEmpty && !pathComponents[0].isEmpty { let peerName: String = pathComponents[0] + if sharedContext.immediateExperimentalUISettings.browserExperiment { + if query.hasPrefix("ipfs/") { + return .externalUrl(url: "ipfs://" + String(query[query.index(query.startIndex, offsetBy: "ipfs/".count)...])) + } + } + if pathComponents[0].hasPrefix("+") || pathComponents[0].hasPrefix("%20") { let component = pathComponents[0].replacingOccurrences(of: "%20", with: "+") if component.rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789+").inverted) == nil { @@ -1016,7 +1023,8 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) return .single(.result(.messageLink(link: result))) } }) - + case let .externalUrl(url): + return .single(.result(.externalUrl(url))) } } @@ -1046,20 +1054,20 @@ public func isTelegraPhLink(_ url: String) -> Bool { return false } -public func parseProxyUrl(_ url: String) -> (host: String, port: Int32, username: String?, password: String?, secret: Data?)? { +public func parseProxyUrl(sharedContext: SharedAccountContext, url: String) -> (host: String, port: Int32, username: String?, password: String?, secret: Data?)? { let schemes = ["http://", "https://", ""] for basePath in baseTelegramMePaths { for scheme in schemes { let basePrefix = scheme + basePath + "/" if url.lowercased().hasPrefix(basePrefix) { - if let internalUrl = parseInternalUrl(query: String(url[basePrefix.endIndex...])), case let .proxy(host, port, username, password, secret) = internalUrl { + if let internalUrl = parseInternalUrl(sharedContext: sharedContext, query: String(url[basePrefix.endIndex...])), case let .proxy(host, port, username, password, secret) = internalUrl { return (host, port, username, password, secret) } } } } if let parsedUrl = URL(string: url), parsedUrl.scheme == "tg", let host = parsedUrl.host, let query = parsedUrl.query { - if let internalUrl = parseInternalUrl(query: host + "?" + query), case let .proxy(host, port, username, password, secret) = internalUrl { + if let internalUrl = parseInternalUrl(sharedContext: sharedContext, query: host + "?" + query), case let .proxy(host, port, username, password, secret) = internalUrl { return (host, port, username, password, secret) } } @@ -1067,20 +1075,20 @@ public func parseProxyUrl(_ url: String) -> (host: String, port: Int32, username return nil } -public func parseStickerPackUrl(_ url: String) -> String? { +public func parseStickerPackUrl(sharedContext: SharedAccountContext, url: String) -> String? { let schemes = ["http://", "https://", ""] for basePath in baseTelegramMePaths { for scheme in schemes { let basePrefix = scheme + basePath + "/" if url.lowercased().hasPrefix(basePrefix) { - if let internalUrl = parseInternalUrl(query: String(url[basePrefix.endIndex...])), case let .stickerPack(name, _) = internalUrl { + if let internalUrl = parseInternalUrl(sharedContext: sharedContext, query: String(url[basePrefix.endIndex...])), case let .stickerPack(name, _) = internalUrl { return name } } } } if let parsedUrl = URL(string: url), parsedUrl.scheme == "tg", let host = parsedUrl.host, let query = parsedUrl.query { - if let internalUrl = parseInternalUrl(query: host + "?" + query), case let .stickerPack(name, _) = internalUrl { + if let internalUrl = parseInternalUrl(sharedContext: sharedContext, query: host + "?" + query), case let .stickerPack(name, _) = internalUrl { return name } } @@ -1088,20 +1096,20 @@ public func parseStickerPackUrl(_ url: String) -> String? { return nil } -public func parseWallpaperUrl(_ url: String) -> WallpaperUrlParameter? { +public func parseWallpaperUrl(sharedContext: SharedAccountContext, url: String) -> WallpaperUrlParameter? { let schemes = ["http://", "https://", ""] for basePath in baseTelegramMePaths { for scheme in schemes { let basePrefix = scheme + basePath + "/" if url.lowercased().hasPrefix(basePrefix) { - if let internalUrl = parseInternalUrl(query: String(url[basePrefix.endIndex...])), case let .wallpaper(wallpaper) = internalUrl { + if let internalUrl = parseInternalUrl(sharedContext: sharedContext, query: String(url[basePrefix.endIndex...])), case let .wallpaper(wallpaper) = internalUrl { return wallpaper } } } } if let parsedUrl = URL(string: url), parsedUrl.scheme == "tg", let host = parsedUrl.host, let query = parsedUrl.query { - if let internalUrl = parseInternalUrl(query: host + "?" + query), case let .wallpaper(wallpaper) = internalUrl { + if let internalUrl = parseInternalUrl(sharedContext: sharedContext, query: host + "?" + query), case let .wallpaper(wallpaper) = internalUrl { return wallpaper } } @@ -1109,6 +1117,27 @@ public func parseWallpaperUrl(_ url: String) -> WallpaperUrlParameter? { return nil } +public func parseAdUrl(sharedContext: SharedAccountContext, url: String) -> ParsedInternalUrl? { + let schemes = ["http://", "https://", ""] + for basePath in baseTelegramMePaths { + for scheme in schemes { + let basePrefix = scheme + basePath + "/" + if url.lowercased().hasPrefix(basePrefix) { + if let internalUrl = parseInternalUrl(sharedContext: sharedContext, query: String(url[basePrefix.endIndex...])), case .peer = internalUrl { + return internalUrl + } + } + } + } + if let parsedUrl = URL(string: url), parsedUrl.scheme == "tg", let host = parsedUrl.host, let query = parsedUrl.query { + if let internalUrl = parseInternalUrl(sharedContext: sharedContext, query: host + "?" + query), case .peer = internalUrl { + return internalUrl + } + } + + return nil +} + private struct UrlHandlingConfiguration { static var defaultValue: UrlHandlingConfiguration { return UrlHandlingConfiguration(domains: [], urlAuthDomains: []) @@ -1178,7 +1207,7 @@ public func resolveUrlImpl(context: AccountContext, peerId: PeerId?, url: String url = basePrefix + String(url[scheme.endIndex...]).replacingOccurrences(of: ".\(basePath)/", with: "").replacingOccurrences(of: ".\(basePath)", with: "") } if url.lowercased().hasPrefix(basePrefix) { - if let internalUrl = parseInternalUrl(query: String(url[basePrefix.endIndex...])) { + if let internalUrl = parseInternalUrl(sharedContext: context.sharedContext, query: String(url[basePrefix.endIndex...])) { return resolveInternalUrl(context: context, url: internalUrl) |> map { result -> ResolveUrlResult in switch result { diff --git a/submodules/WebSearchUI/Sources/WebSearchRecentQueryItem.swift b/submodules/WebSearchUI/Sources/WebSearchRecentQueryItem.swift index 864e29696b7..071baf87dec 100644 --- a/submodules/WebSearchUI/Sources/WebSearchRecentQueryItem.swift +++ b/submodules/WebSearchUI/Sources/WebSearchRecentQueryItem.swift @@ -190,7 +190,7 @@ class WebSearchRecentQueryItemNode: ItemListRevealOptionsItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } diff --git a/submodules/WebUI/BUILD b/submodules/WebUI/BUILD index 9fdbf7604b2..ae4a1350887 100644 --- a/submodules/WebUI/BUILD +++ b/submodules/WebUI/BUILD @@ -18,7 +18,7 @@ swift_library( "//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/AccountContext:AccountContext", "//submodules/AttachmentUI:AttachmentUI", - "//submodules/CounterContollerTitleView:CounterContollerTitleView", + "//submodules/CounterControllerTitleView:CounterControllerTitleView", "//submodules/HexColor:HexColor", "//submodules/PhotoResources:PhotoResources", "//submodules/ShimmerEffect:ShimmerEffect", diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 24b963a7ba4..fbef04c2fb8 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -9,7 +9,7 @@ import SwiftSignalKit import TelegramPresentationData import AccountContext import AttachmentUI -import CounterContollerTitleView +import CounterControllerTitleView import ContextUI import PresentationDataUtils import HexColor @@ -258,7 +258,11 @@ public func generateWebAppThemeParams(_ presentationTheme: PresentationTheme) -> public final class WebAppController: ViewController, AttachmentContainable { public var requestAttachmentMenuExpansion: () -> Void = { } public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } + public var parentController: () -> ViewController? = { + return nil + } public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in } + public var updateTabBarVisibility: (Bool, ContainedViewLayoutTransition) -> Void = { _, _ in } public var cancelPanGesture: () -> Void = { } public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } @@ -584,42 +588,66 @@ public final class WebAppController: ViewController, AttachmentContainable { } func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) { + var completed = false let alertController = textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: nil, text: message, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { - completionHandler() + if !completed { + completed = true + completionHandler() + } })]) alertController.dismissed = { byOutsideTap in if byOutsideTap { - completionHandler() + if !completed { + completed = true + completionHandler() + } } } self.controller?.present(alertController, in: .window(.root)) } func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) { + var completed = false let alertController = textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: nil, text: message, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { - completionHandler(false) + if !completed { + completed = true + completionHandler(false) + } }), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { - completionHandler(true) + if !completed { + completed = true + completionHandler(true) + } })]) alertController.dismissed = { byOutsideTap in if byOutsideTap { - completionHandler(false) + if !completed { + completed = true + completionHandler(false) + } } } self.controller?.present(alertController, in: .window(.root)) } func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) { + var completed = false let promptController = promptController(sharedContext: self.context.sharedContext, updatedPresentationData: self.controller?.updatedPresentationData, text: prompt, value: defaultText, apply: { value in - if let value = value { - completionHandler(value) - } else { - completionHandler(nil) + if !completed { + completed = true + if let value = value { + completionHandler(value) + } else { + completionHandler(nil) + } } }) promptController.dismissed = { byOutsideTap in if byOutsideTap { - completionHandler(nil) + if !completed { + completed = true + completionHandler(nil) + } } } self.controller?.present(promptController, in: .window(.root)) @@ -1501,10 +1529,18 @@ public final class WebAppController: ViewController, AttachmentContainable { var alertTitle: String? let alertText: String if let reason { - alertTitle = self.presentationData.strings.WebApp_AlertBiometryAccessText(botPeer.compactDisplayTitle).string + if case .touchId = LocalAuth.biometricAuthentication { + alertTitle = self.presentationData.strings.WebApp_AlertBiometryAccessTouchIDText(botPeer.compactDisplayTitle).string + } else { + alertTitle = self.presentationData.strings.WebApp_AlertBiometryAccessText(botPeer.compactDisplayTitle).string + } alertText = reason } else { - alertText = self.presentationData.strings.WebApp_AlertBiometryAccessText(botPeer.compactDisplayTitle).string + if case .touchId = LocalAuth.biometricAuthentication { + alertText = self.presentationData.strings.WebApp_AlertBiometryAccessTouchIDText(botPeer.compactDisplayTitle).string + } else { + alertText = self.presentationData.strings.WebApp_AlertBiometryAccessText(botPeer.compactDisplayTitle).string + } } controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: alertTitle, text: alertText, actions: [ TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_No, action: { @@ -1703,7 +1739,7 @@ public final class WebAppController: ViewController, AttachmentContainable { return self.displayNode as! Node } - private var titleView: CounterContollerTitleView? + private var titleView: CounterControllerTitleView? fileprivate let cancelButtonNode: WebAppCancelButtonNode fileprivate let moreButtonNode: MoreButtonNode @@ -1774,8 +1810,8 @@ public final class WebAppController: ViewController, AttachmentContainable { self.navigationItem.rightBarButtonItem?.action = #selector(self.moreButtonPressed) self.navigationItem.rightBarButtonItem?.target = self - let titleView = CounterContollerTitleView(theme: self.presentationData.theme) - titleView.title = CounterContollerTitle(title: params.botName, counter: self.presentationData.strings.Bot_GenericBotStatus) + let titleView = CounterControllerTitleView(theme: self.presentationData.theme) + titleView.title = CounterControllerTitle(title: params.botName, counter: self.presentationData.strings.Bot_GenericBotStatus) self.navigationItem.titleView = titleView self.titleView = titleView diff --git a/versions.json b/versions.json index 55641c1f8f3..522db60acdf 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "1.6.0", + "app": "1.6.1", "xcode": "15.2", "bazel": "7.1.1", "macos": "13.0"