diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 20679c4941..5e6c1900d4 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7365,3 +7365,28 @@ Sorry for the inconvenience."; "CreateExternalStream.StreamKey" = "stream key"; "CreateExternalStream.StartStreamingInfo" = "Once you start broadcasting in your streaming\napp, tap Start Streaming below."; "CreateExternalStream.StartStreaming" = "Start Streaming"; + +"Bot.AddToChat" = "Add to Group or Channel"; +"Bot.AddToChatInfo" = "This bot is able to manage a group or channel."; + +"Bot.AddToChat.Title" = "Add to Group or Channel"; +"Bot.AddToChat.MyChannels" = "CHANNEL I MANAGE"; +"Bot.AddToChat.MyGroups" = "GROUPS I MANAGE"; +"Bot.AddToChat.OtherGroups" = "GROUPS"; + +"Bot.AddToChat.Add.Title" = "Add Bot"; +"Bot.AddToChat.Add.AdminRights" = "Admin Rights"; +"Bot.AddToChat.Add.AddAsAdmin" = "Add Bot as Admin"; +"Bot.AddToChat.Add.AddAsMember" = "Add Bot as Member"; + +"Bot.AddToChat.Add.AdminAlertTitle" = "Add Bot as Admin?"; +"Bot.AddToChat.Add.AdminAlertTextGroup" = "Are you sure you want to add the bot as an admin in the group **%@**?"; +"Bot.AddToChat.Add.AdminAlertTextChannel" = "Are you sure you want to add the bot as an admin in the channel **%@**?"; +"Bot.AddToChat.Add.AdminAlertAdd" = "Add as Admin"; + +"Bot.AddToChat.Add.MemberAlertTitle" = "Add Bot as Member?"; +"Bot.AddToChat.Add.MemberAlertTextGroup" = "Are you sure you want to add the bot as a member in the group **%@**?"; +"Bot.AddToChat.Add.MemberAlertTextChannel" = "Are you sure you want to add the bot as a member in the channel **%@**?"; +"Bot.AddToChat.Add.MemberAlertAdd" = "Add as Member"; + +"PeerInfo.ButtonStop" = "Stop"; diff --git a/submodules/AccountContext/Sources/PeerSelectionController.swift b/submodules/AccountContext/Sources/PeerSelectionController.swift index 84df9170dd..dbb695610e 100644 --- a/submodules/AccountContext/Sources/PeerSelectionController.swift +++ b/submodules/AccountContext/Sources/PeerSelectionController.swift @@ -29,6 +29,7 @@ public struct ChatListNodePeersFilter: OptionSet { public static let includeSavedMessages = ChatListNodePeersFilter(rawValue: 1 << 11) public static let excludeChannels = ChatListNodePeersFilter(rawValue: 1 << 12) + public static let onlyGroupsAndChannels = ChatListNodePeersFilter(rawValue: 1 << 13) } public final class PeerSelectionControllerParams { @@ -44,8 +45,9 @@ public final class PeerSelectionControllerParams { public let pretendPresentedInModal: Bool public let multipleSelection: Bool public let forwardedMessageIds: [EngineMessage.Id] + public let hasTypeHeaders: Bool - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, filter: ChatListNodePeersFilter = [.onlyWriteable], hasChatListSelector: Bool = true, hasContactSelector: Bool = true, hasGlobalSearch: Bool = true, title: String? = nil, attemptSelection: ((Peer) -> Void)? = nil, createNewGroup: (() -> Void)? = nil, pretendPresentedInModal: Bool = false, multipleSelection: Bool = false, forwardedMessageIds: [EngineMessage.Id] = []) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, filter: ChatListNodePeersFilter = [.onlyWriteable], hasChatListSelector: Bool = true, hasContactSelector: Bool = true, hasGlobalSearch: Bool = true, title: String? = nil, attemptSelection: ((Peer) -> Void)? = nil, createNewGroup: (() -> Void)? = nil, pretendPresentedInModal: Bool = false, multipleSelection: Bool = false, forwardedMessageIds: [EngineMessage.Id] = [], hasTypeHeaders: Bool = false) { self.context = context self.updatedPresentationData = updatedPresentationData self.filter = filter @@ -58,6 +60,7 @@ public final class PeerSelectionControllerParams { self.pretendPresentedInModal = pretendPresentedInModal self.multipleSelection = multipleSelection self.forwardedMessageIds = forwardedMessageIds + self.hasTypeHeaders = hasTypeHeaders } } diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index 660b99f8bc..f0aeb85d91 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -292,6 +292,9 @@ public class AttachmentController: ViewController { func switchToController(_ type: AttachmentButtonType, _ ascending: Bool) -> Bool { guard self.currentType != type else { + if self.animating { + return false + } if let controller = self.currentControllers.last { controller.scrollToTopWithTabBar?() controller.requestAttachmentMenuExpansion() @@ -356,8 +359,6 @@ public class AttachmentController: ViewController { snapshotView.frame = self.container.container.frame self.container.clipNode.view.addSubview(snapshotView) - self.animating = true - let _ = (controller.ready.get() |> filter { $0 @@ -383,13 +384,10 @@ public class AttachmentController: ViewController { strongSelf.container.clipNode.layer.animateSpring(from: NSValue(cgPoint: targetPosition), to: NSValue(cgPoint: initialPosition), keyPath: "position", duration: 0.4, delay: 0.0, initialVelocity: 0.0, damping: 70.0, removeOnCompletion: false, completion: { [weak self] finished in if finished { self?.container.clipNode.layer.removeAllAnimations() - self?.animating = false } }) } }) - } else { - strongSelf.animating = false } snapshotView?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.23, removeOnCompletion: false, completion: { [weak snapshotView] _ in diff --git a/submodules/BrowserUI/BUILD b/submodules/BrowserUI/BUILD new file mode 100644 index 0000000000..a55feeb4b4 --- /dev/null +++ b/submodules/BrowserUI/BUILD @@ -0,0 +1,27 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "BrowserUI", + module_name = "BrowserUI", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/AppBundle:AppBundle", + "//submodules/InstantPageUI:InstantPageUI", + "//submodules/ContextUI:ContextUI", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/BrowserUI/Info.plist b/submodules/BrowserUI/Info.plist new file mode 100644 index 0000000000..e1fe4cfb7b --- /dev/null +++ b/submodules/BrowserUI/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/submodules/BrowserUI/Sources/BrowserContent.swift b/submodules/BrowserUI/Sources/BrowserContent.swift new file mode 100644 index 0000000000..8b4ee2872a --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserContent.swift @@ -0,0 +1,62 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit + +final class BrowserContentState { + let title: String + let url: String + let estimatedProgress: Double + let isInstant: Bool + + var canGoBack: Bool + var canGoForward: Bool + + init(title: String, url: String, estimatedProgress: Double, isInstant: Bool, canGoBack: Bool = false, canGoForward: Bool = false) { + self.title = title + self.url = url + self.estimatedProgress = estimatedProgress + self.isInstant = isInstant + self.canGoBack = canGoBack + self.canGoForward = canGoForward + } + + func withUpdatedTitle(_ title: String) -> BrowserContentState { + return BrowserContentState(title: title, url: self.url, estimatedProgress: self.estimatedProgress, isInstant: self.isInstant, canGoBack: self.canGoBack, canGoForward: self.canGoForward) + } + + func withUpdatedUrl(_ url: String) -> BrowserContentState { + return BrowserContentState(title: self.title, url: url, estimatedProgress: self.estimatedProgress, isInstant: self.isInstant, canGoBack: self.canGoBack, canGoForward: self.canGoForward) + } + + func withUpdatedEstimatedProgress(_ estimatedProgress: Double) -> BrowserContentState { + return BrowserContentState(title: self.title, url: self.url, estimatedProgress: estimatedProgress, isInstant: self.isInstant, canGoBack: self.canGoBack, canGoForward: self.canGoForward) + } + + func withUpdatedCanGoBack(_ canGoBack: Bool) -> BrowserContentState { + return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, isInstant: self.isInstant, canGoBack: canGoBack, canGoForward: self.canGoForward) + } + + func withUpdatedCanGoForward(_ canGoForward: Bool) -> BrowserContentState { + return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, isInstant: self.isInstant, canGoBack: self.canGoBack, canGoForward: canGoForward) + } +} + +protocol BrowserContent: ASDisplayNode { + var state: Signal { get } + + func navigateBack() + func navigateForward() + + func setFontSize(_ fontSize: CGFloat) + func setForceSerif(_ force: Bool) + + func setSearch(_ query: String?, completion: ((Int) -> Void)?) + func scrollToPreviousSearchResult(completion: ((Int, Int) -> Void)?) + func scrollToNextSearchResult(completion: ((Int, Int) -> Void)?) + + func scrollToTop() + + func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) +} diff --git a/submodules/BrowserUI/Sources/BrowserFontSizeContextMenuItem.swift b/submodules/BrowserUI/Sources/BrowserFontSizeContextMenuItem.swift new file mode 100644 index 0000000000..bbb3851479 --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserFontSizeContextMenuItem.swift @@ -0,0 +1,272 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramPresentationData +import AppBundle +import ContextUI + +final class BrowserFontSizeContextMenuItem: ContextMenuCustomItem { + private let value: CGFloat + private let decrease: () -> CGFloat + private let increase: () -> CGFloat + private let reset: () -> Void + + init(value: CGFloat, decrease: @escaping () -> CGFloat, increase: @escaping () -> CGFloat, reset: @escaping () -> Void) { + self.value = value + self.decrease = decrease + self.increase = increase + self.reset = reset + } + + func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { + return BrowserFontSizeContextMenuItemNode(presentationData: presentationData, getController: getController, value: self.value, decrease: self.decrease, increase: self.increase, reset: self.reset) + } +} + +private let textFont = Font.regular(17.0) + +private final class BrowserFontSizeContextMenuItemNode: ASDisplayNode, ContextMenuCustomNode { + private var presentationData: PresentationData + + private let leftBackgroundNode: ASDisplayNode + private let leftHighlightedBackgroundNode: ASDisplayNode + private let leftIconNode: ASImageNode + private let leftButtonNode: HighlightTrackingButtonNode + + private let rightBackgroundNode: ASDisplayNode + private let rightHighlightedBackgroundNode: ASDisplayNode + private let rightIconNode: ASImageNode + private let rightButtonNode: HighlightTrackingButtonNode + + private let centerTextNode: ImmediateTextNode + private let centerHighlightedBackgroundNode: ASDisplayNode + private let centerButtonNode: HighlightTrackingButtonNode + + private let leftSeparatorNode: ASDisplayNode + private let rightSeparatorNode: ASDisplayNode + + var value: CGFloat = 1.0 { + didSet { + self.updateValue() + } + } + + private let decrease: () -> CGFloat + private let increase: () -> CGFloat + private let reset: () -> Void + + init(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, value: CGFloat, decrease: @escaping () -> CGFloat, increase: @escaping () -> CGFloat, reset: @escaping () -> Void) { + self.presentationData = presentationData + self.value = value + self.decrease = decrease + self.increase = increase + self.reset = reset + + self.leftBackgroundNode = ASDisplayNode() + self.leftBackgroundNode.isAccessibilityElement = false + self.leftBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor + self.leftHighlightedBackgroundNode = ASDisplayNode() + self.leftHighlightedBackgroundNode.isAccessibilityElement = false + self.leftHighlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor + self.leftHighlightedBackgroundNode.alpha = 0.0 + + self.leftIconNode = ASImageNode() + self.leftIconNode.isAccessibilityElement = false + self.leftIconNode.displaysAsynchronously = false + self.leftIconNode.displayWithoutProcessing = true + self.leftIconNode.isUserInteractionEnabled = false + self.leftIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/DecreaseFont"), color: presentationData.theme.contextMenu.primaryColor) + + self.leftButtonNode = HighlightTrackingButtonNode() + self.leftButtonNode.isAccessibilityElement = true + self.leftButtonNode.accessibilityLabel = presentationData.strings.InstantPage_VoiceOver_DecreaseFontSize + + self.rightBackgroundNode = ASDisplayNode() + self.rightBackgroundNode.isAccessibilityElement = false + self.rightBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor + self.rightHighlightedBackgroundNode = ASDisplayNode() + self.rightHighlightedBackgroundNode.isAccessibilityElement = false + self.rightHighlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor + self.rightHighlightedBackgroundNode.alpha = 0.0 + + self.rightIconNode = ASImageNode() + self.rightIconNode.isAccessibilityElement = false + self.rightIconNode.displaysAsynchronously = false + self.rightIconNode.displayWithoutProcessing = true + self.rightIconNode.isUserInteractionEnabled = false + self.rightIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/IncreaseFont"), color: presentationData.theme.contextMenu.primaryColor) + + self.rightButtonNode = HighlightTrackingButtonNode() + self.rightButtonNode.isAccessibilityElement = true + self.rightButtonNode.accessibilityLabel = presentationData.strings.InstantPage_VoiceOver_IncreaseFontSize + + self.centerTextNode = ImmediateTextNode() + self.centerTextNode.isAccessibilityElement = false + self.centerTextNode.isUserInteractionEnabled = false + self.centerTextNode.displaysAsynchronously = false + self.centerTextNode.textAlignment = .center + + self.centerHighlightedBackgroundNode = ASDisplayNode() + self.centerHighlightedBackgroundNode.isAccessibilityElement = false + self.centerHighlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor + self.centerHighlightedBackgroundNode.alpha = 0.0 + + self.centerButtonNode = HighlightTrackingButtonNode() + self.centerButtonNode.isAccessibilityElement = true + self.centerButtonNode.accessibilityLabel = presentationData.strings.InstantPage_VoiceOver_ResetFontSize + + self.leftSeparatorNode = ASDisplayNode() + self.leftSeparatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor + self.rightSeparatorNode = ASDisplayNode() + self.rightSeparatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor + + super.init() + + self.isUserInteractionEnabled = true + + self.addSubnode(self.leftBackgroundNode) + self.addSubnode(self.leftHighlightedBackgroundNode) + self.addSubnode(self.leftIconNode) + self.addSubnode(self.leftButtonNode) + self.addSubnode(self.rightBackgroundNode) + self.addSubnode(self.rightHighlightedBackgroundNode) + self.addSubnode(self.rightIconNode) + self.addSubnode(self.rightButtonNode) + self.addSubnode(self.centerHighlightedBackgroundNode) + self.addSubnode(self.centerTextNode) + self.addSubnode(self.centerButtonNode) + self.addSubnode(self.leftSeparatorNode) + self.addSubnode(self.rightSeparatorNode) + + self.leftButtonNode.highligthedChanged = { [weak self] highligted in + guard let strongSelf = self else { + return + } + if highligted { + strongSelf.leftHighlightedBackgroundNode.alpha = 1.0 + } else { + strongSelf.leftHighlightedBackgroundNode.alpha = 0.0 + strongSelf.leftHighlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } + } + self.leftButtonNode.addTarget(self, action: #selector(self.leftPressed), forControlEvents: .touchUpInside) + + self.rightButtonNode.highligthedChanged = { [weak self] highligted in + guard let strongSelf = self else { + return + } + if highligted { + strongSelf.rightHighlightedBackgroundNode.alpha = 1.0 + } else { + strongSelf.rightHighlightedBackgroundNode.alpha = 0.0 + strongSelf.rightHighlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } + } + self.rightButtonNode.addTarget(self, action: #selector(self.rightPressed), forControlEvents: .touchUpInside) + + self.centerButtonNode.highligthedChanged = { [weak self] highligted in + guard let strongSelf = self else { + return + } + if highligted { + strongSelf.centerHighlightedBackgroundNode.alpha = 1.0 + } else { + strongSelf.centerHighlightedBackgroundNode.alpha = 0.0 + strongSelf.centerHighlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + } + } + self.centerButtonNode.addTarget(self, action: #selector(self.centerPressed), forControlEvents: .touchUpInside) + + self.updateValue() + } + + func updateTheme(presentationData: PresentationData) { + self.presentationData = presentationData + + self.leftBackgroundNode.backgroundColor = self.presentationData.theme.contextMenu.itemBackgroundColor + self.leftHighlightedBackgroundNode.backgroundColor = self.presentationData.theme.contextMenu.itemHighlightedBackgroundColor + self.leftIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/DecreaseFont"), color: self.presentationData.theme.contextMenu.primaryColor) + + self.rightBackgroundNode.backgroundColor = self.presentationData.theme.contextMenu.itemBackgroundColor + self.rightHighlightedBackgroundNode.backgroundColor = self.presentationData.theme.contextMenu.itemHighlightedBackgroundColor + self.rightIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/IncreaseFont"), color: self.presentationData.theme.contextMenu.primaryColor) + + self.updateValue() + self.leftSeparatorNode.backgroundColor = self.presentationData.theme.contextMenu.itemSeparatorColor + self.rightSeparatorNode.backgroundColor = self.presentationData.theme.contextMenu.itemSeparatorColor + } + + private func updateValue() { + self.centerTextNode.attributedText = NSAttributedString(string: "\(Int(self.value * 100.0))%", font: textFont, textColor: self.presentationData.theme.contextMenu.primaryColor) + let _ = self.centerTextNode.updateLayout(CGSize(width: 70.0, height: .greatestFiniteMagnitude)) + + self.leftButtonNode.isEnabled = self.value > 0.5 + self.leftIconNode.alpha = self.leftButtonNode.isEnabled ? 1.0 : 0.3 + self.rightButtonNode.isEnabled = self.value < 2.0 + self.rightIconNode.alpha = self.rightButtonNode.isEnabled ? 1.0 : 0.3 + self.centerButtonNode.isEnabled = self.value != 1.0 + self.centerTextNode.alpha = self.centerButtonNode.isEnabled ? 1.0 : 0.4 + } + + func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) { + let buttonWidth: CGFloat = 90.0 + let valueWidth: CGFloat = 70.0 + let height: CGFloat = 45.0 + + var textSize = self.centerTextNode.updateLayout(CGSize(width: valueWidth, height: .greatestFiniteMagnitude)) + textSize.width = valueWidth + + return (CGSize(width: buttonWidth * 2.0 + valueWidth, height: height), { size, transition in + let verticalOrigin = floor((size.height - textSize.height) / 2.0) + transition.updateFrameAdditive(node: self.centerTextNode, frame: CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: verticalOrigin), size: textSize)) + + transition.updateFrame(node: self.centerHighlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: buttonWidth, y: 0.0), size: CGSize(width: valueWidth, height: size.height))) + transition.updateFrame(node: self.centerButtonNode, frame: CGRect(origin: CGPoint(x: buttonWidth, y: 0.0), size: CGSize(width: valueWidth, height: size.height))) + + let leftIconSize = self.leftIconNode.image!.size + transition.updateFrameAdditive(node: self.leftIconNode, frame: CGRect(origin: CGPoint(x: floor((buttonWidth - leftIconSize.width) / 2.0), y: floor((size.height - leftIconSize.height) / 2.0)), size: leftIconSize)) + + let rightIconSize = self.leftIconNode.image!.size + transition.updateFrameAdditive(node: self.rightIconNode, frame: CGRect(origin: CGPoint(x: size.width - buttonWidth + floor((buttonWidth - rightIconSize.width) / 2.0), y: floor((size.height - rightIconSize.height) / 2.0)), size: rightIconSize)) + + transition.updateFrame(node: self.leftBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: buttonWidth, height: size.height))) + transition.updateFrame(node: self.leftHighlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: buttonWidth, height: size.height))) + transition.updateFrame(node: self.leftButtonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: buttonWidth, height: size.height))) + + transition.updateFrame(node: self.rightBackgroundNode, frame: CGRect(origin: CGPoint(x: size.width - buttonWidth, y: 0.0), size: CGSize(width: buttonWidth, height: size.height))) + transition.updateFrame(node: self.rightHighlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: size.width - buttonWidth, y: 0.0), size: CGSize(width: buttonWidth, height: size.height))) + transition.updateFrame(node: self.rightButtonNode, frame: CGRect(origin: CGPoint(x: size.width - buttonWidth, y: 0.0), size: CGSize(width: buttonWidth, height: size.height))) + + transition.updateFrame(node: self.leftSeparatorNode, frame: CGRect(origin: CGPoint(x: buttonWidth, y: 0.0), size: CGSize(width: UIScreenPixel, height: size.height))) + transition.updateFrame(node: self.rightSeparatorNode, frame: CGRect(origin: CGPoint(x: size.width - buttonWidth, y: 0.0), size: CGSize(width: UIScreenPixel, height: size.height))) + }) + } + + @objc private func leftPressed() { + let newValue = self.decrease() + self.value = newValue + } + + @objc private func rightPressed() { + let newValue = self.increase() + self.value = newValue + } + + @objc private func centerPressed() { + self.reset() + self.value = 1.0 + } + + func canBeHighlighted() -> Bool { + return false + } + + func updateIsHighlighted(isHighlighted: Bool) { + + } + + func performAction() { + + } +} diff --git a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift new file mode 100644 index 0000000000..646839f41c --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift @@ -0,0 +1,91 @@ +//import Foundation +//import UIKit +//import AsyncDisplayKit +//import TelegramCore +//import Postbox +//import SwiftSignalKit +//import Display +//import TelegramPresentationData +//import TelegramUIPreferences +//import AccountContext +//import AppBundle +//import InstantPageUI +// +//final class BrowserInstantPageContent: ASDisplayNode, BrowserContent { +// private let instantPageNode: InstantPageContentNode +// +// private var _state: BrowserContentState +// private let statePromise: Promise +// +// private let webPage: TelegramMediaWebpage +// private var initialized = false +// +// var state: Signal { +// return self.statePromise.get() +// } +// +// init(context: AccountContext, webPage: TelegramMediaWebpage, url: String) { +// self.webPage = webPage +// +// let presentationData = context.sharedContext.currentPresentationData.with { $0 } +// self.instantPageNode = InstantPageContentNode(context: context, webPage: webPage, settings: nil, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, sourcePeerType: .contact, getNavigationController: { return nil }, present: { _, _ in }, pushController: { _ in }, openPeer: { _ in }, navigateBack: {}) +// +// let title: String +// if case let .Loaded(content) = webPage.content { +// title = content.title ?? "" +// } else { +// title = "" +// } +// +// self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, isInstant: false) +// self.statePromise = Promise(self._state) +// +// super.init() +// +// self.addSubnode(self.instantPageNode) +// } +// +// func navigateBack() { +// +// } +// +// func navigateForward() { +// +// } +// +// func setFontSize(_ fontSize: CGFloat) { +// +// } +// +// func setForceSerif(_ force: Bool) { +// +// } +// +// func setSearch(_ query: String?, completion: ((Int) -> Void)?) { +// +// } +// +// func scrollToPreviousSearchResult(completion: ((Int, Int) -> Void)?) { +// +// } +// +// func scrollToNextSearchResult(completion: ((Int, Int) -> Void)?) { +// +// } +// +// func scrollToTop() { +// +// } +// +// func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) { +// let layout = ContainerViewLayout(size: size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), deviceMetrics: .iPhoneX, intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: insets.bottom, right: 0.0), safeInsets: UIEdgeInsets(top: 0.0, left: insets.left, bottom: 0.0, right: insets.right), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false) +// self.instantPageNode.containerLayoutUpdated(layout, navigationBarHeight: 0.0, transition: transition) +// self.instantPageNode.frame = CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height) +// //transition.updateFrame(view: self.webView, frame: CGRect(origin: CGPoint(x: 0.0, y: 56.0), size: CGSize(width: size.width, height: size.height - 56.0))) +// +// if !self.initialized { +// self.initialized = true +// self.instantPageNode.updateWebPage(self.webPage, anchor: nil) +// } +// } +//} diff --git a/submodules/BrowserUI/Sources/BrowserInteraction.swift b/submodules/BrowserUI/Sources/BrowserInteraction.swift new file mode 100644 index 0000000000..7bddca83b2 --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserInteraction.swift @@ -0,0 +1,35 @@ +import Foundation + +final class BrowserInteraction { + let navigateBack: () -> Void + let navigateForward: () -> Void + let share: () -> Void + let minimize: () -> Void + + let openSearch: () -> Void + let updateSearchQuery: (String) -> Void + let dismissSearch: () -> Void + let scrollToPreviousSearchResult: () -> Void + let scrollToNextSearchResult: () -> Void + + let decreaseFontSize: () -> Void + let increaseFontSize: () -> Void + let resetFontSize: () -> Void + let updateForceSerif: (Bool) -> Void + + init(navigateBack: @escaping () -> Void, navigateForward: @escaping () -> Void, share: @escaping () -> Void, minimize: @escaping () -> Void, openSearch: @escaping () -> Void, updateSearchQuery: @escaping (String) -> Void, dismissSearch: @escaping () -> Void, scrollToPreviousSearchResult: @escaping () -> Void, scrollToNextSearchResult: @escaping () -> Void, decreaseFontSize: @escaping () -> Void, increaseFontSize: @escaping () -> Void, resetFontSize: @escaping () -> Void, updateForceSerif: @escaping (Bool) -> Void) { + self.navigateBack = navigateBack + self.navigateForward = navigateForward + self.share = share + self.minimize = minimize + self.openSearch = openSearch + self.updateSearchQuery = updateSearchQuery + self.dismissSearch = dismissSearch + self.scrollToPreviousSearchResult = scrollToPreviousSearchResult + self.scrollToNextSearchResult = scrollToNextSearchResult + self.decreaseFontSize = decreaseFontSize + self.increaseFontSize = increaseFontSize + self.resetFontSize = resetFontSize + self.updateForceSerif = updateForceSerif + } +} diff --git a/submodules/BrowserUI/Sources/BrowserNavigationBar.swift b/submodules/BrowserUI/Sources/BrowserNavigationBar.swift new file mode 100644 index 0000000000..37e1d31e9f --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserNavigationBar.swift @@ -0,0 +1,416 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramPresentationData +import AppBundle +import ContextUI + +private let closeImage = generateTintedImage(image: UIImage(bundleImageName: "Instant View/Close"), color: .black) +private let settingsImage = generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings"), color: .black) + +private func navigationBarContentNode(for state: BrowserState, currentContentNode: BrowserNavigationBarContentNode?, layoutMetrics: LayoutMetrics, theme: BrowserNavigationBarTheme, strings: PresentationStrings, interaction: BrowserInteraction?) -> BrowserNavigationBarContentNode? { + if let _ = state.search { + if let currentContentNode = currentContentNode as? BrowserNavigationBarSearchContentNode { + currentContentNode.updateState(state) + return currentContentNode + } else { + return BrowserNavigationBarSearchContentNode(theme: theme, strings: strings, state: state, interaction: interaction) + } + } + return nil +} + +final class BrowserNavigationBarTheme { + let backgroundColor: UIColor + let separatorColor: UIColor + let primaryTextColor: UIColor + let loadingProgressColor: UIColor + let readingProgressColor: UIColor + let buttonColor: UIColor + let disabledButtonColor: UIColor + let searchBarFieldColor: UIColor + let searchBarTextColor: UIColor + let searchBarPlaceholderColor: UIColor + let searchBarIconColor: UIColor + let searchBarClearColor: UIColor + let searchBarKeyboardColor: PresentationThemeKeyboardColor + + init(backgroundColor: UIColor, separatorColor: UIColor, primaryTextColor: UIColor, loadingProgressColor: UIColor, readingProgressColor: UIColor, buttonColor: UIColor, disabledButtonColor: UIColor, searchBarFieldColor: UIColor, searchBarTextColor: UIColor, searchBarPlaceholderColor: UIColor, searchBarIconColor: UIColor, searchBarClearColor: UIColor, searchBarKeyboardColor: PresentationThemeKeyboardColor) { + self.backgroundColor = backgroundColor + self.separatorColor = separatorColor + self.primaryTextColor = primaryTextColor + self.loadingProgressColor = loadingProgressColor + self.readingProgressColor = readingProgressColor + self.buttonColor = buttonColor + self.disabledButtonColor = disabledButtonColor + self.searchBarFieldColor = searchBarFieldColor + self.searchBarTextColor = searchBarTextColor + self.searchBarPlaceholderColor = searchBarPlaceholderColor + self.searchBarIconColor = searchBarIconColor + self.searchBarClearColor = searchBarClearColor + self.searchBarKeyboardColor = searchBarKeyboardColor + } +} + +protocol BrowserNavigationBarContentNode: ASDisplayNode { + init(theme: BrowserNavigationBarTheme, strings: PresentationStrings, state: BrowserState, interaction: BrowserInteraction?) + func updateState(_ state: BrowserState) + func updateTheme(_ theme: BrowserNavigationBarTheme) + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) +} + +private final class BrowserLoadingProgressNode: ASDisplayNode { + private var theme: BrowserNavigationBarTheme + + private let foregroundNode: ASDisplayNode + + init(theme: BrowserNavigationBarTheme) { + self.theme = theme + + self.foregroundNode = ASDisplayNode() + self.foregroundNode.backgroundColor = theme.loadingProgressColor + + super.init() + + self.addSubnode(self.foregroundNode) + } + + func updateTheme(_ theme: BrowserNavigationBarTheme) { + self.theme = theme + + self.foregroundNode.backgroundColor = theme.loadingProgressColor + } + + private var _progress: CGFloat = 0.0 + func updateProgress(_ progress: CGFloat, animated: Bool = false) { + if self._progress == progress && animated { + return + } + + var animated = animated + if (progress < self._progress && animated) { + animated = false + } + + let size = self.bounds.size + + self._progress = progress + + let transition: ContainedViewLayoutTransition + if animated && progress > 0.0 { + transition = .animated(duration: 0.7, curve: .spring) + } else { + transition = .immediate + } + + let alpaTransition: ContainedViewLayoutTransition + if animated { + alpaTransition = .animated(duration: 0.3, curve: .easeInOut) + } else { + alpaTransition = .immediate + } + + transition.updateFrame(node: self.foregroundNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width * progress, height: size.height)) + + let alpha: CGFloat = progress < 0.001 || progress > 0.999 ? 0.0 : 1.0 + alpaTransition.updateAlpha(node: self.foregroundNode, alpha: alpha) + } +} + +var browserNavigationBarHeight: CGFloat = 56.0 +var browserNavigationBarCollapsedHeight: CGFloat = 24.0 + +final class BrowserNavigationBar: ASDisplayNode { + private var theme: BrowserNavigationBarTheme + private var strings: PresentationStrings + private var state: BrowserState + var interaction: BrowserInteraction? + + private let containerNode: ASDisplayNode + private let separatorNode: ASDisplayNode + private let readingProgressNode: ASDisplayNode + private let loadingProgressNode: BrowserLoadingProgressNode + + private let closeButton: HighlightableButtonNode + private let closeIconNode: ASImageNode + private let closeIconSmallNode: ASImageNode + let contextSourceNode: ContextExtractedContentContainingNode + private let backButton: HighlightableButtonNode + private let forwardButton: HighlightableButtonNode + private let shareButton: HighlightableButtonNode + private let minimizeButton: HighlightableButtonNode + private let settingsButton: HighlightableButtonNode + private let titleNode: ImmediateTextNode + private let scrollToTopButton: HighlightableButtonNode + private var contentNode: BrowserNavigationBarContentNode? + + private let intrinsicSettingsSize: CGSize + private let intrinsicSmallSettingsSize: CGSize + + private var validLayout: (CGSize, UIEdgeInsets, LayoutMetrics, CGFloat, CGFloat)? + + private var title: (String, Bool) = ("", false) { + didSet { + self.updateTitle() + } + } + private func updateTitle() { + if let (size, insets, layoutMetrics, readingProgress, collapseTransition) = self.validLayout { + self.titleNode.attributedText = NSAttributedString(string: self.title.0, font: Font.with(size: 17.0, design: self.title.1 ? .serif : .regular, weight: .bold), textColor: self.theme.primaryTextColor, paragraphAlignment: .center) + let sideInset: CGFloat = 56.0 + self.titleNode.transform = CATransform3DIdentity + let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - insets.left - insets.right - sideInset * 2.0, height: size.height)) + self.titleNode.frame = CGRect(origin: CGPoint(x: (size.width - titleSize.width) / 2.0, y: size.height - 30.0), size: titleSize) + + self.updateLayout(size: size, insets: insets, layoutMetrics: layoutMetrics, readingProgress: readingProgress, collapseTransition: collapseTransition, transition: .immediate) + } + } + + var close: (() -> Void)? + var openSettings: (() -> Void)? + var scrollToTop: (() -> Void)? + + init(theme: BrowserNavigationBarTheme, strings: PresentationStrings, state: BrowserState) { + self.theme = theme + self.strings = strings + self.state = state + + self.containerNode = ASDisplayNode() + + self.separatorNode = ASDisplayNode() + self.separatorNode.backgroundColor = theme.separatorColor + + self.readingProgressNode = ASDisplayNode() + self.readingProgressNode.isLayerBacked = true + self.readingProgressNode.backgroundColor = theme.readingProgressColor + + self.closeButton = HighlightableButtonNode() + self.closeButton.allowsGroupOpacity = true + self.closeIconNode = ASImageNode() + self.closeIconNode.displaysAsynchronously = false + self.closeIconNode.displayWithoutProcessing = true + self.closeIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/Close"), color: theme.buttonColor) + self.closeIconSmallNode = ASImageNode() + self.closeIconSmallNode.displaysAsynchronously = false + self.closeIconSmallNode.displayWithoutProcessing = true + self.closeIconSmallNode.image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/CloseSmall"), color: theme.buttonColor) + self.closeIconSmallNode.alpha = 0.0 + + self.contextSourceNode = ContextExtractedContentContainingNode() + + self.settingsButton = HighlightableButtonNode() + self.settingsButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings"), color: theme.buttonColor), for: []) + self.intrinsicSettingsSize = CGSize(width: browserNavigationBarHeight, height: browserNavigationBarHeight) + self.intrinsicSmallSettingsSize = CGSize(width: browserNavigationBarCollapsedHeight, height: browserNavigationBarCollapsedHeight) + self.settingsButton.frame = CGRect(origin: CGPoint(), size: self.intrinsicSettingsSize) + + self.backButton = HighlightableButtonNode() + self.backButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Back"), color: theme.buttonColor), for: []) + self.backButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Back"), color: theme.disabledButtonColor), for: [.disabled]) + self.forwardButton = HighlightableButtonNode() + self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Forward"), color: theme.buttonColor), for: []) + self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Forward"), color: theme.disabledButtonColor), for: [.disabled]) + self.shareButton = HighlightableButtonNode() + self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat List/NavigationShare"), color: theme.buttonColor), for: []) + self.minimizeButton = HighlightableButtonNode() + self.minimizeButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Minimize"), color: theme.buttonColor), for: []) + + self.titleNode = ImmediateTextNode() + self.titleNode.textAlignment = .center + self.titleNode.maximumNumberOfLines = 1 + + self.scrollToTopButton = HighlightableButtonNode() + + self.loadingProgressNode = BrowserLoadingProgressNode(theme: theme) + + super.init() + + self.clipsToBounds = true + self.containerNode.backgroundColor = theme.backgroundColor + + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.readingProgressNode) + self.containerNode.addSubnode(self.closeButton) + self.closeButton.addSubnode(self.closeIconNode) + self.closeButton.addSubnode(self.closeIconSmallNode) + self.containerNode.addSubnode(self.contextSourceNode) + self.contextSourceNode.addSubnode(self.settingsButton) + self.containerNode.addSubnode(self.titleNode) + self.containerNode.addSubnode(self.scrollToTopButton) + self.containerNode.addSubnode(self.loadingProgressNode) + self.containerNode.addSubnode(self.separatorNode) + + self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: .touchUpInside) + self.settingsButton.addTarget(self, action: #selector(self.settingsPressed), forControlEvents: .touchUpInside) + self.scrollToTopButton.addTarget(self, action: #selector(self.scrollToTopPressed), forControlEvents: .touchUpInside) + + self.title = (state.content?.title ?? "", state.content?.isInstant ?? false) + } + + func updateState(_ state: BrowserState) { + self.state = state + + if let (size, insets, layoutMetrics, readingProgress, collapseTransition) = self.validLayout { + self.updateLayout(size: size, insets: insets, layoutMetrics: layoutMetrics, readingProgress: readingProgress, collapseTransition: collapseTransition, transition: .animated(duration: 0.2, curve: .easeInOut)) + } + + self.title = (state.content?.title ?? "", state.content?.isInstant ?? false) + self.loadingProgressNode.updateProgress(CGFloat(state.content?.estimatedProgress ?? 0.0), animated: true) + } + + func updateTheme(_ theme: BrowserNavigationBarTheme) { + guard self.theme !== theme else { + return + } + self.theme = theme + + self.containerNode.backgroundColor = theme.backgroundColor + self.separatorNode.backgroundColor = theme.separatorColor + self.closeIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/Close"), color: theme.buttonColor) + self.closeIconSmallNode.image = generateTintedImage(image: UIImage(bundleImageName: "Instant View/CloseSmall"), color: theme.buttonColor) + self.settingsButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings"), color: theme.buttonColor), for: []) + self.readingProgressNode.backgroundColor = theme.readingProgressColor + self.loadingProgressNode.updateTheme(theme) + self.updateTitle() + } + + @objc private func closePressed() { + self.close?() + } + + @objc private func settingsPressed() { + self.openSettings?() + } + + @objc private func scrollToTopPressed() { + self.scrollToTop?() + } + + func updateLayout(size: CGSize, insets: UIEdgeInsets, layoutMetrics: LayoutMetrics, readingProgress: CGFloat, collapseTransition: CGFloat, transition: ContainedViewLayoutTransition) { + let hadValidLayout = self.validLayout != nil + self.validLayout = (size, insets, layoutMetrics, readingProgress, collapseTransition) + + var dismissedContentNode: ASDisplayNode? + var immediatelyLayoutContentNodeAndAnimateAppearance = false + if let contentNode = navigationBarContentNode(for: self.state, currentContentNode: self.contentNode, layoutMetrics: layoutMetrics, theme: self.theme, strings: self.strings, interaction: self.interaction) { + if contentNode !== self.contentNode { + dismissedContentNode = self.contentNode + immediatelyLayoutContentNodeAndAnimateAppearance = true + self.containerNode.insertSubnode(contentNode, belowSubnode: self.separatorNode) + self.contentNode = contentNode + } + } else { + dismissedContentNode = self.contentNode + self.contentNode = nil + } + + let expandTransition = 1.0 - collapseTransition + + let maxBarHeight: CGFloat + let minBarHeight: CGFloat + if insets.top.isZero { + maxBarHeight = browserNavigationBarHeight + minBarHeight = browserNavigationBarCollapsedHeight + } else { + maxBarHeight = insets.top + 44.0 + minBarHeight = insets.top + browserNavigationBarCollapsedHeight + } + + let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: -(maxBarHeight - minBarHeight) * collapseTransition), size: size) + transition.updateFrame(node: self.containerNode, frame: containerFrame) + + transition.updateFrame(node: self.readingProgressNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: floorToScreenPixels(size.width * readingProgress), height: size.height))) + + transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(), size: CGSize(width: 100.0, height: size.height))) + if let image = self.closeIconNode.image { + let closeIconSize = image.size + + let arrowHeight: CGFloat + if expandTransition < 1.0 { + arrowHeight = floor(12.0 * expandTransition + 18.0) + } else { + arrowHeight = 30.0 + } + let scaledIconSize = CGSize(width: closeIconSize.width * arrowHeight / closeIconSize.height, height: arrowHeight) + let arrowOffset = floor(9.0 * expandTransition + 3.0) + transition.updateFrame(node: self.closeIconNode, frame: CGRect(origin: CGPoint(x: insets.left + 8.0, y: size.height - arrowHeight - arrowOffset), size: scaledIconSize)) + } + + let offsetScaleTransition: CGFloat + let buttonScaleTransition: CGFloat + if expandTransition < 1.0 { + offsetScaleTransition = expandTransition + buttonScaleTransition = ((expandTransition * self.intrinsicSettingsSize.height) + ((1.0 - expandTransition) * self.intrinsicSmallSettingsSize.height)) / self.intrinsicSettingsSize.height + } else { + offsetScaleTransition = 1.0 + buttonScaleTransition = 1.0 + } + + let alphaTransition = min(1.0, offsetScaleTransition * offsetScaleTransition) + + let maxSettingsOffset = floor(self.intrinsicSettingsSize.height / 2.0) + let minSettingsOffset = floor(self.intrinsicSmallSettingsSize.height / 2.0) + let settingsOffset = expandTransition * maxSettingsOffset + (1.0 - expandTransition) * minSettingsOffset + + transition.updateTransformScale(node: self.titleNode, scale: 0.65 + expandTransition * 0.35) + transition.updatePosition(node: self.titleNode, position: CGPoint(x: size.width / 2.0, y: size.height - settingsOffset)) + + self.contextSourceNode.frame = CGRect(origin: CGPoint(x: size.width - 56.0, y: 0.0), size: CGSize(width: 56.0, height: 56.0)) + + transition.updateTransformScale(node: self.settingsButton, scale: buttonScaleTransition) + transition.updatePosition(node: self.settingsButton, position: CGPoint(x: 56.0 - insets.right - buttonScaleTransition * self.intrinsicSettingsSize.width / 2.0, y: size.height - settingsOffset)) + transition.updateAlpha(node: self.settingsButton, alpha: alphaTransition) + + transition.updateFrame(node: self.scrollToTopButton, frame: CGRect(origin: CGPoint(x: insets.left + 64.0, y: 0.0), size: CGSize(width: size.width - insets.left - insets.right - 64.0 * 2.0, height: size.height))) + + let loadingProgressHeight: CGFloat = 2.0 + transition.updateFrame(node: self.loadingProgressNode, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height - loadingProgressHeight - UIScreenPixel), size: CGSize(width: size.width, height: loadingProgressHeight))) + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) + + let constrainedSize = CGSize(width: size.width, height: size.height) + + if let contentNode = self.contentNode { + let contentNodeFrame = CGRect(origin: CGPoint(x: insets.left, y: 0.0), size: constrainedSize) + contentNode.updateLayout(size: constrainedSize, transition: transition) + + if immediatelyLayoutContentNodeAndAnimateAppearance { + contentNode.alpha = 0.0 + } + + transition.updateFrame(node: contentNode, frame: contentNodeFrame) + transition.updateAlpha(node: contentNode, alpha: 1.0) + } + + if let dismissedContentNode = dismissedContentNode { + var alphaCompleted = false + let frameCompleted = true + let completed = { [weak self, weak dismissedContentNode] in + if let strongSelf = self, let dismissedContentNode = dismissedContentNode, strongSelf.contentNode === dismissedContentNode { + return + } + if frameCompleted && alphaCompleted { + dismissedContentNode?.removeFromSupernode() + } + } + + transition.updateAlpha(node: dismissedContentNode, alpha: 0.0, completion: { _ in + alphaCompleted = true + completed() + }) + } + + if !hadValidLayout { + self.updateTitle() + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + if let result = result, result.isDescendant(of: self.containerNode.view) || result == self.containerNode.view { + return result + } + return nil + } +} diff --git a/submodules/BrowserUI/Sources/BrowserNavigationBarSearchContentNode.swift b/submodules/BrowserUI/Sources/BrowserNavigationBarSearchContentNode.swift new file mode 100644 index 0000000000..a0739789fb --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserNavigationBarSearchContentNode.swift @@ -0,0 +1,79 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramPresentationData +import SearchBarNode +import AppBundle + +private let searchBarFont = Font.regular(17.0) + +private extension SearchBarNodeTheme { + convenience init(navigationBarTheme: BrowserNavigationBarTheme) { + self.init(background: navigationBarTheme.backgroundColor, separator: .clear, inputFill: navigationBarTheme.searchBarFieldColor, primaryText: navigationBarTheme.searchBarTextColor, placeholder: navigationBarTheme.searchBarPlaceholderColor, inputIcon: navigationBarTheme.searchBarIconColor, inputClear: navigationBarTheme.searchBarClearColor, accent: navigationBarTheme.buttonColor, keyboard: navigationBarTheme.searchBarKeyboardColor) + } +} + +final class BrowserNavigationBarSearchContentNode: ASDisplayNode, BrowserNavigationBarContentNode { + private var theme: BrowserNavigationBarTheme + private let strings: PresentationStrings + private var state: BrowserState + private var interaction: BrowserInteraction? + + private let searchBar: SearchBarNode + + init(theme: BrowserNavigationBarTheme, strings: PresentationStrings, state: BrowserState, interaction: BrowserInteraction?) { + self.theme = theme + self.strings = strings + self.state = state + self.interaction = interaction + + let searchBarTheme = SearchBarNodeTheme(navigationBarTheme: self.theme) + self.searchBar = SearchBarNode(theme: searchBarTheme, strings: strings, fieldStyle: .modern) + self.searchBar.placeholderString = NSAttributedString(string: "Search on this page", font: searchBarFont, textColor: searchBarTheme.placeholder) + + super.init() + + self.backgroundColor = theme.backgroundColor + + self.addSubnode(self.searchBar) + + self.searchBar.cancel = { [weak self] in + self?.searchBar.deactivate(clear: false) + self?.interaction?.dismissSearch() + } + + self.searchBar.textUpdated = { [weak self] query, _ in + self?.interaction?.updateSearchQuery(query) + } + } + + override func didLoad() { + super.didLoad() + + self.searchBar.activate() + } + + func updateState(_ state: BrowserState) { + guard let searchState = state.search else { + return + } + + self.searchBar.text = searchState.query + } + + func updateTheme(_ theme: BrowserNavigationBarTheme) { + guard self.theme !== theme else { + return + } + self.theme = theme + + self.backgroundColor = theme.backgroundColor + self.searchBar.updateThemeAndStrings(theme: SearchBarNodeTheme(navigationBarTheme: self.theme), strings: self.strings) + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + self.searchBar.updateLayout(boundingSize: size, leftInset: 0.0, rightInset: 0.0, transition: .immediate) + self.searchBar.frame = CGRect(origin: CGPoint(), size: size) + } +} diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift new file mode 100644 index 0000000000..bacef153ea --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -0,0 +1,520 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import TelegramCore +import Postbox +import SwiftSignalKit +import Display +import TelegramPresentationData +import TelegramUIPreferences +import ContextUI +import AccountContext +import ShareController +import OpenInExternalAppUI + +private final class InstantPageContextExtractedContentSource: ContextExtractedContentSource { + let keepInPlace: Bool = false + let ignoreContentTouches: Bool = false + let blurBackground: Bool = false + + private weak var navigationBar: BrowserNavigationBar? + + init(navigationBar: BrowserNavigationBar) { + self.navigationBar = navigationBar + } + + func takeView() -> ContextControllerTakeViewInfo? { + guard let navigationBar = self.navigationBar else { + return nil + } + return ContextControllerTakeViewInfo(contentContainingNode: navigationBar.contextSourceNode, contentAreaInScreenSpace: navigationBar.convert(navigationBar.contextSourceNode.frame.offsetBy(dx: 0.0, dy: 40.0), to: nil)) + } + + func putBack() -> ContextControllerPutBackViewInfo? { + guard let navigationBar = self.navigationBar else { + return nil + } + return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: navigationBar.convert(navigationBar.contextSourceNode.frame.offsetBy(dx: 0.0, dy: 40.0), to: nil)) + } +} + +public enum BrowserSubject { +// case instantPage(TelegramMediaWebpage, String) + case webPage(String) + + var isInstant: Bool { + return false +// if case .instantPage = self { +// return true +// } else { +// return false +// } + } +} + +public final class BrowserScreen: ViewController { + private let context: AccountContext + private let subject: BrowserSubject + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + private let _ready = Promise() + override public var ready: Promise { + return self._ready + } + + public init(context: AccountContext, subject: BrowserSubject) { + self.context = context + self.subject = subject + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + super.init(navigationBarPresentationData: nil) + + self.navigationPresentation = .modal + + self.presentationDataDisposable = (context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + guard let strongSelf = self, strongSelf.presentationData.theme !== presentationData.theme else { + return + } + strongSelf.presentationData = presentationData + (strongSelf.displayNode as! BrowserScreenNode).updatePresentationData(presentationData) + }) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + override public func loadDisplayNode() { + self.displayNode = BrowserScreenNode(context: self.context, presentationData: self.presentationData, subject: self.subject, titleUpdated: { [weak self] title in + self?.title = title + }) + (self.displayNode as! BrowserScreenNode).present = { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) + } + (self.displayNode as! BrowserScreenNode).minimize = { +// if let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController { +// navigationController.minimizeViewController(strongSelf, animated: true) +// } + } + (self.displayNode as! BrowserScreenNode).close = { [weak self] in + self?.dismiss() + } + self.displayNodeDidLoad() + + self._ready.set(.single(true)) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + let navigationHeight: CGFloat + if case .modal = self.navigationPresentation { + navigationHeight = 56.0 + } else { + navigationHeight = 44.0 + } + + (self.displayNode as! BrowserScreenNode).containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition) + } +} + +struct BrowserSearchState { + var query: String + var results: (Int, Int)? + + func withUpdatedQuery(_ query: String) -> BrowserSearchState { + return BrowserSearchState(query: query, results: self.results) + } + + func withUpdatedResults(_ results: (Int, Int)?) -> BrowserSearchState { + return BrowserSearchState(query: self.query, results: results) + } +} + +struct BrowserPresentationState { + let fontSize: CGFloat + let forceSerif: Bool + + func withUpdatedFontSize(_ fontSize: CGFloat) -> BrowserPresentationState { + return BrowserPresentationState(fontSize: fontSize, forceSerif: self.forceSerif) + } + + func withUpdatedForceSerif(_ forceSerif: Bool) -> BrowserPresentationState { + return BrowserPresentationState(fontSize: self.fontSize, forceSerif: forceSerif) + } +} + +final class BrowserState { + let content: BrowserContentState? + let presentation: BrowserPresentationState + let search: BrowserSearchState? + + init(content: BrowserContentState? = nil, presentation: BrowserPresentationState, search: BrowserSearchState? = nil) { + self.content = content + self.presentation = presentation + self.search = search + } + + func withUpdatedContent(_ content: BrowserContentState) -> BrowserState { + return BrowserState(content: content, presentation: self.presentation, search: self.search) + } + + func withUpdatedPresentation(_ presentation: BrowserPresentationState) -> BrowserState { + return BrowserState(content: content, presentation: presentation, search: self.search) + } + + func withUpdatedSearch(_ search: BrowserSearchState?) -> BrowserState { + return BrowserState(content: self.content, presentation: self.presentation, search: search) + } +} + +private final class BrowserTheme { + let backgroundColor: UIColor + let navigationBar: BrowserNavigationBarTheme + let toolbar: BrowserToolbarTheme + + init(backgroundColor: UIColor, navigationBar: BrowserNavigationBarTheme, toolbar: BrowserToolbarTheme) { + self.backgroundColor = backgroundColor + self.navigationBar = navigationBar + self.toolbar = toolbar + } +} + +extension BrowserTheme { + convenience init(presentationTheme: PresentationTheme) { + self.init(backgroundColor: presentationTheme.list.plainBackgroundColor, + navigationBar: BrowserNavigationBarTheme( + backgroundColor: presentationTheme.rootController.navigationBar.opaqueBackgroundColor, + separatorColor: presentationTheme.rootController.navigationBar.separatorColor, + primaryTextColor: presentationTheme.rootController.navigationBar.primaryTextColor, + loadingProgressColor: presentationTheme.rootController.navigationBar.accentTextColor, + readingProgressColor: presentationTheme.rootController.navigationBar.segmentedBackgroundColor, + buttonColor: presentationTheme.rootController.navigationBar.primaryTextColor, + disabledButtonColor: presentationTheme.chat.inputPanel.inputTextColor.withAlphaComponent(0.3), + searchBarFieldColor: presentationTheme.rootController.navigationSearchBar.inputFillColor, + searchBarTextColor: presentationTheme.rootController.navigationSearchBar.inputTextColor, + searchBarPlaceholderColor: presentationTheme.rootController.navigationSearchBar.inputPlaceholderTextColor, + searchBarIconColor: presentationTheme.rootController.navigationSearchBar.inputIconColor, + searchBarClearColor: presentationTheme.rootController.navigationSearchBar.inputClearButtonColor, + searchBarKeyboardColor: presentationTheme.rootController.keyboardColor + ), + toolbar: BrowserToolbarTheme( + backgroundColor: presentationTheme.chat.inputPanel.panelBackgroundColor, + separatorColor: presentationTheme.chat.inputPanel.panelSeparatorColor, + buttonColor: presentationTheme.chat.inputPanel.inputTextColor, + disabledButtonColor: presentationTheme.chat.inputPanel.inputTextColor.withAlphaComponent(0.3))) + } +} + +private final class BrowserScreenNode: ViewControllerTracingNode { + private let context: AccountContext + private var presentationData: PresentationData + private let subject: BrowserSubject + + private var interaction: BrowserInteraction? + + private var browserState: BrowserState + private var browserStatePromise: Promise + private var stateDisposable: Disposable? + + private let navigationBar: BrowserNavigationBar + private let toolbar: BrowserToolbar + private let contentContainerNode: ASDisplayNode + private var content: BrowserContent? + private var contentStateDisposable: Disposable? + + private var validLayout: (ContainerViewLayout, CGFloat)? + + var present: ((ViewController, Any?) -> Void)? + var minimize: (() -> Void)? + var close: (() -> Void)? + let titleUpdated: (String) -> Void + + init(context: AccountContext, presentationData: PresentationData, subject: BrowserSubject, titleUpdated: @escaping ((String) -> Void)) { + self.context = context + self.presentationData = presentationData + self.subject = subject + self.titleUpdated = titleUpdated + + self.browserState = BrowserState(content: BrowserContentState(title: "", url: "", estimatedProgress: 0.0, isInstant: subject.isInstant), presentation: BrowserPresentationState(fontSize: 1.0, forceSerif: false)) + self.browserStatePromise = Promise(self.browserState) + + let theme = BrowserTheme(presentationTheme: self.presentationData.theme) + + self.navigationBar = BrowserNavigationBar(theme: theme.navigationBar, strings: self.presentationData.strings, state: self.browserState) + self.toolbar = BrowserToolbar(theme: theme.toolbar, strings: self.presentationData.strings, state: self.browserState) + self.contentContainerNode = ASDisplayNode() + + super.init() + + self.backgroundColor = theme.backgroundColor + + self.addSubnode(self.contentContainerNode) + self.addSubnode(self.toolbar) + self.addSubnode(self.navigationBar) + + self.navigationBar.close = { [weak self] in + self?.close?() + } + self.navigationBar.openSettings = { [weak self] in + self?.openSettings() + } + self.navigationBar.scrollToTop = { [weak self] in + self?.scrollToTop() + } + + self.stateDisposable = (self.browserStatePromise.get() + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let strongSelf = self else { + return + } + strongSelf.titleUpdated(state.content?.title ?? "") + + strongSelf.navigationBar.updateState(state) + strongSelf.toolbar.updateState(state) + + if let search = state.search, !search.query.isEmpty { + strongSelf.content?.setSearch(search.query, completion: { [weak self] count in + if let strongSelf = self { + strongSelf.updateState { $0.withUpdatedSearch($0.search?.withUpdatedResults((0, count))) } + } + }) + } else { + strongSelf.content?.setSearch(nil, completion: nil) + } + }) + + let content: BrowserContent + switch self.subject { + case let .webPage(url): + content = BrowserWebContent(url: url) +// case let .instantPage(webPage, url): +// content = BrowserInstantPageContent(context: context, webPage: webPage, url: url) + } + + self.contentContainerNode.addSubnode(content) + self.content = content + self.contentStateDisposable = (content.state + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let strongSelf = self else { + return + } + strongSelf.browserState = strongSelf.browserState.withUpdatedContent(state) + strongSelf.browserStatePromise.set(.single(strongSelf.browserState)) + + if strongSelf.isNodeLoaded { + strongSelf.content?.view.disablesInteractiveTransitionGestureRecognizer = state.canGoBack + } + }) + + self.interaction = BrowserInteraction(navigateBack: { [weak self] in + self?.content?.navigateBack() + }, navigateForward: { [weak self] in + self?.content?.navigateForward() + }, share: { [weak self] in + if let strongSelf = self, let url = strongSelf.browserState.content?.url { + let controller = ShareController(context: context, subject: .url(url)) + strongSelf.present?(controller, nil) + } + }, minimize: { [weak self] in + self?.minimize?() + }, openSearch: { [weak self] in + self?.updateState { $0.withUpdatedSearch(BrowserSearchState(query: "", results: nil)) } + }, updateSearchQuery: { [weak self] text in + self?.updateState { $0.withUpdatedSearch(BrowserSearchState(query: text, results: nil)) } + }, dismissSearch: { [weak self] in + self?.updateState { $0.withUpdatedSearch(nil) } + }, scrollToPreviousSearchResult: { [weak self] in + self?.content?.scrollToPreviousSearchResult(completion: { [weak self] index, count in + if let strongSelf = self { + strongSelf.updateState { $0.withUpdatedSearch($0.search?.withUpdatedResults((index, count))) } + } + }) + }, scrollToNextSearchResult: { [weak self] in + self?.content?.scrollToNextSearchResult(completion: { [weak self] index, count in + if let strongSelf = self { + strongSelf.updateState { $0.withUpdatedSearch($0.search?.withUpdatedResults((index, count))) } + } + }) + }, decreaseFontSize: { [weak self] in + if let strongSelf = self { + strongSelf.updateState { $0.withUpdatedPresentation($0.presentation.withUpdatedFontSize(max(0.5, $0.presentation.fontSize - 0.25))) } + + strongSelf.content?.setFontSize(strongSelf.browserState.presentation.fontSize) + } + }, increaseFontSize: { [weak self] in + if let strongSelf = self { + strongSelf.updateState { $0.withUpdatedPresentation($0.presentation.withUpdatedFontSize(min(2.0, $0.presentation.fontSize + 0.25))) } + + strongSelf.content?.setFontSize(strongSelf.browserState.presentation.fontSize) + } + }, resetFontSize: { [weak self] in + if let strongSelf = self { + strongSelf.updateState { $0.withUpdatedPresentation($0.presentation.withUpdatedFontSize(1.0)) } + + strongSelf.content?.setFontSize(strongSelf.browserState.presentation.fontSize) + } + }, updateForceSerif: { [weak self] force in + if let strongSelf = self { + strongSelf.updateState { $0.withUpdatedPresentation($0.presentation.withUpdatedForceSerif(force)) } + + strongSelf.content?.setForceSerif(force) + } + }) + + self.navigationBar.interaction = self.interaction + self.toolbar.interaction = self.interaction + } + + deinit { + self.stateDisposable?.dispose() + self.contentStateDisposable?.dispose() + } + + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + + let theme = BrowserTheme(presentationTheme: self.presentationData.theme) + + self.backgroundColor = theme.backgroundColor + self.navigationBar.updateTheme(theme.navigationBar) + self.toolbar.updateTheme(theme.toolbar) + } + + func updateState(_ f: (BrowserState) -> BrowserState) { + self.browserState = f(self.browserState) + self.browserStatePromise.set(.single(self.browserState)) + } + + func openSettings() { + self.view.endEditing(true) + + let checkIcon: (PresentationTheme) -> UIImage? = { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Check"), color: theme.contextMenu.primaryColor) } + let emptyIcon: (PresentationTheme) -> UIImage? = { _ in + return nil + } + + let settings = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings]) + |> take(1) + |> map { sharedData -> WebBrowserSettings in + if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.webBrowserSettings]?.get(WebBrowserSettings.self) { + return current + } else { + return WebBrowserSettings.defaultSettings + } + } + + let _ = (settings + |> deliverOnMainQueue).start(next: { [weak self] settings in + guard let strongSelf = self else { + return + } + + let forceSerif = strongSelf.browserState.presentation.forceSerif + + let source: ContextContentSource = .extracted(InstantPageContextExtractedContentSource(navigationBar: strongSelf.navigationBar)) + + let fontItem = BrowserFontSizeContextMenuItem(value: strongSelf.browserState.presentation.fontSize, decrease: { [weak self] in + self?.interaction?.decreaseFontSize() + return self?.browserState.presentation.fontSize ?? 1.0 + }, increase: { [weak self] in + self?.interaction?.increaseFontSize() + return self?.browserState.presentation.fontSize ?? 1.0 + }, reset: { [weak self] in + self?.interaction?.resetFontSize() + }) + + var defaultWebBrowser: String? = settings.defaultWebBrowser + if defaultWebBrowser == nil || defaultWebBrowser == "inAppSafari" { + defaultWebBrowser = "safari" + } + + let url = strongSelf.browserState.content?.url ?? "" + let openInOptions = availableOpenInOptions(context: strongSelf.context, item: .url(url: url)) + let openInTitle: String + let openInUrl: String + if let option = openInOptions.first(where: { $0.identifier == defaultWebBrowser }) { + openInTitle = option.title + if case let .openUrl(url) = option.action() { + openInUrl = url + } else { + openInUrl = url + } + } else { + openInTitle = "Safari" + openInUrl = url + } + + let items: [ContextMenuItem] = [ + .custom(fontItem, false), + .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.InstantPage_FontSanFrancisco, icon: forceSerif ? emptyIcon : checkIcon, action: { [weak self] (controller, action) in + if let strongSelf = self { + strongSelf.interaction?.updateForceSerif(false) + action(.default) + } + })), .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.InstantPage_FontNewYork, textFont: .custom(Font.with(size: 17.0, design: .serif, traits: [])), icon: forceSerif ? checkIcon : emptyIcon, action: { [weak self] (controller, action) in + if let strongSelf = self { + strongSelf.interaction?.updateForceSerif(true) + action(.default) + } + })), + .separator, + .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.InstantPage_Search, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Search"), color: theme.contextMenu.primaryColor) }, action: { [weak self] (controller, action) in + if let strongSelf = self { + strongSelf.interaction?.openSearch() + action(.default) + } + })), + .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.InstantPage_OpenInBrowser(openInTitle).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] (controller, action) in + if let strongSelf = self { + strongSelf.context.sharedContext.applicationBindings.openUrl(openInUrl) + } + action(.default) + }))] + + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: source, items: .single(ContextController.Items(content: .list(items)))) + strongSelf.present?(controller, nil) + }) + } + + func scrollToTop() { + self.content?.scrollToTop() + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.validLayout = (layout, navigationHeight) + + self.updatePanelsLayout(transition: transition) + } + + private func updatePanelsLayout(transition: ContainedViewLayoutTransition) { + guard let (layout, navigationHeight) = self.validLayout else { + return + } + + var insets = layout.insets(options: .input) + insets.left += layout.safeInsets.left + insets.right += layout.safeInsets.right + + let navigationBarFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: navigationHeight)) + transition.updateFrame(node: self.navigationBar, frame: navigationBarFrame) + self.navigationBar.updateLayout(size: navigationBarFrame.size, insets: insets, layoutMetrics: layout.metrics, readingProgress: 0.0, collapseTransition: 0.0, transition: transition) + + let toolbarSize = self.toolbar.updateLayout(width: layout.size.width, insets: insets, layoutMetrics: layout.metrics, collapseTransition: 0.0, transition: transition) + let toolbarFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarSize.height), size: toolbarSize) + transition.updateFrame(node: self.toolbar, frame: toolbarFrame) + + let contentFrame = CGRect(origin: CGPoint(), size: layout.size) + transition.updateFrame(node: self.contentContainerNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + if let content = self.content { + content.updateLayout(size: layout.size, insets: insets, transition: transition) + transition.updateFrame(node: content, frame: contentFrame) + } + } +} diff --git a/submodules/BrowserUI/Sources/BrowserStackContainerNode.swift b/submodules/BrowserUI/Sources/BrowserStackContainerNode.swift new file mode 100644 index 0000000000..2e5c58f3ee --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserStackContainerNode.swift @@ -0,0 +1,445 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import AppBundle + +let maxInteritemSpacing: CGFloat = 240.0 +let sectionInsetTop: CGFloat = 40.0 +let sectionInsetBottom: CGFloat = 0.0 +let zOffset: CGFloat = -60.0 + +let perspectiveCorrection: CGFloat = -1.0 / 1000.0 +let maxRotationAngle: CGFloat = -CGFloat.pi / 2.2 + +extension CATransform3D { + func interpolate(other: CATransform3D, progress: CGFloat) -> CATransform3D { + var vectors = Array(repeating: 0.0, count: 16) + vectors[0] = self.m11 + (other.m11 - self.m11) * progress + vectors[1] = self.m12 + (other.m12 - self.m12) * progress + vectors[2] = self.m13 + (other.m13 - self.m13) * progress + vectors[3] = self.m14 + (other.m14 - self.m14) * progress + vectors[4] = self.m21 + (other.m21 - self.m21) * progress + vectors[5] = self.m22 + (other.m22 - self.m22) * progress + vectors[6] = self.m23 + (other.m23 - self.m23) * progress + vectors[7] = self.m24 + (other.m24 - self.m24) * progress + vectors[8] = self.m31 + (other.m31 - self.m31) * progress + vectors[9] = self.m32 + (other.m32 - self.m32) * progress + vectors[10] = self.m33 + (other.m33 - self.m33) * progress + vectors[11] = self.m34 + (other.m34 - self.m34) * progress + vectors[12] = self.m41 + (other.m41 - self.m41) * progress + vectors[13] = self.m42 + (other.m42 - self.m42) * progress + vectors[14] = self.m43 + (other.m43 - self.m43) * progress + vectors[15] = self.m44 + (other.m44 - self.m44) * progress + + return CATransform3D(m11: vectors[0], m12: vectors[1], m13: vectors[2], m14: vectors[3], m21: vectors[4], m22: vectors[5], m23: vectors[6], m24: vectors[7], m31: vectors[8], m32: vectors[9], m33: vectors[10], m34: vectors[11], m41: vectors[12], m42: vectors[13], m43: vectors[14], m44: vectors[15]) + } +} + + +private func angle(for origin: CGFloat, itemCount: Int, bounds: CGRect, contentHeight: CGFloat?) -> CGFloat { + var rotationAngle = rotationAngleAt0(itemCount: itemCount) + + var contentOffset = bounds.origin.y + if contentOffset < 0.0 { + contentOffset *= 2.0 + } +// } else if let contentHeight = contentHeight, bounds.maxY > contentHeight { +//// let maxContentOffset = contentHeight - bounds.height +//// let delta = contentOffset - maxContentOffset +//// contentOffset = maxContentOffset + delta / 2.0 +// } + + var yOnScreen = origin - contentOffset - sectionInsetTop + if yOnScreen < 0 { + yOnScreen = 0 + } else if yOnScreen > bounds.height { + yOnScreen = bounds.height + } + + let maxRotationVariance = maxRotationAngle - rotationAngleAt0(itemCount: itemCount) + rotationAngle += (maxRotationVariance / bounds.height) * yOnScreen + + return rotationAngle +} + +private func final3dTransform(for origin: CGFloat, size: CGSize, contentHeight: CGFloat?, itemCount: Int, forcedAngle: CGFloat? = nil, additionalAngle: CGFloat? = nil, bounds: CGRect) -> CATransform3D { + var transform = CATransform3DIdentity + transform.m34 = perspectiveCorrection + + let rotationAngle = forcedAngle ?? angle(for: origin, itemCount: itemCount, bounds: bounds, contentHeight: contentHeight) + var effectiveRotationAngle = rotationAngle + if let additionalAngle = additionalAngle { + effectiveRotationAngle += additionalAngle + } + + let r = size.height / 2.0 + abs(zOffset / sin(rotationAngle)) + + let zTranslation = r * sin(rotationAngle) + let yTranslation: CGFloat = r * (1 - cos(rotationAngle)) + + let zTranslateTransform = CATransform3DTranslate(transform, 0.0, -yTranslation, zTranslation) + + let rotateTransform = CATransform3DRotate(zTranslateTransform, effectiveRotationAngle, 1.0, 0.0, 0.0) + + return rotateTransform +} + +private func interitemSpacing(itemCount: Int, bounds: CGRect) -> CGFloat { + var interitemSpacing = maxInteritemSpacing + if itemCount > 0 { + interitemSpacing = (bounds.height - sectionInsetTop - sectionInsetBottom) / CGFloat(min(itemCount, 5)) + } + return interitemSpacing +} + +private func frameForIndex(index: Int, size: CGSize, itemCount: Int, bounds: CGRect) -> CGRect { + let spacing = interitemSpacing(itemCount: itemCount, bounds: bounds) + let y = sectionInsetTop + spacing * CGFloat(index) + let origin = CGPoint(x: 0, y: y) + + return CGRect(origin: origin, size: size) +} + +private func rotationAngleAt0(itemCount: Int) -> CGFloat { + let multiplier: CGFloat = min(CGFloat(itemCount), 5.0) - 1.0 + return -CGFloat.pi / 7.0 - CGFloat.pi / 7.0 * multiplier / 4.0 +} + +private let shadowImage: UIImage? = { + return generateImage(CGSize(width: 1.0, height: 640.0), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let gradientColors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.55).cgColor, UIColor.black.withAlphaComponent(0.55).cgColor] as CFArray + + var locations: [CGFloat] = [0.0, 0.65, 1.0] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: bounds.height), options: []) + }) +}() + +class StackItemContainerNode: ASDisplayNode { + private let node: ASDisplayNode + private let shadowNode: ASImageNode + + var tapped: (() -> Void)? + var highlighted: ((Bool) -> Void)? + + init(node: ASDisplayNode) { + self.node = node + self.shadowNode = ASImageNode() + self.shadowNode.displaysAsynchronously = false + self.shadowNode.displayWithoutProcessing = true + self.shadowNode.contentMode = .scaleToFill + + super.init() + + self.clipsToBounds = true + self.cornerRadius = 10.0 + applySmoothRoundedCorners(self.layer) + + self.shadowNode.image = shadowImage + + self.addSubnode(self.node) + self.addSubnode(self.shadowNode) + } + + override func didLoad() { + super.didLoad() + + let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) + recognizer.tapActionAtPoint = { point in + return .waitForSingleTap + } + recognizer.highlight = { [weak self] point in + if let point = point, point.x > 280.0 { + self?.highlighted?(true) + } else { + self?.highlighted?(false) + } + } + self.view.addGestureRecognizer(recognizer) + } + + func animateIn() { + self.shadowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + } + + @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .ended: + if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .tap: + self.tapped?() + default: + break + } + } + default: + break + } + } + + override func layout() { + super.layout() + + self.node.frame = self.bounds + self.shadowNode.frame = self.bounds + } +} + +public class StackContainerNode: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDelegate { + private let scrollNode: ASScrollNode + private var nodes: [StackItemContainerNode] + + private var deleteGestureRecognizer: UIPanGestureRecognizer? + private var offsetsForDeletingItems: [Int: CGPoint]? + private var currentDeletingIndexPath: Int? + private var deletingOffset: CGFloat? + + private var animatingIn = false + + private var validLayout: CGSize? + + override public init() { + self.scrollNode = ASScrollNode() + self.nodes = [] + + super.init() + + self.backgroundColor = .black + + self.addSubnode(self.scrollNode) + } + + override public func didLoad() { + super.didLoad() + + if #available(iOS 11.0, *) { + self.scrollNode.view.contentInsetAdjustmentBehavior = .never + } + + self.scrollNode.view.delegate = self + self.scrollNode.view.alwaysBounceVertical = true + + let deleteGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(didPanToDelete(gestureRecognizer:))) + deleteGestureRecognizer.delegate = self + deleteGestureRecognizer.delaysTouchesBegan = true + self.scrollNode.view.addGestureRecognizer(deleteGestureRecognizer) + self.deleteGestureRecognizer = deleteGestureRecognizer + } + + func item(forYPosition y: CGFloat) -> Int? { + let itemCount = self.nodes.count + let bounds = self.scrollNode.bounds + + let spacing = interitemSpacing(itemCount: itemCount, bounds: bounds) + return max(0, min(Int(floor((y - sectionInsetTop) / spacing)), itemCount - 1)) + } + + public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer else { + return false + } + + let touch = panGesture.location(in: gestureRecognizer.view) + let velocity = panGesture.velocity(in: gestureRecognizer.view) + + if abs(velocity.x) > abs(velocity.y), let item = self.item(forYPosition: touch.y) { + return item > 0 + } + return false + } + + @objc func didPanToDelete(gestureRecognizer: UIPanGestureRecognizer) { + let scrollView = self.scrollNode.view + + switch gestureRecognizer.state { + case .began: + let touch = gestureRecognizer.location(in: scrollView) + guard let item = self.item(forYPosition: touch.y) else { return } + + self.currentDeletingIndexPath = item + case .changed: + guard let _ = self.currentDeletingIndexPath else { return } + + var delta = gestureRecognizer.translation(in: scrollView) + delta.y = 0 + + if let offset = self.deletingOffset { + self.deletingOffset = offset + delta.x + } else { + self.deletingOffset = delta.x + } + + gestureRecognizer.setTranslation(.zero, in: scrollView) + + self.updateLayout() + case .ended: + if let _ = self.currentDeletingIndexPath { + if let offset = self.deletingOffset { + if offset < -self.frame.width / 2.0 { + self.deletingOffset = -self.frame.width + } else { + self.deletingOffset = nil + self.currentDeletingIndexPath = nil + } + } + } + + UIView.animate(withDuration: 0.3) { + self.updateLayout() + } + case .cancelled, .failed: + self.currentDeletingIndexPath = nil + self.deletingOffset = nil + default: + break + } + } + + func setup() { + let images: [UIImage] = [UIImage(bundleImageName: "Settings/test1")!, UIImage(bundleImageName: "Settings/test5")!, UIImage(bundleImageName: "Settings/test4")!, UIImage(bundleImageName: "Settings/test3")!, UIImage(bundleImageName: "Settings/test2")!] + for i in 0 ..< 5 { + let node = ASImageNode() + node.image = images[i] + + let containerNode = StackItemContainerNode(node: node) + containerNode.tapped = { [weak self] in + self?.animateIn(index: i) + } + containerNode.highlighted = { [weak self] highlighted in + self?.highlight(index: i, value: highlighted) + } + self.nodes.append(containerNode) + } + + var index: Int = 0 + let bounds = self.scrollNode.view.bounds + let itemCount = self.nodes.count + + for node in self.nodes { + self.scrollNode.addSubnode(node) + + let size = CGSize(width: self.frame.width, height: self.frame.height) + let frame = frameForIndex(index: index, size: size, itemCount: itemCount, bounds: bounds) + node.frame = frame + let transform = final3dTransform(for: frame.minY, size: frame.size, contentHeight: nil, itemCount: itemCount, bounds: bounds) + node.transform = transform + index += 1 + } + + if let lastFrame = self.nodes.last?.frame { + self.scrollNode.view.contentSize = CGSize(width: self.frame.width, height: lastFrame.minY) + } + } + + public func animateIn(index: Int) { + let node = self.nodes[index] + + self.animatingIn = true + self.scrollNode.view.isUserInteractionEnabled = false + node.animateIn() + UIView.animate(withDuration: 0.3) { + node.transform = CATransform3DIdentity + node.position = CGPoint(x: self.scrollNode.frame.width / 2.0, y: self.scrollNode.frame.height / 2.0) + } + + for i in 0 ..< index { + let node = self.nodes[i] + node.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -550.0), duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, mediaTimingFunction: nil, removeOnCompletion: false, additive: true, force: false, completion: nil) + } + + for i in (index + 1) ..< self.nodes.count { + let node = self.nodes[i] + node.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: 550.0), duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, mediaTimingFunction: nil, removeOnCompletion: false, additive: true, force: false, completion: nil) + } + } + + public func highlight(index: Int, value: Bool) { + let node = self.nodes[index] + + let bounds = self.scrollNode.view.bounds + let contentHeight = self.scrollNode.view.contentSize.height + let itemCount = self.nodes.count + + UIView.animate(withDuration: 0.4) { + let transform = final3dTransform(for: node.frame.minY, size: node.frame.size, contentHeight: contentHeight, itemCount: itemCount, additionalAngle: value ? 0.04 : nil, bounds: bounds) + node.transform = transform + } + } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + guard !self.animatingIn else { + return + } + self.updateLayout() + } + + func updateLayout() { + let bounds = self.scrollNode.view.bounds + let contentHeight = self.scrollNode.view.contentSize.height + let itemCount = self.nodes.count + + var index: Int = 0 + for node in self.nodes { + let initialTransform = final3dTransform(for: node.frame.minY, size: node.frame.size, contentHeight: contentHeight, itemCount: itemCount, bounds: bounds) + let initialFrame = frameForIndex(index: index, size: node.frame.size, itemCount: itemCount, bounds: bounds) + + var targetTransform: CATransform3D? + var targetPosition: CGPoint? + + var finalPosition = initialFrame.center + + if let deletingIndex = self.currentDeletingIndexPath, let offset = self.deletingOffset { + if deletingIndex == index { + finalPosition = CGPoint(x: self.frame.width / 2.0 + min(offset, 0.0), y: node.position.y) + } else if index < deletingIndex { + let frame = frameForIndex(index: index, size: node.frame.size, itemCount: itemCount - 1, bounds: bounds) + targetPosition = frame.center + + let spacing = interitemSpacing(itemCount: itemCount - 1, bounds: bounds) + targetTransform = final3dTransform(for: frame.minY, size: node.frame.size, contentHeight: contentHeight - node.frame.height - spacing, itemCount: itemCount - 1, bounds: bounds) + } else { + let frame = frameForIndex(index: index - 1, size: node.frame.size, itemCount: itemCount - 1, bounds: bounds) + targetPosition = frame.center + + let spacing = interitemSpacing(itemCount: itemCount - 1, bounds: bounds) + targetTransform = final3dTransform(for: frame.minY, size: node.frame.size, contentHeight: contentHeight - node.frame.height - spacing, itemCount: itemCount - 1, bounds: bounds) + } + } else { + node.position = initialFrame.center + } + + var finalTransform = initialTransform + if let targetTransform = targetTransform, let offset = self.deletingOffset { + let progress = min(1.0, abs(offset / (self.frame.width))) + finalTransform = initialTransform.interpolate(other: targetTransform, progress: progress) + } + + if let targetPosition = targetPosition, let offset = self.deletingOffset { + let progress = min(1.0, abs(offset / (self.frame.width))) + finalPosition = CGPoint(x: finalPosition.x + (targetPosition.x - finalPosition.x) * progress, y: finalPosition.y + (targetPosition.y - finalPosition.y) * progress) + } + + node.transform = finalTransform + node.position = finalPosition + + index += 1 + } + } + + public func update(size: CGSize) { + let hadValidLayout = self.validLayout != nil + self.validLayout = size + + self.scrollNode.frame = CGRect(origin: CGPoint(), size: size) + + if !hadValidLayout { + self.setup() + } + } +} diff --git a/submodules/BrowserUI/Sources/BrowserToolbar.swift b/submodules/BrowserUI/Sources/BrowserToolbar.swift new file mode 100644 index 0000000000..1c74f35507 --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserToolbar.swift @@ -0,0 +1,165 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramPresentationData +import AppBundle + +private func toolbarContentNode(for state: BrowserState, currentContentNode: BrowserToolbarContentNode?, layoutMetrics: LayoutMetrics, theme: BrowserToolbarTheme, strings: PresentationStrings, interaction: BrowserInteraction?) -> BrowserToolbarContentNode? { + guard case .compact = layoutMetrics.widthClass else { + return nil + } + if let _ = state.search { + if let currentContentNode = currentContentNode as? BrowserToolbarSearchContentNode { + currentContentNode.updateState(state) + return currentContentNode + } else { + return BrowserToolbarSearchContentNode(theme: theme, strings: strings, state: state, interaction: interaction) + } + } else { + if let currentContentNode = currentContentNode as? BrowserToolbarNavigationContentNode { + currentContentNode.updateState(state) + return currentContentNode + } else { + return BrowserToolbarNavigationContentNode(theme: theme, strings: strings, state: state, interaction: interaction) + } + } +} + +final class BrowserToolbarTheme { + let backgroundColor: UIColor + let separatorColor: UIColor + let buttonColor: UIColor + let disabledButtonColor: UIColor + + init(backgroundColor: UIColor, separatorColor: UIColor, buttonColor: UIColor, disabledButtonColor: UIColor) { + self.backgroundColor = backgroundColor + self.separatorColor = separatorColor + self.buttonColor = buttonColor + self.disabledButtonColor = disabledButtonColor + } +} + +protocol BrowserToolbarContentNode: ASDisplayNode { + init(theme: BrowserToolbarTheme, strings: PresentationStrings, state: BrowserState, interaction: BrowserInteraction?) + func updateState(_ state: BrowserState) + func updateTheme(_ theme: BrowserToolbarTheme) + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) +} + +private let toolbarHeight: CGFloat = 49.0 + +final class BrowserToolbar: ASDisplayNode { + private var theme: BrowserToolbarTheme + private let strings: PresentationStrings + private var state: BrowserState + var interaction: BrowserInteraction? + + private let containerNode: ASDisplayNode + private let separatorNode: ASDisplayNode + private var contentNode: BrowserToolbarContentNode? + + private var validLayout: (CGFloat, UIEdgeInsets, LayoutMetrics, CGFloat)? + + init(theme: BrowserToolbarTheme, strings: PresentationStrings, state: BrowserState) { + self.theme = theme + self.strings = strings + self.state = state + + self.containerNode = ASDisplayNode() + self.separatorNode = ASDisplayNode() + self.separatorNode.backgroundColor = theme.separatorColor + + super.init() + + self.clipsToBounds = true + self.containerNode.backgroundColor = theme.backgroundColor + + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.separatorNode) + } + + func updateState(_ state: BrowserState) { + self.state = state + if let (width, insets, layoutMetrics, collapseTransition) = self.validLayout { + let _ = self.updateLayout(width: width, insets: insets, layoutMetrics: layoutMetrics, collapseTransition: collapseTransition, transition: .animated(duration: 0.2, curve: .easeInOut)) + } + } + + func updateTheme(_ theme: BrowserToolbarTheme) { + guard self.theme !== theme else { + return + } + self.theme = theme + + self.containerNode.backgroundColor = theme.backgroundColor + self.separatorNode.backgroundColor = theme.separatorColor + self.contentNode?.updateTheme(theme) + } + + func updateLayout(width: CGFloat, insets: UIEdgeInsets, layoutMetrics: LayoutMetrics, collapseTransition: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize { + self.validLayout = (width, insets, layoutMetrics, collapseTransition) + + var dismissedContentNode: ASDisplayNode? + var immediatelyLayoutContentNodeAndAnimateAppearance = false + if let contentNode = toolbarContentNode(for: self.state, currentContentNode: self.contentNode, layoutMetrics: layoutMetrics, theme: self.theme, strings: self.strings, interaction: self.interaction) { + if contentNode !== self.contentNode { + dismissedContentNode = self.contentNode + immediatelyLayoutContentNodeAndAnimateAppearance = true + self.containerNode.insertSubnode(contentNode, belowSubnode: self.separatorNode) + self.contentNode = contentNode + } + } else { + dismissedContentNode = self.contentNode + self.contentNode = nil + } + + let effectiveCollapseTransition = self.contentNode == nil ? 1.0 : collapseTransition + + let height = toolbarHeight + insets.bottom + + let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: height * effectiveCollapseTransition), size: CGSize(width: width, height: height)) + transition.updateFrame(node: self.containerNode, frame: containerFrame) + + transition.updateFrame(node: self.separatorNode, frame: CGRect(x: 0.0, y: 0.0, width: width, height: UIScreenPixel)) + + let constrainedSize = CGSize(width: width - insets.left - insets.right, height: toolbarHeight) + + if let contentNode = self.contentNode { + let contentNodeFrame = CGRect(origin: CGPoint(x: insets.left, y: 0.0), size: constrainedSize) + contentNode.updateLayout(size: constrainedSize, transition: transition) + + if immediatelyLayoutContentNodeAndAnimateAppearance { + contentNode.frame = contentNodeFrame.offsetBy(dx: 0.0, dy: contentNodeFrame.height) + contentNode.alpha = 0.0 + } + + transition.updateFrame(node: contentNode, frame: contentNodeFrame) + transition.updateAlpha(node: contentNode, alpha: 1.0) + } + + if let dismissedContentNode = dismissedContentNode { + var frameCompleted = false + var alphaCompleted = false + let completed = { [weak self, weak dismissedContentNode] in + if let strongSelf = self, let dismissedContentNode = dismissedContentNode, strongSelf.contentNode === dismissedContentNode { + return + } + if frameCompleted && alphaCompleted { + dismissedContentNode?.removeFromSupernode() + } + } + let transitionTargetY = dismissedContentNode.frame.height + transition.updateFrame(node: dismissedContentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: transitionTargetY), size: dismissedContentNode.frame.size), completion: { _ in + frameCompleted = true + completed() + }) + + transition.updateAlpha(node: dismissedContentNode, alpha: 0.0, completion: { _ in + alphaCompleted = true + completed() + }) + } + return CGSize(width: width, height: height) + } +} diff --git a/submodules/BrowserUI/Sources/BrowserToolbarNavigationContentNode.swift b/submodules/BrowserUI/Sources/BrowserToolbarNavigationContentNode.swift new file mode 100644 index 0000000000..f99b3ba109 --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserToolbarNavigationContentNode.swift @@ -0,0 +1,103 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramPresentationData +import AppBundle + +final class BrowserToolbarNavigationContentNode: ASDisplayNode, BrowserToolbarContentNode { + private var theme: BrowserToolbarTheme + private var state: BrowserState + private var interaction: BrowserInteraction? + + private let backButton: HighlightableButtonNode + private let forwardButton: HighlightableButtonNode + private let shareButton: HighlightableButtonNode + private let minimizeButton: HighlightableButtonNode + + private var validLayout: CGSize? + + init(theme: BrowserToolbarTheme, strings: PresentationStrings, state: BrowserState, interaction: BrowserInteraction?) { + self.theme = theme + self.state = state + self.interaction = interaction + + self.backButton = HighlightableButtonNode() + self.backButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Back"), color: theme.buttonColor), for: []) + self.backButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Back"), color: theme.disabledButtonColor), for: [.disabled]) + self.forwardButton = HighlightableButtonNode() + self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Forward"), color: theme.buttonColor), for: []) + self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Forward"), color: theme.disabledButtonColor), for: [.disabled]) + self.shareButton = HighlightableButtonNode() + self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat List/NavigationShare"), color: theme.buttonColor), for: []) + self.minimizeButton = HighlightableButtonNode() + self.minimizeButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Instant View/Minimize"), color: theme.buttonColor), for: []) + + super.init() + + self.addSubnode(self.backButton) + self.addSubnode(self.forwardButton) + self.addSubnode(self.shareButton) + self.addSubnode(self.minimizeButton) + + self.backButton.isEnabled = false + self.forwardButton.isEnabled = false + + self.backButton.addTarget(self, action: #selector(self.backPressed), forControlEvents: .touchUpInside) + self.forwardButton.addTarget(self, action: #selector(self.forwardPressed), forControlEvents: .touchUpInside) + self.shareButton.addTarget(self, action: #selector(self.sharePressed), forControlEvents: .touchUpInside) + self.minimizeButton.addTarget(self, action: #selector(self.minimizePressed), forControlEvents: .touchUpInside) + } + + func updateState(_ state: BrowserState) { + self.state = state + + self.backButton.isEnabled = state.content?.canGoBack ?? false + self.forwardButton.isEnabled = state.content?.canGoForward ?? false + } + + func updateTheme(_ theme: BrowserToolbarTheme) { + guard self.theme !== theme else { + return + } + self.theme = theme + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + let isFirstLayout = self.validLayout == nil + self.validLayout = size + + var transition = transition + if isFirstLayout { + transition = .immediate + } + + let buttons = [self.backButton, self.forwardButton, self.shareButton, self.minimizeButton] + let sideInset: CGFloat = 5.0 + let buttonSize = CGSize(width: 50.0, height: size.height) + + let spacing: CGFloat = (size.width - buttonSize.width * CGFloat(buttons.count) - sideInset * 2.0) / CGFloat(buttons.count - 1) + var offset: CGFloat = sideInset + for button in buttons { + transition.updateFrame(node: button, frame: CGRect(origin: CGPoint(x: offset, y: 0.0), size: buttonSize)) + offset += buttonSize.width + spacing + } + } + + @objc private func backPressed() { + self.interaction?.navigateBack() + } + + @objc private func forwardPressed() { + self.interaction?.navigateForward() + } + + @objc private func sharePressed() { + self.interaction?.share() + } + + @objc private func minimizePressed() { + self.interaction?.minimize() + } +} + diff --git a/submodules/BrowserUI/Sources/BrowserToolbarSearchContentNode.swift b/submodules/BrowserUI/Sources/BrowserToolbarSearchContentNode.swift new file mode 100644 index 0000000000..443080dd19 --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserToolbarSearchContentNode.swift @@ -0,0 +1,95 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramPresentationData +import AppBundle + +final class BrowserToolbarSearchContentNode: ASDisplayNode, BrowserToolbarContentNode { + private var theme: BrowserToolbarTheme + private let strings: PresentationStrings + private var state: BrowserState + private var interaction: BrowserInteraction? + + private let upButton: HighlightableButtonNode + private let downButton: HighlightableButtonNode + private let resultsNode: ImmediateTextNode + + private var validLayout: CGSize? + + init(theme: BrowserToolbarTheme, strings: PresentationStrings, state: BrowserState, interaction: BrowserInteraction?) { + self.theme = theme + self.strings = strings + self.state = state + self.interaction = interaction + + self.upButton = HighlightableButtonNode() + self.upButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/UpButton"), color: theme.buttonColor), for: .normal) + self.upButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/UpButton"), color: theme.disabledButtonColor), for: .disabled) + self.downButton = HighlightableButtonNode() + self.downButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/DownButton"), color: theme.buttonColor), for: .normal) + self.downButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/DownButton"), color: theme.disabledButtonColor), for: .disabled) + self.resultsNode = ImmediateTextNode() + + super.init() + + self.addSubnode(self.upButton) + self.addSubnode(self.downButton) + self.addSubnode(self.resultsNode) + + self.upButton.addTarget(self, action: #selector(self.upPressed), forControlEvents: .touchUpInside) + self.downButton.addTarget(self, action: #selector(self.downPressed), forControlEvents: .touchUpInside) + } + + func updateState(_ state: BrowserState) { + self.state = state + + if let size = self.validLayout { + self.updateLayout(size: size, transition: .immediate) + } + } + + func updateTheme(_ theme: BrowserToolbarTheme) { + guard self.theme !== theme else { + return + } + self.theme = theme + + self.upButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/UpButton"), color: theme.buttonColor), for: .normal) + self.upButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/UpButton"), color: theme.disabledButtonColor), for: .disabled) + self.downButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/DownButton"), color: theme.buttonColor), for: .normal) + self.downButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/DownButton"), color: theme.disabledButtonColor), for: .disabled) + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = size + + let buttonSize = CGSize(width: 40.0, height: size.height) + + let resultsText: String + if let results = self.state.search?.results { + if results.1 > 0 { + resultsText = self.strings.Items_NOfM("\(results.0 + 1)", "\(results.1)").string + } else { + resultsText = self.strings.Conversation_SearchNoResults + } + } else { + resultsText = "" + } + + self.resultsNode.attributedText = NSAttributedString(string: resultsText, font: Font.regular(15.0), textColor: self.theme.buttonColor, paragraphAlignment: .natural) + let resultsSize = self.resultsNode.updateLayout(size) + self.resultsNode.frame = CGRect(origin: CGPoint(x: size.width - 48.0 - 43.0 - resultsSize.width - 12.0, y: floor((size.height - resultsSize.height) / 2.0)), size: resultsSize) + + self.downButton.frame = CGRect(origin: CGPoint(x: size.width - 48.0, y: 0.0), size: buttonSize) + self.upButton.frame = CGRect(origin: CGPoint(x: size.width - 48.0 - 43.0, y: 0.0), size: buttonSize) + } + + @objc private func upPressed() { + self.interaction?.scrollToPreviousSearchResult() + } + + @objc private func downPressed() { + self.interaction?.scrollToNextSearchResult() + } +} diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift new file mode 100644 index 0000000000..758a0f50cc --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -0,0 +1,209 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import TelegramCore +import Postbox +import SwiftSignalKit +import Display +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext +import WebKit +import AppBundle + +final class BrowserWebContent: ASDisplayNode, BrowserContent { + private let webView: WKWebView + + private var _state: BrowserContentState + private let statePromise: Promise + + var state: Signal { + return self.statePromise.get() + } + + init(url: String) { + let configuration = WKWebViewConfiguration() + + self.webView = WKWebView(frame: CGRect(), configuration: configuration) + if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { + self.webView.allowsLinkPreview = false + } + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.webView.scrollView.contentInsetAdjustmentBehavior = .never + } + + var title: String = "" + if let parsedUrl = URL(string: url) { + let request = URLRequest(url: parsedUrl) + self.webView.load(request) + + title = parsedUrl.host ?? "" + } + + self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, isInstant: false) + self.statePromise = Promise(self._state) + + super.init() + + self.webView.allowsBackForwardNavigationGestures = true + self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.title), options: [], context: nil) + self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.url), options: [], context: nil) + self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: [], context: nil) + self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.canGoBack), options: [], context: nil) + self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.canGoForward), options: [], context: nil) + } + + deinit { + self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.title)) + self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.url)) + self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress)) + self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.canGoBack)) + self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.canGoForward)) + } + + override func didLoad() { + super.didLoad() + + self.view.addSubview(self.webView) + } + + func setFontSize(_ fontSize: CGFloat) { + let js = "document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust='\(Int(fontSize * 100.0))%'" + self.webView.evaluateJavaScript(js, completionHandler: nil) + } + + func setForceSerif(_ force: Bool) { + let js: String + if force { + js = "document.getElementsByTagName(\'body\')[0].style.fontFamily = 'Georgia, serif';" + } else { + js = "document.getElementsByTagName(\'body\')[0].style.fontFamily = '\"Lucida Grande\", \"Lucida Sans Unicode\", Arial, Helvetica, Verdana, sans-serif';" + } + self.webView.evaluateJavaScript(js) { _, _ in + } + } + + private var didSetupSearch = false + private func setupSearch(completion: @escaping () -> Void) { + guard !self.didSetupSearch else { + completion() + return + } + + let bundle = getAppBundle() + guard let scriptPath = bundle.path(forResource: "UIWebViewSearch", ofType: "js") else { + return + } + guard let scriptData = try? Data(contentsOf: URL(fileURLWithPath: scriptPath)) else { + return + } + guard let script = String(data: scriptData, encoding: .utf8) else { + return + } + self.didSetupSearch = true + self.webView.evaluateJavaScript(script, completionHandler: { _, error in + if error != nil { + print() + } + completion() + }) + } + + var previousQuery: String? + func setSearch(_ query: String?, completion: ((Int) -> Void)?) { + guard self.previousQuery != query else { + return + } + self.previousQuery = query + self.setupSearch { [weak self] in + if let query = query { + let js = "uiWebview_HighlightAllOccurencesOfString('\(query)')" + self?.webView.evaluateJavaScript(js, completionHandler: { [weak self] _, _ in + let js = "uiWebview_SearchResultCount" + self?.webView.evaluateJavaScript(js, completionHandler: { [weak self] result, _ in + if let result = result as? NSNumber { + self?.searchResultsCount = result.intValue + completion?(result.intValue) + } else { + completion?(0) + } + }) + }) + } else { + let js = "uiWebview_RemoveAllHighlights()" + self?.webView.evaluateJavaScript(js, completionHandler: nil) + + self?.currentSearchResult = 0 + self?.searchResultsCount = 0 + } + } + } + + private var currentSearchResult: Int = 0 + private var searchResultsCount: Int = 0 + + func scrollToPreviousSearchResult(completion: ((Int, Int) -> Void)?) { + let searchResultsCount = self.searchResultsCount + var index = self.currentSearchResult - 1 + if index < 0 { + index = searchResultsCount - 1 + } + self.currentSearchResult = index + + let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')" + self.webView.evaluateJavaScript(js, completionHandler: { _, _ in + completion?(index, searchResultsCount) + }) + } + + func scrollToNextSearchResult(completion: ((Int, Int) -> Void)?) { + let searchResultsCount = self.searchResultsCount + var index = self.currentSearchResult + 1 + if index >= searchResultsCount { + index = 0 + } + self.currentSearchResult = index + + let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')" + self.webView.evaluateJavaScript(js, completionHandler: { _, _ in + completion?(index, searchResultsCount) + }) + } + + func navigateBack() { + self.webView.goBack() + } + + func navigateForward() { + self.webView.goForward() + } + + func scrollToTop() { + self.webView.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.webView.scrollView.contentInset.top), animated: true) + } + + func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) { + transition.updateFrame(view: self.webView, frame: CGRect(origin: CGPoint(x: 0.0, y: 56.0), size: CGSize(width: size.width, height: size.height - 56.0))) + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + let updateState: ((BrowserContentState) -> BrowserContentState) -> Void = { f in + let updated = f(self._state) + self._state = updated + self.statePromise.set(.single(self._state)) + } + + if keyPath == "title" { + updateState { $0.withUpdatedTitle(self.webView.title ?? "") } + } else if keyPath == "url" { + updateState { $0.withUpdatedUrl(self.webView.url?.absoluteString ?? "") } + self.didSetupSearch = false + } else if keyPath == "estimatedProgress" { + updateState { $0.withUpdatedEstimatedProgress(self.webView.estimatedProgress) } + } else if keyPath == "canGoBack" { + updateState { $0.withUpdatedCanGoBack(self.webView.canGoBack) } + } else if keyPath == "canGoForward" { + updateState { $0.withUpdatedCanGoForward(self.webView.canGoForward) } + } + } +} diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 9f77b07889..ccc1b70d52 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -979,24 +979,31 @@ public final class ChatListNode: ListView { guard !filter.contains(.excludeSecretChats) || peer.peerId.namespace != Namespaces.Peer.SecretChat else { return false } guard !filter.contains(.onlyPrivateChats) || peer.peerId.namespace == Namespaces.Peer.CloudUser else { return false } - if filter.contains(.onlyGroups) { - var isGroup: Bool = false - if case let .channel(peer) = peer.chatMainPeer, case .group = peer.info { - isGroup = true - } else if peer.peerId.namespace == Namespaces.Peer.CloudGroup { - isGroup = true - } - if !isGroup { - return false - } - } - - if filter.contains(.onlyChannels) { - if case let .channel(peer) = peer.chatMainPeer, case .broadcast = peer.info { - return true + if filter.contains(.onlyGroupsAndChannels) { + if case .channel = peer.chatMainPeer { + } else if case .legacyGroup = peer.chatMainPeer { } else { return false } + } else { + if filter.contains(.onlyGroups) { + var isGroup: Bool = false + if case let .channel(peer) = peer.chatMainPeer, case .group = peer.info { + isGroup = true + } else if peer.peerId.namespace == Namespaces.Peer.CloudGroup { + isGroup = true + } + if !isGroup { + return false + } + } + + if filter.contains(.onlyChannels) { + if case let .channel(peer) = peer.chatMainPeer, case .broadcast = peer.info { + } else { + return false + } + } } if filter.contains(.excludeChannels) { @@ -1013,6 +1020,28 @@ public final class ChatListNode: ListView { return false } } + + if filter.contains(.onlyManageable) && filter.contains(.excludeDisabled) { + if let peer = peer.peers[peer.peerId] { + var canManage = false + if case let .legacyGroup(peer) = peer { + switch peer.role { + case .creator, .admin: + canManage = true + default: + break + } + } + + if canManage { + } else if case let .channel(peer) = peer, case .group = peer.info, peer.hasPermission(.inviteMembers) { + } else { + return false + } + } else { + return false + } + } return true } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index ef11865c02..742ce753e6 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import Postbox import TelegramCore import TelegramPresentationData import MergeLists @@ -432,8 +433,7 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState result.append(.HeaderEntry) } - if !view.hasLater, case let .peers(_, _, additionalCategories, - _) = mode { + if !view.hasLater, case let .peers(_, _, additionalCategories, _) = mode { var index = 0 for category in additionalCategories.reversed(){ result.append(.AdditionalCategory(index: index, id: category.id, title: category.title, image: category.icon, appearance: category.appearance, selected: state.selectedAdditionalCategoryIds.contains(category.id), presentationData: state.presentationData)) diff --git a/submodules/ComponentFlow/Source/Components/VStack.swift b/submodules/ComponentFlow/Source/Components/VStack.swift new file mode 100644 index 0000000000..1f92bf8b21 --- /dev/null +++ b/submodules/ComponentFlow/Source/Components/VStack.swift @@ -0,0 +1,8 @@ +// +// VStack.swift +// _idx_ComponentFlow_0C0A11F8_ios_min9.0 +// +// Created by Ilya Laktyushin on 15.03.2022. +// + +import Foundation diff --git a/submodules/ItemListUI/Sources/ItemListControllerFooterItem.swift b/submodules/ItemListUI/Sources/ItemListControllerFooterItem.swift new file mode 100644 index 0000000000..9b0d4a0fa0 --- /dev/null +++ b/submodules/ItemListUI/Sources/ItemListControllerFooterItem.swift @@ -0,0 +1,19 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display + +public protocol ItemListControllerFooterItem { + func isEqual(to: ItemListControllerFooterItem) -> Bool + func node(current: ItemListControllerFooterItemNode?) -> ItemListControllerFooterItemNode +} + +open class ItemListControllerFooterItemNode: ASDisplayNode { + open func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat { + return 0.0 + } + + open func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) { + + } +} diff --git a/submodules/ItemListUI/Sources/ItemListControllerNode.swift b/submodules/ItemListUI/Sources/ItemListControllerNode.swift index 5e22f4f52e..693fd63e18 100644 --- a/submodules/ItemListUI/Sources/ItemListControllerNode.swift +++ b/submodules/ItemListUI/Sources/ItemListControllerNode.swift @@ -127,6 +127,7 @@ private struct ItemListNodeTransition { let emptyStateItem: ItemListControllerEmptyStateItem? let searchItem: ItemListControllerSearch? let toolbarItem: ItemListToolbarItem? + let footerItem: ItemListControllerFooterItem? let focusItemTag: ItemListItemTag? let ensureVisibleItemTag: ItemListItemTag? let scrollToItem: ListViewScrollToItem? @@ -145,6 +146,7 @@ public final class ItemListNodeState { let emptyStateItem: ItemListControllerEmptyStateItem? let searchItem: ItemListControllerSearch? let toolbarItem: ItemListToolbarItem? + let footerItem: ItemListControllerFooterItem? let animateChanges: Bool let crossfadeState: Bool let scrollEnabled: Bool @@ -152,13 +154,14 @@ public final class ItemListNodeState { let ensureVisibleItemTag: ItemListItemTag? let initialScrollToItem: ListViewScrollToItem? - public init(presentationData: ItemListPresentationData, entries: [T], style: ItemListStyle, focusItemTag: ItemListItemTag? = nil, ensureVisibleItemTag: ItemListItemTag? = nil, emptyStateItem: ItemListControllerEmptyStateItem? = nil, searchItem: ItemListControllerSearch? = nil, toolbarItem: ItemListToolbarItem? = nil, initialScrollToItem: ListViewScrollToItem? = nil, crossfadeState: Bool = false, animateChanges: Bool = true, scrollEnabled: Bool = true) { + public init(presentationData: ItemListPresentationData, entries: [T], style: ItemListStyle, focusItemTag: ItemListItemTag? = nil, ensureVisibleItemTag: ItemListItemTag? = nil, emptyStateItem: ItemListControllerEmptyStateItem? = nil, searchItem: ItemListControllerSearch? = nil, toolbarItem: ItemListToolbarItem? = nil, footerItem: ItemListControllerFooterItem? = nil, initialScrollToItem: ListViewScrollToItem? = nil, crossfadeState: Bool = false, animateChanges: Bool = true, scrollEnabled: Bool = true) { self.presentationData = presentationData self.entries = entries.map { $0 } self.style = style self.emptyStateItem = emptyStateItem self.searchItem = searchItem self.toolbarItem = toolbarItem + self.footerItem = footerItem self.crossfadeState = crossfadeState self.animateChanges = animateChanges self.focusItemTag = focusItemTag @@ -248,6 +251,9 @@ open class ItemListControllerNode: ASDisplayNode { private var toolbarItem: ItemListToolbarItem? + private var footerItem: ItemListControllerFooterItem? + private var footerItemNode: ItemListControllerFooterItemNode? + private let transitionDisposable = MetaDisposable() private var enqueuedTransitions: [ItemListNodeTransition] = [] @@ -338,6 +344,10 @@ open class ItemListControllerNode: ASDisplayNode { self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in self?.visibleBottomContentOffsetChanged?(offset) + + if let strongSelf = self { + strongSelf.updateFooterBackgroundAlpha() + } } self.listNode.visibleContentOffsetChanged = { [weak self] offset in @@ -415,7 +425,7 @@ open class ItemListControllerNode: ASDisplayNode { scrollToItem = state.initialScrollToItem } - return ItemListNodeTransition(theme: presentationData.theme, strings: presentationData.strings, entries: transition, updateStyle: updatedStyle, emptyStateItem: state.emptyStateItem, searchItem: state.searchItem, toolbarItem: state.toolbarItem, focusItemTag: state.focusItemTag, ensureVisibleItemTag: state.ensureVisibleItemTag, scrollToItem: scrollToItem, firstTime: previous == nil, animated: previous != nil && state.animateChanges, animateAlpha: previous != nil && state.animateChanges, crossfade: state.crossfadeState, mergedEntries: state.entries, scrollEnabled: state.scrollEnabled) + return ItemListNodeTransition(theme: presentationData.theme, strings: presentationData.strings, entries: transition, updateStyle: updatedStyle, emptyStateItem: state.emptyStateItem, searchItem: state.searchItem, toolbarItem: state.toolbarItem, footerItem: state.footerItem, focusItemTag: state.focusItemTag, ensureVisibleItemTag: state.ensureVisibleItemTag, scrollToItem: scrollToItem, firstTime: previous == nil, animated: previous != nil && state.animateChanges, animateAlpha: previous != nil && state.animateChanges, crossfade: state.crossfadeState, mergedEntries: state.entries, scrollEnabled: state.scrollEnabled) }) |> deliverOnMainQueue).start(next: { [weak self] transition in if let strongSelf = self { @@ -466,6 +476,20 @@ open class ItemListControllerNode: ASDisplayNode { }) } + func updateFooterBackgroundAlpha() { + guard let footerItemNode = self.footerItemNode else { + return + } + + switch self.listNode.visibleBottomContentOffset() { + case let .known(value): + let backgroundAlpha: CGFloat = min(30.0, value) / 30.0 + footerItemNode.updateBackgroundAlpha(backgroundAlpha, transition: .immediate) + case .unknown, .none: + footerItemNode.updateBackgroundAlpha(1.0, transition: .immediate) + } + } + open func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, additionalInsets: UIEdgeInsets) { var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight @@ -539,6 +563,11 @@ open class ItemListControllerNode: ASDisplayNode { } } + if let footerItemNode = self.footerItemNode { + let footerHeight = footerItemNode.updateLayout(layout: layout, transition: transition) + insets.bottom += footerHeight + } + self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) @@ -826,11 +855,38 @@ open class ItemListControllerNode: ASDisplayNode { self.emptyStateNode = nil } } + var updateFooterItem = false + if let footerItem = self.footerItem, let updatedFooterItem = transition.footerItem { + updateFooterItem = !footerItem.isEqual(to: updatedFooterItem) + } else if (self.footerItem != nil) != (transition.footerItem != nil) { + updateFooterItem = true + } + if updateFooterItem { + self.footerItem = transition.footerItem + if let footerItem = transition.footerItem { + let updatedNode = footerItem.node(current: self.footerItemNode) + if let footerItemNode = self.footerItemNode, updatedNode !== footerItemNode { + footerItemNode.removeFromSupernode() + } + if self.footerItemNode !== updatedNode { + self.footerItemNode = updatedNode + if let validLayout = self.validLayout { + let _ = updatedNode.updateLayout(layout: validLayout.0, transition: .immediate) + } + self.addSubnode(updatedNode) + } + } else if let footerItemNode = self.footerItemNode { + footerItemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak footerItemNode] _ in + footerItemNode?.removeFromSupernode() + }) + self.footerItemNode = nil + } + } self.listNode.scrollEnabled = transition.scrollEnabled if updateSearchItem { self.requestLayout?(.animated(duration: 0.3, curve: .spring)) - } else if updateToolbarItem, let (layout, navigationBarHeight, additionalInsets) = self.validLayout { + } else if updateToolbarItem || updateFooterItem, let (layout, navigationBarHeight, additionalInsets) = self.validLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .spring), additionalInsets: additionalInsets) } } diff --git a/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift b/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift index 82b365e02f..326474c424 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift @@ -439,10 +439,14 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { } override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + self.layer.allowsGroupOpacity = true + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4, completion: { [weak self] _ in + self?.layer.allowsGroupOpacity = false + }) } override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.allowsGroupOpacity = true self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } diff --git a/submodules/ItemListUI/Sources/Items/ItemListTextItem.swift b/submodules/ItemListUI/Sources/Items/ItemListTextItem.swift index f2594cef95..a96c38e4d7 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListTextItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListTextItem.swift @@ -23,15 +23,17 @@ public class ItemListTextItem: ListViewItem, ItemListItem { public let sectionId: ItemListSectionId let linkAction: ((ItemListTextItemLinkAction) -> Void)? let style: ItemListStyle + let trimBottomInset: Bool public let isAlwaysPlain: Bool = true public let tag: ItemListItemTag? - public init(presentationData: ItemListPresentationData, text: ItemListTextItemText, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil, style: ItemListStyle = .blocks, tag: ItemListItemTag? = nil) { + public init(presentationData: ItemListPresentationData, text: ItemListTextItemText, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil, style: ItemListStyle = .blocks, tag: ItemListItemTag? = nil, trimBottomInset: Bool = false) { self.presentationData = presentationData self.text = text self.sectionId = sectionId self.linkAction = linkAction self.style = style + self.trimBottomInset = trimBottomInset self.tag = tag } @@ -141,6 +143,10 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { } contentSize = CGSize(width: params.width, height: titleLayout.size.height + topInset + bottomInset) + if item.trimBottomInset { + insets.bottom -= 44.0 + } + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) return (layout, { [weak self] in diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 381c666358..7fa0134d7d 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -128,6 +128,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { public var presentWebSearch: (MediaGroupsScreen) -> Void = { _ in } public var getCaptionPanelView: () -> TGCaptionPanelView? = { return nil } + private var completed = false public var legacyCompletion: (_ signals: [Any], _ silently: Bool, _ scheduleTime: Int32?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void = { _, _, _, _, _ in } public var requestAttachmentMenuExpansion: () -> Void = { } @@ -671,10 +672,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } fileprivate func send(asFile: Bool = false, silently: Bool, scheduleTime: Int32?, animated: Bool, completion: @escaping () -> Void) { - self.controller?.dismissAllTooltips() + guard let controller = self.controller, !controller.completed else { + return + } + controller.dismissAllTooltips() var hasHeic = false - let allItems = self.controller?.interaction?.selectionState?.selectedItems() ?? [] + let allItems = controller.interaction?.selectionState?.selectedItems() ?? [] for item in allItems { if item is TGCameraCapturedVideo { } else if let asset = item as? TGMediaAsset, asset.uniformTypeIdentifier.contains("heic") { @@ -684,10 +688,11 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } let proceed: (Bool) -> Void = { convertToJpeg in - guard let signals = TGMediaAssetsController.resultSignals(for: self.controller?.interaction?.selectionState, editingContext: self.controller?.interaction?.editingState, intent: asFile ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent, currentItem: nil, storeAssets: true, convertToJpeg: convertToJpeg, descriptionGenerator: legacyAssetPickerItemGenerator(), saveEditedPhotos: true) else { + guard let signals = TGMediaAssetsController.resultSignals(for: controller.interaction?.selectionState, editingContext: controller.interaction?.editingState, intent: asFile ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent, currentItem: nil, storeAssets: true, convertToJpeg: convertToJpeg, descriptionGenerator: legacyAssetPickerItemGenerator(), saveEditedPhotos: true) else { return } - self.controller?.legacyCompletion(signals, silently, scheduleTime, { [weak self] identifier in + controller.completed = true + controller.legacyCompletion(signals, silently, scheduleTime, { [weak self] identifier in return !asFile ? self?.getItemSnapshot(identifier) : nil }, { [weak self] in completion() @@ -696,7 +701,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } if asFile && hasHeic { - self.controller?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.MediaPicker_JpegConversionText, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.MediaPicker_KeepHeic, action: { + controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.MediaPicker_JpegConversionText, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.MediaPicker_KeepHeic, action: { proceed(false) }), TextAlertAction(type: .genericAction, title: self.presentationData.strings.MediaPicker_ConvertToJpeg, action: { proceed(true) diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminAddBotFooterItem.swift b/submodules/PeerInfoUI/Sources/ChannelAdminAddBotFooterItem.swift new file mode 100644 index 0000000000..49e9d48a91 --- /dev/null +++ b/submodules/PeerInfoUI/Sources/ChannelAdminAddBotFooterItem.swift @@ -0,0 +1,125 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramPresentationData +import ItemListUI +import PresentationDataUtils +import SolidRoundedButtonNode + +final class ChannelAdminAddBotFooterItem: ItemListControllerFooterItem { + let theme: PresentationTheme + let title: String + let action: () -> Void + + init(theme: PresentationTheme, title: String, action: @escaping () -> Void) { + self.theme = theme + self.title = title + self.action = action + } + + func isEqual(to: ItemListControllerFooterItem) -> Bool { + if let item = to as? ChannelAdminAddBotFooterItem { + return self.theme === item.theme && self.title == item.title + } else { + return false + } + } + + func node(current: ItemListControllerFooterItemNode?) -> ItemListControllerFooterItemNode { + if let current = current as? ChannelAdminAddBotFooterItemNode { + current.item = self + return current + } else { + return ChannelAdminAddBotFooterItemNode(item: self) + } + } +} + +final class ChannelAdminAddBotFooterItemNode: ItemListControllerFooterItemNode { + private let backgroundNode: NavigationBackgroundNode + private let separatorNode: ASDisplayNode + private let buttonNode: SolidRoundedButtonNode + + private var validLayout: ContainerViewLayout? + + var item: ChannelAdminAddBotFooterItem { + didSet { + self.updateItem() + if let layout = self.validLayout { + let _ = self.updateLayout(layout: layout, transition: .immediate) + } + } + } + + init(item: ChannelAdminAddBotFooterItem) { + self.item = item + + self.backgroundNode = NavigationBackgroundNode(color: item.theme.rootController.tabBar.backgroundColor) + self.separatorNode = ASDisplayNode() + + self.buttonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: .black, foregroundColor: .white), height: 50.0, cornerRadius: 12.0) + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.separatorNode) + self.addSubnode(self.buttonNode) + + self.updateItem() + } + + private func updateItem() { + self.backgroundNode.updateColor(color: self.item.theme.rootController.tabBar.backgroundColor, transition: .immediate) + self.separatorNode.backgroundColor = self.item.theme.rootController.tabBar.separatorColor + self.buttonNode.updateTheme(SolidRoundedButtonTheme(backgroundColor: self.item.theme.list.itemCheckColors.fillColor, foregroundColor: self.item.theme.list.itemCheckColors.foregroundColor)) + self.buttonNode.title = self.item.title + + self.buttonNode.pressed = { [weak self] in + self?.item.action() + } + } + + override func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) { + transition.updateAlpha(node: self.backgroundNode, alpha: alpha) + transition.updateAlpha(node: self.separatorNode, alpha: alpha) + } + + override func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat { + self.validLayout = layout + + let buttonInset: CGFloat = 16.0 + let buttonWidth = layout.size.width - layout.safeInsets.left - layout.safeInsets.right - buttonInset * 2.0 + let buttonHeight = self.buttonNode.updateLayout(width: buttonWidth, transition: transition) + let inset: CGFloat = 9.0 + + let insets = layout.insets(options: [.input]) + + var panelHeight: CGFloat = buttonHeight + inset * 2.0 + let totalPanelHeight: CGFloat + if let inputHeight = layout.inputHeight, inputHeight > 0.0 { + totalPanelHeight = panelHeight + insets.bottom + } else { + panelHeight += insets.bottom + totalPanelHeight = panelHeight + } + + let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - totalPanelHeight), size: CGSize(width: layout.size.width, height: panelHeight)) + transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + buttonInset, y: panelFrame.minY + inset), size: CGSize(width: buttonWidth, height: buttonHeight))) + + transition.updateFrame(node: self.backgroundNode, frame: panelFrame) + self.backgroundNode.update(size: panelFrame.size, transition: transition) + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: panelFrame.origin, size: CGSize(width: panelFrame.width, height: UIScreenPixel))) + + return panelHeight + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + if self.backgroundNode.frame.contains(point) { + return true + } else { + return false + } + } +} diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift index 21d8807251..a9a07c0095 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift @@ -13,11 +13,13 @@ import PresentationDataUtils import ItemListAvatarAndNameInfoItem import Emoji import LocalizedPeerData +import Markdown private let rankMaxLength: Int32 = 16 private final class ChannelAdminControllerArguments { let context: AccountContext + let updateAdminRights: (Bool) -> Void let toggleRight: (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void let toggleRightWhileDisabled: (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void let transferOwnership: () -> Void @@ -27,8 +29,9 @@ private final class ChannelAdminControllerArguments { let dismissInput: () -> Void let animateError: () -> Void - init(context: AccountContext, toggleRight: @escaping (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void, toggleRightWhileDisabled: @escaping (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void, transferOwnership: @escaping () -> Void, updateRank: @escaping (String, String) -> Void, updateFocusedOnRank: @escaping (Bool) -> Void, dismissAdmin: @escaping () -> Void, dismissInput: @escaping () -> Void, animateError: @escaping () -> Void) { + init(context: AccountContext, updateAdminRights: @escaping (Bool) -> Void, toggleRight: @escaping (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void, toggleRightWhileDisabled: @escaping (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void, transferOwnership: @escaping () -> Void, updateRank: @escaping (String, String) -> Void, updateFocusedOnRank: @escaping (Bool) -> Void, dismissAdmin: @escaping () -> Void, dismissInput: @escaping () -> Void, animateError: @escaping () -> Void) { self.context = context + self.updateAdminRights = updateAdminRights self.toggleRight = toggleRight self.toggleRightWhileDisabled = toggleRightWhileDisabled self.transferOwnership = transferOwnership @@ -43,6 +46,7 @@ private final class ChannelAdminControllerArguments { private enum ChannelAdminSection: Int32 { case info case rank + case adminRights case rights case transfer case dismiss @@ -65,6 +69,7 @@ private enum ChannelAdminEntryStableId: Hashable { case rankTitle case rank case rankInfo + case adminRights case rightsTitle case right(TelegramChatAdminRightsFlags) case addAdminsInfo @@ -77,6 +82,7 @@ private enum ChannelAdminEntry: ItemListNodeEntry { case rankTitle(PresentationTheme, String, Int32?, Int32) case rank(PresentationTheme, PresentationStrings, String, String, Bool) case rankInfo(PresentationTheme, String) + case adminRights(PresentationTheme, String, Bool) case rightsTitle(PresentationTheme, String) case rightItem(PresentationTheme, Int, String, TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags, Bool, Bool) case addAdminsInfo(PresentationTheme, String) @@ -89,6 +95,8 @@ private enum ChannelAdminEntry: ItemListNodeEntry { return ChannelAdminSection.info.rawValue case .rankTitle, .rank, .rankInfo: return ChannelAdminSection.rank.rawValue + case .adminRights: + return ChannelAdminSection.adminRights.rawValue case .rightsTitle, .rightItem, .addAdminsInfo: return ChannelAdminSection.rights.rawValue case .transfer: @@ -108,6 +116,8 @@ private enum ChannelAdminEntry: ItemListNodeEntry { return .rank case .rankInfo: return .rankInfo + case .adminRights: + return .adminRights case .rightsTitle: return .rightsTitle case let .rightItem(_, _, _, right, _, _, _): @@ -163,6 +173,12 @@ private enum ChannelAdminEntry: ItemListNodeEntry { } else { return false } + case let .adminRights(lhsTheme, lhsText, lhsValue): + if case let .adminRights(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } case let .rightsTitle(lhsTheme, lhsText): if case let .rightsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -226,16 +242,23 @@ private enum ChannelAdminEntry: ItemListNodeEntry { default: return true } + case .adminRights: + switch rhs { + case .info, .adminRights: + return false + default: + return true + } case .rightsTitle: switch rhs { - case .info, .rightsTitle: + case .info, .adminRights, .rightsTitle: return false default: return true } case let .rightItem(_, lhsIndex, _, _, _, _, _): switch rhs { - case .info, .rightsTitle: + case .info, .adminRights, .rightsTitle: return false case let .rightItem(_, rhsIndex, _, _, _, _, _): return lhsIndex < rhsIndex @@ -244,35 +267,35 @@ private enum ChannelAdminEntry: ItemListNodeEntry { } case .addAdminsInfo: switch rhs { - case .info, .rightsTitle, .rightItem, .addAdminsInfo: + case .info, .adminRights, .rightsTitle, .rightItem, .addAdminsInfo: return false default: return true } case .transfer: switch rhs { - case .info, .rightsTitle, .rightItem, .addAdminsInfo, .transfer: + case .info, .adminRights, .rightsTitle, .rightItem, .addAdminsInfo, .transfer: return false default: return true } case .rankTitle: switch rhs { - case .info, .rightsTitle, .rightItem, .addAdminsInfo, .transfer, .rankTitle: + case .info, .adminRights, .rightsTitle, .rightItem, .addAdminsInfo, .transfer, .rankTitle: return false default: return true } case .rank: switch rhs { - case .info, .rightsTitle, .rightItem, .addAdminsInfo, .transfer, .rankTitle, .rank: + case .info, .adminRights, .rightsTitle, .rightItem, .addAdminsInfo, .transfer, .rankTitle, .rank: return false default: return true } case .rankInfo: switch rhs { - case .info, .rightsTitle, .rightItem, .addAdminsInfo, .transfer, .rankTitle, .rank, .rankInfo: + case .info, .adminRights, .rightsTitle, .rightItem, .addAdminsInfo, .transfer, .rankTitle, .rank, .rankInfo: return false default: return true @@ -310,7 +333,12 @@ private enum ChannelAdminEntry: ItemListNodeEntry { arguments.dismissInput() }) case let .rankInfo(_, text): - return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section, trimBottomInset: true) + case let .adminRights(_, text, value): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, type: .regular, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + arguments.updateAdminRights(value) + }, activatedWhileDisabled: { + }) case let .rightsTitle(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .rightItem(_, _, text, right, flags, value, enabled): @@ -334,12 +362,14 @@ private enum ChannelAdminEntry: ItemListNodeEntry { } private struct ChannelAdminControllerState: Equatable { + let adminRights: Bool let updatedFlags: TelegramChatAdminRightsFlags? let updatedRank: String? let updating: Bool let focusedOnRank: Bool - init(updatedFlags: TelegramChatAdminRightsFlags? = nil, updatedRank: String? = nil, updating: Bool = false, focusedOnRank: Bool = false) { + init(adminRights: Bool = true, updatedFlags: TelegramChatAdminRightsFlags? = nil, updatedRank: String? = nil, updating: Bool = false, focusedOnRank: Bool = false) { + self.adminRights = adminRights self.updatedFlags = updatedFlags self.updatedRank = updatedRank self.updating = updating @@ -347,6 +377,9 @@ private struct ChannelAdminControllerState: Equatable { } static func ==(lhs: ChannelAdminControllerState, rhs: ChannelAdminControllerState) -> Bool { + if lhs.adminRights != rhs.adminRights { + return false + } if lhs.updatedFlags != rhs.updatedFlags { return false } @@ -362,20 +395,24 @@ private struct ChannelAdminControllerState: Equatable { return true } + func withUpdatedAdminRights(_ adminRights: Bool) -> ChannelAdminControllerState { + return ChannelAdminControllerState(adminRights: adminRights, updatedFlags: self.updatedFlags, updatedRank: self.updatedRank, updating: self.updating, focusedOnRank: self.focusedOnRank) + } + func withUpdatedUpdatedFlags(_ updatedFlags: TelegramChatAdminRightsFlags?) -> ChannelAdminControllerState { - return ChannelAdminControllerState(updatedFlags: updatedFlags, updatedRank: self.updatedRank, updating: self.updating, focusedOnRank: self.focusedOnRank) + return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: updatedFlags, updatedRank: self.updatedRank, updating: self.updating, focusedOnRank: self.focusedOnRank) } func withUpdatedUpdatedRank(_ updatedRank: String?) -> ChannelAdminControllerState { - return ChannelAdminControllerState(updatedFlags: self.updatedFlags, updatedRank: updatedRank, updating: self.updating, focusedOnRank: self.focusedOnRank) + return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: self.updatedFlags, updatedRank: updatedRank, updating: self.updating, focusedOnRank: self.focusedOnRank) } func withUpdatedUpdating(_ updating: Bool) -> ChannelAdminControllerState { - return ChannelAdminControllerState(updatedFlags: self.updatedFlags, updatedRank: self.updatedRank, updating: updating, focusedOnRank: self.focusedOnRank) + return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: self.updatedFlags, updatedRank: self.updatedRank, updating: updating, focusedOnRank: self.focusedOnRank) } func withUpdatedFocusedOnRank(_ focusedOnRank: Bool) -> ChannelAdminControllerState { - return ChannelAdminControllerState(updatedFlags: self.updatedFlags, updatedRank: self.updatedRank, updating: self.updating, focusedOnRank: focusedOnRank) + return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: self.updatedFlags, updatedRank: self.updatedRank, updating: self.updating, focusedOnRank: focusedOnRank) } } @@ -497,7 +534,7 @@ private func areAllAdminRightsEnabled(_ flags: TelegramChatAdminRightsFlags, gro } } -private func channelAdminControllerEntries(presentationData: PresentationData, state: ChannelAdminControllerState, accountPeerId: PeerId, channelView: PeerView, adminView: PeerView, initialParticipant: ChannelParticipant?, canEdit: Bool) -> [ChannelAdminEntry] { +private func channelAdminControllerEntries(presentationData: PresentationData, state: ChannelAdminControllerState, accountPeerId: PeerId, channelView: PeerView, adminView: PeerView, initialParticipant: ChannelParticipant?, invite: Bool, canEdit: Bool) -> [ChannelAdminEntry] { var entries: [ChannelAdminEntry] = [] if let channel = channelView.peers[channelView.peerId] as? TelegramChannel, let admin = adminView.peers[adminView.peerId] { @@ -517,7 +554,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s var canDismiss = false let isGroup: Bool - let maskRightsFlags: TelegramChatAdminRightsFlags + var maskRightsFlags: TelegramChatAdminRightsFlags let rightsOrder: [TelegramChatAdminRightsFlags] switch channel.info { @@ -548,6 +585,12 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s ] } + if invite { + maskRightsFlags.remove(.canManageCalls) + maskRightsFlags.remove(.canBeAnonymous) + maskRightsFlags.remove(.canAddAdmins) + } + if isCreator { if isGroup { entries.append(.rightsTitle(presentationData.theme, presentationData.strings.Channel_EditAdmin_PermissionsHeader)) @@ -581,64 +624,70 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s } } } else { - entries.append(.rightsTitle(presentationData.theme, presentationData.strings.Channel_EditAdmin_PermissionsHeader)) - - if let channelPeer = channelView.peers[channelView.peerId], canEditAdminRights(accountPeerId: accountPeerId, channelPeer: channelPeer, initialParticipant: initialParticipant) { - let accountUserRightsFlags: TelegramChatAdminRightsFlags - if channel.flags.contains(.isCreator) { - accountUserRightsFlags = maskRightsFlags - } else if let adminRights = channel.adminRights { - accountUserRightsFlags = maskRightsFlags.intersection(adminRights.rights) - } else { - accountUserRightsFlags = [] - } - - let currentRightsFlags: TelegramChatAdminRightsFlags - if let updatedFlags = state.updatedFlags { - currentRightsFlags = updatedFlags - } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminRights, _, _) = initialParticipant, let adminRights = maybeAdminRights { - currentRightsFlags = adminRights.rights.rights - } else { - currentRightsFlags = accountUserRightsFlags.subtracting(.canAddAdmins).subtracting(.canBeAnonymous) - } - - var index = 0 - for right in rightsOrder { - if accountUserRightsFlags.contains(right) { - entries.append(.rightItem(presentationData.theme, index, stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup, isChannel: isChannel, defaultBannedRights: channel.defaultBannedRights), right, currentRightsFlags, currentRightsFlags.contains(right), !state.updating && admin.id != accountPeerId && !rightEnabledByDefault(channelPeer: channel, right: right))) - index += 1 - } - } - - if accountUserRightsFlags.contains(.canAddAdmins) { - entries.append(.addAdminsInfo(presentationData.theme, currentRightsFlags.contains(.canAddAdmins) ? presentationData.strings.Channel_EditAdmin_PermissinAddAdminOn : presentationData.strings.Channel_EditAdmin_PermissinAddAdminOff)) - } - - if let admin = admin as? TelegramUser, admin.botInfo == nil && !admin.isDeleted && channel.flags.contains(.isCreator) && areAllAdminRightsEnabled(currentRightsFlags, group: isGroup, except: .canBeAnonymous) { - canTransfer = true - } + if let adminPeer = adminView.peers[adminView.peerId] as? TelegramUser, adminPeer.botInfo != nil, invite { + entries.append(.adminRights(presentationData.theme, presentationData.strings.Bot_AddToChat_Add_AdminRights, state.adminRights)) + } - if let initialParticipant = initialParticipant, case .member = initialParticipant, admin.id != accountPeerId { + if !invite || state.adminRights { + entries.append(.rightsTitle(presentationData.theme, presentationData.strings.Channel_EditAdmin_PermissionsHeader)) + + if let channelPeer = channelView.peers[channelView.peerId], canEditAdminRights(accountPeerId: accountPeerId, channelPeer: channelPeer, initialParticipant: initialParticipant) { + let accountUserRightsFlags: TelegramChatAdminRightsFlags if channel.flags.contains(.isCreator) { - canDismiss = true + accountUserRightsFlags = maskRightsFlags + } else if let adminRights = channel.adminRights { + accountUserRightsFlags = maskRightsFlags.intersection(adminRights.rights) } else { - switch initialParticipant { - case .creator: - break - case let .member(_, _, adminInfo, _, _): - if let adminInfo = adminInfo { - if adminInfo.promotedBy == accountPeerId || adminInfo.canBeEditedByAccountPeer { - canDismiss = true - } - } + accountUserRightsFlags = [] + } + + let currentRightsFlags: TelegramChatAdminRightsFlags + if let updatedFlags = state.updatedFlags { + currentRightsFlags = updatedFlags + } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminRights, _, _) = initialParticipant, let adminRights = maybeAdminRights { + currentRightsFlags = adminRights.rights.rights + } else { + currentRightsFlags = accountUserRightsFlags.subtracting(.canAddAdmins).subtracting(.canBeAnonymous) + } + + var index = 0 + for right in rightsOrder { + if accountUserRightsFlags.contains(right) { + entries.append(.rightItem(presentationData.theme, index, stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup, isChannel: isChannel, defaultBannedRights: channel.defaultBannedRights), right, currentRightsFlags, currentRightsFlags.contains(right), !state.updating && admin.id != accountPeerId && !rightEnabledByDefault(channelPeer: channel, right: right))) + index += 1 } } - } - } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminInfo, _, _) = initialParticipant, let adminInfo = maybeAdminInfo { - var index = 0 - for right in rightsOrder { - entries.append(.rightItem(presentationData.theme, index, stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup, isChannel: isChannel, defaultBannedRights: channel.defaultBannedRights), right, adminInfo.rights.rights, adminInfo.rights.rights.contains(right), false)) - index += 1 + + if accountUserRightsFlags.contains(.canAddAdmins) { + entries.append(.addAdminsInfo(presentationData.theme, currentRightsFlags.contains(.canAddAdmins) ? presentationData.strings.Channel_EditAdmin_PermissinAddAdminOn : presentationData.strings.Channel_EditAdmin_PermissinAddAdminOff)) + } + + if let admin = admin as? TelegramUser, admin.botInfo == nil && !admin.isDeleted && channel.flags.contains(.isCreator) && areAllAdminRightsEnabled(currentRightsFlags, group: isGroup, except: .canBeAnonymous) { + canTransfer = true + } + + if let initialParticipant = initialParticipant, case let .member(_, _, adminInfo, _, _) = initialParticipant, admin.id != accountPeerId, adminInfo != nil { + if channel.flags.contains(.isCreator) { + canDismiss = true + } else { + switch initialParticipant { + case .creator: + break + case let .member(_, _, adminInfo, _, _): + if let adminInfo = adminInfo { + if adminInfo.promotedBy == accountPeerId || adminInfo.canBeEditedByAccountPeer { + canDismiss = true + } + } + } + } + } + } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminInfo, _, _) = initialParticipant, let adminInfo = maybeAdminInfo { + var index = 0 + for right in rightsOrder { + entries.append(.rightItem(presentationData.theme, index, stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup, isChannel: isChannel, defaultBannedRights: channel.defaultBannedRights), right, adminInfo.rights.rights, adminInfo.rights.rights.contains(right), false)) + index += 1 + } } } } @@ -689,58 +738,72 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s entries.append(.rankTitle(presentationData.theme, presentationData.strings.Group_EditAdmin_RankTitle.uppercased(), rankEnabled && state.focusedOnRank ? Int32(currentRank?.count ?? 0) : nil, rankMaxLength)) entries.append(.rank(presentationData.theme, presentationData.strings, isCreator ? presentationData.strings.Group_EditAdmin_RankOwnerPlaceholder : presentationData.strings.Group_EditAdmin_RankAdminPlaceholder, currentRank ?? "", rankEnabled)) } else { - entries.append(.rightsTitle(presentationData.theme, presentationData.strings.Channel_EditAdmin_PermissionsHeader)) - - let isGroup = true - let isChannel = false - let maskRightsFlags: TelegramChatAdminRightsFlags = .groupSpecific - let rightsOrder: [TelegramChatAdminRightsFlags] = [ - .canChangeInfo, - .canDeleteMessages, - .canBanUsers, - .canInviteUsers, - .canPinMessages, - .canManageCalls, - .canBeAnonymous, - .canAddAdmins - ] - - let accountUserRightsFlags: TelegramChatAdminRightsFlags = maskRightsFlags - - let currentRightsFlags: TelegramChatAdminRightsFlags - if let updatedFlags = state.updatedFlags { - currentRightsFlags = updatedFlags - } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminRights, _, _) = initialParticipant, let adminRights = maybeAdminRights { - currentRightsFlags = adminRights.rights.rights.subtracting(.canAddAdmins).subtracting(.canBeAnonymous) - } else { - currentRightsFlags = accountUserRightsFlags.subtracting(.canAddAdmins).subtracting(.canBeAnonymous) + if let adminPeer = adminView.peers[adminView.peerId] as? TelegramUser, adminPeer.botInfo != nil, invite { + entries.append(.adminRights(presentationData.theme, presentationData.strings.Bot_AddToChat_Add_AdminRights, state.adminRights)) } - var accountIsCreator = false - if case .creator = group.role { - accountIsCreator = true - } - - var index = 0 - for right in rightsOrder { - if accountUserRightsFlags.contains(right) { - entries.append(.rightItem(presentationData.theme, index, stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup, isChannel: isChannel, defaultBannedRights: group.defaultBannedRights), right, currentRightsFlags, currentRightsFlags.contains(right), !state.updating && accountIsCreator)) - index += 1 + if !invite || state.adminRights { + entries.append(.rightsTitle(presentationData.theme, presentationData.strings.Channel_EditAdmin_PermissionsHeader)) + + let isGroup = true + let isChannel = false + var maskRightsFlags: TelegramChatAdminRightsFlags = .groupSpecific + let rightsOrder: [TelegramChatAdminRightsFlags] = [ + .canChangeInfo, + .canDeleteMessages, + .canBanUsers, + .canInviteUsers, + .canPinMessages, + .canManageCalls, + .canBeAnonymous, + .canAddAdmins + ] + + if invite { + maskRightsFlags.remove(.canManageCalls) + maskRightsFlags.remove(.canBeAnonymous) + maskRightsFlags.remove(.canAddAdmins) } - } - - if accountUserRightsFlags.contains(.canAddAdmins) { - entries.append(.addAdminsInfo(presentationData.theme, currentRightsFlags.contains(.canAddAdmins) ? presentationData.strings.Channel_EditAdmin_PermissinAddAdminOn : presentationData.strings.Channel_EditAdmin_PermissinAddAdminOff)) - } - - if let admin = admin as? TelegramUser, case .creator = group.role, admin.botInfo == nil && !admin.isDeleted && areAllAdminRightsEnabled(currentRightsFlags, group: true, except: .canBeAnonymous) { - entries.append(.transfer(presentationData.theme, presentationData.strings.Group_EditAdmin_TransferOwnership)) + + let accountUserRightsFlags: TelegramChatAdminRightsFlags = maskRightsFlags + + let currentRightsFlags: TelegramChatAdminRightsFlags + if let updatedFlags = state.updatedFlags { + currentRightsFlags = updatedFlags + } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminRights, _, _) = initialParticipant, let adminRights = maybeAdminRights { + currentRightsFlags = adminRights.rights.rights.subtracting(.canAddAdmins).subtracting(.canBeAnonymous) + } else { + currentRightsFlags = accountUserRightsFlags.subtracting(.canAddAdmins).subtracting(.canBeAnonymous) + } + + var accountIsCreator = false + if case .creator = group.role { + accountIsCreator = true + } + + var index = 0 + for right in rightsOrder { + if accountUserRightsFlags.contains(right) { + entries.append(.rightItem(presentationData.theme, index, stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup, isChannel: isChannel, defaultBannedRights: group.defaultBannedRights), right, currentRightsFlags, currentRightsFlags.contains(right), !state.updating && accountIsCreator)) + index += 1 + } + } + + if accountUserRightsFlags.contains(.canAddAdmins) { + entries.append(.addAdminsInfo(presentationData.theme, currentRightsFlags.contains(.canAddAdmins) ? presentationData.strings.Channel_EditAdmin_PermissinAddAdminOn : presentationData.strings.Channel_EditAdmin_PermissinAddAdminOff)) + } + + if let admin = admin as? TelegramUser, case .creator = group.role, admin.botInfo == nil && !admin.isDeleted && areAllAdminRightsEnabled(currentRightsFlags, group: true, except: .canBeAnonymous) { + entries.append(.transfer(presentationData.theme, presentationData.strings.Group_EditAdmin_TransferOwnership)) + } + + let placeholder = isCreator ? presentationData.strings.Group_EditAdmin_RankOwnerPlaceholder : presentationData.strings.Group_EditAdmin_RankAdminPlaceholder + entries.append(.rankTitle(presentationData.theme, presentationData.strings.Group_EditAdmin_RankTitle.uppercased(), rankEnabled && state.focusedOnRank ? Int32(currentRank?.count ?? 0) : nil, rankMaxLength)) + entries.append(.rank(presentationData.theme, presentationData.strings, placeholder, currentRank ?? "", rankEnabled)) + entries.append(.rankInfo(presentationData.theme, presentationData.strings.Group_EditAdmin_RankInfo(placeholder).string)) } - entries.append(.rankTitle(presentationData.theme, presentationData.strings.Group_EditAdmin_RankTitle.uppercased(), rankEnabled && state.focusedOnRank ? Int32(currentRank?.count ?? 0) : nil, rankMaxLength)) - entries.append(.rank(presentationData.theme, presentationData.strings, isCreator ? presentationData.strings.Group_EditAdmin_RankOwnerPlaceholder : presentationData.strings.Group_EditAdmin_RankAdminPlaceholder, currentRank ?? "", rankEnabled)) - - if let initialParticipant = initialParticipant, case .member = initialParticipant, admin.id != accountPeerId { + if let initialParticipant = initialParticipant, case let .member(_, _, adminInfo, _, _) = initialParticipant, admin.id != accountPeerId, adminInfo != nil { entries.append(.dismiss(presentationData.theme, presentationData.strings.Channel_Moderator_AccessLevelRevoke)) } } @@ -749,7 +812,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s return entries } -public func channelAdminController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: PeerId, adminId: PeerId, initialParticipant: ChannelParticipant?, updated: @escaping (TelegramChatAdminRights?) -> Void, upgradedToSupergroup: @escaping (PeerId, @escaping () -> Void) -> Void, transferedOwnership: @escaping (PeerId) -> Void) -> ViewController { +public func channelAdminController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: PeerId, adminId: PeerId, initialParticipant: ChannelParticipant?, invite: Bool = false, updated: @escaping (TelegramChatAdminRights?) -> Void, upgradedToSupergroup: @escaping (PeerId, @escaping () -> Void) -> Void, transferedOwnership: @escaping (PeerId) -> Void) -> ViewController { let statePromise = ValuePromise(ChannelAdminControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: ChannelAdminControllerState()) let updateState: ((ChannelAdminControllerState) -> ChannelAdminControllerState) -> Void = { f in @@ -777,7 +840,11 @@ public func channelAdminController(context: AccountContext, updatedPresentationD upgradedToSupergroup(peerId, completion) } - let arguments = ChannelAdminControllerArguments(context: context, toggleRight: { right, flags in + let arguments = ChannelAdminControllerArguments(context: context, updateAdminRights: { value in + updateState { current in + return current.withUpdatedAdminRights(value) + } + }, toggleRight: { right, flags in updateState { current in var updated = flags if flags.contains(right) { @@ -907,26 +974,187 @@ public func channelAdminController(context: AccountContext, updatedPresentationD focusItemTag = ChannelAdminEntryTag.rank } - var rightNavigationButton: ItemListNavigationButton? - if state.updating { - rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) - } else if canEdit { - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + let rightButtonActionImpl = { + if invite && !state.adminRights { + updateState { current in + return current.withUpdatedUpdating(true) + } if let channel = channelView.peers[channelView.peerId] as? TelegramChannel { - if let initialParticipant = initialParticipant { - var updateFlags: TelegramChatAdminRightsFlags? - var updateRank: String? + updateRightsDisposable.set((context.engine.peers.addChannelMember(peerId: peerId, memberId: adminId) |> deliverOnMainQueue).start(error: { error in updateState { current in - updateFlags = current.updatedFlags - updateRank = current.updatedRank?.trimmingCharacters(in: .whitespacesAndNewlines) - return current + return current.withUpdatedUpdating(false) } - - if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsEmoji { - errorImpl?() - return + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + var text = presentationData.strings.Login_UnknownError + switch error { + case .tooMuchJoined: + text = presentationData.strings.Group_ErrorSupergroupConversionNotPossible + case .restricted: + if let admin = adminView.peers[adminView.peerId] { + switch channel.info { + case .broadcast: + text = presentationData.strings.Privacy_GroupsAndChannels_InviteToChannelError(EnginePeer(admin).compactDisplayTitle, EnginePeer(admin).compactDisplayTitle).string + case .group: + text = presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(EnginePeer(admin).compactDisplayTitle, EnginePeer(admin).compactDisplayTitle).string + } + } + case .notMutualContact: + if case .broadcast = channel.info { + text = presentationData.strings.Channel_AddUserLeftError + } else { + text = presentationData.strings.GroupInfo_AddUserLeftError + } + default: + break } - + presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + }, completed: { + updated(nil) + dismissImpl?() + })) + } else if let _ = channelView.peers[channelView.peerId] as? TelegramGroup { + updateRightsDisposable.set((context.engine.peers.addGroupMember(peerId: peerId, memberId: adminId) |> deliverOnMainQueue).start(error: { error in + updateState { current in + return current.withUpdatedUpdating(false) + } + if case .privacy = error, let admin = adminView.peers[adminView.peerId] { + presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(EnginePeer(admin).compactDisplayTitle, EnginePeer(admin).compactDisplayTitle).string, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + } + }, completed: { + updated(nil) + dismissImpl?() + })) + } + } else if let channel = channelView.peers[channelView.peerId] as? TelegramChannel { + if let initialParticipant = initialParticipant { + var updateFlags: TelegramChatAdminRightsFlags? + var updateRank: String? + updateState { current in + updateFlags = current.updatedFlags + updateRank = current.updatedRank?.trimmingCharacters(in: .whitespacesAndNewlines) + return current + } + + if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsEmoji { + errorImpl?() + return + } + + let maskRightsFlags: TelegramChatAdminRightsFlags + switch channel.info { + case .broadcast: + maskRightsFlags = .broadcastSpecific + case .group: + maskRightsFlags = .groupSpecific + } + + var currentRank: String? + var currentFlags: TelegramChatAdminRightsFlags? + switch initialParticipant { + case let .creator(_, adminInfo, rank): + currentRank = rank + currentFlags = adminInfo?.rights.rights ?? maskRightsFlags.subtracting(.canBeAnonymous) + case let .member(_, _, adminInfo, _, rank): + if updateFlags == nil { + if adminInfo?.rights == nil { + if channel.flags.contains(.isCreator) { + updateFlags = maskRightsFlags.subtracting([.canAddAdmins, .canBeAnonymous]) + } else if let adminRights = channel.adminRights { + updateFlags = maskRightsFlags.intersection(adminRights.rights).subtracting([.canAddAdmins, .canBeAnonymous]) + } else { + updateFlags = [] + } + } else { + updateFlags = adminInfo?.rights.rights + } + } + currentRank = rank + currentFlags = adminInfo?.rights.rights + } + + let effectiveRank = updateRank ?? currentRank + if effectiveRank?.containsEmoji ?? false { + errorImpl?() + return + } + + if updateFlags != currentFlags { + updateState { current in + return current.withUpdatedUpdating(true) + } + updateRightsDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(engine: context.engine, peerId: peerId, memberId: adminId, adminRights: TelegramChatAdminRights(rights: updateFlags ?? []), rank: effectiveRank) |> deliverOnMainQueue).start(error: { error in + updateState { current in + return current.withUpdatedUpdating(false) + } + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + var text = presentationData.strings.Login_UnknownError + switch error { + case .generic: + break + case let .addMemberError(addMemberError): + switch addMemberError { + case .tooMuchJoined: + text = presentationData.strings.Group_ErrorSupergroupConversionNotPossible + case .restricted: + if let admin = adminView.peers[adminView.peerId] { + switch channel.info { + case .broadcast: + text = presentationData.strings.Privacy_GroupsAndChannels_InviteToChannelError(EnginePeer(admin).compactDisplayTitle, EnginePeer(admin).compactDisplayTitle).string + case .group: + text = presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(EnginePeer(admin).compactDisplayTitle, EnginePeer(admin).compactDisplayTitle).string + } + } + case .notMutualContact: + if case .broadcast = channel.info { + text = presentationData.strings.Channel_AddUserLeftError + } else { + text = presentationData.strings.GroupInfo_AddUserLeftError + } + default: + break + } + case .adminsTooMuch: + if case .broadcast = channel.info { + text = presentationData.strings.Channel_ErrorAdminsTooMuch + } else { + text = presentationData.strings.Group_ErrorAdminsTooMuch + } + } + presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + }, completed: { + updated(TelegramChatAdminRights(rights: updateFlags ?? [])) + dismissImpl?() + })) + } else if let updateRank = updateRank, let currentFlags = currentFlags { + updateState { current in + return current.withUpdatedUpdating(true) + } + updateRightsDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(engine: context.engine, peerId: peerId, memberId: adminId, adminRights: TelegramChatAdminRights(rights: currentFlags), rank: updateRank) |> deliverOnMainQueue).start(error: { _ in + + }, completed: { + updated(TelegramChatAdminRights(rights: currentFlags)) + dismissImpl?() + })) + } else { + dismissImpl?() + } + } else if canEdit { + var updateFlags: TelegramChatAdminRightsFlags? + var updateRank: String? + updateState { current in + updateFlags = current.updatedFlags + updateRank = current.updatedRank?.trimmingCharacters(in: .whitespacesAndNewlines) + return current + } + + if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsEmoji { + errorImpl?() + return + } + + if updateFlags == nil { let maskRightsFlags: TelegramChatAdminRightsFlags switch channel.info { case .broadcast: @@ -935,62 +1163,31 @@ public func channelAdminController(context: AccountContext, updatedPresentationD maskRightsFlags = .groupSpecific } - var currentRank: String? - var currentFlags: TelegramChatAdminRightsFlags? - switch initialParticipant { - case let .creator(_, adminInfo, rank): - currentRank = rank - currentFlags = adminInfo?.rights.rights ?? maskRightsFlags.subtracting(.canBeAnonymous) - case let .member(_, _, adminInfo, _, rank): - if updateFlags == nil { - if adminInfo?.rights == nil { - if channel.flags.contains(.isCreator) { - updateFlags = maskRightsFlags.subtracting([.canAddAdmins, .canBeAnonymous]) - } else if let adminRights = channel.adminRights { - updateFlags = maskRightsFlags.intersection(adminRights.rights).subtracting([.canAddAdmins, .canBeAnonymous]) - } else { - updateFlags = [] - } - } else { - updateFlags = adminInfo?.rights.rights - } - } - currentRank = rank - currentFlags = adminInfo?.rights.rights + if channel.flags.contains(.isCreator) { + updateFlags = maskRightsFlags.subtracting([.canAddAdmins, .canBeAnonymous]) + } else if let adminRights = channel.adminRights { + updateFlags = maskRightsFlags.intersection(adminRights.rights).subtracting([.canAddAdmins, .canBeAnonymous]) + } else { + updateFlags = [] } - - let effectiveRank = updateRank ?? currentRank - if effectiveRank?.containsEmoji ?? false { - errorImpl?() - return + } + + if let updateFlags = updateFlags { + updateState { current in + return current.withUpdatedUpdating(true) } - - if updateFlags != currentFlags { - updateState { current in - return current.withUpdatedUpdating(true) - } - updateRightsDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(engine: context.engine, peerId: peerId, memberId: adminId, adminRights: TelegramChatAdminRights(rights: updateFlags ?? []), rank: effectiveRank) |> deliverOnMainQueue).start(error: { error in - updateState { current in - return current.withUpdatedUpdating(false) - } - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } + updateRightsDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(engine: context.engine, peerId: peerId, memberId: adminId, adminRights: TelegramChatAdminRights(rights: updateFlags), rank: updateRank) |> deliverOnMainQueue).start(error: { error in + if case let .addMemberError(addMemberError) = error, let admin = adminView.peers[adminView.peerId] { var text = presentationData.strings.Login_UnknownError - switch error { - case .generic: - break - case let .addMemberError(addMemberError): - switch addMemberError { + switch addMemberError { case .tooMuchJoined: text = presentationData.strings.Group_ErrorSupergroupConversionNotPossible case .restricted: - if let admin = adminView.peers[adminView.peerId] { - switch channel.info { - case .broadcast: - text = presentationData.strings.Privacy_GroupsAndChannels_InviteToChannelError(EnginePeer(admin).compactDisplayTitle, EnginePeer(admin).compactDisplayTitle).string - case .group: - text = presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(EnginePeer(admin).compactDisplayTitle, EnginePeer(admin).compactDisplayTitle).string - } + switch channel.info { + case .broadcast: + text = presentationData.strings.Privacy_GroupsAndChannels_InviteToChannelError(EnginePeer(admin).compactDisplayTitle, EnginePeer(admin).compactDisplayTitle).string + case .group: + text = presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(EnginePeer(admin).compactDisplayTitle, EnginePeer(admin).compactDisplayTitle).string } case .notMutualContact: if case .broadcast = channel.info { @@ -1000,225 +1197,196 @@ public func channelAdminController(context: AccountContext, updatedPresentationD } default: break - } - case .adminsTooMuch: - if case .broadcast = channel.info { - text = presentationData.strings.Channel_ErrorAdminsTooMuch - } else { - text = presentationData.strings.Group_ErrorAdminsTooMuch - } } presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) - }, completed: { - updated(TelegramChatAdminRights(rights: updateFlags ?? [])) - dismissImpl?() - })) - } else if let updateRank = updateRank, let currentFlags = currentFlags { - updateState { current in - return current.withUpdatedUpdating(true) + } else if case .adminsTooMuch = error { + let text: String + if case .broadcast = channel.info { + text = presentationData.strings.Channel_ErrorAdminsTooMuch + } else { + text = presentationData.strings.Group_ErrorAdminsTooMuch + } + presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) } - updateRightsDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(engine: context.engine, peerId: peerId, memberId: adminId, adminRights: TelegramChatAdminRights(rights: currentFlags), rank: updateRank) |> deliverOnMainQueue).start(error: { _ in - - }, completed: { - updated(TelegramChatAdminRights(rights: currentFlags)) - dismissImpl?() - })) - } else { dismissImpl?() - } - } else if canEdit { - var updateFlags: TelegramChatAdminRightsFlags? - var updateRank: String? + }, completed: { + updated(TelegramChatAdminRights(rights: updateFlags)) + dismissImpl?() + })) + } + } + } else if let _ = channelView.peers[channelView.peerId] as? TelegramGroup { + var updateFlags: TelegramChatAdminRightsFlags? + var updateRank: String? + updateState { current in + updateFlags = current.updatedFlags + if let updatedRank = current.updatedRank, !updatedRank.isEmpty { + updateRank = updatedRank.trimmingCharacters(in: .whitespacesAndNewlines) + } + return current + } + + if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsEmoji { + errorImpl?() + return + } + + let maskRightsFlags: TelegramChatAdminRightsFlags = .groupSpecific + let defaultFlags = maskRightsFlags.subtracting([.canBeAnonymous, .canAddAdmins]) + + if updateFlags == nil { + updateFlags = defaultFlags + } + + if let updateFlags = updateFlags { + if initialParticipant?.adminInfo == nil && updateFlags == defaultFlags && updateRank == nil { updateState { current in - updateFlags = current.updatedFlags - updateRank = current.updatedRank?.trimmingCharacters(in: .whitespacesAndNewlines) - return current + return current.withUpdatedUpdating(true) } - - if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsEmoji { - errorImpl?() - return - } - - if updateFlags == nil { - let maskRightsFlags: TelegramChatAdminRightsFlags - switch channel.info { - case .broadcast: - maskRightsFlags = .broadcastSpecific - case .group: - maskRightsFlags = .groupSpecific + updateRightsDisposable.set((context.engine.peers.addGroupAdmin(peerId: peerId, adminId: adminId) + |> deliverOnMainQueue).start(error: { error in + if case let .addMemberError(error) = error, case .privacy = error, let admin = adminView.peers[adminView.peerId] { + presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(EnginePeer(admin).compactDisplayTitle, EnginePeer(admin).compactDisplayTitle).string, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + } else if case .adminsTooMuch = error { + presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Group_ErrorAdminsTooMuch, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) } - if channel.flags.contains(.isCreator) { - updateFlags = maskRightsFlags.subtracting([.canAddAdmins, .canBeAnonymous]) - } else if let adminRights = channel.adminRights { - updateFlags = maskRightsFlags.intersection(adminRights.rights).subtracting([.canAddAdmins, .canBeAnonymous]) - } else { - updateFlags = [] - } + dismissImpl?() + }, completed: { + dismissImpl?() + })) + } else if updateFlags != defaultFlags || updateRank != nil { + enum WrappedUpdateChannelAdminRightsError { + case direct(UpdateChannelAdminRightsError) + case conversionTooManyChannels + case conversionFailed } - if let updateFlags = updateFlags { - updateState { current in - return current.withUpdatedUpdating(true) + let signal = context.engine.peers.convertGroupToSupergroup(peerId: peerId) + |> map(Optional.init) + |> `catch` { error -> Signal in + switch error { + case .tooManyChannels: + return .fail(.conversionTooManyChannels) + default: + return .fail(.conversionFailed) } - updateRightsDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(engine: context.engine, peerId: peerId, memberId: adminId, adminRights: TelegramChatAdminRights(rights: updateFlags), rank: updateRank) |> deliverOnMainQueue).start(error: { error in - if case let .addMemberError(addMemberError) = error, let admin = adminView.peers[adminView.peerId] { + } + |> mapToSignal { upgradedPeerId -> Signal in + guard let upgradedPeerId = upgradedPeerId else { + return .fail(.conversionFailed) + } + return context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(engine: context.engine, peerId: upgradedPeerId, memberId: adminId, adminRights: TelegramChatAdminRights(rights: updateFlags), rank: updateRank) + |> mapError { error -> WrappedUpdateChannelAdminRightsError in + return .direct(error) + } + |> mapToSignal { _ -> Signal in + return .complete() + } + |> then(.single(upgradedPeerId)) + } + |> deliverOnMainQueue + + updateState { current in + return current.withUpdatedUpdating(true) + } + updateRightsDisposable.set(signal.start(next: { upgradedPeerId in + if let upgradedPeerId = upgradedPeerId { + upgradedToSupergroup(upgradedPeerId, { + dismissImpl?() + }) + } + }, error: { error in + updateState { current in + return current.withUpdatedUpdating(false) + } + + switch error { + case let .direct(error): + if case let .addMemberError(error) = error { var text = presentationData.strings.Login_UnknownError - switch addMemberError { - case .tooMuchJoined: - text = presentationData.strings.Group_ErrorSupergroupConversionNotPossible - case .restricted: - switch channel.info { - case .broadcast: - text = presentationData.strings.Privacy_GroupsAndChannels_InviteToChannelError(EnginePeer(admin).compactDisplayTitle, EnginePeer(admin).compactDisplayTitle).string - case .group: - text = presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(EnginePeer(admin).compactDisplayTitle, EnginePeer(admin).compactDisplayTitle).string - } - case .notMutualContact: - if case .broadcast = channel.info { - text = presentationData.strings.Channel_AddUserLeftError - } else { - text = presentationData.strings.GroupInfo_AddUserLeftError - } - default: - break + if case .restricted = error, let admin = adminView.peers[adminView.peerId] { + text = presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(EnginePeer(admin).compactDisplayTitle, EnginePeer(admin).compactDisplayTitle).string + } else if case .tooMuchJoined = error { + text = presentationData.strings.Invite_ChannelsTooMuch } presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) - } else if case .adminsTooMuch = error { - let text: String - if case .broadcast = channel.info { - text = presentationData.strings.Channel_ErrorAdminsTooMuch - } else { - text = presentationData.strings.Group_ErrorAdminsTooMuch - } - presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) - } - dismissImpl?() - }, completed: { - updated(TelegramChatAdminRights(rights: updateFlags)) - dismissImpl?() - })) - } - } - } else if let _ = channelView.peers[channelView.peerId] as? TelegramGroup { - var updateFlags: TelegramChatAdminRightsFlags? - var updateRank: String? - updateState { current in - updateFlags = current.updatedFlags - if let updatedRank = current.updatedRank, !updatedRank.isEmpty { - updateRank = updatedRank.trimmingCharacters(in: .whitespacesAndNewlines) - } - return current - } - - if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsEmoji { - errorImpl?() - return - } - - let maskRightsFlags: TelegramChatAdminRightsFlags = .groupSpecific - let defaultFlags = maskRightsFlags.subtracting([.canBeAnonymous, .canAddAdmins]) - - if updateFlags == nil { - updateFlags = defaultFlags - } - - if let updateFlags = updateFlags { - if initialParticipant?.adminInfo == nil && updateFlags == defaultFlags && updateRank == nil { - updateState { current in - return current.withUpdatedUpdating(true) - } - updateRightsDisposable.set((context.engine.peers.addGroupAdmin(peerId: peerId, adminId: adminId) - |> deliverOnMainQueue).start(error: { error in - if case let .addMemberError(error) = error, case .privacy = error, let admin = adminView.peers[adminView.peerId] { - presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(EnginePeer(admin).compactDisplayTitle, EnginePeer(admin).compactDisplayTitle).string, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) } else if case .adminsTooMuch = error { presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Group_ErrorAdminsTooMuch, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) } - - dismissImpl?() - }, completed: { - dismissImpl?() - })) - } else if updateFlags != defaultFlags || updateRank != nil { - enum WrappedUpdateChannelAdminRightsError { - case direct(UpdateChannelAdminRightsError) - case conversionTooManyChannels - case conversionFailed + case .conversionFailed, .conversionTooManyChannels: + pushControllerImpl?(oldChannelsController(context: context, intent: .upgrade)) } - let signal = context.engine.peers.convertGroupToSupergroup(peerId: peerId) - |> map(Optional.init) - |> `catch` { error -> Signal in - switch error { - case .tooManyChannels: - return .fail(.conversionTooManyChannels) - default: - return .fail(.conversionFailed) - } - } - |> mapToSignal { upgradedPeerId -> Signal in - guard let upgradedPeerId = upgradedPeerId else { - return .fail(.conversionFailed) - } - return context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(engine: context.engine, peerId: upgradedPeerId, memberId: adminId, adminRights: TelegramChatAdminRights(rights: updateFlags), rank: updateRank) - |> mapError { error -> WrappedUpdateChannelAdminRightsError in - return .direct(error) - } - |> mapToSignal { _ -> Signal in - return .complete() - } - |> then(.single(upgradedPeerId)) - } - |> deliverOnMainQueue - - updateState { current in - return current.withUpdatedUpdating(true) - } - updateRightsDisposable.set(signal.start(next: { upgradedPeerId in - if let upgradedPeerId = upgradedPeerId { - upgradedToSupergroup(upgradedPeerId, { - dismissImpl?() - }) - } - }, error: { error in - updateState { current in - return current.withUpdatedUpdating(false) - } - - switch error { - case let .direct(error): - if case let .addMemberError(error) = error { - var text = presentationData.strings.Login_UnknownError - if case .restricted = error, let admin = adminView.peers[adminView.peerId] { - text = presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(EnginePeer(admin).compactDisplayTitle, EnginePeer(admin).compactDisplayTitle).string - } else if case .tooMuchJoined = error { - text = presentationData.strings.Invite_ChannelsTooMuch - } - presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) - } else if case .adminsTooMuch = error { - presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Group_ErrorAdminsTooMuch, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) - } - case .conversionFailed, .conversionTooManyChannels: - pushControllerImpl?(oldChannelsController(context: context, intent: .upgrade)) - } - - dismissImpl?() - })) - } else { dismissImpl?() - } + })) } else { dismissImpl?() } + } else { + dismissImpl?() } + } + } + + var rightNavigationButton: ItemListNavigationButton? + if state.updating { + rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) + } else if canEdit { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + rightButtonActionImpl() }) } - let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(initialParticipant?.adminInfo == nil ? presentationData.strings.Channel_Management_AddModerator : presentationData.strings.Channel_Moderator_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + var footerItem: ItemListControllerFooterItem? - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelAdminControllerEntries(presentationData: presentationData, state: state, accountPeerId: context.account.peerId, channelView: channelView, adminView: adminView, initialParticipant: initialParticipant, canEdit: canEdit), style: .blocks, focusItemTag: focusItemTag, ensureVisibleItemTag: nil, emptyStateItem: nil, animateChanges: true) + let title: String + if initialParticipant?.adminInfo == nil { + var isGroup: Bool = false + var peerTitle: String = "" + if let peer = channelView.peers[channelView.peerId] as? TelegramGroup { + isGroup = true + peerTitle = peer.title + } else if let peer = channelView.peers[channelView.peerId] as? TelegramChannel { + if case .group = peer.info { + isGroup = true + } + peerTitle = peer.title + } + + if let admin = adminView.peers[adminView.peerId] as? TelegramUser, admin.botInfo != nil && invite { + title = presentationData.strings.Bot_AddToChat_Add_Title + rightNavigationButton = nil + footerItem = ChannelAdminAddBotFooterItem(theme: presentationData.theme, title: state.adminRights ? presentationData.strings.Bot_AddToChat_Add_AddAsAdmin : presentationData.strings.Bot_AddToChat_Add_AddAsMember, action: { + if state.adminRights { + let theme = AlertControllerTheme(presentationData: presentationData) + let attributedTitle = NSAttributedString(string: presentationData.strings.Bot_AddToChat_Add_AdminAlertTitle, font: Font.medium(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) + + let text = isGroup ? presentationData.strings.Bot_AddToChat_Add_AdminAlertTextGroup(peerTitle).string : presentationData.strings.Bot_AddToChat_Add_AdminAlertTextChannel(peerTitle).string + + let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor) + let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center) + + let controller = richTextAlertController(context: context, title: attributedTitle, text: attributedText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Bot_AddToChat_Add_AdminAlertAdd, action: { + rightButtonActionImpl() + }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + })], actionLayout: .vertical) + presentControllerImpl?(controller, nil) + } else { + rightButtonActionImpl() + } + }) + } else { + title = presentationData.strings.Channel_Management_AddModerator + } + } else { + title = presentationData.strings.Channel_Moderator_Title + } + + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelAdminControllerEntries(presentationData: presentationData, state: state, accountPeerId: context.account.peerId, channelView: channelView, adminView: adminView, initialParticipant: initialParticipant, invite: invite, canEdit: canEdit), style: .blocks, focusItemTag: focusItemTag, ensureVisibleItemTag: nil, emptyStateItem: nil, footerItem: footerItem, animateChanges: true) return (controllerState, (listState, arguments)) } diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 4b2b3d5646..3cc332b2d7 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -1093,7 +1093,7 @@ public final class Transaction { assert(!self.disposed) self.postbox?.deviceContactImportInfoTable.enumerateDeviceContactImportInfoItems(f) } - + public func getChatListNamespaceEntries(groupId: PeerGroupId, namespace: MessageId.Namespace, summaryTag: MessageTags?) -> [ChatListNamespaceEntry] { assert(!self.disposed) guard let postbox = self.postbox else { diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsEmptyStateItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsEmptyStateItem.swift index bf29388ef2..ead6d0cec4 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsEmptyStateItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsEmptyStateItem.swift @@ -90,9 +90,7 @@ final class RecentSessionsEmptyStateItemNode: ItemListControllerEmptyStateItemNo if layout.size.width == 320 { textVisible = false } - - self.backgroundColor = .red - + let titleSize = self.titleNode.measure(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - layout.intrinsicInsets.left - layout.intrinsicInsets.right - 50.0, height: max(1.0, layout.size.height - insets.top - insets.bottom))) let textSize = self.textNode.measure(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - layout.intrinsicInsets.left - layout.intrinsicInsets.right - 50.0, height: max(1.0, layout.size.height - insets.top - insets.bottom))) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMarkupMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMarkupMessageAttribute.swift index 815f8139cc..7e558a81cf 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMarkupMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReplyMarkupMessageAttribute.swift @@ -12,6 +12,7 @@ public enum ReplyMarkupButtonAction: PostboxCoding, Equatable { case urlAuth(url: String, buttonId: Int32) case setupPoll(isQuiz: Bool?) case openUserProfile(peerId: PeerId) + case addToChat public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("v", orElse: 0) { @@ -37,6 +38,8 @@ public enum ReplyMarkupButtonAction: PostboxCoding, Equatable { self = .setupPoll(isQuiz: decoder.decodeOptionalInt32ForKey("isq").flatMap { $0 != 0 }) case 10: self = .openUserProfile(peerId: PeerId(decoder.decodeInt64ForKey("peerId", orElse: 0))) + case 11: + self = .addToChat default: self = .text } @@ -79,6 +82,8 @@ public enum ReplyMarkupButtonAction: PostboxCoding, Equatable { case let .openUserProfile(peerId): encoder.encodeInt32(10, forKey: "v") encoder.encodeInt64(peerId.toInt64(), forKey: "peerId") + case .addToChat: + encoder.encodeInt32(11, forKey: "v") } } } diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift index 6ce6f8634b..651e2f8c55 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift @@ -540,6 +540,7 @@ public final class PrincipalThemeAdditionalGraphics { public let chatBubbleActionButtonIncomingLocationIconImage: UIImage public let chatBubbleActionButtonIncomingPaymentIconImage: UIImage public let chatBubbleActionButtonIncomingProfileIconImage: UIImage + public let chatBubbleActionButtonIncomingAddToChatIconImage: UIImage public let chatBubbleActionButtonOutgoingMessageIconImage: UIImage public let chatBubbleActionButtonOutgoingLinkIconImage: UIImage @@ -548,6 +549,7 @@ public final class PrincipalThemeAdditionalGraphics { public let chatBubbleActionButtonOutgoingLocationIconImage: UIImage public let chatBubbleActionButtonOutgoingPaymentIconImage: UIImage public let chatBubbleActionButtonOutgoingProfileIconImage: UIImage + public let chatBubbleActionButtonOutgoingAddToChatIconImage: UIImage public let chatEmptyItemLockIcon: UIImage public let emptyChatListCheckIcon: UIImage @@ -591,6 +593,7 @@ public final class PrincipalThemeAdditionalGraphics { self.chatBubbleActionButtonIncomingLocationIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLocation"), color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonIncomingPaymentIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotPayment"), color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonIncomingProfileIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotProfile"), color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsTextColor, wallpaper: wallpaper))! + self.chatBubbleActionButtonIncomingAddToChatIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotAddToChat"), color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonOutgoingMessageIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotMessage"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonOutgoingLinkIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLink"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonOutgoingShareIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotShare"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! @@ -598,6 +601,7 @@ public final class PrincipalThemeAdditionalGraphics { self.chatBubbleActionButtonOutgoingLocationIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLocation"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonOutgoingPaymentIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotPayment"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonOutgoingProfileIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotProfile"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! + self.chatBubbleActionButtonOutgoingAddToChatIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotAddToChat"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! self.chatEmptyItemLockIcon = generateImage(CGSize(width: 9.0, height: 13.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/BotAddToChat.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/BotAddToChat.imageset/Contents.json new file mode 100644 index 0000000000..0e8e9b3c7b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/BotAddToChat.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Size=10px.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/BotAddToChat.imageset/Size=10px.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/BotAddToChat.imageset/Size=10px.pdf new file mode 100644 index 0000000000..34cd21b4ac --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/BotAddToChat.imageset/Size=10px.pdf @@ -0,0 +1,83 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.170013 0.170002 cm +0.000000 0.000000 0.000000 scn +5.660000 8.830000 m +5.660000 9.288396 5.288396 9.660000 4.830000 9.660000 c +4.371603 9.660000 4.000000 9.288396 4.000000 8.830000 c +4.000000 5.660000 l +0.830000 5.660000 l +0.371604 5.660000 0.000000 5.288396 0.000000 4.830000 c +0.000000 4.371603 0.371604 4.000000 0.830000 4.000000 c +4.000000 4.000000 l +4.000000 0.830000 l +4.000000 0.371604 4.371603 0.000000 4.830000 0.000000 c +5.288396 0.000000 5.660000 0.371604 5.660000 0.830000 c +5.660000 4.000000 l +8.830000 4.000000 l +9.288396 4.000000 9.660000 4.371603 9.660000 4.830000 c +9.660000 5.288396 9.288396 5.660000 8.830000 5.660000 c +5.660000 5.660000 l +5.660000 8.830000 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 756 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 10.000000 10.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000846 00000 n +0000000868 00000 n +0000001041 00000 n +0000001115 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1174 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonStop.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonStop.imageset/Contents.json new file mode 100644 index 0000000000..f1eba03d05 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonStop.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "stop.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonStop.imageset/stop.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonStop.imageset/stop.pdf new file mode 100644 index 0000000000..8895b230a7 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonStop.imageset/stop.pdf @@ -0,0 +1,111 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 5.500000 5.500000 cm +0.000000 0.000000 0.000000 scn +4.167446 17.594112 m +1.405888 14.832555 l +1.405885 14.832552 l +0.887032 14.313698 0.627604 14.054272 0.442079 13.751522 c +0.277593 13.483105 0.156380 13.190471 0.082890 12.884362 c +0.000000 12.539099 0.000000 12.172214 0.000000 11.438441 c +0.000000 7.545784 l +0.000000 6.810102 0.000000 6.442260 0.083289 6.096203 c +0.157133 5.789393 0.278921 5.496157 0.444164 5.227307 c +0.630544 4.924067 0.891141 4.664457 1.412334 4.145239 c +4.168560 1.399452 l +4.168570 1.399441 l +4.687047 0.882929 4.946289 0.624670 5.248544 0.439999 c +5.516528 0.276268 5.808558 0.155630 6.113966 0.082491 c +6.458433 0.000000 6.824364 0.000000 7.556225 0.000000 c +11.454216 0.000000 l +12.189898 0.000000 12.557740 0.000000 12.903797 0.083290 c +13.210607 0.157133 13.503843 0.278921 13.772693 0.444164 c +14.075933 0.630545 14.335543 0.891140 14.854761 1.412334 c +17.600548 4.168560 l +18.117069 4.687044 18.375328 4.946287 18.560001 5.248544 c +18.723732 5.516527 18.844370 5.808558 18.917509 6.113965 c +19.000000 6.458433 19.000000 6.824364 19.000000 7.556226 c +19.000000 11.438440 l +19.000000 12.172213 19.000000 12.539099 18.917110 12.884362 c +18.843620 13.190471 18.722406 13.483105 18.557920 13.751522 c +18.372395 14.054272 18.112968 14.313700 17.594112 14.832554 c +14.832554 17.594112 l +14.832548 17.594120 l +14.832542 17.594124 l +14.313695 18.112970 14.054270 18.372396 13.751521 18.557920 c +13.483105 18.722406 13.190471 18.843620 12.884362 18.917110 c +12.539100 19.000000 12.172214 19.000000 11.438441 19.000000 c +7.561559 19.000000 l +6.827787 19.000000 6.460901 19.000000 6.115638 18.917110 c +5.809529 18.843620 5.516895 18.722406 5.248478 18.557920 c +4.945728 18.372395 4.686301 18.112968 4.167446 17.594112 c +h +5.500000 10.500000 m +4.947715 10.500000 4.500000 10.052285 4.500000 9.500000 c +4.500000 8.947715 4.947715 8.500000 5.500000 8.500000 c +13.500000 8.500000 l +14.052285 8.500000 14.500000 8.947715 14.500000 9.500000 c +14.500000 10.052285 14.052284 10.500000 13.500000 10.500000 c +5.500000 10.500000 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 2118 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002208 00000 n +0000002231 00000 n +0000002404 00000 n +0000002478 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2537 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatButtonKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatButtonKeyboardInputNode.swift index ee3274981a..54b4aa55dd 100644 --- a/submodules/TelegramUI/Sources/ChatButtonKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatButtonKeyboardInputNode.swift @@ -215,6 +215,8 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { self.controllerInteraction.openPollCreation(isQuiz) case let .openUserProfile(peerId): self.controllerInteraction.openPeer(peerId, .info, nil, nil) + case .addToChat: + self.controllerInteraction.openAddToChat() } if dismissIfOnce { if let message = self.message { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 796bffaece..39b0aee13f 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -3278,6 +3278,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } strongSelf.openResolved(result: .join(joinHash), sourceMessageId: nil) + }, openAddToChat: { [weak self] in + guard let strongSelf = self else { + return + } + let peerId = strongSelf.presentationInterfaceState.chatLocation.peerId + strongSelf.context.sharedContext.openResolvedUrl(.groupBotStart(peerId: peerId, payload: ""), context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.effectiveNavigationController, openPeer: { id, navigation in + }, sendFile: nil, + sendSticker: nil, + requestMessageActionUrlAuth: nil, + joinVoiceChat: nil, + present: { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) + }, dismissInput: { [weak self] in + self?.view.endEditing(true) + }, contentContext: nil) }, requestMessageUpdate: { [weak self] id in if let strongSelf = self { strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id) diff --git a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift index acff7b34b1..fc16e56473 100644 --- a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift @@ -131,6 +131,7 @@ public final class ChatControllerInteraction { let commitEmojiInteraction: (MessageId, String, EmojiInteraction, TelegramMediaFile) -> Void let openLargeEmojiInfo: (String, String?, TelegramMediaFile) -> Void let openJoinLink: (String) -> Void + let openAddToChat: () -> Void let requestMessageUpdate: (MessageId) -> Void let cancelInteractiveKeyboardGestures: () -> Void @@ -230,6 +231,7 @@ public final class ChatControllerInteraction { commitEmojiInteraction: @escaping (MessageId, String, EmojiInteraction, TelegramMediaFile) -> Void, openLargeEmojiInfo: @escaping (String, String?, TelegramMediaFile) -> Void, openJoinLink: @escaping (String) -> Void, + openAddToChat: @escaping () -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, @@ -315,6 +317,7 @@ public final class ChatControllerInteraction { self.commitEmojiInteraction = commitEmojiInteraction self.openLargeEmojiInfo = openLargeEmojiInfo self.openJoinLink = openJoinLink + self.openAddToChat = openAddToChat self.requestMessageUpdate = requestMessageUpdate self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures @@ -374,6 +377,7 @@ public final class ChatControllerInteraction { }, commitEmojiInteraction: { _, _, _, _ in }, openLargeEmojiInfo: { _, _, _ in }, openJoinLink: { _ in + }, openAddToChat: { }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, diff --git a/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift b/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift index 7d89c921f9..fb16be1edb 100644 --- a/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift @@ -106,6 +106,8 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { iconImage = incoming ? graphics.chatBubbleActionButtonIncomingPaymentIconImage : graphics.chatBubbleActionButtonOutgoingPaymentIconImage case .openUserProfile: iconImage = incoming ? graphics.chatBubbleActionButtonIncomingProfileIconImage : graphics.chatBubbleActionButtonOutgoingProfileIconImage + case .addToChat: + iconImage = incoming ? graphics.chatBubbleActionButtonIncomingAddToChatIconImage : graphics.chatBubbleActionButtonOutgoingAddToChatIconImage default: iconImage = nil } diff --git a/submodules/TelegramUI/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Sources/ChatMessageItemView.swift index c111678fb1..b67c735cb4 100644 --- a/submodules/TelegramUI/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Sources/ChatMessageItemView.swift @@ -854,6 +854,8 @@ public class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol break case let .openUserProfile(peerId): item.controllerInteraction.openPeer(peerId, .info, nil, nil) + case .addToChat: + item.controllerInteraction.openAddToChat() } } } diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index dd65022613..4be11e8f61 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -532,6 +532,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, commitEmojiInteraction: { _, _, _, _ in }, openLargeEmojiInfo: { _, _, _ in }, openJoinLink: { _ in + }, openAddToChat: { }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, diff --git a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift index 66cc0c9175..9bea55cb0d 100644 --- a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift +++ b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift @@ -159,6 +159,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { }, commitEmojiInteraction: { _, _, _, _ in }, openLargeEmojiInfo: { _, _, _ in }, openJoinLink: { _ in + }, openAddToChat: { }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index fc65e751bb..0953af5d5c 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -23,6 +23,8 @@ import ChatInterfaceState import TelegramCallsUI import UndoUI import ImportStickerPackUI +import PeerInfoUI +import Markdown private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer { if case .default = navigation { @@ -66,38 +68,86 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur case let .botStart(peerId, payload): openPeer(peerId, .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .interactive))) case let .groupBotStart(botPeerId, payload): - let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .onlyGroups, .onlyManageable], title: presentationData.strings.UserInfo_InviteBotToGroup)) + let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyGroupsAndChannels, .onlyManageable, .excludeDisabled], hasContactSelector: false, title: presentationData.strings.Bot_AddToChat_Title)) controller.peerSelected = { [weak controller] peer in let peerId = peer.id - if payload.isEmpty { - if peerId.namespace == Namespaces.Peer.CloudGroup { - let _ = (context.engine.peers.addGroupMember(peerId: peerId, memberId: botPeerId) - |> deliverOnMainQueue).start(completed: { - controller?.dismiss() - }) - } else { - let _ = (context.engine.peers.addChannelMember(peerId: peerId, memberId: botPeerId) - |> deliverOnMainQueue).start(completed: { - controller?.dismiss() - }) + let addMemberImpl = { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let theme = AlertControllerTheme(presentationData: presentationData) + let attributedTitle = NSAttributedString(string: presentationData.strings.Bot_AddToChat_Add_MemberAlertTitle, font: Font.medium(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) + + var isGroup: Bool = false + var peerTitle: String = "" + if let peer = peer as? TelegramGroup { + isGroup = true + peerTitle = peer.title + } else if let peer = peer as? TelegramChannel { + if case .group = peer.info { + isGroup = true + } + peerTitle = peer.title } - } else { - let _ = (context.engine.messages.requestStartBotInGroup(botPeerId: botPeerId, groupPeerId: peerId, payload: payload) - |> deliverOnMainQueue).start(next: { result in - if let navigationController = navigationController { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId))) + + let text = isGroup ? presentationData.strings.Bot_AddToChat_Add_MemberAlertTextGroup(peerTitle).string : presentationData.strings.Bot_AddToChat_Add_MemberAlertTextChannel(peerTitle).string + + let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor) + let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center) + + let controller = richTextAlertController(context: context, title: attributedTitle, text: attributedText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Bot_AddToChat_Add_MemberAlertAdd, action: { + if payload.isEmpty { + if peerId.namespace == Namespaces.Peer.CloudGroup { + let _ = (context.engine.peers.addGroupMember(peerId: peerId, memberId: botPeerId) + |> deliverOnMainQueue).start(completed: { + controller?.dismiss() + }) + } else { + let _ = (context.engine.peers.addChannelMember(peerId: peerId, memberId: botPeerId) + |> deliverOnMainQueue).start(completed: { + controller?.dismiss() + }) + } + } else { + let _ = (context.engine.messages.requestStartBotInGroup(botPeerId: botPeerId, groupPeerId: peerId, payload: payload) + |> deliverOnMainQueue).start(next: { result in + if let navigationController = navigationController { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId))) + } + switch result { + case let .channelParticipant(participant): + context.peerChannelMemberCategoriesContextsManager.externallyAdded(peerId: peerId, participant: participant) + case .none: + break + } + controller?.dismiss() + }, error: { _ in + + }) } - switch result { - case let .channelParticipant(participant): - context.peerChannelMemberCategoriesContextsManager.externallyAdded(peerId: peerId, participant: participant) - case .none: - break - } - controller?.dismiss() - }, error: { _ in - - }) + }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + })], actionLayout: .vertical) + present(controller, nil) + } + + if let peer = peer as? TelegramChannel { + if peer.flags.contains(.isCreator) || peer.adminRights != nil { + let controller = channelAdminController(context: context, peerId: peerId, adminId: botPeerId, initialParticipant: nil, invite: true, updated: { _ in + controller?.dismiss() + }, upgradedToSupergroup: { _, _ in }, transferedOwnership: { _ in }) + navigationController?.pushViewController(controller) + } else { + addMemberImpl() + } + } else if let peer = peer as? TelegramGroup { + if case .member = peer.role { + addMemberImpl() + } else { + let controller = channelAdminController(context: context, peerId: peerId, adminId: botPeerId, initialParticipant: nil, invite: true, updated: { _ in + controller?.dismiss() + }, upgradedToSupergroup: { _, _ in }, transferedOwnership: { _ in }) + navigationController?.pushViewController(controller) + } } } dismissInput() diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index 40d308e359..0c71cb069c 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -12,6 +12,7 @@ import UrlEscaping import PassportUI import UrlHandling import OpenInExternalAppUI +import BrowserUI public struct ParsedSecureIdUrl { public let peerId: PeerId @@ -732,6 +733,8 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur let _ = (settings |> deliverOnMainQueue).start(next: { settings in if settings.defaultWebBrowser == nil { +// let controller = BrowserScreen(context: context, subject: .webPage(parsedUrl.absoluteString)) +// navigationController?.pushViewController(controller) if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { if let window = navigationController?.view.window { let controller = SFSafariViewController(url: parsedUrl) diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index f34f83fb66..3f1c0f8324 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -151,6 +151,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, commitEmojiInteraction: { _, _, _, _ in }, openLargeEmojiInfo: { _, _, _ in }, openJoinLink: { _ in + }, openAddToChat: { }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(backgroundNode: nil)) diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift index 17eb17eefc..82fe677bf7 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift @@ -1037,6 +1037,11 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro if isOpenedFromChat { result.append(.search) } + + if user.botInfo != nil, let cachedData = cachedData as? CachedUserData, !cachedData.isBlocked { + result.append(.stop) + } + if (isSecretChat && !isContact) || user.flags.contains(.isSupport) { } else { result.append(.more) @@ -1085,7 +1090,7 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro if channel.isVerified || channel.adminRights != nil || channel.flags.contains(.isCreator) { canReport = false } - + var hasMore = false if canReport || canViewStats { hasMore = true diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index 8520b31a72..6ed3bd201c 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -34,6 +34,7 @@ enum PeerInfoHeaderButtonKey: Hashable { case addMember case search case leave + case stop } enum PeerInfoHeaderButtonIcon { @@ -47,6 +48,7 @@ enum PeerInfoHeaderButtonIcon { case addMember case search case leave + case stop } final class PeerInfoHeaderButtonNode: HighlightableButtonNode { @@ -241,6 +243,8 @@ final class PeerInfoHeaderButtonNode: HighlightableButtonNode { imageName = "Peer Info/ButtonSearch" case .leave: imageName = nil + case .stop: + imageName = "Peer Info/ButtonStop" } if let imageName = imageName, let image = generateTintedImage(image: UIImage(bundleImageName: imageName), color: .white) { let imageRect = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size) @@ -2851,6 +2855,9 @@ final class PeerInfoHeaderNode: ASDisplayNode { case .leave: buttonText = presentationData.strings.PeerInfo_ButtonLeave buttonIcon = .leave + case .stop: + buttonText = presentationData.strings.PeerInfo_ButtonStop + buttonIcon = .stop } var isActive = true diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 285a0d770b..67463a0aba 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -918,11 +918,20 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } else { if user.flags.contains(.isSupport) || data.isContact { } else { - items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 4, text: user.botInfo != nil ? presentationData.strings.Bot_Stop : presentationData.strings.Conversation_BlockUser, color: .destructive, action: { - interaction.updateBlocked(true) - })) + if user.botInfo == nil { + items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 4, text: presentationData.strings.Conversation_BlockUser, color: .destructive, action: { + interaction.updateBlocked(true) + })) + } } } + + if let botInfo = user.botInfo, botInfo.flags.contains(.worksWithGroups) { + items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 5, text: presentationData.strings.Bot_AddToChat, color: .accent, action: { + interaction.openAddBotToGroup() + })) + items[.peerInfo]!.append(PeerInfoScreenCommentItem(id: 6, text: presentationData.strings.Bot_AddToChatInfo)) + } } if let encryptionKeyFingerprint = data.encryptionKeyFingerprint { @@ -939,12 +948,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } else if let channel = data.peer as? TelegramChannel { let ItemUsername = 1 let ItemAbout = 2 - let ItemAdmins = 3 - let ItemMembers = 4 - let ItemMemberRequests = 5 - let ItemBanned = 6 - let ItemLocationHeader = 7 - let ItemLocation = 8 + let ItemLocationHeader = 3 + let ItemLocation = 4 if let location = (data.cachedData as? CachedChannelData)?.peerGeoLocation { items[.groupLocation]!.append(PeerInfoScreenHeaderItem(id: ItemLocationHeader, text: presentationData.strings.GroupInfo_Location.uppercased())) @@ -1001,37 +1006,6 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese interaction.requestLayout() })) } - - if case .broadcast = channel.info { - var canEditMembers = false - if channel.hasPermission(.banMembers) { - canEditMembers = true - } - if canEditMembers { - if channel.adminRights != nil || channel.flags.contains(.isCreator) { - let adminCount = cachedData.participantsSummary.adminCount ?? 0 - let memberCount = cachedData.participantsSummary.memberCount ?? 0 - let bannedCount = cachedData.participantsSummary.kickedCount ?? 0 - - items[.peerInfo]!.append(PeerInfoScreenDisclosureItem(id: ItemAdmins, label: .text("\(adminCount == 0 ? "" : "\(presentationStringsFormattedNumber(adminCount, presentationData.dateTimeFormat.groupingSeparator))")"), text: presentationData.strings.GroupInfo_Administrators, icon: UIImage(bundleImageName: "Chat/Info/GroupAdminsIcon"), action: { - interaction.openParticipantsSection(.admins) - })) - items[.peerInfo]!.append(PeerInfoScreenDisclosureItem(id: ItemMembers, label: .text("\(memberCount == 0 ? "" : "\(presentationStringsFormattedNumber(memberCount, presentationData.dateTimeFormat.groupingSeparator))")"), text: presentationData.strings.Channel_Info_Subscribers, icon: UIImage(bundleImageName: "Chat/Info/GroupMembersIcon"), action: { - interaction.openParticipantsSection(.members) - })) - - if let count = data.requests?.count, count > 0 { - items[.peerInfo]!.append(PeerInfoScreenDisclosureItem(id: ItemMemberRequests, label: .badge(presentationStringsFormattedNumber(count, presentationData.dateTimeFormat.groupingSeparator), presentationData.theme.list.itemAccentColor), text: presentationData.strings.GroupInfo_MemberRequests, icon: UIImage(bundleImageName: "Chat/Info/GroupRequestsIcon"), action: { - interaction.openParticipantsSection(.memberRequests) - })) - } - - items[.peerInfo]!.append(PeerInfoScreenDisclosureItem(id: ItemBanned, label: .text("\(bannedCount == 0 ? "" : "\(presentationStringsFormattedNumber(bannedCount, presentationData.dateTimeFormat.groupingSeparator))")"), text: presentationData.strings.GroupInfo_Permissions_Removed, icon: UIImage(bundleImageName: "Chat/Info/GroupRemovedIcon"), action: { - interaction.openParticipantsSection(.banned) - })) - } - } - } } } else if let group = data.peer as? TelegramGroup { if let cachedData = data.cachedData as? CachedGroupData { @@ -1143,6 +1117,11 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr let ItemDeleteChannel = 6 let ItemReactions = 7 + let ItemAdmins = 8 + let ItemMembers = 9 + let ItemMemberRequests = 10 + let ItemBanned = 11 + let isCreator = channel.flags.contains(.isCreator) if isCreator { @@ -1220,6 +1199,44 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr items[.peerSettings]!.append(PeerInfoScreenCommentItem(id: ItemSignMessagesHelp, text: presentationData.strings.Channel_SignMessages_Help)) } + var canEditMembers = false + if channel.hasPermission(.banMembers) { + canEditMembers = true + } + if canEditMembers { + if channel.adminRights != nil || channel.flags.contains(.isCreator) { + let adminCount: Int32 + let memberCount: Int32 + let bannedCount: Int32 + if let cachedData = data.cachedData as? CachedChannelData { + adminCount = cachedData.participantsSummary.adminCount ?? 0 + memberCount = cachedData.participantsSummary.memberCount ?? 0 + bannedCount = cachedData.participantsSummary.kickedCount ?? 0 + } else { + adminCount = 0 + memberCount = 0 + bannedCount = 0 + } + + items[.peerAdditionalSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemAdmins, label: .text("\(adminCount == 0 ? "" : "\(presentationStringsFormattedNumber(adminCount, presentationData.dateTimeFormat.groupingSeparator))")"), text: presentationData.strings.GroupInfo_Administrators, icon: UIImage(bundleImageName: "Chat/Info/GroupAdminsIcon"), action: { + interaction.openParticipantsSection(.admins) + })) + items[.peerAdditionalSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemMembers, label: .text("\(memberCount == 0 ? "" : "\(presentationStringsFormattedNumber(memberCount, presentationData.dateTimeFormat.groupingSeparator))")"), text: presentationData.strings.Channel_Info_Subscribers, icon: UIImage(bundleImageName: "Chat/Info/GroupMembersIcon"), action: { + interaction.openParticipantsSection(.members) + })) + + if let count = data.requests?.count, count > 0 { + items[.peerAdditionalSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemMemberRequests, label: .badge(presentationStringsFormattedNumber(count, presentationData.dateTimeFormat.groupingSeparator), presentationData.theme.list.itemAccentColor), text: presentationData.strings.GroupInfo_MemberRequests, icon: UIImage(bundleImageName: "Chat/Info/GroupRequestsIcon"), action: { + interaction.openParticipantsSection(.memberRequests) + })) + } + + items[.peerAdditionalSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemBanned, label: .text("\(bannedCount == 0 ? "" : "\(presentationStringsFormattedNumber(bannedCount, presentationData.dateTimeFormat.groupingSeparator))")"), text: presentationData.strings.GroupInfo_Permissions_Removed, icon: UIImage(bundleImageName: "Chat/Info/GroupRemovedIcon"), action: { + interaction.openParticipantsSection(.banned) + })) + } + } + if isCreator { items[.peerActions]!.append(PeerInfoScreenActionItem(id: ItemDeleteChannel, text: presentationData.strings.ChannelInfo_DeleteChannel, color: .destructive, icon: nil, alignment: .natural, action: { interaction.openDeletePeer() @@ -2248,6 +2265,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate }, commitEmojiInteraction: { _, _, _, _ in }, openLargeEmojiInfo: { _, _, _ in }, openJoinLink: { _ in + }, openAddToChat: { }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, @@ -3654,15 +3672,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } if let user = peer as? TelegramUser { - if let botInfo = user.botInfo { - if botInfo.flags.contains(.worksWithGroups) { - items.append(.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_InviteBotToGroup, icon: { theme in - generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Groups"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, f in - f(.dismissWithoutContent) - self?.openAddBotToGroup() - }))) - } + if let _ = user.botInfo { if user.username != nil { items.append(.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_ShareBot, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) @@ -3886,6 +3896,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate self.openChatWithMessageSearch() case .leave: self.openLeavePeer() + case .stop: + self.updateBlocked(block: true) } } @@ -4775,7 +4787,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate guard let controller = self.controller else { return } - context.sharedContext.openResolvedUrl(.groupBotStart(peerId: peerId, payload: ""), context: self.context, urlContext: .generic, navigationController: controller.navigationController as? NavigationController, openPeer: { id, navigation in + self.context.sharedContext.openResolvedUrl(.groupBotStart(peerId: peerId, payload: ""), context: self.context, urlContext: .generic, navigationController: controller.navigationController as? NavigationController, openPeer: { id, navigation in }, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, @@ -6107,7 +6119,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } } - private weak var mediaGalleryContextMenu: ContextController? func displaySharedMediaFastScrollingTooltip() { diff --git a/submodules/TelegramUI/Sources/PeerSelectionController.swift b/submodules/TelegramUI/Sources/PeerSelectionController.swift index a4ab1eab5b..9a140aa9b8 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionController.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionController.swift @@ -59,6 +59,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon private let hasGlobalSearch: Bool private let pretendPresentedInModal: Bool private let forwardedMessageIds: [EngineMessage.Id] + private let hasTypeHeaders: Bool override public var _presentedInModal: Bool { get { @@ -87,6 +88,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon self.createNewGroup = params.createNewGroup self.pretendPresentedInModal = params.pretendPresentedInModal self.forwardedMessageIds = params.forwardedMessageIds + self.hasTypeHeaders = params.hasTypeHeaders super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) @@ -151,7 +153,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon } override public func loadDisplayNode() { - self.displayNode = PeerSelectionControllerNode(context: self.context, presentationData: self.presentationData, filter: self.filter, hasChatListSelector: self.hasChatListSelector, hasContactSelector: self.hasContactSelector, hasGlobalSearch: self.hasGlobalSearch, forwardedMessageIds: self.forwardedMessageIds, createNewGroup: self.createNewGroup, present: { [weak self] c, a in + self.displayNode = PeerSelectionControllerNode(context: self.context, presentationData: self.presentationData, filter: self.filter, hasChatListSelector: self.hasChatListSelector, hasContactSelector: self.hasContactSelector, hasGlobalSearch: self.hasGlobalSearch, forwardedMessageIds: self.forwardedMessageIds, hasTypeHeaders: self.hasTypeHeaders, createNewGroup: self.createNewGroup, present: { [weak self] c, a in self?.present(c, in: .window(.root), with: a) }, presentInGlobalOverlay: { [weak self] c, a in self?.presentInGlobalOverlay(c, with: a) diff --git a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift index 26941aefaa..4709862458 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift @@ -25,6 +25,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { private let filter: ChatListNodePeersFilter private let hasGlobalSearch: Bool private let forwardedMessageIds: [EngineMessage.Id] + private let hasTypeHeaders: Bool private var presentationInterfaceState: ChatPresentationInterfaceState private var interfaceInteraction: ChatPanelInterfaceInteraction? @@ -80,7 +81,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { return (self.presentationData, self.presentationDataPromise.get()) } - init(context: AccountContext, presentationData: PresentationData, filter: ChatListNodePeersFilter, hasChatListSelector: Bool, hasContactSelector: Bool, hasGlobalSearch: Bool, forwardedMessageIds: [EngineMessage.Id], createNewGroup: (() -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) { + init(context: AccountContext, presentationData: PresentationData, filter: ChatListNodePeersFilter, hasChatListSelector: Bool, hasContactSelector: Bool, hasGlobalSearch: Bool, forwardedMessageIds: [EngineMessage.Id], hasTypeHeaders: Bool, createNewGroup: (() -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) { self.context = context self.present = present self.presentInGlobalOverlay = presentInGlobalOverlay @@ -88,6 +89,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.filter = filter self.hasGlobalSearch = hasGlobalSearch self.forwardedMessageIds = forwardedMessageIds + self.hasTypeHeaders = hasTypeHeaders self.presentationData = presentationData @@ -112,13 +114,13 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.segmentedControlNode = nil } - var chatListcategories: [ChatListNodeAdditionalCategory] = [] + var chatListCategories: [ChatListNodeAdditionalCategory] = [] if let _ = createNewGroup { - chatListcategories.append(ChatListNodeAdditionalCategory(id: 0, icon: PresentationResourcesItemList.createGroupIcon(self.presentationData.theme), title: self.presentationData.strings.PeerSelection_ImportIntoNewGroup, appearance: .action)) + chatListCategories.append(ChatListNodeAdditionalCategory(id: 0, icon: PresentationResourcesItemList.createGroupIcon(self.presentationData.theme), title: self.presentationData.strings.PeerSelection_ImportIntoNewGroup, appearance: .action)) } - self.chatListNode = ChatListNode(context: context, groupId: .root, previewing: false, fillPreloadItems: false, mode: .peers(filter: filter, isSelecting: false, additionalCategories: chatListcategories, chatListFilters: nil), theme: self.presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) + self.chatListNode = ChatListNode(context: context, groupId: .root, previewing: false, fillPreloadItems: false, mode: .peers(filter: filter, isSelecting: false, additionalCategories: chatListCategories, chatListFilters: nil), theme: self.presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) super.init() diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index fca422eabb..b1ffc898ab 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1325,6 +1325,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, commitEmojiInteraction: { _, _, _, _ in }, openLargeEmojiInfo: { _, _, _ in }, openJoinLink: { _ in + }, openAddToChat: { }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,