Add bot to chat improvements

This commit is contained in:
Ilya Laktyushin 2022-03-16 01:17:28 +04:00
parent 8ce13713b9
commit 9dddffe708
51 changed files with 3817 additions and 496 deletions

View File

@ -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";

View File

@ -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<PresentationData, NoError>)? = 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<PresentationData, NoError>)? = 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
}
}

View File

@ -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

View File

@ -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",
],
)

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
</plist>

View File

@ -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<BrowserContentState, NoError> { 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)
}

View File

@ -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() {
}
}

View File

@ -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<BrowserContentState>
//
// private let webPage: TelegramMediaWebpage
// private var initialized = false
//
// var state: Signal<BrowserContentState, NoError> {
// 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<BrowserContentState>(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)
// }
// }
//}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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<Bool>()
override public var ready: Promise<Bool> {
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<BrowserState>
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<BrowserState>(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)
}
}
}

View File

@ -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<CGFloat>(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()
}
}
}

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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<BrowserContentState>
var state: Signal<BrowserContentState, NoError> {
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<BrowserContentState>(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) }
}
}
}

View File

@ -979,6 +979,13 @@ 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(.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 {
@ -993,11 +1000,11 @@ public final class ChatListNode: ListView {
if filter.contains(.onlyChannels) {
if case let .channel(peer) = peer.chatMainPeer, case .broadcast = peer.info {
return true
} else {
return false
}
}
}
if filter.contains(.excludeChannels) {
if case let .channel(peer) = peer.chatMainPeer, case .broadcast = peer.info {
@ -1014,6 +1021,28 @@ public final class ChatListNode: ListView {
}
}
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
}
default:

View File

@ -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))

View File

@ -0,0 +1,8 @@
//
// VStack.swift
// _idx_ComponentFlow_0C0A11F8_ios_min9.0
//
// Created by Ilya Laktyushin on 15.03.2022.
//
import Foundation

View File

@ -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) {
}
}

View File

@ -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<T: ItemListNodeEntry>(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<T: ItemListNodeEntry>(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)
}
}

View File

@ -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)
}

View File

@ -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

View File

@ -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)

View File

@ -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
}
}
}

View File

@ -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,6 +624,11 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
}
}
} else {
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 !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) {
@ -618,7 +666,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
canTransfer = true
}
if let initialParticipant = initialParticipant, case .member = initialParticipant, admin.id != accountPeerId {
if let initialParticipant = initialParticipant, case let .member(_, _, adminInfo, _, _) = initialParticipant, admin.id != accountPeerId, adminInfo != nil {
if channel.flags.contains(.isCreator) {
canDismiss = true
} else {
@ -642,6 +690,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
}
}
}
}
if canTransfer {
entries.append(.transfer(presentationData.theme, isGroup ? presentationData.strings.Group_EditAdmin_TransferOwnership : presentationData.strings.Channel_EditAdmin_TransferOwnership))
@ -689,11 +738,16 @@ 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 {
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 !invite || state.adminRights {
entries.append(.rightsTitle(presentationData.theme, presentationData.strings.Channel_EditAdmin_PermissionsHeader))
let isGroup = true
let isChannel = false
let maskRightsFlags: TelegramChatAdminRightsFlags = .groupSpecific
var maskRightsFlags: TelegramChatAdminRightsFlags = .groupSpecific
let rightsOrder: [TelegramChatAdminRightsFlags] = [
.canChangeInfo,
.canDeleteMessages,
@ -705,6 +759,12 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
.canAddAdmins
]
if invite {
maskRightsFlags.remove(.canManageCalls)
maskRightsFlags.remove(.canBeAnonymous)
maskRightsFlags.remove(.canAddAdmins)
}
let accountUserRightsFlags: TelegramChatAdminRightsFlags = maskRightsFlags
let currentRightsFlags: TelegramChatAdminRightsFlags
@ -737,10 +797,13 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
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, isCreator ? presentationData.strings.Group_EditAdmin_RankOwnerPlaceholder : presentationData.strings.Group_EditAdmin_RankAdminPlaceholder, currentRank ?? "", rankEnabled))
entries.append(.rank(presentationData.theme, presentationData.strings, placeholder, currentRank ?? "", rankEnabled))
entries.append(.rankInfo(presentationData.theme, presentationData.strings.Group_EditAdmin_RankInfo(placeholder).string))
}
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<PresentationData, NoError>)? = 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<PresentationData, NoError>)? = 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,12 +974,59 @@ 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 {
updateRightsDisposable.set((context.engine.peers.addChannelMember(peerId: peerId, memberId: adminId) |> 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 .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?
@ -1213,12 +1327,66 @@ public func channelAdminController(context: AccountContext, updatedPresentationD
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))
}

View File

@ -91,8 +91,6 @@ final class RecentSessionsEmptyStateItemNode: ItemListControllerEmptyStateItemNo
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)))

View File

@ -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")
}
}
}

View File

@ -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))

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Size=10px.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "stop.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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

View File

@ -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 {

View File

@ -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)

View File

@ -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,

View File

@ -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
}

View File

@ -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()
}
}
}

View File

@ -532,6 +532,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, commitEmojiInteraction: { _, _, _, _ in
}, openLargeEmojiInfo: { _, _, _ in
}, openJoinLink: { _ in
}, openAddToChat: {
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,

View File

@ -159,6 +159,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
}, commitEmojiInteraction: { _, _, _, _ in
}, openLargeEmojiInfo: { _, _, _ in
}, openJoinLink: { _ in
}, openAddToChat: {
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -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,10 +68,34 @@ 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
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
}
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)
@ -99,6 +125,30 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
})
}
}), 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()
navigationController?.pushViewController(controller)

View File

@ -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)

View File

@ -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))

View File

@ -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)

View File

@ -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

View File

@ -918,13 +918,22 @@ 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: {
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 {
items[.peerInfo]!.append(PeerInfoScreenDisclosureEncryptionKeyItem(id: 6, text: presentationData.strings.Profile_EncryptionKey, fingerprint: encryptionKeyFingerprint, action: {
interaction.openEncryptionKey()
@ -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() {

View File

@ -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)

View File

@ -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()

View File

@ -1325,6 +1325,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}, commitEmojiInteraction: { _, _, _, _ in
}, openLargeEmojiInfo: { _, _, _ in
}, openJoinLink: { _ in
}, openAddToChat: {
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,