mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
Add bot to chat improvements
This commit is contained in:
parent
8ce13713b9
commit
9dddffe708
@ -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";
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
27
submodules/BrowserUI/BUILD
Normal file
27
submodules/BrowserUI/BUILD
Normal 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",
|
||||
],
|
||||
)
|
22
submodules/BrowserUI/Info.plist
Normal file
22
submodules/BrowserUI/Info.plist
Normal 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>
|
62
submodules/BrowserUI/Sources/BrowserContent.swift
Normal file
62
submodules/BrowserUI/Sources/BrowserContent.swift
Normal 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)
|
||||
}
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
91
submodules/BrowserUI/Sources/BrowserInstantPageContent.swift
Normal file
91
submodules/BrowserUI/Sources/BrowserInstantPageContent.swift
Normal 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)
|
||||
// }
|
||||
// }
|
||||
//}
|
35
submodules/BrowserUI/Sources/BrowserInteraction.swift
Normal file
35
submodules/BrowserUI/Sources/BrowserInteraction.swift
Normal 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
|
||||
}
|
||||
}
|
416
submodules/BrowserUI/Sources/BrowserNavigationBar.swift
Normal file
416
submodules/BrowserUI/Sources/BrowserNavigationBar.swift
Normal 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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
520
submodules/BrowserUI/Sources/BrowserScreen.swift
Normal file
520
submodules/BrowserUI/Sources/BrowserScreen.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
445
submodules/BrowserUI/Sources/BrowserStackContainerNode.swift
Normal file
445
submodules/BrowserUI/Sources/BrowserStackContainerNode.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
165
submodules/BrowserUI/Sources/BrowserToolbar.swift
Normal file
165
submodules/BrowserUI/Sources/BrowserToolbar.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
209
submodules/BrowserUI/Sources/BrowserWebContent.swift
Normal file
209
submodules/BrowserUI/Sources/BrowserWebContent.swift
Normal 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) }
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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))
|
||||
|
8
submodules/ComponentFlow/Source/Components/VStack.swift
Normal file
8
submodules/ComponentFlow/Source/Components/VStack.swift
Normal file
@ -0,0 +1,8 @@
|
||||
//
|
||||
// VStack.swift
|
||||
// _idx_ComponentFlow_0C0A11F8_ios_min9.0
|
||||
//
|
||||
// Created by Ilya Laktyushin on 15.03.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
125
submodules/PeerInfoUI/Sources/ChannelAdminAddBotFooterItem.swift
Normal file
125
submodules/PeerInfoUI/Sources/ChannelAdminAddBotFooterItem.swift
Normal 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
@ -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 {
|
||||
|
@ -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)))
|
||||
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Chat/Message/BotAddToChat.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Message/BotAddToChat.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Size=10px.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
83
submodules/TelegramUI/Images.xcassets/Chat/Message/BotAddToChat.imageset/Size=10px.pdf
vendored
Normal file
83
submodules/TelegramUI/Images.xcassets/Chat/Message/BotAddToChat.imageset/Size=10px.pdf
vendored
Normal 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
|
12
submodules/TelegramUI/Images.xcassets/Peer Info/ButtonStop.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Peer Info/ButtonStop.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "stop.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
111
submodules/TelegramUI/Images.xcassets/Peer Info/ButtonStop.imageset/stop.pdf
vendored
Normal file
111
submodules/TelegramUI/Images.xcassets/Peer Info/ButtonStop.imageset/stop.pdf
vendored
Normal 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
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -532,6 +532,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
}, commitEmojiInteraction: { _, _, _, _ in
|
||||
}, openLargeEmojiInfo: { _, _, _ in
|
||||
}, openJoinLink: { _ in
|
||||
}, openAddToChat: {
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,
|
||||
|
@ -159,6 +159,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
|
||||
}, commitEmojiInteraction: { _, _, _, _ in
|
||||
}, openLargeEmojiInfo: { _, _, _ in
|
||||
}, openJoinLink: { _ in
|
||||
}, openAddToChat: {
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
||||
|
@ -1325,6 +1325,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
}, commitEmojiInteraction: { _, _, _, _ in
|
||||
}, openLargeEmojiInfo: { _, _, _ in
|
||||
}, openJoinLink: { _ in
|
||||
}, openAddToChat: {
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
|
Loading…
x
Reference in New Issue
Block a user