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,24 +979,31 @@ public final class ChatListNode: ListView {
guard !filter.contains(.excludeSecretChats) || peer.peerId.namespace != Namespaces.Peer.SecretChat else { return false }
guard !filter.contains(.onlyPrivateChats) || peer.peerId.namespace == Namespaces.Peer.CloudUser else { return false }
if filter.contains(.onlyGroups) {
var isGroup: Bool = false
if case let .channel(peer) = peer.chatMainPeer, case .group = peer.info {
isGroup = true
} else if peer.peerId.namespace == Namespaces.Peer.CloudGroup {
isGroup = true
}
if !isGroup {
return false
}
}
if filter.contains(.onlyChannels) {
if case let .channel(peer) = peer.chatMainPeer, case .broadcast = peer.info {
return true
if filter.contains(.onlyGroupsAndChannels) {
if case .channel = peer.chatMainPeer {
} else if case .legacyGroup = peer.chatMainPeer {
} else {
return false
}
} else {
if filter.contains(.onlyGroups) {
var isGroup: Bool = false
if case let .channel(peer) = peer.chatMainPeer, case .group = peer.info {
isGroup = true
} else if peer.peerId.namespace == Namespaces.Peer.CloudGroup {
isGroup = true
}
if !isGroup {
return false
}
}
if filter.contains(.onlyChannels) {
if case let .channel(peer) = peer.chatMainPeer, case .broadcast = peer.info {
} else {
return false
}
}
}
if filter.contains(.excludeChannels) {
@ -1013,6 +1020,28 @@ public final class ChatListNode: ListView {
return false
}
}
if filter.contains(.onlyManageable) && filter.contains(.excludeDisabled) {
if let peer = peer.peers[peer.peerId] {
var canManage = false
if case let .legacyGroup(peer) = peer {
switch peer.role {
case .creator, .admin:
canManage = true
default:
break
}
}
if canManage {
} else if case let .channel(peer) = peer, case .group = peer.info, peer.hasPermission(.inviteMembers) {
} else {
return false
}
} else {
return false
}
}
return true
}

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

File diff suppressed because it is too large Load Diff

View File

@ -1093,7 +1093,7 @@ public final class Transaction {
assert(!self.disposed)
self.postbox?.deviceContactImportInfoTable.enumerateDeviceContactImportInfoItems(f)
}
public func getChatListNamespaceEntries(groupId: PeerGroupId, namespace: MessageId.Namespace, summaryTag: MessageTags?) -> [ChatListNamespaceEntry] {
assert(!self.disposed)
guard let postbox = self.postbox else {

View File

@ -90,9 +90,7 @@ final class RecentSessionsEmptyStateItemNode: ItemListControllerEmptyStateItemNo
if layout.size.width == 320 {
textVisible = false
}
self.backgroundColor = .red
let titleSize = self.titleNode.measure(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - layout.intrinsicInsets.left - layout.intrinsicInsets.right - 50.0, height: max(1.0, layout.size.height - insets.top - insets.bottom)))
let textSize = self.textNode.measure(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - layout.intrinsicInsets.left - layout.intrinsicInsets.right - 50.0, height: max(1.0, layout.size.height - insets.top - insets.bottom)))

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,38 +68,86 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
case let .botStart(peerId, payload):
openPeer(peerId, .withBotStartPayload(ChatControllerInitialBotStart(payload: payload, behavior: .interactive)))
case let .groupBotStart(botPeerId, payload):
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .onlyGroups, .onlyManageable], title: presentationData.strings.UserInfo_InviteBotToGroup))
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyGroupsAndChannels, .onlyManageable, .excludeDisabled], hasContactSelector: false, title: presentationData.strings.Bot_AddToChat_Title))
controller.peerSelected = { [weak controller] peer in
let peerId = peer.id
if payload.isEmpty {
if peerId.namespace == Namespaces.Peer.CloudGroup {
let _ = (context.engine.peers.addGroupMember(peerId: peerId, memberId: botPeerId)
|> deliverOnMainQueue).start(completed: {
controller?.dismiss()
})
} else {
let _ = (context.engine.peers.addChannelMember(peerId: peerId, memberId: botPeerId)
|> deliverOnMainQueue).start(completed: {
controller?.dismiss()
})
let addMemberImpl = {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let theme = AlertControllerTheme(presentationData: presentationData)
let attributedTitle = NSAttributedString(string: presentationData.strings.Bot_AddToChat_Add_MemberAlertTitle, font: Font.medium(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
var isGroup: Bool = false
var peerTitle: String = ""
if let peer = peer as? TelegramGroup {
isGroup = true
peerTitle = peer.title
} else if let peer = peer as? TelegramChannel {
if case .group = peer.info {
isGroup = true
}
peerTitle = peer.title
}
} else {
let _ = (context.engine.messages.requestStartBotInGroup(botPeerId: botPeerId, groupPeerId: peerId, payload: payload)
|> deliverOnMainQueue).start(next: { result in
if let navigationController = navigationController {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId)))
let text = isGroup ? presentationData.strings.Bot_AddToChat_Add_MemberAlertTextGroup(peerTitle).string : presentationData.strings.Bot_AddToChat_Add_MemberAlertTextChannel(peerTitle).string
let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor)
let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor)
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center)
let controller = richTextAlertController(context: context, title: attributedTitle, text: attributedText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Bot_AddToChat_Add_MemberAlertAdd, action: {
if payload.isEmpty {
if peerId.namespace == Namespaces.Peer.CloudGroup {
let _ = (context.engine.peers.addGroupMember(peerId: peerId, memberId: botPeerId)
|> deliverOnMainQueue).start(completed: {
controller?.dismiss()
})
} else {
let _ = (context.engine.peers.addChannelMember(peerId: peerId, memberId: botPeerId)
|> deliverOnMainQueue).start(completed: {
controller?.dismiss()
})
}
} else {
let _ = (context.engine.messages.requestStartBotInGroup(botPeerId: botPeerId, groupPeerId: peerId, payload: payload)
|> deliverOnMainQueue).start(next: { result in
if let navigationController = navigationController {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId)))
}
switch result {
case let .channelParticipant(participant):
context.peerChannelMemberCategoriesContextsManager.externallyAdded(peerId: peerId, participant: participant)
case .none:
break
}
controller?.dismiss()
}, error: { _ in
})
}
switch result {
case let .channelParticipant(participant):
context.peerChannelMemberCategoriesContextsManager.externallyAdded(peerId: peerId, participant: participant)
case .none:
break
}
controller?.dismiss()
}, error: { _ in
})
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
})], actionLayout: .vertical)
present(controller, nil)
}
if let peer = peer as? TelegramChannel {
if peer.flags.contains(.isCreator) || peer.adminRights != nil {
let controller = channelAdminController(context: context, peerId: peerId, adminId: botPeerId, initialParticipant: nil, invite: true, updated: { _ in
controller?.dismiss()
}, upgradedToSupergroup: { _, _ in }, transferedOwnership: { _ in })
navigationController?.pushViewController(controller)
} else {
addMemberImpl()
}
} else if let peer = peer as? TelegramGroup {
if case .member = peer.role {
addMemberImpl()
} else {
let controller = channelAdminController(context: context, peerId: peerId, adminId: botPeerId, initialParticipant: nil, invite: true, updated: { _ in
controller?.dismiss()
}, upgradedToSupergroup: { _, _ in }, transferedOwnership: { _ in })
navigationController?.pushViewController(controller)
}
}
}
dismissInput()

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)
@ -1085,7 +1090,7 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
if channel.isVerified || channel.adminRights != nil || channel.flags.contains(.isCreator) {
canReport = false
}
var hasMore = false
if canReport || canViewStats {
hasMore = true

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,11 +918,20 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
} else {
if user.flags.contains(.isSupport) || data.isContact {
} else {
items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 4, text: user.botInfo != nil ? presentationData.strings.Bot_Stop : presentationData.strings.Conversation_BlockUser, color: .destructive, action: {
interaction.updateBlocked(true)
}))
if user.botInfo == nil {
items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 4, text: presentationData.strings.Conversation_BlockUser, color: .destructive, action: {
interaction.updateBlocked(true)
}))
}
}
}
if let botInfo = user.botInfo, botInfo.flags.contains(.worksWithGroups) {
items[.peerInfo]!.append(PeerInfoScreenActionItem(id: 5, text: presentationData.strings.Bot_AddToChat, color: .accent, action: {
interaction.openAddBotToGroup()
}))
items[.peerInfo]!.append(PeerInfoScreenCommentItem(id: 6, text: presentationData.strings.Bot_AddToChatInfo))
}
}
if let encryptionKeyFingerprint = data.encryptionKeyFingerprint {
@ -939,12 +948,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
} else if let channel = data.peer as? TelegramChannel {
let ItemUsername = 1
let ItemAbout = 2
let ItemAdmins = 3
let ItemMembers = 4
let ItemMemberRequests = 5
let ItemBanned = 6
let ItemLocationHeader = 7
let ItemLocation = 8
let ItemLocationHeader = 3
let ItemLocation = 4
if let location = (data.cachedData as? CachedChannelData)?.peerGeoLocation {
items[.groupLocation]!.append(PeerInfoScreenHeaderItem(id: ItemLocationHeader, text: presentationData.strings.GroupInfo_Location.uppercased()))
@ -1001,37 +1006,6 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
interaction.requestLayout()
}))
}
if case .broadcast = channel.info {
var canEditMembers = false
if channel.hasPermission(.banMembers) {
canEditMembers = true
}
if canEditMembers {
if channel.adminRights != nil || channel.flags.contains(.isCreator) {
let adminCount = cachedData.participantsSummary.adminCount ?? 0
let memberCount = cachedData.participantsSummary.memberCount ?? 0
let bannedCount = cachedData.participantsSummary.kickedCount ?? 0
items[.peerInfo]!.append(PeerInfoScreenDisclosureItem(id: ItemAdmins, label: .text("\(adminCount == 0 ? "" : "\(presentationStringsFormattedNumber(adminCount, presentationData.dateTimeFormat.groupingSeparator))")"), text: presentationData.strings.GroupInfo_Administrators, icon: UIImage(bundleImageName: "Chat/Info/GroupAdminsIcon"), action: {
interaction.openParticipantsSection(.admins)
}))
items[.peerInfo]!.append(PeerInfoScreenDisclosureItem(id: ItemMembers, label: .text("\(memberCount == 0 ? "" : "\(presentationStringsFormattedNumber(memberCount, presentationData.dateTimeFormat.groupingSeparator))")"), text: presentationData.strings.Channel_Info_Subscribers, icon: UIImage(bundleImageName: "Chat/Info/GroupMembersIcon"), action: {
interaction.openParticipantsSection(.members)
}))
if let count = data.requests?.count, count > 0 {
items[.peerInfo]!.append(PeerInfoScreenDisclosureItem(id: ItemMemberRequests, label: .badge(presentationStringsFormattedNumber(count, presentationData.dateTimeFormat.groupingSeparator), presentationData.theme.list.itemAccentColor), text: presentationData.strings.GroupInfo_MemberRequests, icon: UIImage(bundleImageName: "Chat/Info/GroupRequestsIcon"), action: {
interaction.openParticipantsSection(.memberRequests)
}))
}
items[.peerInfo]!.append(PeerInfoScreenDisclosureItem(id: ItemBanned, label: .text("\(bannedCount == 0 ? "" : "\(presentationStringsFormattedNumber(bannedCount, presentationData.dateTimeFormat.groupingSeparator))")"), text: presentationData.strings.GroupInfo_Permissions_Removed, icon: UIImage(bundleImageName: "Chat/Info/GroupRemovedIcon"), action: {
interaction.openParticipantsSection(.banned)
}))
}
}
}
}
} else if let group = data.peer as? TelegramGroup {
if let cachedData = data.cachedData as? CachedGroupData {
@ -1143,6 +1117,11 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
let ItemDeleteChannel = 6
let ItemReactions = 7
let ItemAdmins = 8
let ItemMembers = 9
let ItemMemberRequests = 10
let ItemBanned = 11
let isCreator = channel.flags.contains(.isCreator)
if isCreator {
@ -1220,6 +1199,44 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
items[.peerSettings]!.append(PeerInfoScreenCommentItem(id: ItemSignMessagesHelp, text: presentationData.strings.Channel_SignMessages_Help))
}
var canEditMembers = false
if channel.hasPermission(.banMembers) {
canEditMembers = true
}
if canEditMembers {
if channel.adminRights != nil || channel.flags.contains(.isCreator) {
let adminCount: Int32
let memberCount: Int32
let bannedCount: Int32
if let cachedData = data.cachedData as? CachedChannelData {
adminCount = cachedData.participantsSummary.adminCount ?? 0
memberCount = cachedData.participantsSummary.memberCount ?? 0
bannedCount = cachedData.participantsSummary.kickedCount ?? 0
} else {
adminCount = 0
memberCount = 0
bannedCount = 0
}
items[.peerAdditionalSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemAdmins, label: .text("\(adminCount == 0 ? "" : "\(presentationStringsFormattedNumber(adminCount, presentationData.dateTimeFormat.groupingSeparator))")"), text: presentationData.strings.GroupInfo_Administrators, icon: UIImage(bundleImageName: "Chat/Info/GroupAdminsIcon"), action: {
interaction.openParticipantsSection(.admins)
}))
items[.peerAdditionalSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemMembers, label: .text("\(memberCount == 0 ? "" : "\(presentationStringsFormattedNumber(memberCount, presentationData.dateTimeFormat.groupingSeparator))")"), text: presentationData.strings.Channel_Info_Subscribers, icon: UIImage(bundleImageName: "Chat/Info/GroupMembersIcon"), action: {
interaction.openParticipantsSection(.members)
}))
if let count = data.requests?.count, count > 0 {
items[.peerAdditionalSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemMemberRequests, label: .badge(presentationStringsFormattedNumber(count, presentationData.dateTimeFormat.groupingSeparator), presentationData.theme.list.itemAccentColor), text: presentationData.strings.GroupInfo_MemberRequests, icon: UIImage(bundleImageName: "Chat/Info/GroupRequestsIcon"), action: {
interaction.openParticipantsSection(.memberRequests)
}))
}
items[.peerAdditionalSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemBanned, label: .text("\(bannedCount == 0 ? "" : "\(presentationStringsFormattedNumber(bannedCount, presentationData.dateTimeFormat.groupingSeparator))")"), text: presentationData.strings.GroupInfo_Permissions_Removed, icon: UIImage(bundleImageName: "Chat/Info/GroupRemovedIcon"), action: {
interaction.openParticipantsSection(.banned)
}))
}
}
if isCreator {
items[.peerActions]!.append(PeerInfoScreenActionItem(id: ItemDeleteChannel, text: presentationData.strings.ChannelInfo_DeleteChannel, color: .destructive, icon: nil, alignment: .natural, action: {
interaction.openDeletePeer()
@ -2248,6 +2265,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
}, commitEmojiInteraction: { _, _, _, _ in
}, openLargeEmojiInfo: { _, _, _ in
}, openJoinLink: { _ in
}, openAddToChat: {
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
@ -3654,15 +3672,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
}
if let user = peer as? TelegramUser {
if let botInfo = user.botInfo {
if botInfo.flags.contains(.worksWithGroups) {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_InviteBotToGroup, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Groups"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
self?.openAddBotToGroup()
})))
}
if let _ = user.botInfo {
if user.username != nil {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_ShareBot, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor)
@ -3886,6 +3896,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
self.openChatWithMessageSearch()
case .leave:
self.openLeavePeer()
case .stop:
self.updateBlocked(block: true)
}
}
@ -4775,7 +4787,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
guard let controller = self.controller else {
return
}
context.sharedContext.openResolvedUrl(.groupBotStart(peerId: peerId, payload: ""), context: self.context, urlContext: .generic, navigationController: controller.navigationController as? NavigationController, openPeer: { id, navigation in
self.context.sharedContext.openResolvedUrl(.groupBotStart(peerId: peerId, payload: ""), context: self.context, urlContext: .generic, navigationController: controller.navigationController as? NavigationController, openPeer: { id, navigation in
}, sendFile: nil,
sendSticker: nil,
requestMessageActionUrlAuth: nil,
@ -6107,7 +6119,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
}
}
private weak var mediaGalleryContextMenu: ContextController?
func displaySharedMediaFastScrollingTooltip() {

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,