Various improvements

This commit is contained in:
Ilya Laktyushin 2026-05-28 16:50:05 +02:00
parent 5fbe230308
commit 3e6363abf9
351 changed files with 11565 additions and 12031 deletions

View file

@ -1570,6 +1570,8 @@ plist_fragment(
<string>here-location</string>
<string>yandexmaps</string>
<string>yandexnavi</string>
<string>yandextaxi</string>
<string>yangoride</string>
<string>comgooglemaps</string>
<string>youtube</string>
<string>twitter</string>
@ -1598,6 +1600,7 @@ plist_fragment(
<string>dolphin</string>
<string>instagram-stories</string>
<string>yangomaps</string>
<string>vivaldi</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>

View file

@ -16339,3 +16339,6 @@ Error: %8$@";
"RecentSessions.ConnectedBot.TerminateCheckbox" = "Also terminate %@";
"Chat.GuestChatMessageTooltip" = "This bot can't read the chat only the messages where it was mentioned.";
"MediaPicker.SetNewGroupPhoto" = "Set new group photo";
"MediaPicker.SetNewChannelPhoto" = "Set new channel photo";

View file

@ -185,6 +185,22 @@ public enum WallpaperUrlParameter {
case gradient([UInt32], Int32?)
}
public enum PeerType: Equatable {
case user(isBot: Bool)
case group
case channel
public static func getType(for peer: EnginePeer) -> PeerType {
if case let .user(user) = peer {
return .user(isBot: user.botInfo != nil)
} else if case let .channel(channel) = peer, case .broadcast = channel.info {
return .channel
} else {
return .group
}
}
}
public struct ResolvedBotChoosePeerTypes: OptionSet {
public var rawValue: UInt32
@ -1484,7 +1500,7 @@ public protocol SharedAccountContext: AnyObject {
func makeBotPreviewEditorScreen(context: AccountContext, source: Any?, target: Stories.PendingTarget, transitionArguments: (UIView, CGRect, UIImage?)?, transitionOut: @escaping () -> BotPreviewEditorTransitionOut?, externalState: MediaEditorTransitionOutExternalState, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController
func makeStickerEditorScreen(context: AccountContext, source: Any?, mode: StickerEditorMode, transitionArguments: (UIView, CGRect, UIImage?)?, completion: @escaping (TelegramMediaFile, [String], @escaping () -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController
func makeStickerMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController
func makeAvatarMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, canDelete: Bool, performDelete: @escaping () -> Void, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> (ViewController?, Any?)
func makeAvatarMediaPickerScreen(context: AccountContext, peerType: PeerType, getSourceRect: @escaping () -> CGRect?, canDelete: Bool, performDelete: @escaping () -> Void, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> (ViewController?, Any?)
func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, forCollage: Bool, selectionLimit: Int?, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, multipleCompletion: @escaping ([Any], Bool) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController
func makeStickerPickerScreen(context: AccountContext, inputData: Promise<StickerPickerInput>, completion: @escaping (FileMediaReference) -> Void) -> ViewController
func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController

View file

@ -264,6 +264,23 @@ public extension ChatMessageItemAssociatedData {
return false
}
}
func isPollVotingRestricted(poll: TelegramMediaPoll, accountTestingEnvironment: Bool, currentTimestamp: Int32) -> Bool {
if !poll.countries.isEmpty, let accountCountry = self.accountCountry, !poll.countries.contains(accountCountry) {
return true
}
if poll.restrictToSubscribers {
let period: Int32 = accountTestingEnvironment ? 5 * 60 : 24 * 60 * 60
if !self.isParticipant {
return true
} else if let invitedOn = self.invitedOn, invitedOn + period > currentTimestamp {
return true
}
}
return false
}
}
public enum ChatControllerInteractionLongTapAction {

View file

@ -78,7 +78,7 @@ public final class AdInfoScreen: ViewController {
self.scrollNode = ASScrollNode()
self.scrollNode.view.showsVerticalScrollIndicator = true
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.scrollsToTop = true
self.scrollNode.view.scrollsToTop = false
self.scrollNode.view.delaysContentTouches = false
self.scrollNode.view.canCancelContentTouches = true
if #available(iOS 11.0, *) {

View file

@ -341,6 +341,7 @@
textView.opaque = NO;
}
textView.textContainerInset = self.textContainerInset;
textView.scrollsToTop = false;
// Configure textView with UITextInputTraits
{
@ -366,6 +367,7 @@
_placeholderTextKitComponents.textView = [[ASTextKitComponentsTextView alloc] initWithFrame:CGRectZero textContainer:_placeholderTextKitComponents.textContainer];
_placeholderTextKitComponents.textView.userInteractionEnabled = NO;
_placeholderTextKitComponents.textView.accessibilityElementsHidden = YES;
_placeholderTextKitComponents.textView.scrollsToTop = false;
configureTextView(_placeholderTextKitComponents.textView);
// Create and configure our text view.

View file

@ -1563,12 +1563,22 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
if let data = view.cachedData as? CachedUserData {
return data.sendPaidMessageStars
} else if let channel = peerViewMainPeer(view) as? TelegramChannel {
if channel.isMonoForum, let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = view.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect) {
return nil
} else if let cachedData = view.cachedData as? CachedChannelData, let sendPaidMessageStarsValue = cachedData.sendPaidMessageStars, sendPaidMessageStarsValue == .zero {
return nil
if channel.isMonoForum {
if let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = view.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect) {
return nil
} else if let cachedData = view.cachedData as? CachedChannelData, let value = cachedData.sendPaidMessageStars, value == .zero {
return nil
} else {
return channel.sendPaidMessageStars
}
} else {
return channel.sendPaidMessageStars
if channel.flags.contains(.isCreator) || channel.adminRights != nil {
return nil
} else if let cachedData = view.cachedData as? CachedChannelData, let value = cachedData.sendPaidMessageStars {
return value == .zero ? nil : value
} else {
return channel.sendPaidMessageStars
}
}
} else {
return nil
@ -1600,6 +1610,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
self.containerNode.layer.cornerCurve = .continuous
}
self.scrollNode.view.scrollsToTop = false
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.showsVerticalScrollIndicator = false

View file

@ -11,10 +11,21 @@ swift_library(
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/LocalAuth:LocalAuth",
"//submodules/AccountContext:AccountContext",
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
"//submodules/Components/ResizableSheetComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/TelegramUI/Components/AlertComponent",
"//submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent",
"//submodules/ItemListUI:ItemListUI",
"//submodules/PasswordSetupUI:PasswordSetupUI",
"//submodules/PhotoResources:PhotoResources",

View file

@ -117,6 +117,7 @@ final class BotCheckoutActionButton: HighlightTrackingButtonNode {
} else {
applePayButton = PKPaymentButton(paymentButtonType: .buy, paymentButtonStyle: .black)
}
applePayButton.cornerRadius = BotCheckoutActionButton.height * 0.5
applePayButton.addTarget(self, action: #selector(self.applePayButtonPressed), for: .touchUpInside)
self.view.addSubview(applePayButton)
self.applePayButton = applePayButton

View file

@ -141,9 +141,10 @@ public final class BotCheckoutController: ViewController {
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, style: .glass))
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self._hasGlassStyle = true
var title = self.presentationData.strings.Checkout_Title
if invoice.flags.contains(.isTest) {
@ -151,7 +152,7 @@ public final class BotCheckoutController: ViewController {
}
self.title = title
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed))
}
required public init(coder aDecoder: NSCoder) {

View file

@ -1077,13 +1077,13 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
if methods.isEmpty {
openNewCard(nil, nil)
} else {
strongSelf.present(BotCheckoutPaymentMethodSheetController(context: strongSelf.context, currentMethod: strongSelf.currentPaymentMethod, methods: methods, applyValue: { method in
strongSelf.controller?.push(BotCheckoutPaymentMethodScreen(context: strongSelf.context, currentMethod: strongSelf.currentPaymentMethod, methods: methods, applyValue: { method in
applyPaymentMethod(method)
}, newCard: {
openNewCard(nil, nil)
}, otherMethod: { url, title in
openNewCard(url, title)
}), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}))
}
}
}
@ -1091,14 +1091,14 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
openShippingMethodImpl = { [weak self] in
if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let shippingOptions = strongSelf.currentValidatedFormInfo?.shippingOptions, !shippingOptions.isEmpty {
strongSelf.controller?.view.endEditing(true)
strongSelf.present(BotCheckoutPaymentShippingOptionSheetController(context: strongSelf.context, currency: paymentFormValue.invoice.currency, options: shippingOptions, currentId: strongSelf.currentShippingOptionId, applyValue: { id in
strongSelf.controller?.push(BotCheckoutShippingOptionScreen(context: strongSelf.context, currency: paymentFormValue.invoice.currency, options: shippingOptions, currentId: strongSelf.currentShippingOptionId, applyValue: { id in
if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo {
strongSelf.currentShippingOptionId = id
strongSelf.paymentFormAndInfo.set(.single((paymentFormValue, currentFormInfo, strongSelf.currentValidatedFormInfo, strongSelf.currentShippingOptionId, strongSelf.currentPaymentMethod, strongSelf.currentTipAmount)))
strongSelf.updateActionButton()
}
}), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}))
}
}

View file

@ -206,7 +206,7 @@ class BotCheckoutHeaderItemNode: ListViewItemNode {
}
let contentSize = CGSize(width: params.width, height: contentHeight)
let insets = itemListNeighborsPlainInsets(neighbors)
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)

View file

@ -60,14 +60,16 @@ final class BotCheckoutInfoController: ViewController {
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, style: .glass))
self._hasGlassStyle = true
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
self.doneItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed))
self.title = self.presentationData.strings.CheckoutInfo_Title
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed))
self.navigationItem.rightBarButtonItem = self.doneItem
self.doneItem?.isEnabled = false
}
@ -84,13 +86,13 @@ final class BotCheckoutInfoController: ViewController {
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}, openCountrySelection: { [weak self] in
if let strongSelf = self {
let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: strongSelf.presentationData.theme, displayCodes: false)
let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: strongSelf.presentationData.theme, displayCodes: false, glass: true)
controller.completeWithCountryCode = { _, id in
if let strongSelf = self {
strongSelf.controllerNode.updateCountry(id)
}
}
strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
strongSelf.push(controller)
}
}, updateStatus: { [weak self] status in
if let strongSelf = self {

View file

@ -455,7 +455,7 @@ final class BotCheckoutInfoControllerNode: ViewControllerTracingNode, ASScrollVi
let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0))
var sideInset: CGFloat = 0.0
if layout.size.width >= 375.0 {
if layout.size.width >= 320.0 {
sideInset = inset
}

View file

@ -55,14 +55,16 @@ final class BotCheckoutNativeCardEntryController: ViewController {
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, style: .glass))
self._hasGlassStyle = true
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
self.doneItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed))
self.title = self.presentationData.strings.Checkout_NewCard_Title
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed))
self.navigationItem.rightBarButtonItem = self.doneItem
self.doneItem?.isEnabled = false
}
@ -78,13 +80,13 @@ final class BotCheckoutNativeCardEntryController: ViewController {
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}, openCountrySelection: { [weak self] in
if let strongSelf = self {
let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: strongSelf.presentationData.theme, displayCodes: false)
let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: strongSelf.presentationData.theme, displayCodes: false, glass: true)
controller.completeWithCountryCode = { _, id in
if let strongSelf = self {
strongSelf.controllerNode.updateCountry(id)
}
}
strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
strongSelf.push(controller)
}
}, updateStatus: { [weak self] status in
if let strongSelf = self {

View file

@ -468,7 +468,7 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode,
let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0))
var sideInset: CGFloat = 0.0
if layout.size.width >= 375.0 {
if layout.size.width >= 320.0 {
sideInset = inset
}

View file

@ -1,374 +1,96 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import AccountContext
import ComponentFlow
import AlertComponent
import AlertInputFieldComponent
private final class BotCheckoutPassworInputFieldNode: ASDisplayNode, UITextFieldDelegate {
private var theme: PresentationTheme
private let backgroundNode: ASImageNode
private let textInputNode: TextFieldNode
private let placeholderNode: ASTextNode
var updateHeight: (() -> Void)?
var complete: (() -> Void)?
var textChanged: ((String) -> Void)?
private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0)
private let inputInsets = UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0)
var text: String {
get {
return self.textInputNode.textField.text ?? ""
}
set {
self.textInputNode.textField.text = newValue
self.placeholderNode.isHidden = !newValue.isEmpty
}
}
var placeholder: String = "" {
didSet {
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
}
}
init(theme: PresentationTheme, placeholder: String) {
self.theme = theme
self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0)
self.textInputNode = TextFieldNode()
self.textInputNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(17.0), NSAttributedString.Key.foregroundColor: theme.actionSheet.inputTextColor]
self.textInputNode.textField.clipsToBounds = true
self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
self.textInputNode.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
self.textInputNode.textField.returnKeyType = .done
self.textInputNode.textField.isSecureTextEntry = true
self.textInputNode.textField.tintColor = theme.actionSheet.controlAccentColor
self.textInputNode.textField.textColor = theme.actionSheet.inputTextColor
self.placeholderNode = ASTextNode()
self.placeholderNode.isUserInteractionEnabled = false
self.placeholderNode.displaysAsynchronously = false
self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
super.init()
self.textInputNode.textField.delegate = self
self.textInputNode.textField.addTarget(self, action: #selector(self.textDidChange), for: .editingChanged)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.textInputNode)
self.addSubnode(self.placeholderNode)
}
func updateTheme(_ theme: PresentationTheme) {
self.theme = theme
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0)
self.textInputNode.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
self.textInputNode.textField.tintColor = self.theme.actionSheet.controlAccentColor
self.textInputNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(17.0), NSAttributedString.Key.foregroundColor: theme.actionSheet.inputTextColor]
self.textInputNode.textField.textColor = self.theme.actionSheet.inputTextColor
}
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
let backgroundInsets = self.backgroundInsets
let inputInsets = self.inputInsets
let textFieldHeight = self.calculateTextFieldMetrics(width: width)
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom))
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
let placeholderSize = self.placeholderNode.measure(backgroundFrame.size)
transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize))
transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height)))
return panelHeight
}
func activateInput() {
self.textInputNode.becomeFirstResponder()
}
func deactivateInput() {
self.textInputNode.resignFirstResponder()
}
func shake() {
self.layer.addShakeAnimation()
func botCheckoutPasswordEntryController(context: AccountContext, strings: PresentationStrings, passwordTip: String?, cartTitle: String, period: Int32, requiresBiometrics: Bool, completion: @escaping (TemporaryTwoStepPasswordToken) -> Void) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let inputState = AlertInputFieldComponent.ExternalState()
let doneInProgressPromise = ValuePromise<Bool>(false)
let doneIsEnabled: Signal<Bool, NoError> = combineLatest(inputState.valueSignal, doneInProgressPromise.get())
|> map { value, isInProgress in
return !value.isEmpty && !isInProgress
}
@objc func textDidChange() {
self.updateTextNodeText(animated: true)
self.textChanged?(self.textInputNode.textField.text ?? "")
self.placeholderNode.isHidden = !(self.textInputNode.textField.text ?? "").isEmpty
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if text == "\n" {
self.complete?()
return false
}
return true
}
private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat {
let backgroundInsets = self.backgroundInsets
let inputInsets = self.inputInsets
let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right, height: CGFloat.greatestFiniteMagnitude)).height))
return min(61.0, max(33.0, unboundTextFieldHeight))
}
private func updateTextNodeText(animated: Bool) {
let backgroundInsets = self.backgroundInsets
let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width)
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
if !self.bounds.size.height.isEqual(to: panelHeight) {
self.updateHeight?()
}
}
@objc func clearPressed() {
self.textInputNode.textField.text = nil
self.deactivateInput()
}
}
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
content.append(AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
AlertTitleComponent(title: strings.Checkout_PasswordEntry_Title)
)
))
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(strings.Checkout_PasswordEntry_Text(cartTitle).string))
)
))
private final class BotCheckoutPasswordAlertContentNode: AlertContentNode {
private let context: AccountContext
private let period: Int32
private let requiresBiometrics: Bool
private let completion: (TemporaryTwoStepPasswordToken) -> Void
private let titleNode: ASTextNode
private let textNode: ASTextNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private let cancelActionNode: TextAlertContentActionNode
private let doneActionNode: TextAlertContentActionNode
let inputFieldNode: BotCheckoutPassworInputFieldNode
private var validLayout: CGSize?
private var isVerifying = false
private let disposable = MetaDisposable()
private let hapticFeedback = HapticFeedback()
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, passwordTip: String?, cardTitle: String, period: Int32, requiresBiometrics: Bool, cancel: @escaping () -> Void, completion: @escaping (TemporaryTwoStepPasswordToken) -> Void) {
self.context = context
self.period = period
self.requiresBiometrics = requiresBiometrics
self.completion = completion
let alertTheme = AlertControllerTheme(presentationTheme: theme, fontSize: .regular)
let titleNode = ASTextNode()
titleNode.attributedText = NSAttributedString(string: strings.Checkout_PasswordEntry_Title, font: Font.semibold(17.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center)
titleNode.displaysAsynchronously = false
titleNode.isUserInteractionEnabled = false
titleNode.maximumNumberOfLines = 1
titleNode.truncationMode = .byTruncatingTail
self.titleNode = titleNode
self.textNode = ASTextNode()
self.textNode.attributedText = NSAttributedString(string: strings.Checkout_PasswordEntry_Text(cardTitle).string, font: Font.regular(13.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center)
self.textNode.displaysAsynchronously = false
self.textNode.isUserInteractionEnabled = false
self.inputFieldNode = BotCheckoutPassworInputFieldNode(theme: theme, placeholder: passwordTip ?? "")
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodesSeparator.backgroundColor = theme.actionSheet.opaqueItemSeparatorColor
self.cancelActionNode = TextAlertContentActionNode(theme: alertTheme, action: TextAlertAction(type: .genericAction, title: strings.Common_Cancel, action: {
cancel()
}))
var doneImpl: (() -> Void)?
self.doneActionNode = TextAlertContentActionNode(theme: alertTheme, action: TextAlertAction(type: .defaultAction, title: strings.Checkout_PasswordEntry_Pay, action: {
doneImpl?()
}))
self.actionNodes = [self.cancelActionNode, self.doneActionNode]
var actionVerticalSeparators: [ASDisplayNode] = []
if self.actionNodes.count > 1 {
for _ in 0 ..< self.actionNodes.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
separatorNode.backgroundColor = theme.actionSheet.opaqueItemSeparatorColor
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.addSubnode(self.inputFieldNode)
self.inputFieldNode.textChanged = { [weak self] _ in
if let strongSelf = self {
strongSelf.updateState()
}
}
self.updateState()
doneImpl = { [weak self] in
self?.verify()
}
var applyImpl: (() -> Void)?
content.append(AnyComponentWithIdentity(
id: "input",
component: AnyComponent(
AlertInputFieldComponent(
context: context,
placeholder: passwordTip ?? "",
isSecureTextEntry: true,
autocapitalizationType: .none,
autocorrectionType: .no,
isInitiallyFocused: true,
externalState: inputState,
returnKeyAction: {
applyImpl?()
}
)
)
))
var isVerifying = false
let disposable = MetaDisposable()
var dismissImpl: (() -> Void)?
let alertController = AlertScreen(
configuration: AlertScreen.Configuration(allowInputInset: true),
content: content,
actions: [
.init(title: strings.Common_Cancel),
.init(title: strings.Checkout_PasswordEntry_Pay, type: .default, action: {
applyImpl?()
}, autoDismiss: false, isEnabled: doneIsEnabled, progress: doneInProgressPromise.get())
],
updatedPresentationData: (initial: presentationData, signal: context.sharedContext.presentationData)
)
alertController.dismissed = { _ in
disposable.dispose()
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let previousLayout = self.validLayout
self.validLayout = size
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
let titleSize = titleNode.measure(CGSize(width: size.width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude))
let textSize = self.textNode.measure(CGSize(width: size.width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude))
let actionsHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionsHeight))
minActionsWidth += actionTitleSize.width + actionTitleInsets
}
let contentWidth = max(max(titleSize.width, textSize.width), minActionsWidth)
let spacing: CGFloat = 6.0
let titleFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - titleSize.width) / 2.0), y: insets.top), size: titleSize)
transition.updateFrame(node: titleNode, frame: titleFrame)
let textFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - textSize.width) / 2.0), y: titleFrame.maxY + spacing), size: textSize)
transition.updateFrame(node: self.textNode, frame: textFrame)
let resultSize = CGSize(width: contentWidth + insets.left + insets.right, height: titleSize.height + spacing + textSize.height + actionsHeight + insets.top + insets.bottom + 46.0)
let inputFieldWidth = resultSize.width
let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition)
transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: resultSize.height - 36.0 - actionsHeight - insets.bottom, width: resultSize.width, height: inputFieldHeight))
self.actionNodesSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
}
separatorIndex += 1
let currentActionWidth: CGFloat
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
let actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionsHeight))
actionOffset += currentActionWidth
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
if previousLayout == nil {
self.inputFieldNode.activateInput()
}
return resultSize
}
@objc func textFieldChanged(_ textField: UITextField) {
self.updateState()
}
private func updateState() {
var enabled = true
if self.isVerifying || self.inputFieldNode.text.isEmpty {
enabled = false
}
self.doneActionNode.actionEnabled = enabled
}
private func verify() {
let text = self.inputFieldNode.text
guard !text.isEmpty else {
applyImpl = {
let password = inputState.value
guard !isVerifying, !password.isEmpty else {
return
}
self.isVerifying = true
self.disposable.set((self.context.engine.auth.requestTemporaryTwoStepPasswordToken(password: text, period: self.period, requiresBiometrics: self.requiresBiometrics) |> deliverOnMainQueue).start(next: { [weak self] token in
if let strongSelf = self {
strongSelf.completion(token)
}
}, error: { [weak self] _ in
if let strongSelf = self {
strongSelf.inputFieldNode.shake()
strongSelf.hapticFeedback.error()
strongSelf.isVerifying = false
strongSelf.updateState()
}
}))
self.updateState()
}
}
func botCheckoutPasswordEntryController(context: AccountContext, strings: PresentationStrings, passwordTip: String?, cartTitle: String, period: Int32, requiresBiometrics: Bool, completion: @escaping (TemporaryTwoStepPasswordToken) -> Void) -> AlertController {
var dismissImpl: (() -> Void)?
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: BotCheckoutPasswordAlertContentNode(context: context, theme: presentationData.theme, strings: strings, passwordTip: passwordTip, cardTitle: cartTitle, period: period, requiresBiometrics: requiresBiometrics, cancel: {
dismissImpl?()
}, completion: { token in
completion(token)
dismissImpl?()
}))
dismissImpl = { [weak controller] in
controller?.dismissAnimated()
isVerifying = true
doneInProgressPromise.set(true)
disposable.set((context.engine.auth.requestTemporaryTwoStepPasswordToken(password: password, period: period, requiresBiometrics: requiresBiometrics)
|> deliverOnMainQueue).start(next: { token in
completion(token)
dismissImpl?()
}, error: { _ in
inputState.animateError()
isVerifying = false
doneInProgressPromise.set(false)
}))
}
return controller
dismissImpl = { [weak alertController] in
alertController?.dismiss(completion: nil)
}
return alertController
}

View file

@ -0,0 +1,536 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramCore
import AccountContext
import ViewControllerComponent
import ResizableSheetComponent
import TelegramPresentationData
import PresentationDataUtils
import MultilineTextComponent
import ButtonComponent
import ListSectionComponent
import ListActionItemComponent
import GlassBarButtonComponent
import BundleIconComponent
struct BotCheckoutPaymentWebToken: Equatable {
let title: String
let data: String
var saveOnServer: Bool
}
enum BotCheckoutPaymentMethod: Equatable {
case savedCredentials(BotPaymentSavedCredentials)
case webToken(BotCheckoutPaymentWebToken)
case applePay
case other(BotPaymentMethod)
var title: String {
switch self {
case let .savedCredentials(credentials):
switch credentials {
case let .card(_, title):
return title
}
case let .webToken(token):
return token.title
case .applePay:
return "Apple Pay"
case let .other(method):
return method.title
}
}
}
private func splitSavedCardTitle(_ title: String) -> (String, String?) {
guard let separatorIndex = title.lastIndex(of: "*") else {
return (title, nil)
}
let name = String(title[..<separatorIndex]).trimmingCharacters(in: .whitespacesAndNewlines)
let suffix = String(title[title.index(after: separatorIndex)...]).trimmingCharacters(in: .whitespacesAndNewlines)
guard !name.isEmpty, !suffix.isEmpty else {
return (title, nil)
}
return (name, "•••• \(suffix)")
}
private final class BotCheckoutPaymentMethodContentComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let methods: [BotCheckoutPaymentMethod]
let selectedMethod: BotCheckoutPaymentMethod?
let selectMethod: (BotCheckoutPaymentMethod) -> Void
let addCard: () -> Void
init(
methods: [BotCheckoutPaymentMethod],
selectedMethod: BotCheckoutPaymentMethod?,
selectMethod: @escaping (BotCheckoutPaymentMethod) -> Void,
addCard: @escaping () -> Void
) {
self.methods = methods
self.selectedMethod = selectedMethod
self.selectMethod = selectMethod
self.addCard = addCard
}
static func ==(lhs: BotCheckoutPaymentMethodContentComponent, rhs: BotCheckoutPaymentMethodContentComponent) -> Bool {
if lhs.methods != rhs.methods {
return false
}
if lhs.selectedMethod != rhs.selectedMethod {
return false
}
return true
}
final class View: UIView {
private let section = ComponentView<Empty>()
private var component: BotCheckoutPaymentMethodContentComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: BotCheckoutPaymentMethodContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let environment = environment[ViewControllerComponentContainer.Environment.self].value
let theme = environment.theme.withModalBlocksBackground()
let itemFontSize: CGFloat = 17.0
let sideInset: CGFloat = 16.0
var contentHeight: CGFloat = 76.0 + 9.0
var items: [AnyComponentWithIdentity<Empty>] = []
for i in 0 ..< component.methods.count {
let method = component.methods[i]
let isSelected = method == component.selectedMethod
var title = method.title
var icon: ListActionItemComponent.Icon?
var accessory: ListActionItemComponent.Accessory?
switch method {
case let .savedCredentials(credentials):
switch credentials {
case let .card(_, cardTitle):
let (cardName, cardSuffix) = splitSavedCardTitle(cardTitle)
title = cardName
if let cardSuffix {
accessory = .custom(ListActionItemComponent.CustomAccessory(
component: AnyComponentWithIdentity(
id: AnyHashable("card-suffix-\(i)-\(cardSuffix)"),
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: cardSuffix,
font: Font.regular(itemFontSize),
textColor: theme.list.itemSecondaryTextColor
)),
maximumNumberOfLines: 1
))
),
insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 16.0)
))
}
}
case .applePay:
title = "Apple Pay"
icon = ListActionItemComponent.Icon(
component: AnyComponentWithIdentity(
id: AnyHashable("apple-pay"),
component: AnyComponent(BundleIconComponent(
name: "Bot Payments/ApplePayLogo",
tintColor: nil
))
)
)
case let .webToken(token):
let (cardName, cardSuffix) = splitSavedCardTitle(token.title)
title = cardName
if let cardSuffix {
accessory = .custom(ListActionItemComponent.CustomAccessory(
component: AnyComponentWithIdentity(
id: AnyHashable("card-suffix-\(i)-\(cardSuffix)"),
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: cardSuffix,
font: Font.regular(itemFontSize),
textColor: theme.list.itemSecondaryTextColor
)),
maximumNumberOfLines: 1
))
),
insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 16.0)
))
}
case let .other(method):
title = method.title
}
items.append(AnyComponentWithIdentity(id: AnyHashable("method-\(i)"), component: AnyComponent(ListActionItemComponent(
theme: theme,
style: .glass,
title: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: title,
font: Font.regular(itemFontSize),
textColor: theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
)),
leftIcon: .check(ListActionItemComponent.LeftIcon.Check(
isSelected: isSelected,
toggle: {
component.selectMethod(method)
}
)),
icon: icon,
accessory: accessory,
action: { _ in
component.selectMethod(method)
}
))))
}
items.append(AnyComponentWithIdentity(id: AnyHashable("add-card"), component: AnyComponent(ListActionItemComponent(
theme: theme,
style: .glass,
title: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Checkout_PaymentMethod_New,
font: Font.regular(itemFontSize),
textColor: theme.list.itemAccentColor
)),
maximumNumberOfLines: 1
)),
contentInsets: UIEdgeInsets(top: 12.0, left: 45.0, bottom: 12.0, right: 0.0),
leftIcon: nil,
icon: nil,
accessory: nil,
action: { _ in
component.addCard()
}
))))
self.section.parentState = state
let sectionSize = self.section.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: theme,
style: .glass,
header: nil,
footer: nil,
items: items
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let sectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: sectionSize)
if let sectionView = self.section.view {
if sectionView.superview == nil {
self.addSubview(sectionView)
}
transition.setFrame(view: sectionView, frame: sectionFrame)
}
contentHeight += sectionSize.height
contentHeight += 112.0
return CGSize(width: availableSize.width, height: contentHeight)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class BotCheckoutPaymentMethodScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let currentMethod: BotCheckoutPaymentMethod?
let methods: [BotCheckoutPaymentMethod]
let applyValue: (BotCheckoutPaymentMethod) -> Void
let newCard: () -> Void
let otherMethod: (String, String) -> Void
init(
context: AccountContext,
currentMethod: BotCheckoutPaymentMethod?,
methods: [BotCheckoutPaymentMethod],
applyValue: @escaping (BotCheckoutPaymentMethod) -> Void,
newCard: @escaping () -> Void,
otherMethod: @escaping (String, String) -> Void
) {
self.context = context
self.currentMethod = currentMethod
self.methods = methods
self.applyValue = applyValue
self.newCard = newCard
self.otherMethod = otherMethod
}
static func ==(lhs: BotCheckoutPaymentMethodScreenComponent, rhs: BotCheckoutPaymentMethodScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.currentMethod != rhs.currentMethod {
return false
}
if lhs.methods != rhs.methods {
return false
}
return true
}
final class View: UIView {
private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, ResizableSheetComponentEnvironment)>()
private let animateOut = ActionSlot<Action<Void>>()
private var component: BotCheckoutPaymentMethodScreenComponent?
private weak var state: EmptyComponentState?
private var selectedMethod: BotCheckoutPaymentMethod?
private var isDismissing = false
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: BotCheckoutPaymentMethodScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
if self.component == nil {
if let currentMethod = component.currentMethod, component.methods.contains(currentMethod) {
self.selectedMethod = currentMethod
} else {
self.selectedMethod = nil
}
}
self.component = component
self.state = state
let environmentValue = environment[ViewControllerComponentContainer.Environment.self].value
let controller = environmentValue.controller
let theme = environmentValue.theme.withModalBlocksBackground()
let dismiss: (Bool, (() -> Void)?) -> Void = { [weak self] animated, completion in
guard let self, !self.isDismissing else {
return
}
self.isDismissing = true
let performDismiss: () -> Void = {
if let controller = controller() as? BotCheckoutPaymentMethodScreen {
controller.completePendingDismiss()
controller.dismiss(animated: false)
}
completion?()
}
if animated {
self.animateOut.invoke(Action { _ in
performDismiss()
})
} else {
performDismiss()
}
}
let sheetSize = self.sheet.update(
transition: transition,
component: AnyComponent(ResizableSheetComponent<ViewControllerComponentContainer.Environment>(
content: AnyComponent<ViewControllerComponentContainer.Environment>(BotCheckoutPaymentMethodContentComponent(
methods: component.methods,
selectedMethod: self.selectedMethod,
selectMethod: { [weak self] method in
guard let self else {
return
}
self.selectedMethod = method
self.state?.updated(transition: .spring(duration: 0.35))
},
addCard: { [weak self] in
guard let self, let component = self.component else {
return
}
dismiss(true, {
component.newCard()
})
}
)),
titleItem: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environmentValue.strings.Checkout_PaymentMethod,
font: Font.semibold(17.0),
textColor: theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
)),
leftItem: AnyComponent(
GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: theme.overallDarkAppearance,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.chat.inputPanel.panelControlColor
)
)),
action: { _ in
dismiss(true, nil)
}
)
),
//TODO:localize
bottomItem: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: theme.list.itemCheckColors.fillColor,
foreground: theme.list.itemCheckColors.foregroundColor,
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(
id: AnyHashable("proceed"),
component: AnyComponent(ButtonTextContentComponent(
text: "Proceed",
badge: 0,
textColor: theme.list.itemCheckColors.foregroundColor,
badgeBackground: theme.list.itemCheckColors.foregroundColor,
badgeForeground: theme.list.itemCheckColors.fillColor
))
),
isEnabled: self.selectedMethod != nil,
displaysProgress: false,
action: { [weak self] in
guard let self, let component = self.component, let selectedMethod = self.selectedMethod else {
return
}
dismiss(true, {
switch selectedMethod {
case let .other(method):
component.otherMethod(method.url, method.title)
default:
component.applyValue(selectedMethod)
}
})
}
)),
backgroundColor: .color(theme.list.modalBlocksBackgroundColor),
animateOut: self.animateOut
)),
environment: {
environmentValue
ResizableSheetComponentEnvironment(
theme: theme,
statusBarHeight: environmentValue.statusBarHeight,
safeInsets: environmentValue.safeInsets,
inputHeight: 0.0,
metrics: environmentValue.metrics,
deviceMetrics: environmentValue.deviceMetrics,
isDisplaying: environmentValue.isVisible,
isCentered: environmentValue.metrics.widthClass == .regular,
screenSize: availableSize,
regularMetricsSize: nil,
dismiss: { animated in
dismiss(animated, nil)
}
)
},
forceUpdate: true,
containerSize: availableSize
)
self.sheet.parentState = state
if let sheetView = self.sheet.view {
if sheetView.superview == nil {
self.addSubview(sheetView)
}
transition.setFrame(view: sheetView, frame: CGRect(origin: .zero, size: sheetSize))
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class BotCheckoutPaymentMethodScreen: ViewControllerComponentContainer {
private var isDismissed = false
private var dismissCompletion: (() -> Void)?
init(context: AccountContext, currentMethod: BotCheckoutPaymentMethod?, methods: [BotCheckoutPaymentMethod], applyValue: @escaping (BotCheckoutPaymentMethod) -> Void, newCard: @escaping () -> Void, otherMethod: @escaping (String, String) -> Void) {
super.init(
context: context,
component: BotCheckoutPaymentMethodScreenComponent(
context: context,
currentMethod: currentMethod,
methods: methods,
applyValue: applyValue,
newCard: newCard,
otherMethod: otherMethod
),
navigationBarAppearance: .none
)
self.statusBar.statusBarStyle = .Ignore
self.navigationPresentation = .flatModal
self.blocksBackgroundWhenInOverlay = true
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
fileprivate func completePendingDismiss() {
let dismissCompletion = self.dismissCompletion
self.dismissCompletion = nil
dismissCompletion?()
}
func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
override func dismiss(completion: (() -> Void)? = nil) {
if !self.isDismissed {
self.isDismissed = true
self.dismissCompletion = completion
if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
} else {
self.completePendingDismiss()
self.dismiss(animated: false)
}
}
}
}

View file

@ -1,262 +0,0 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import AccountContext
import AppBundle
struct BotCheckoutPaymentWebToken: Equatable {
let title: String
let data: String
var saveOnServer: Bool
}
enum BotCheckoutPaymentMethod: Equatable {
case savedCredentials(BotPaymentSavedCredentials)
case webToken(BotCheckoutPaymentWebToken)
case applePay
case other(BotPaymentMethod)
var title: String {
switch self {
case let .savedCredentials(credentials):
switch credentials {
case let .card(_, title):
return title
}
case let .webToken(token):
return token.title
case .applePay:
return "Apple Pay"
case let .other(method):
return method.title
}
}
}
final class BotCheckoutPaymentMethodSheetController: ActionSheetController {
private var presentationDisposable: Disposable?
init(context: AccountContext, currentMethod: BotCheckoutPaymentMethod?, methods: [BotCheckoutPaymentMethod], applyValue: @escaping (BotCheckoutPaymentMethod) -> Void, newCard: @escaping () -> Void, otherMethod: @escaping (String, String) -> Void) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
super.init(theme: ActionSheetControllerTheme(presentationData: presentationData))
self.presentationDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData)
}
}).strict()
var items: [ActionSheetItem] = []
items.append(ActionSheetTextItem(title: strings.Checkout_PaymentMethod))
for method in methods {
let title: String
let icon: UIImage?
switch method {
case let .savedCredentials(credentials):
switch credentials {
case let .card(_, cardTitle):
title = cardTitle
icon = nil
}
case let .webToken(token):
title = token.title
icon = nil
case .applePay:
title = "Apple Pay"
icon = UIImage(bundleImageName: "Bot Payments/ApplePayLogo")?.precomposed()
case let .other(method):
title = method.title
icon = nil
}
let value: Bool?
if let currentMethod = currentMethod {
value = method == currentMethod
} else {
value = nil
}
items.append(BotCheckoutPaymentMethodItem(title: title, icon: icon, value: value, action: { [weak self] _ in
if case let .other(method) = method {
otherMethod(method.url, method.title)
} else {
applyValue(method)
}
self?.dismissAnimated()
}))
}
items.append(ActionSheetButtonItem(title: strings.Checkout_PaymentMethod_New, action: { [weak self] in
self?.dismissAnimated()
newCard()
}))
self.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in
self?.dismissAnimated()
}),
])
])
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDisposable?.dispose()
}
}
public class BotCheckoutPaymentMethodItem: ActionSheetItem {
public let title: String
public let icon: UIImage?
public let value: Bool?
public let action: (Bool) -> Void
public init(title: String, icon: UIImage?, value: Bool?, action: @escaping (Bool) -> Void) {
self.title = title
self.icon = icon
self.value = value
self.action = action
}
public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
let node = BotCheckoutPaymentMethodItemNode(theme: theme)
node.setItem(self)
return node
}
public func updateNode(_ node: ActionSheetItemNode) {
guard let node = node as? BotCheckoutPaymentMethodItemNode else {
assertionFailure()
return
}
node.setItem(self)
node.requestLayoutUpdate()
}
}
public class BotCheckoutPaymentMethodItemNode: ActionSheetItemNode {
private let defaultFont: UIFont
private let theme: ActionSheetControllerTheme
private var item: BotCheckoutPaymentMethodItem?
private let button: HighlightTrackingButton
private let titleNode: ASTextNode
private let iconNode: ASImageNode
private let checkNode: ASImageNode
public override init(theme: ActionSheetControllerTheme) {
self.theme = theme
self.defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0))
self.button = HighlightTrackingButton()
self.titleNode = ASTextNode()
self.titleNode.maximumNumberOfLines = 1
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
self.checkNode = ASImageNode()
self.checkNode.isUserInteractionEnabled = false
self.checkNode.displayWithoutProcessing = true
self.checkNode.displaysAsynchronously = false
self.checkNode.image = generateImage(CGSize(width: 14.0, height: 11.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(theme.controlAccentColor.cgColor)
context.setLineWidth(2.0)
context.move(to: CGPoint(x: 12.0, y: 1.0))
context.addLine(to: CGPoint(x: 4.16482734, y: 9.0))
context.addLine(to: CGPoint(x: 1.0, y: 5.81145833))
context.strokePath()
})
super.init(theme: theme)
self.view.addSubview(self.button)
self.addSubnode(self.titleNode)
self.addSubnode(self.iconNode)
self.addSubnode(self.checkNode)
self.button.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.backgroundNode.backgroundColor = theme.itemHighlightedBackgroundColor
} else {
UIView.animate(withDuration: 0.3, animations: {
strongSelf.backgroundNode.backgroundColor = theme.itemBackgroundColor
})
}
}
}
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
}
func setItem(_ item: BotCheckoutPaymentMethodItem) {
self.item = item
self.titleNode.attributedText = NSAttributedString(string: item.title, font: self.defaultFont, textColor: self.theme.primaryTextColor)
self.iconNode.image = item.icon
if let value = item.value {
self.checkNode.isHidden = !value
} else {
self.checkNode.isHidden = true
}
}
public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let size = CGSize(width: constrainedSize.width, height: 57.0)
self.button.frame = CGRect(origin: CGPoint(), size: size)
var checkInset: CGFloat = 15.0
if let _ = self.item?.value {
checkInset = 44.0
}
let iconSize: CGSize
if let image = self.iconNode.image {
iconSize = image.size
} else {
iconSize = CGSize()
}
let titleSize = self.titleNode.measure(CGSize(width: size.width - 44.0 - iconSize.width - 15.0 - 8.0, height: size.height))
self.titleNode.frame = CGRect(origin: CGPoint(x: checkInset, y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize)
self.iconNode.frame = CGRect(origin: CGPoint(x: size.width - 15.0 - iconSize.width, y: floorToScreenPixels((size.height - iconSize.height) / 2.0)), size: iconSize)
if let image = self.checkNode.image {
self.checkNode.frame = CGRect(origin: CGPoint(x: floor((44.0 - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)
}
self.updateInternalLayout(size, constrainedSize: constrainedSize)
return size
}
@objc func buttonPressed() {
if let item = self.item {
let updatedValue: Bool
if let value = item.value {
updatedValue = !value
} else {
updatedValue = true
}
item.action(updatedValue)
}
}
}

View file

@ -1,224 +0,0 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import AccountContext
import TelegramStringFormatting
final class BotCheckoutPaymentShippingOptionSheetController: ActionSheetController {
private var presentationDisposable: Disposable?
init(context: AccountContext, currency: String, options: [BotPaymentShippingOption], currentId: String?, applyValue: @escaping (String) -> Void) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
super.init(theme: ActionSheetControllerTheme(presentationData: presentationData))
self.presentationDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData)
}
}).strict()
var items: [ActionSheetItem] = []
items.append(ActionSheetTextItem(title: strings.Checkout_ShippingMethod))
let dismissAction: () -> Void = { [weak self] in
self?.dismissAnimated()
}
let toggleCheck: (String, Int) -> Void = { [weak self] id, itemIndex in
for i in 0 ..< options.count {
self?.updateItem(groupIndex: 0, itemIndex: i + 1, { item in
if let item = item as? BotCheckoutPaymentShippingOptionItem, let value = item.value {
return BotCheckoutPaymentShippingOptionItem(title: item.title, label: item.label, value: i == itemIndex ? !value : false, action: item.action)
}
return item
})
}
applyValue(id)
dismissAction()
}
var itemIndex = 0
for option in options {
let index = itemIndex
var totalPrice: Int64 = 0
for price in option.prices {
totalPrice += price.amount
}
let value: Bool?
if let currentId = currentId {
value = option.id == currentId
} else {
value = nil
}
items.append(BotCheckoutPaymentShippingOptionItem(title: option.title, label: formatCurrencyAmount(totalPrice, currency: currency), value: value, action: { value in
toggleCheck(option.id, index)
}))
itemIndex += 1
}
self.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in
self?.dismissAnimated()
}),
])
])
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDisposable?.dispose()
}
}
public class BotCheckoutPaymentShippingOptionItem: ActionSheetItem {
public let title: String
public let label: String
public let value: Bool?
public let action: (Bool) -> Void
public init(title: String, label: String, value: Bool?, action: @escaping (Bool) -> Void) {
self.title = title
self.label = label
self.value = value
self.action = action
}
public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
let node = BotCheckoutPaymentShippingOptionItemNode(theme: theme)
node.setItem(self)
return node
}
public func updateNode(_ node: ActionSheetItemNode) {
guard let node = node as? BotCheckoutPaymentShippingOptionItemNode else {
assertionFailure()
return
}
node.setItem(self)
node.requestLayoutUpdate()
}
}
public class BotCheckoutPaymentShippingOptionItemNode: ActionSheetItemNode {
private let defaultFont: UIFont
private let theme: ActionSheetControllerTheme
private var item: BotCheckoutPaymentShippingOptionItem?
private let button: HighlightTrackingButton
private let titleNode: ASTextNode
private let labelNode: ASTextNode
private let checkNode: ASImageNode
public override init(theme: ActionSheetControllerTheme) {
self.theme = theme
self.defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0))
self.button = HighlightTrackingButton()
self.titleNode = ASTextNode()
self.titleNode.maximumNumberOfLines = 1
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
self.labelNode = ASTextNode()
self.labelNode.maximumNumberOfLines = 1
self.labelNode.isUserInteractionEnabled = false
self.labelNode.displaysAsynchronously = false
self.checkNode = ASImageNode()
self.checkNode.isUserInteractionEnabled = false
self.checkNode.displayWithoutProcessing = true
self.checkNode.displaysAsynchronously = false
self.checkNode.image = generateImage(CGSize(width: 14.0, height: 11.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(theme.controlAccentColor.cgColor)
context.setLineWidth(2.0)
context.move(to: CGPoint(x: 12.0, y: 1.0))
context.addLine(to: CGPoint(x: 4.16482734, y: 9.0))
context.addLine(to: CGPoint(x: 1.0, y: 5.81145833))
context.strokePath()
})
super.init(theme: theme)
self.view.addSubview(self.button)
self.addSubnode(self.titleNode)
self.addSubnode(self.labelNode)
self.addSubnode(self.checkNode)
self.button.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemHighlightedBackgroundColor
} else {
UIView.animate(withDuration: 0.3, animations: {
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemBackgroundColor
})
}
}
}
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
}
func setItem(_ item: BotCheckoutPaymentShippingOptionItem) {
self.item = item
self.titleNode.attributedText = NSAttributedString(string: item.title, font: self.defaultFont, textColor: self.theme.primaryTextColor)
self.labelNode.attributedText = NSAttributedString(string: item.label, font: self.defaultFont, textColor: self.theme.primaryTextColor)
if let value = item.value {
self.checkNode.isHidden = !value
} else {
self.checkNode.isHidden = true
}
}
public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let size = CGSize(width: constrainedSize.width, height: 57.0)
self.button.frame = CGRect(origin: CGPoint(), size: size)
var checkInset: CGFloat = 15.0
if let _ = self.item?.value {
checkInset = 44.0
}
let labelSize = self.labelNode.measure(CGSize(width: size.width - 44.0 - 15.0 - 8.0, height: size.height))
let titleSize = self.titleNode.measure(CGSize(width: size.width - 44.0 - labelSize.width - 15.0 - 8.0, height: size.height))
self.titleNode.frame = CGRect(origin: CGPoint(x: checkInset, y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize)
self.labelNode.frame = CGRect(origin: CGPoint(x: size.width - 15.0 - labelSize.width, y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize)
if let image = self.checkNode.image {
self.checkNode.frame = CGRect(origin: CGPoint(x: floor((44.0 - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)
}
self.updateInternalLayout(size, constrainedSize: constrainedSize)
return size
}
@objc func buttonPressed() {
if let item = self.item {
let updatedValue: Bool
if let value = item.value {
updatedValue = !value
} else {
updatedValue = true
}
item.action(updatedValue)
}
}
}

View file

@ -0,0 +1,418 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import AccountContext
import TelegramStringFormatting
import ViewControllerComponent
import ResizableSheetComponent
import TelegramPresentationData
import PresentationDataUtils
import MultilineTextComponent
import ButtonComponent
import ListSectionComponent
import ListActionItemComponent
import GlassBarButtonComponent
import BundleIconComponent
private final class BotCheckoutShippingOptionContentComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let currency: String
let options: [BotPaymentShippingOption]
let selectedId: String?
let selectOption: (String) -> Void
init(
currency: String,
options: [BotPaymentShippingOption],
selectedId: String?,
selectOption: @escaping (String) -> Void
) {
self.currency = currency
self.options = options
self.selectedId = selectedId
self.selectOption = selectOption
}
static func ==(lhs: BotCheckoutShippingOptionContentComponent, rhs: BotCheckoutShippingOptionContentComponent) -> Bool {
if lhs.currency != rhs.currency {
return false
}
if lhs.options != rhs.options {
return false
}
if lhs.selectedId != rhs.selectedId {
return false
}
return true
}
final class View: UIView {
private let section = ComponentView<Empty>()
private var component: BotCheckoutShippingOptionContentComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: BotCheckoutShippingOptionContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let environment = environment[ViewControllerComponentContainer.Environment.self].value
let theme = environment.theme.withModalBlocksBackground()
let itemFontSize: CGFloat = 17.0
let sideInset: CGFloat = 16.0
var contentHeight: CGFloat = 76.0 + 9.0
var items: [AnyComponentWithIdentity<Empty>] = []
for option in component.options {
let totalPrice = option.prices.reduce(Int64(0)) { current, price in
return current + price.amount
}
let priceText = formatCurrencyAmount(totalPrice, currency: component.currency)
let isSelected = option.id == component.selectedId
items.append(AnyComponentWithIdentity(id: option.id, component: AnyComponent(ListActionItemComponent(
theme: theme,
style: .glass,
title: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: option.title,
font: Font.regular(itemFontSize),
textColor: theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
)),
leftIcon: .check(ListActionItemComponent.LeftIcon.Check(
isSelected: isSelected,
toggle: {
component.selectOption(option.id)
}
)),
icon: nil,
accessory: .custom(ListActionItemComponent.CustomAccessory(
component: AnyComponentWithIdentity(
id: AnyHashable("price-\(option.id)"),
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: priceText,
font: Font.regular(itemFontSize),
textColor: theme.list.itemSecondaryTextColor
)),
maximumNumberOfLines: 1
))
),
insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 16.0)
)),
action: { _ in
component.selectOption(option.id)
}
))))
}
self.section.parentState = state
let sectionSize = self.section.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: theme,
style: .glass,
header: nil,
footer: nil,
items: items
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let sectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: sectionSize)
if let sectionView = self.section.view {
if sectionView.superview == nil {
self.addSubview(sectionView)
}
transition.setFrame(view: sectionView, frame: sectionFrame)
}
contentHeight += sectionSize.height
contentHeight += 112.0
return CGSize(width: availableSize.width, height: contentHeight)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class BotCheckoutShippingOptionScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let currency: String
let options: [BotPaymentShippingOption]
let currentId: String?
let applyValue: (String) -> Void
init(
context: AccountContext,
currency: String,
options: [BotPaymentShippingOption],
currentId: String?,
applyValue: @escaping (String) -> Void
) {
self.context = context
self.currency = currency
self.options = options
self.currentId = currentId
self.applyValue = applyValue
}
static func ==(lhs: BotCheckoutShippingOptionScreenComponent, rhs: BotCheckoutShippingOptionScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.currency != rhs.currency {
return false
}
if lhs.options != rhs.options {
return false
}
if lhs.currentId != rhs.currentId {
return false
}
return true
}
final class View: UIView {
private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, ResizableSheetComponentEnvironment)>()
private let animateOut = ActionSlot<Action<Void>>()
private var component: BotCheckoutShippingOptionScreenComponent?
private weak var state: EmptyComponentState?
private var selectedId: String?
private var isDismissing = false
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: BotCheckoutShippingOptionScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
if self.component == nil {
if let currentId = component.currentId, component.options.contains(where: { $0.id == currentId }) {
self.selectedId = currentId
} else {
self.selectedId = nil
}
}
self.component = component
self.state = state
let environmentValue = environment[ViewControllerComponentContainer.Environment.self].value
let controller = environmentValue.controller
let theme = environmentValue.theme.withModalBlocksBackground()
let dismiss: (Bool) -> Void = { [weak self] animated in
guard let self, !self.isDismissing else {
return
}
self.isDismissing = true
let performDismiss: () -> Void = {
if let controller = controller() as? BotCheckoutShippingOptionScreen {
controller.completePendingDismiss()
controller.dismiss(animated: false)
}
}
if animated {
self.animateOut.invoke(Action { _ in
performDismiss()
})
} else {
performDismiss()
}
}
let sheetSize = self.sheet.update(
transition: transition,
component: AnyComponent(ResizableSheetComponent<ViewControllerComponentContainer.Environment>(
content: AnyComponent<ViewControllerComponentContainer.Environment>(BotCheckoutShippingOptionContentComponent(
currency: component.currency,
options: component.options,
selectedId: self.selectedId,
selectOption: { [weak self] id in
guard let self else {
return
}
self.selectedId = id
self.state?.updated(transition: .spring(duration: 0.35))
}
)),
titleItem: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environmentValue.strings.Checkout_ShippingMethod,
font: Font.semibold(17.0),
textColor: theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
)),
leftItem: AnyComponent(
GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: theme.overallDarkAppearance,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.chat.inputPanel.panelControlColor
)
)),
action: { _ in
dismiss(true)
}
)
),
//TODO:localize
bottomItem: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: theme.list.itemCheckColors.fillColor,
foreground: theme.list.itemCheckColors.foregroundColor,
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(
id: AnyHashable("proceed"),
component: AnyComponent(ButtonTextContentComponent(
text: "Proceed",
badge: 0,
textColor: theme.list.itemCheckColors.foregroundColor,
badgeBackground: theme.list.itemCheckColors.foregroundColor,
badgeForeground: theme.list.itemCheckColors.fillColor
))
),
isEnabled: self.selectedId != nil,
displaysProgress: false,
action: { [weak self] in
guard let self, let component = self.component, let selectedId = self.selectedId else {
return
}
component.applyValue(selectedId)
dismiss(true)
}
)),
backgroundColor: .color(theme.list.modalBlocksBackgroundColor),
animateOut: self.animateOut
)),
environment: {
environmentValue
ResizableSheetComponentEnvironment(
theme: theme,
statusBarHeight: environmentValue.statusBarHeight,
safeInsets: environmentValue.safeInsets,
inputHeight: 0.0,
metrics: environmentValue.metrics,
deviceMetrics: environmentValue.deviceMetrics,
isDisplaying: environmentValue.isVisible,
isCentered: environmentValue.metrics.widthClass == .regular,
screenSize: availableSize,
regularMetricsSize: nil,
dismiss: { animated in
dismiss(animated)
}
)
},
forceUpdate: true,
containerSize: availableSize
)
self.sheet.parentState = state
if let sheetView = self.sheet.view {
if sheetView.superview == nil {
self.addSubview(sheetView)
}
transition.setFrame(view: sheetView, frame: CGRect(origin: .zero, size: sheetSize))
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class BotCheckoutShippingOptionScreen: ViewControllerComponentContainer {
private var isDismissed = false
private var dismissCompletion: (() -> Void)?
init(context: AccountContext, currency: String, options: [BotPaymentShippingOption], currentId: String?, applyValue: @escaping (String) -> Void) {
super.init(
context: context,
component: BotCheckoutShippingOptionScreenComponent(
context: context,
currency: currency,
options: options,
currentId: currentId,
applyValue: applyValue
),
navigationBarAppearance: .none
)
self.statusBar.statusBarStyle = .Ignore
self.navigationPresentation = .flatModal
self.blocksBackgroundWhenInOverlay = true
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
fileprivate func completePendingDismiss() {
let dismissCompletion = self.dismissCompletion
self.dismissCompletion = nil
dismissCompletion?()
}
func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
override func dismiss(completion: (() -> Void)? = nil) {
if !self.isDismissed {
self.isDismissed = true
self.dismissCompletion = completion
if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
} else {
self.completePendingDismiss()
self.dismiss(animated: false)
}
}
}
}

View file

@ -65,7 +65,7 @@ public final class BrowserBookmarksScreen: ViewController {
}, tapMessage: nil, clickThroughMessage: { _, _ in
}, toggleMessagesSelection: { _, _ in
}, sendCurrentMessage: { _, _ in
}, sendMessage: { _ in
}, sendMessage: { _, _ in
}, sendSticker: { _, _, _, _, _, _, _, _, _ in
return false
}, sendEmoji: { _, _, _ in
@ -83,8 +83,8 @@ public final class BrowserBookmarksScreen: ViewController {
controller.dismiss()
}
}, openExternalInstantPage: { _ in
}, shareCurrentLocation: {
}, shareAccountContact: {
}, shareCurrentLocation: { _ in
}, shareAccountContact: { _ in
}, sendBotCommand: { _, _ in
}, openInstantPage: { message, _ in
if let openMessageImpl = openMessageImpl {
@ -134,7 +134,7 @@ public final class BrowserBookmarksScreen: ViewController {
}, displaySwipeToReplyHint: {
}, dismissReplyMarkupMessage: { _ in
}, openMessagePollResults: { _, _ in
}, openPollCreation: { _ in
}, openPollCreation: { _, _ in
}, openPollMedia: { _, _ in
}, displayPollSolution: { _, _ in
}, displayPsa: { _, _ in

View file

@ -166,6 +166,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
self.scrollNode.view.delaysContentTouches = false
self.scrollNode.view.delegate = self
self.scrollNode.view.scrollsToTop = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
@ -1532,12 +1533,12 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = OpenInActionSheetController(context: self.context, item: .url(url: baseUrl), openUrl: { [weak self] url in
let actionSheet = OpenInOptionsScreen(context: self.context, item: .url(url: baseUrl), openUrl: { [weak self] url in
if let self {
self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
}
})
self.present(actionSheet, nil)
self.push(actionSheet)
}
private func openMedia(_ media: InstantPageMedia) {

View file

@ -34,7 +34,40 @@ public class Readability: NSObject, WKNavigationDelegate {
if let (html, subresources) = extractHtmlString(from: archiveData) {
self.subresources = subresources
self.webView.loadHTMLString(html, baseURL: url.baseURL)
self.sanitizeHtmlString(html) { [weak self] html in
guard let self else {
return
}
self.webView.loadHTMLString(html, baseURL: url.baseURL)
}
}
}
private func sanitizeHtmlString(_ html: String, completion: @escaping (String) -> Void) {
guard let readerModeJS = loadFile(name: "ReaderMode", type: "js") else {
completion(htmlByRemovingScriptTags(html))
return
}
let domPurifyJS = extractDOMPurifyScript(from: readerModeJS) ?? readerModeJS
self.webView.evaluateJavaScript(domPurifyJS) { [weak self] _, error in
guard let self else {
return
}
guard error == nil, let htmlLiteral = javascriptStringLiteral(html) else {
completion(htmlByRemovingScriptTags(html))
return
}
let sanitizeJS = """
(function(html) {
return DOMPurify.sanitize(html, {WHOLE_DOCUMENT: true, ADD_TAGS: ["iframe"]});
})(\(htmlLiteral));
"""
self.webView.evaluateJavaScript(sanitizeJS) { result, _ in
completion((result as? String) ?? htmlByRemovingScriptTags(html))
}
}
}
@ -49,6 +82,7 @@ public class Readability: NSObject, WKNavigationDelegate {
return
}
guard let page = parseJson(result, url: self.url.absoluteString) else {
completion(nil, error)
return
}
completion(page, nil)
@ -95,6 +129,32 @@ func loadFile(name: String, type: String) -> String? {
return userScript
}
private func extractDOMPurifyScript(from readerModeJS: String) -> String? {
guard let range = readerModeJS.range(of: "\n\n(function () {") else {
return nil
}
return String(readerModeJS[..<range.lowerBound])
}
private func javascriptStringLiteral(_ input: String) -> String? {
guard let data = try? JSONSerialization.data(withJSONObject: [input], options: []),
var arrayString = String(data: data, encoding: .utf8),
arrayString.count >= 2 else {
return nil
}
arrayString.removeFirst()
arrayString.removeLast()
return arrayString
}
private func htmlByRemovingScriptTags(_ input: String) -> String {
guard let regex = try? NSRegularExpression(pattern: "<script\\b[^>]*>[\\s\\S]*?</script\\s*>", options: [.caseInsensitive]) else {
return input
}
let range = NSRange(input.startIndex ..< input.endIndex, in: input)
return regex.stringByReplacingMatches(in: input, options: [], range: range, withTemplate: "")
}
private func extractHtmlString(from webArchiveData: Data) -> (String, [Any]?)? {
if let webArchiveDict = try? PropertyListSerialization.propertyList(from: webArchiveData, format: nil) as? [String: Any],
let mainResource = webArchiveDict["WebMainResource"] as? [String: Any],
@ -708,30 +768,38 @@ private func parseVideo(_ input: [String: Any], _ media: inout [EngineMedia.Id:
)
}
private func firstElement(withTag tag: String, in input: [Any], skippingSubtreesWithTag skippedTag: String? = nil) -> [String: Any]? {
for item in input {
guard let item = item as? [String: Any] else {
continue
}
let itemTag = item["tag"] as? String
if itemTag == tag {
return item
}
if itemTag == skippedTag {
continue
}
if let content = item["content"] as? [Any], let result = firstElement(withTag: tag, in: content, skippingSubtreesWithTag: skippedTag) {
return result
}
}
return nil
}
private func parseFigure(_ input: [String: Any], _ media: inout [EngineMedia.Id: EngineRawMedia]) -> InstantPageBlock? {
guard let content = input["content"] as? [Any] else {
return nil
}
var block: InstantPageBlock?
var caption: RichText?
for item in content {
if let item = item as? [String: Any], let tag = item["tag"] as? String {
if tag == "p", let content = item["content"] as? [Any] {
for item in content {
if let item = item as? [String: Any], let tag = item["tag"] as? String {
if tag == "iframe" {
block = parseVideo(item, &media)
}
}
}
} else if tag == "iframe" {
block = parseVideo(item, &media)
} else if tag == "img" {
block = parseImage(item, &media)
} else if tag == "figcaption" {
caption = trim(parseRichText(item, &media))
}
}
if let iframe = firstElement(withTag: "iframe", in: content, skippingSubtreesWithTag: "figcaption") {
block = parseVideo(iframe, &media)
} else if let image = firstElement(withTag: "img", in: content, skippingSubtreesWithTag: "figcaption") {
block = parseImage(image, &media)
}
if let figcaption = firstElement(withTag: "figcaption", in: content) {
caption = trim(parseRichText(figcaption, &media))
}
guard var block else {
return nil

View file

@ -1868,7 +1868,7 @@ public final class CalendarMessageScreen: ViewController {
self._hasGlassStyle = true
self.navigationPresentation = .modal
self.navigationItem.setLeftBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(dismissPressed)), animated: false)
self.navigationItem.setLeftBarButton(UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(dismissPressed)), animated: false)
self.navigationItem.setTitle(self.presentationData.strings.MessageCalendar_Title, animated: false)
if self.enableMessageRangeDeletion {
@ -1894,7 +1894,7 @@ public final class CalendarMessageScreen: ViewController {
self.node.toggleSelectionMode()
if self.node.selectionState != nil {
self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.toggleSelectPressed)), animated: true)
self.navigationItem.setRightBarButton(UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.toggleSelectPressed)), animated: true)
} else {
self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Select, style: .plain, target: self, action: #selector(self.toggleSelectPressed)), animated: true)
}
@ -1904,7 +1904,7 @@ public final class CalendarMessageScreen: ViewController {
self.node.selectDay(timestamp: timestamp)
if self.node.selectionState != nil {
self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.toggleSelectPressed)), animated: true)
self.navigationItem.setRightBarButton(UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.toggleSelectPressed)), animated: true)
}
}

View file

@ -213,7 +213,7 @@ public final class CallListController: TelegramBaseController {
if let isEmpty = self.isEmpty, isEmpty {
} else {
if self.editingMode {
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed))
} else {
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed))
}
@ -222,7 +222,7 @@ public final class CallListController: TelegramBaseController {
//self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.callPressed))
case .navigation:
if self.editingMode {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed))
} else {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed))
}
@ -679,7 +679,7 @@ public final class CallListController: TelegramBaseController {
switch self.mode {
case .tab:
self.navigationItem.setLeftBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)), animated: true)
self.navigationItem.setLeftBarButton(UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed)), animated: true)
self.navigationItem.setRightBarButton(UIBarButtonItem(customDisplayNode: buttonNode), animated: true)
self.navigationItem.rightBarButtonItem?.setCustomAction({
@ -691,7 +691,7 @@ public final class CallListController: TelegramBaseController {
pressedImpl?()
})
self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)), animated: true)
self.navigationItem.setRightBarButton(UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed)), animated: true)
}
self.controllerNode.updateState { state in

View file

@ -940,7 +940,7 @@ final class CallListControllerNode: ASDisplayNode {
insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top)
let inset: CGFloat
if layout.size.width >= 375.0 {
if layout.size.width >= 320.0 {
inset = max(16.0, floor((layout.size.width - 674.0) / 2.0))
} else {
inset = 0.0

View file

@ -129,6 +129,7 @@ swift_library(
"//submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent",
"//submodules/TelegramUI/Components/AvatarComponent",
"//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController",
"//submodules/TelegramUI/Components/GlassControls",
],
visibility = [
"//visibility:public",

View file

@ -6244,7 +6244,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if case .chatList(.root) = self.chatListDisplayNode.mainContainerNode.location {
super.setToolbar(toolbar, transition: transition)
} else {
self.chatListDisplayNode.toolbar = toolbar
self.chatListDisplayNode.toolbarData = toolbar
self.requestLayout(transition: transition)
}
}

View file

@ -28,6 +28,7 @@ import MediaPlaybackHeaderPanelComponent
import LiveLocationHeaderPanelComponent
import ChatListHeaderNoticeComponent
import ChatListFilterTabContainerNode
import GlassControls
public enum ChatListContainerNodeFilter: Equatable {
case all
@ -1134,8 +1135,8 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
let navigationBarView = ComponentView<Empty>()
weak var controller: ChatListControllerImpl?
var toolbar: Toolbar?
private var toolbarNode: ToolbarNode?
private var toolbar: ComponentView<Empty>?
var toolbarData: Toolbar?
var toolbarActionSelected: ((ToolbarActionOption) -> Void)?
private var isSearchDisplayControllerActive: ChatListNavigationBar.ActiveSearch?
@ -1395,10 +1396,6 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
self.mainContainerNode.updatePresentationData(presentationData)
self.inlineStackContainerNode?.updatePresentationData(presentationData)
self.searchDisplayController?.updatePresentationData(presentationData)
if let toolbarNode = self.toolbarNode {
toolbarNode.updateTheme(ToolbarTheme(rootControllerTheme: self.presentationData.theme))
}
}
private func updateNavigationBar(layout: ContainerViewLayout, deferScrollApplication: Bool, transition: ComponentTransition) -> (navigationHeight: CGFloat, storiesInset: CGFloat) {
@ -1842,53 +1839,101 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
insets.left += layout.safeInsets.left
insets.right += layout.safeInsets.right
if let toolbar = self.toolbar {
var tabBarHeight: CGFloat
var options: ContainerViewLayoutInsetOptions = []
if layout.metrics.widthClass == .regular {
options.insert(.input)
if let toolbarData = self.toolbarData {
var panelsBottomInset: CGFloat = layout.insets(options: []).bottom
if layout.metrics.widthClass == .regular, let inputHeight = layout.inputHeight, inputHeight != 0.0 {
panelsBottomInset = inputHeight + 8.0
}
var heightInset: CGFloat = 0.0
if case .forum = self.location {
heightInset = 4.0
}
let bottomInset: CGFloat = layout.insets(options: options).bottom
if !layout.safeInsets.left.isZero {
tabBarHeight = 34.0 + bottomInset
insets.bottom += 34.0
if panelsBottomInset == 0.0 {
panelsBottomInset = 8.0
} else {
tabBarHeight = 49.0 - heightInset + bottomInset
insets.bottom += 49.0 - heightInset
panelsBottomInset = max(panelsBottomInset, 8.0)
}
let toolbarFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - tabBarHeight), size: CGSize(width: layout.size.width, height: tabBarHeight))
let sideInset: CGFloat = 20.0
let toolbarHeight = 44.0
let toolbarFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - panelsBottomInset - toolbarHeight), size: CGSize(width: layout.size.width - sideInset * 2.0, height: toolbarHeight))
if let toolbarNode = self.toolbarNode {
transition.updateFrame(node: toolbarNode, frame: toolbarFrame)
toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: bottomInset, toolbar: toolbar, transition: transition)
let toolbar: ComponentView<Empty>
var toolbarTransition = ComponentTransition(transition)
if let current = self.toolbar {
toolbar = current
} else {
let toolbarNode = ToolbarNode(theme: ToolbarTheme(rootControllerTheme: self.presentationData.theme), displaySeparator: true, left: { [weak self] in
self?.toolbarActionSelected?(.left)
}, right: { [weak self] in
self?.toolbarActionSelected?(.right)
}, middle: { [weak self] in
self?.toolbarActionSelected?(.middle)
})
toolbarNode.frame = toolbarFrame
toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: bottomInset, toolbar: toolbar, transition: .immediate)
self.addSubnode(toolbarNode)
self.toolbarNode = toolbarNode
if transition.isAnimated {
toolbarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
toolbar = ComponentView()
self.toolbar = toolbar
toolbarTransition = .immediate
}
let _ = toolbar.update(
transition: toolbarTransition,
component: AnyComponent(GlassControlPanelComponent(
theme: self.presentationData.theme,
leftItem: toolbarData.leftAction.flatMap { value in
return GlassControlPanelComponent.Item(
items: [GlassControlGroupComponent.Item(
id: "left_" + value.title,
content: .text(value.title),
action: value.isEnabled ? { [weak self] in
guard let self else {
return
}
self.toolbarActionSelected?(.left)
} : nil
)],
background: .panel
)
},
centralItem: toolbarData.middleAction.flatMap { value in
return GlassControlPanelComponent.Item(
items: [GlassControlGroupComponent.Item(
id: "right_" + value.title,
content: .text(value.title),
action: value.isEnabled ? { [weak self] in
guard let self else {
return
}
self.toolbarActionSelected?(.middle)
} : nil
)],
background: .panel
)
},
rightItem: toolbarData.rightAction.flatMap { value in
return GlassControlPanelComponent.Item(
items: [GlassControlGroupComponent.Item(
id: "right_" + value.title,
content: .text(value.title),
action: value.isEnabled ? { [weak self] in
guard let self else {
return
}
self.toolbarActionSelected?(.right)
} : nil
)],
background: .panel
)
},
centerAlignmentIfPossible: true
)),
environment: {},
containerSize: toolbarFrame.size
)
if let toolbarView = toolbar.view {
if toolbarView.superview == nil {
self.view.addSubview(toolbarView)
toolbarView.alpha = 0.0
}
toolbarTransition.setFrame(view: toolbarView, frame: toolbarFrame)
ComponentTransition(transition).setAlpha(view: toolbarView, alpha: 1.0)
}
} else if let toolbar = self.toolbar {
self.toolbar = nil
if let toolbarView = toolbar.view {
ComponentTransition(transition).setAlpha(view: toolbarView, alpha: 0.0, completion: { [weak toolbarView] _ in
toolbarView?.removeFromSuperview()
})
}
} else if let toolbarNode = self.toolbarNode {
self.toolbarNode = nil
transition.updateAlpha(node: toolbarNode, alpha: 0.0, completion: { [weak toolbarNode] _ in
toolbarNode?.removeFromSupernode()
})
}
var childrenLayout = layout

View file

@ -2071,6 +2071,11 @@ public func chatListFilterPresetController(context: AccountContext, currentPrese
)
|> deliverOnMainQueue
|> map { presentationData, stateWithPeers, peerView, premiumLimits, sharedLinks, currentPreset -> (ItemListControllerState, (ItemListNodeState, Any)) in
var presentationData = presentationData
let updatedTheme = presentationData.theme.withModalBlocksBackground()
presentationData = presentationData.withUpdated(theme: updatedTheme)
let (state, includePeers, excludePeers) = stateWithPeers
let isPremium = peerView.peers[peerView.peerId]?.isPremium ?? false

View file

@ -613,6 +613,11 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
)
)
|> map { presentationData, state, filtersWithCountsValue, preferences, updatedFilterOrderValue, suggestedFilters, peer, allLimits, displayTags -> (ItemListControllerState, (ItemListNodeState, Any)) in
var presentationData = presentationData
let updatedTheme = presentationData.theme.withModalBlocksBackground()
presentationData = presentationData.withUpdated(theme: updatedTheme)
let isPremium = peer?.isPremium ?? false
let limits = allLimits.0
let premiumLimits = allLimits.1

View file

@ -425,9 +425,9 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)))
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight)))
transition.updateFrame(node: strongSelf.titleNode.textNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset), size: titleLayout.size))
transition.updateFrame(node: strongSelf.titleNode.textNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset + 1.0), size: titleLayout.size))
let labelFrame = CGRect(origin: CGPoint(x: params.width - rightArrowInset - labelLayout.size.width + revealOffset, y: verticalInset), size: labelLayout.size)
let labelFrame = CGRect(origin: CGPoint(x: params.width - rightArrowInset - labelLayout.size.width + revealOffset, y: verticalInset + 1.0), size: labelLayout.size)
strongSelf.labelNode.frame = labelFrame
transition.updateAlpha(node: strongSelf.labelNode, alpha: reorderControlSizeAndApply != nil ? 0.0 : 1.0)

View file

@ -215,14 +215,8 @@ public class ChatListFilterPresetListSuggestedItemNode: ListViewItemNode, ItemLi
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor:labelBadgeColor), backgroundColor: nil, maximumNumberOfLines: multilineLabel ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: labelConstrain, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let verticalInset: CGFloat
switch item.systemStyle {
case .glass:
verticalInset = 15.0
case .legacy:
verticalInset = 11.0
}
let titleSpacing: CGFloat = 3.0
let verticalInset: CGFloat = 11.0
let titleSpacing: CGFloat = 2.0
let height: CGFloat
height = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + labelLayout.size.height

View file

@ -6542,10 +6542,10 @@ private final class EmptyResultsButton: Component {
transition: transition,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: component.theme.list.itemCheckColors.fillColor,
foreground: component.theme.list.itemCheckColors.foregroundColor,
pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
cornerRadius: 10.0
pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: buttonContent,
isEnabled: isEnabled,
@ -6557,7 +6557,7 @@ private final class EmptyResultsButton: Component {
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 50.0)
containerSize: CGSize(width: availableSize.width, height: 52.0)
)
if let buttonView = self.button.view {
if buttonView.superview == nil {

View file

@ -185,6 +185,7 @@ class ChatListArchiveInfoItemNode: ListViewItemNode, ASScrollViewDelegate {
self.view.disablesInteractiveTransitionGestureRecognizer = true
self.scrollNode.view.scrollsToTop = false
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.isPagingEnabled = true
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate

View file

@ -640,9 +640,9 @@ private let ungroupIcon = ItemListRevealOptionIcon.animation(animation: "anim_un
private let readIcon = ItemListRevealOptionIcon.animation(animation: "anim_read", scale: 1.0, offset: 0.0, replaceColors: nil, flip: false)
private let unreadIcon = ItemListRevealOptionIcon.animation(animation: "anim_unread", scale: 1.0, offset: 0.0, replaceColors: [0x2194fa], flip: false)
private let archiveIcon = ItemListRevealOptionIcon.animation(animation: "anim_archive", scale: 1.0, offset: 2.0, replaceColors: [0xa9a9ad], flip: false)
private let unarchiveIcon = ItemListRevealOptionIcon.animation(animation: "anim_unarchive", scale: 0.642, offset: -9.0, replaceColors: [0xa9a9ad], flip: false)
private let hideIcon = ItemListRevealOptionIcon.animation(animation: "anim_hide", scale: 1.0, offset: 2.0, replaceColors: [0xbdbdc2], flip: false)
private let unhideIcon = ItemListRevealOptionIcon.animation(animation: "anim_hide", scale: 1.0, offset: -20.0, replaceColors: [0xbdbdc2], flip: true)
private let unarchiveIcon = ItemListRevealOptionIcon.animation(animation: "anim_unarchive", scale: 0.52, offset: -6.0, replaceColors: [0xa9a9ad], flip: false)
private let hideIcon = ItemListRevealOptionIcon.animation(animation: "anim_hide", scale: 1.1, offset: 2.0, replaceColors: [0xbdbdc2], flip: false)
private let unhideIcon = ItemListRevealOptionIcon.animation(animation: "anim_hide", scale: 1.0, offset: -15.0, replaceColors: [0xbdbdc2], flip: true)
private let startIcon = ItemListRevealOptionIcon.animation(animation: "anim_play", scale: 1.0, offset: 0.0, replaceColors: [0xbdbdc2], flip: false)
private let closeIcon = ItemListRevealOptionIcon.animation(animation: "anim_pause", scale: 1.0, offset: 0.0, replaceColors: [0xbdbdc2], flip: false)
@ -1393,8 +1393,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
private var isHighlighted: Bool = false
private var isRevealHighlighted: Bool = false
private var isSeparatorHiddenByLowerReveal: Bool = false
private weak var revealHighlightedUpperNeighbor: ChatListItemNode?
private var keepRevealHighlightUntilClosed: Bool = false
private var nextHasActiveRevealControls: Bool = false
private var skipFadeout: Bool = false
private var customAnimationInProgress: Bool = false
@ -1744,7 +1743,6 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
deinit {
self.revealHighlightedUpperNeighbor?.updateSeparatorHiddenByLowerReveal(false, transition: .immediate)
self.cachedDataDisposable.dispose()
}
@ -2147,42 +2145,13 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
private func updateSeparatorAlpha(transition: ContainedViewLayoutTransition, inlineNavigationProgress: CGFloat? = nil) {
let revealSeparatorAlpha: CGFloat = (self.isRevealHighlighted || self.nextHasActiveRevealControls || self.isSeparatorHiddenByLowerReveal) ? 0.0 : 1.0
let revealSeparatorAlpha: CGFloat = (self.isRevealHighlighted || self.nextHasActiveRevealControls) ? 0.0 : 1.0
if let inlineNavigationProgress = inlineNavigationProgress ?? self.item?.interaction.inlineNavigationLocation?.progress {
transition.updateAlpha(node: self.separatorNode, alpha: (1.0 - inlineNavigationProgress) * revealSeparatorAlpha)
} else {
transition.updateAlpha(node: self.separatorNode, alpha: revealSeparatorAlpha)
}
}
private func updateSeparatorHiddenByLowerReveal(_ value: Bool, transition: ContainedViewLayoutTransition) {
self.isSeparatorHiddenByLowerReveal = value
self.updateSeparatorAlpha(transition: transition)
}
private func updateUpperNeighborSeparatorForReveal(_ value: Bool, transition: ContainedViewLayoutTransition) {
if value {
var upperNeighbor: ChatListItemNode?
if let supernode = self.supernode, let subnodes = supernode.subnodes {
for case let node as ChatListItemNode in subnodes {
if node !== self && abs(node.frame.maxY - self.frame.minY) <= 2.0 {
if upperNeighbor == nil || node.frame.maxY > upperNeighbor!.frame.maxY {
upperNeighbor = node
}
}
}
}
if self.revealHighlightedUpperNeighbor !== upperNeighbor {
self.revealHighlightedUpperNeighbor?.updateSeparatorHiddenByLowerReveal(false, transition: transition)
self.revealHighlightedUpperNeighbor = upperNeighbor
}
upperNeighbor?.updateSeparatorHiddenByLowerReveal(true, transition: transition)
} else if let upperNeighbor = self.revealHighlightedUpperNeighbor {
upperNeighbor.updateSeparatorHiddenByLowerReveal(false, transition: transition)
self.revealHighlightedUpperNeighbor = nil
}
}
override public func tapped() {
guard let item = self.item, item.editing else {
@ -3559,7 +3528,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
let (mentionBadgeLayout, mentionBadgeApply) = mentionBadgeLayout(CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), badgeDiameter, badgeFont, currentMentionBadgeImage, mentionBadgeContent)
var actionButtonTitleNodeLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if case .none = badgeContent, case .none = mentionBadgeContent, case let .chat(itemPeer) = contentPeer, case let .user(user) = itemPeer.chatMainPeer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) {
if !item.editing, case .none = badgeContent, case .none = mentionBadgeContent, case let .chat(itemPeer) = contentPeer, case let .user(user) = itemPeer.chatMainPeer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) {
actionButtonTitleNodeLayoutAndApply = makeActionButtonTitleNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.ChatList_InlineButtonOpenApp, font: Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)), textColor: theme.unreadBadgeActiveTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
}
@ -4413,16 +4382,17 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.statusNode.fontSize = item.presentationData.fontSize.itemListBaseFontSize
let _ = strongSelf.statusNode.transitionToState(statusState, animated: animateContent)
let rightAccessoryVerticalOffset: CGFloat = -2.0
var nextBadgeX: CGFloat = contentRect.maxX
if let _ = currentBadgeBackgroundImage {
let badgeFrame = CGRect(x: nextBadgeX - badgeLayout.width, y: contentRect.maxY - badgeLayout.height - 2.0, width: badgeLayout.width, height: badgeLayout.height)
let badgeFrame = CGRect(x: nextBadgeX - badgeLayout.width, y: contentRect.maxY - badgeLayout.height - 2.0 + rightAccessoryVerticalOffset, width: badgeLayout.width, height: badgeLayout.height)
transition.updateFrame(node: strongSelf.badgeNode, frame: badgeFrame)
nextBadgeX -= badgeLayout.width + 6.0
}
if currentMentionBadgeImage != nil || currentBadgeBackgroundImage != nil {
let badgeFrame = CGRect(x: nextBadgeX - mentionBadgeLayout.width, y: contentRect.maxY - mentionBadgeLayout.height - 2.0, width: mentionBadgeLayout.width, height: mentionBadgeLayout.height)
let badgeFrame = CGRect(x: nextBadgeX - mentionBadgeLayout.width, y: contentRect.maxY - mentionBadgeLayout.height - 2.0 + rightAccessoryVerticalOffset, width: mentionBadgeLayout.width, height: mentionBadgeLayout.height)
transition.updateFrame(node: strongSelf.mentionBadgeNode, frame: badgeFrame)
nextBadgeX -= mentionBadgeLayout.width + 6.0
@ -4433,7 +4403,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.pinnedIconNode.isHidden = false
let pinnedIconSize = currentPinnedIconImage.size
let pinnedIconFrame = CGRect(x: nextBadgeX - pinnedIconSize.width, y: contentRect.maxY - pinnedIconSize.height - 2.0, width: pinnedIconSize.width, height: pinnedIconSize.height)
let pinnedIconFrame = CGRect(x: nextBadgeX - pinnedIconSize.width, y: contentRect.maxY - pinnedIconSize.height - 2.0 + rightAccessoryVerticalOffset, width: pinnedIconSize.width, height: pinnedIconSize.height)
strongSelf.pinnedIconNode.frame = pinnedIconFrame
nextBadgeX -= pinnedIconSize.width + 6.0
@ -4450,11 +4420,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
let actionButtonSize = CGSize(width: actionButtonTitleNodeLayout.size.width + actionButtonSideInset * 2.0, height: actionButtonTitleNodeLayout.size.height + actionButtonTopInset + actionButtonBottomInset)
var actionButtonFrame = CGRect(x: nextBadgeX - actionButtonSize.width, y: contentRect.minY + floor((contentRect.height - actionButtonSize.height) * 0.5), width: actionButtonSize.width, height: actionButtonSize.height)
actionButtonFrame.origin.y = max(actionButtonFrame.origin.y, dateFrame.maxY + floor(item.presentationData.fontSize.itemListBaseFontSize * 4.0 / 17.0))
actionButtonFrame.origin.y += 4.0
let actionButtonNode: HighlightableButtonNode
var animateActionButtonIn = false
if let current = strongSelf.actionButtonNode {
actionButtonNode = current
} else {
animateActionButtonIn = true
actionButtonNode = HighlightableButtonNode()
strongSelf.actionButtonNode = actionButtonNode
strongSelf.mainContentContainerNode.addSubnode(actionButtonNode)
@ -4483,23 +4456,40 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
actionButtonNode.addSubnode(actionButtonTitleNode)
}
actionButtonNode.isUserInteractionEnabled = true
actionButtonNode.frame = actionButtonFrame
actionButtonBackgroundView.frame = CGRect(origin: CGPoint(), size: actionButtonFrame.size)
actionButtonTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((actionButtonFrame.width - actionButtonTitleNodeLayout.size.width) * 0.5), y: actionButtonTopInset), size: actionButtonTitleNodeLayout.size)
if animateActionButtonIn {
actionButtonNode.alpha = 0.0
}
transition.updateAlpha(node: actionButtonNode, alpha: 1.0)
nextBadgeX -= actionButtonSize.width + 6.0
} else {
if let actionButtonTitleNode = strongSelf.actionButtonTitleNode {
actionButtonTitleNode.removeFromSupernode()
strongSelf.actionButtonTitleNode = nil
}
if let actionButtonBackgroundView = strongSelf.actionButtonBackgroundView {
actionButtonBackgroundView.removeFromSuperview()
strongSelf.actionButtonBackgroundView = nil
}
if let actionButtonNode = strongSelf.actionButtonNode {
actionButtonNode.removeFromSupernode()
let actionButtonTitleNode = strongSelf.actionButtonTitleNode
let actionButtonBackgroundView = strongSelf.actionButtonBackgroundView
actionButtonNode.isUserInteractionEnabled = false
strongSelf.actionButtonTitleNode = nil
strongSelf.actionButtonBackgroundView = nil
strongSelf.actionButtonNode = nil
transition.updateAlpha(node: actionButtonNode, alpha: 0.0, completion: { [weak actionButtonNode, weak actionButtonTitleNode, weak actionButtonBackgroundView] _ in
actionButtonTitleNode?.removeFromSupernode()
actionButtonBackgroundView?.removeFromSuperview()
actionButtonNode?.removeFromSupernode()
})
} else {
if let actionButtonTitleNode = strongSelf.actionButtonTitleNode {
actionButtonTitleNode.removeFromSupernode()
strongSelf.actionButtonTitleNode = nil
}
if let actionButtonBackgroundView = strongSelf.actionButtonBackgroundView {
actionButtonBackgroundView.removeFromSuperview()
strongSelf.actionButtonBackgroundView = nil
}
}
}
@ -5140,7 +5130,6 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
} else {
strongSelf.updateSeparatorAlpha(transition: transition)
}
strongSelf.updateUpperNeighborSeparatorForReveal(strongSelf.isRevealHighlighted, transition: transition)
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData {
if customMessageListData.hideSeparator {
@ -5336,12 +5325,22 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
let highlightedBackgroundFrame = self.highlightedBackgroundNode.frame
transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: offset, y: highlightedBackgroundFrame.minY), size: highlightedBackgroundFrame.size))
let isRevealHighlighted = !offset.isZero
if !offset.isZero {
self.keepRevealHighlightUntilClosed = true
}
let isRevealHighlighted = !offset.isZero || self.keepRevealHighlightUntilClosed
if self.isRevealHighlighted != isRevealHighlighted {
self.isRevealHighlighted = isRevealHighlighted
self.updateIsHighlighted(transition: transition)
}
self.updateUpperNeighborSeparatorForReveal(isRevealHighlighted, transition: transition)
}
private func clearRevealHighlightIfNeeded(transition: ContainedViewLayoutTransition) {
self.keepRevealHighlightUntilClosed = false
if self.revealOffset.isZero && self.isRevealHighlighted {
self.isRevealHighlighted = false
self.updateIsHighlighted(transition: transition)
}
}
override public func touchesToOtherItemsPrevented() {
@ -5349,9 +5348,11 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
if let item = self.item {
item.interaction.setPeerIdWithRevealedOptions(nil, nil)
}
self.clearRevealHighlightIfNeeded(transition: .immediate)
}
override public func revealOptionsInteractivelyOpened() {
self.keepRevealHighlightUntilClosed = true
if let item = self.item {
switch item.index {
case let .chatList(index):
@ -5363,6 +5364,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
override public func revealOptionsInteractivelyClosed() {
self.clearRevealHighlightIfNeeded(transition: .animated(duration: 0.2, curve: .easeInOut))
if let item = self.item {
switch item.index {
case let .chatList(index):

View file

@ -7,19 +7,22 @@ public final class Image: Component {
public let size: CGSize?
public let contentMode: UIImageView.ContentMode
public let cornerRadius: CGFloat
public let flipHorizontally: Bool
public init(
image: UIImage?,
tintColor: UIColor? = nil,
size: CGSize? = nil,
contentMode: UIImageView.ContentMode = .scaleToFill,
cornerRadius: CGFloat = 0.0
cornerRadius: CGFloat = 0.0,
flipHorizontally: Bool = false
) {
self.image = image
self.tintColor = tintColor
self.size = size
self.contentMode = contentMode
self.cornerRadius = cornerRadius
self.flipHorizontally = flipHorizontally
}
public static func ==(lhs: Image, rhs: Image) -> Bool {
@ -38,6 +41,9 @@ public final class Image: Component {
if lhs.cornerRadius != rhs.cornerRadius {
return false
}
if lhs.flipHorizontally != rhs.flipHorizontally {
return false
}
return true
}
@ -51,7 +57,11 @@ public final class Image: Component {
}
func update(component: Image, availableSize: CGSize, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.image = component.image
if component.flipHorizontally, let cgImage = component.image?.cgImage {
self.image = UIImage(cgImage: cgImage, scale: component.image?.scale ?? 0.0, orientation: .upMirrored)
} else {
self.image = component.image
}
self.contentMode = component.contentMode
self.clipsToBounds = component.cornerRadius > 0.0

View file

@ -11,15 +11,17 @@ public final class BundleIconComponent: Component {
public let scaleFactor: CGFloat
public let shadowColor: UIColor?
public let shadowBlur: CGFloat
public let flipHorizontally: Bool
public let flipVertically: Bool
public init(name: String, tintColor: UIColor?, maxSize: CGSize? = nil, scaleFactor: CGFloat = 1.0, shadowColor: UIColor? = nil, shadowBlur: CGFloat = 0.0, flipVertically: Bool = false) {
public init(name: String, tintColor: UIColor?, maxSize: CGSize? = nil, scaleFactor: CGFloat = 1.0, shadowColor: UIColor? = nil, shadowBlur: CGFloat = 0.0, flipHorizontally: Bool = false, flipVertically: Bool = false) {
self.name = name
self.tintColor = tintColor
self.maxSize = maxSize
self.scaleFactor = scaleFactor
self.shadowColor = shadowColor
self.shadowBlur = shadowBlur
self.flipHorizontally = flipHorizontally
self.flipVertically = flipVertically
}
@ -42,6 +44,9 @@ public final class BundleIconComponent: Component {
if lhs.shadowBlur != rhs.shadowBlur {
return false
}
if lhs.flipHorizontally != rhs.flipHorizontally {
return false
}
if lhs.flipVertically != rhs.flipVertically {
return false
}
@ -77,7 +82,9 @@ public final class BundleIconComponent: Component {
}
})
}
if component.flipVertically, let cgImage = image?.cgImage {
if component.flipHorizontally, let cgImage = image?.cgImage {
self.image = UIImage(cgImage: cgImage, scale: image?.scale ?? 0.0, orientation: .upMirrored)
} else if component.flipVertically, let cgImage = image?.cgImage {
self.image = UIImage(cgImage: cgImage, scale: image?.scale ?? 0.0, orientation: .down)
} else {
self.image = image

View file

@ -291,6 +291,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
}
self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true
self.scrollNode.view.scrollsToTop = false
self.itemNodes = reactions.map { reaction, count in
return ItemNode(context: context, availableReactions: availableReactions, reaction: reaction, animationCache: animationCache, animationRenderer: animationRenderer, count: count)
@ -897,6 +898,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
}
self.scrollNode.clipsToBounds = false
self.scrollNode.view.scrollsToTop = false
super.init()

View file

@ -104,19 +104,19 @@ public final class DeviceLocationManager: NSObject {
self.currentTopMode = topMode
if let topMode = topMode {
self.log?("setting mode \(topMode)")
switch topMode {
case .preciseForeground:
self.manager.allowsBackgroundLocationUpdates = false
case .preciseAlways:
self.manager.allowsBackgroundLocationUpdates = true
}
if previousTopMode == nil {
if !self.requestedAuthorization {
self.requestedAuthorization = true
self.manager.requestAlwaysAuthorization()
}
switch topMode {
case .preciseForeground:
self.manager.allowsBackgroundLocationUpdates = false
case .preciseAlways:
self.manager.allowsBackgroundLocationUpdates = true
}
self.manager.startUpdatingLocation()
self.manager.startUpdatingHeading()
}

View file

@ -88,6 +88,12 @@ final class ActionSheetControllerNode: ASDisplayNode, ASScrollViewDelegate {
}
}
override func didLoad() {
super.didLoad()
self.scrollNode.view.scrollsToTop = false
}
func performHighlightedAction() {
self.itemGroupsContainerNode.performHighlightedAction()
}

View file

@ -52,6 +52,7 @@ final class ActionSheetItemGroupNode: ASDisplayNode, ASScrollViewDelegate {
self.scrollNode.view.canCancelContentTouches = true
self.scrollNode.view.showsVerticalScrollIndicator = false
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.scrollsToTop = false
super.init()

View file

@ -72,8 +72,21 @@ public extension CALayer {
func makeAnimation(from: Any?, to: Any, keyPath: String, timingFunction: String, duration: Double, delay: Double = 0.0, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) -> CAAnimation {
if timingFunction.hasPrefix(kCAMediaTimingFunctionCustomSpringPrefix) {
let components = timingFunction.components(separatedBy: "_")
let damping = Float(components[1]) ?? 100.0
let initialVelocity = Float(components[2]) ?? 0.0
let mass: Float
let stiffness: Float
let damping: Float
let initialVelocity: Float
if components.count >= 5 {
mass = Float(components[1]) ?? 5.0
stiffness = Float(components[2]) ?? 900.0
damping = Float(components[3]) ?? 100.0
initialVelocity = Float(components[4]) ?? 0.0
} else {
mass = 5.0
stiffness = 900.0
damping = components.count > 1 ? (Float(components[1]) ?? 100.0) : 100.0
initialVelocity = components.count > 2 ? (Float(components[2]) ?? 0.0) : 0.0
}
let animation = CASpringAnimation(keyPath: keyPath)
animation.fromValue = from
@ -83,10 +96,10 @@ public extension CALayer {
if let completion = completion {
animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion)
}
animation.mass = CGFloat(mass)
animation.stiffness = CGFloat(stiffness)
animation.damping = CGFloat(damping)
animation.initialVelocity = CGFloat(initialVelocity)
animation.mass = 5.0
animation.stiffness = 900.0
animation.duration = animation.settlingDuration
animation.timingFunction = CAMediaTimingFunction.init(name: .linear)
let k = Float(UIView.animationDurationFactor())

View file

@ -14,7 +14,7 @@ public enum ContainedViewLayoutTransitionCurve: Equatable, Hashable {
case easeInOut
case easeIn
case spring
case customSpring(damping: CGFloat, initialVelocity: CGFloat)
case customSpring(mass: CGFloat = 5.0, stiffness: CGFloat = 900.0, damping: CGFloat, initialVelocity: CGFloat)
case custom(Float, Float, Float, Float)
public static var slide: ContainedViewLayoutTransitionCurve {
@ -52,8 +52,8 @@ public extension ContainedViewLayoutTransitionCurve {
return CAMediaTimingFunctionName.easeIn.rawValue
case .spring:
return kCAMediaTimingFunctionSpring
case let .customSpring(damping, initialVelocity):
return "\(kCAMediaTimingFunctionCustomSpringPrefix)_\(damping)_\(initialVelocity)"
case let .customSpring(mass, stiffness, damping, initialVelocity):
return "\(kCAMediaTimingFunctionCustomSpringPrefix)_\(mass)_\(stiffness)_\(damping)_\(initialVelocity)"
case .custom:
return CAMediaTimingFunctionName.easeInEaseOut.rawValue
}
@ -124,8 +124,8 @@ private extension CALayer {
let timingFunction: String
let mediaTimingFunction: CAMediaTimingFunction?
switch curve {
case .spring:
timingFunction = kCAMediaTimingFunctionSpring
case .spring, .customSpring:
timingFunction = curve.timingFunction
mediaTimingFunction = nil
default:
timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue

View file

@ -509,6 +509,7 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur
self.scroller.contentSize = CGSize(width: 0.0, height: infiniteScrollSize * 2.0)
self.scroller.isHidden = true
self.scroller.delegate = self.wrappedScrollViewDelegate
self.scroller.scrollsToTop = false
self.view.addSubview(self.scroller)
self.scroller.panGestureRecognizer.cancelsTouchesInView = true
self.view.addGestureRecognizer(self.scroller.panGestureRecognizer)

View file

@ -172,6 +172,7 @@ open class NavigationController: UINavigationController, ContainableController,
private var inCallStatusBar: StatusBar?
private var updateInCallStatusBarState: CallStatusBarNode?
private var globalScrollToTopNode: ScrollToTopNode?
private var windowScrollToTopProxyViews: WindowScrollToTopProxyViews?
private var rootContainer: RootContainer?
private var rootModalFrame: NavigationModalFrame?
private var modalContainers: [NavigationModalContainer] = []
@ -331,6 +332,54 @@ open class NavigationController: UINavigationController, ContainableController,
}
deinit {
self.windowScrollToTopProxyViews?.update(window: nil, mode: .disabled, referenceView: nil)
}
private func getWindowScrollToTopProxyViews() -> WindowScrollToTopProxyViews {
if let windowScrollToTopProxyViews = self.windowScrollToTopProxyViews {
return windowScrollToTopProxyViews
}
let windowScrollToTopProxyViews = WindowScrollToTopProxyViews(scrollToTop: { [weak self] subject in
self?.scrollToTop(subject)
})
self.windowScrollToTopProxyViews = windowScrollToTopProxyViews
return windowScrollToTopProxyViews
}
private func windowScrollToTopReferenceView(window: UIWindow) -> UIView? {
var view: UIView? = self.view
while let currentView = view {
if currentView.superview === window {
return currentView
}
view = currentView.superview
}
return nil
}
private func updateWindowScrollToTopProxyViews(layout: ContainerViewLayout) {
guard let window = self.view.window, let rootContainer = self.rootContainer else {
(self.globalScrollToTopNode?.view as? ScrollToTopView)?.scrollsToTop = true
self.windowScrollToTopProxyViews?.update(window: nil, mode: .disabled, referenceView: nil)
return
}
(self.globalScrollToTopNode?.view as? ScrollToTopView)?.scrollsToTop = false
let referenceView = self.windowScrollToTopReferenceView(window: window)
let proxyViews = self.getWindowScrollToTopProxyViews()
switch rootContainer {
case .flat:
let scrollToTopHeight = max(layout.statusBarHeight ?? layout.safeInsets.top, 1.0)
let frame = self.view.convert(CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: scrollToTopHeight)), to: window)
proxyViews.update(window: window, mode: .flat(frame: frame), referenceView: referenceView)
case let .split(container):
let frames = container.scrollToTopProxyFrames(layout: layout)
proxyViews.update(window: window, mode: .split(
masterFrame: container.view.convert(frames.master, to: window),
detailFrame: container.view.convert(frames.detail, to: window)
), referenceView: referenceView)
}
}
public func combinedSupportedOrientations(currentOrientationToLock: UIInterfaceOrientationMask) -> ViewControllerSupportedOrientations {
@ -498,7 +547,7 @@ open class NavigationController: UINavigationController, ContainableController,
}
if let globalScrollToTopNode = self.globalScrollToTopNode {
globalScrollToTopNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: layout.size.width, height: 1))
globalScrollToTopNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: layout.size.width, height: 1.0))
}
var overlayContainerLayout = layout
@ -1018,8 +1067,6 @@ open class NavigationController: UINavigationController, ContainableController,
case let .flat(flatContainer):
let splitContainer = NavigationSplitContainer(theme: self.theme, controllerRemoved: { [weak self] controller in
self?.controllerRemoved(controller)
}, scrollToTop: { [weak self] subject in
self?.scrollToTop(subject)
})
if let detailsPlaceholderNode = self.detailsPlaceholderNode {
self.displayNode.insertSubnode(splitContainer, aboveSubnode: detailsPlaceholderNode)
@ -1053,8 +1100,6 @@ open class NavigationController: UINavigationController, ContainableController,
} else {
let splitContainer = NavigationSplitContainer(theme: self.theme, controllerRemoved: { [weak self] controller in
self?.controllerRemoved(controller)
}, scrollToTop: { [weak self] subject in
self?.scrollToTop(subject)
})
if let detailsPlaceholderNode = self.detailsPlaceholderNode {
self.displayNode.insertSubnode(splitContainer, aboveSubnode: detailsPlaceholderNode)
@ -1385,6 +1430,8 @@ open class NavigationController: UINavigationController, ContainableController,
}
}
self.updateWindowScrollToTopProxyViews(layout: layout)
self.isUpdatingContainers = false
if notifyGlobalOverlayControllersUpdated {

View file

@ -91,6 +91,7 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
self.scrollNode.view.clipsToBounds = false
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
self.scrollNode.view.tag = 0x5C4011
self.scrollNode.view.scrollsToTop = false
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in
guard let strongSelf = self, !strongSelf.isDismissed else {

View file

@ -11,8 +11,6 @@ enum NavigationSplitContainerScrollToTop {
final class NavigationSplitContainer: ASDisplayNode {
private var theme: NavigationControllerTheme
private let masterScrollToTopView: ScrollToTopView
private let detailScrollToTopView: ScrollToTopView
private let masterContainer: NavigationContainer
private let detailContainer: NavigationContainer
private let separator: ASDisplayNode
@ -39,18 +37,9 @@ final class NavigationSplitContainer: ASDisplayNode {
self.detailContainer.isInFocus = isInFocus
}
init(theme: NavigationControllerTheme, controllerRemoved: @escaping (ViewController) -> Void, scrollToTop: @escaping (NavigationSplitContainerScrollToTop) -> Void) {
init(theme: NavigationControllerTheme, controllerRemoved: @escaping (ViewController) -> Void) {
self.theme = theme
self.masterScrollToTopView = ScrollToTopView(frame: CGRect())
self.masterScrollToTopView.action = {
scrollToTop(.master)
}
self.detailScrollToTopView = ScrollToTopView(frame: CGRect())
self.detailScrollToTopView.action = {
scrollToTop(.detail)
}
self.masterContainer = NavigationContainer(isFlat: false, controllerRemoved: controllerRemoved)
self.masterContainer.clipsToBounds = true
@ -65,8 +54,6 @@ final class NavigationSplitContainer: ASDisplayNode {
self.addSubnode(self.masterContainer)
self.addSubnode(self.detailContainer)
self.addSubnode(self.separator)
self.view.addSubview(self.masterScrollToTopView)
self.view.addSubview(self.detailScrollToTopView)
}
func hasNonReadyControllers() -> Bool {
@ -83,13 +70,21 @@ final class NavigationSplitContainer: ASDisplayNode {
self.separator.backgroundColor = theme.navigationBar.separatorColor
}
func scrollToTopProxyFrames(layout: ContainerViewLayout) -> (master: CGRect, detail: CGRect) {
let masterWidth: CGFloat = min(max(320.0, floor(layout.size.width / 3.0)), floor(layout.size.width / 2.0))
let detailWidth = layout.size.width - masterWidth
let scrollToTopHeight = max(layout.statusBarHeight ?? layout.safeInsets.top, 1.0)
return (
master: CGRect(origin: CGPoint(), size: CGSize(width: masterWidth, height: scrollToTopHeight)),
detail: CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: detailWidth, height: scrollToTopHeight))
)
}
func update(layout: ContainerViewLayout, masterControllers: [ViewController], detailControllers: [ViewController], detailsPlaceholderNode: NavigationDetailsPlaceholderNode?, transition: ContainedViewLayoutTransition) {
let masterWidth: CGFloat = min(max(320.0, floor(layout.size.width / 3.0)), floor(layout.size.width / 2.0))
let detailWidth = layout.size.width - masterWidth
self.masterScrollToTopView.frame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: masterWidth, height: 10.0))
self.detailScrollToTopView.frame = CGRect(origin: CGPoint(x: masterWidth, y: -1.0), size: CGSize(width: detailWidth, height: 1.0))
transition.updateFrame(node: self.masterContainer, frame: CGRect(origin: CGPoint(), size: CGSize(width: masterWidth, height: layout.size.height)))
transition.updateFrame(node: self.detailContainer, frame: CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: detailWidth, height: layout.size.height)))
transition.updateFrame(node: self.separator, frame: CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: UIScreenPixel, height: layout.size.height)))

View file

@ -35,10 +35,6 @@ class ScrollToTopView: UIScrollView, UIScrollViewDelegate {
return false
}
func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
print("scrollViewDidScrollToTop")
}
}
class ScrollToTopNode: ASDisplayNode {
@ -52,3 +48,83 @@ class ScrollToTopNode: ASDisplayNode {
})
}
}
final class WindowScrollToTopProxyViews {
enum Mode {
case disabled
case flat(frame: CGRect)
case split(masterFrame: CGRect, detailFrame: CGRect)
}
private let flatView: ScrollToTopView
private let masterView: ScrollToTopView
private let detailView: ScrollToTopView
init(scrollToTop: @escaping (NavigationSplitContainerScrollToTop) -> Void) {
self.flatView = ScrollToTopView(frame: CGRect())
self.masterView = ScrollToTopView(frame: CGRect())
self.detailView = ScrollToTopView(frame: CGRect())
self.flatView.action = {
scrollToTop(.master)
}
self.masterView.action = {
scrollToTop(.master)
}
self.detailView.action = {
scrollToTop(.detail)
}
}
deinit {
self.disable(self.flatView)
self.disable(self.masterView)
self.disable(self.detailView)
}
func update(window: UIWindow?, mode: Mode, referenceView: UIView?) {
guard let window else {
self.disable(self.flatView)
self.disable(self.masterView)
self.disable(self.detailView)
return
}
switch mode {
case .disabled:
self.disable(self.flatView)
self.disable(self.masterView)
self.disable(self.detailView)
case let .flat(frame):
self.activate(self.flatView, in: window, frame: frame, referenceView: referenceView)
self.disable(self.masterView)
self.disable(self.detailView)
case let .split(masterFrame, detailFrame):
self.disable(self.flatView)
self.activate(self.masterView, in: window, frame: masterFrame, referenceView: referenceView)
self.activate(self.detailView, in: window, frame: detailFrame, referenceView: referenceView)
}
}
private func activate(_ view: ScrollToTopView, in window: UIWindow, frame: CGRect, referenceView: UIView?) {
if let referenceView, referenceView.superview === window {
if view.superview !== window {
view.removeFromSuperview()
}
window.insertSubview(view, aboveSubview: referenceView)
} else if view.superview !== window {
view.removeFromSuperview()
window.addSubview(view)
}
view.isHidden = false
view.scrollsToTop = true
view.frame = frame
}
private func disable(_ view: ScrollToTopView) {
view.scrollsToTop = false
view.isHidden = true
view.removeFromSuperview()
}
}

View file

@ -108,6 +108,10 @@ swift_library(
"//submodules/TelegramUI/Components/StickerPickerScreen",
"//submodules/TelegramUI/Components/MediaEditor/ImageObjectSeparation",
"//submodules/TelegramUI/Components/SegmentControlComponent",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/TelegramUI/Components/LiquidLens",
"//submodules/TelegramUI/Components/TabSelectionRecognizer",
"//submodules/TelegramUI/Components/GlassControls",
],
visibility = [
"//visibility:public",

View file

@ -14,6 +14,8 @@ import SegmentControlComponent
import MultilineTextComponent
import HexColor
import MediaEditor
import GlassBarButtonComponent
import BundleIconComponent
private let palleteColors: [UInt32] = [
0xffffff, 0xebebeb, 0xd6d6d6, 0xc2c2c2, 0xadadad, 0x999999, 0x858585, 0x707070, 0x5c5c5c, 0x474747, 0x333333, 0x000000,
@ -1819,30 +1821,6 @@ private final class ColorPickerContent: CombinedComponent {
}
final class State: ComponentState {
var cachedEyedropperImage: UIImage?
var eyedropperImage: UIImage {
let eyedropperImage: UIImage
if let image = self.cachedEyedropperImage {
eyedropperImage = image
} else {
eyedropperImage = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Eyedropper"), color: .white)!
self.cachedEyedropperImage = eyedropperImage
}
return eyedropperImage
}
var cachedCloseImage: UIImage?
var closeImage: UIImage {
let closeImage: UIImage
if let image = self.cachedCloseImage {
closeImage = image
} else {
closeImage = generateCloseButtonImage(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xffffff))!
self.cachedCloseImage = closeImage
}
return closeImage
}
var selectedMode: Int = 0
var selectedColor: DrawingColor
@ -1884,8 +1862,8 @@ private final class ColorPickerContent: CombinedComponent {
}
static var body: Body {
let eyedropperButton = Child(Button.self)
let closeButton = Child(Button.self)
let eyedropperButton = Child(GlassBarButtonComponent.self)
let closeButton = Child(GlassBarButtonComponent.self)
let title = Child(MultilineTextComponent.self)
let modeControl = Child(SegmentControlComponent.self)
@ -1918,50 +1896,45 @@ private final class ColorPickerContent: CombinedComponent {
let sideInset: CGFloat = 16.0
let eyedropperButton = eyedropperButton.update(
component: Button(
content: AnyComponent(
Image(image: state.eyedropperImage)
component: GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: true,
state: .glass,
component: AnyComponentWithIdentity(
id: "icon",
component: AnyComponent(BundleIconComponent(name: "Media Editor/Eyedropper", tintColor: .white))
),
action: { [weak component] in
component?.eyedropper()
action: { _ in
component.eyedropper()
}
).minSize(CGSize(width: 30.0, height: 30.0)),
availableSize: CGSize(width: 19.0, height: 19.0),
),
availableSize: CGSize(width: 44.0, height: 44.0),
transition: .immediate
)
context.add(eyedropperButton
.position(CGPoint(x: environment.safeInsets.left + eyedropperButton.size.width + 1.0, y: 29.0))
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - eyedropperButton.size.width / 2.0 - 16.0, y: 16.0 + eyedropperButton.size.height / 2.0))
)
let closeButton = closeButton.update(
component: Button(
content: AnyComponent(ZStack([
AnyComponentWithIdentity(
id: "background",
component: AnyComponent(
BlurredBackgroundComponent(
color: UIColor(rgb: 0x888888, alpha: 0.1)
)
)
),
AnyComponentWithIdentity(
id: "icon",
component: AnyComponent(
Image(image: state.closeImage)
)
),
])),
action: { [weak component] in
component?.dismiss()
component: GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: true,
state: .glass,
component: AnyComponentWithIdentity(
id: "icon",
component: AnyComponent(BundleIconComponent(name: "Navigation/Close", tintColor: .white))
),
action: { _ in
component.dismiss()
}
),
availableSize: CGSize(width: 30.0, height: 30.0),
transition: .immediate
)
context.add(closeButton
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - closeButton.size.width - 1.0, y: 29.0))
.clipsToBounds(true)
.cornerRadius(15.0)
.position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
)
let title = title.update(
@ -1979,12 +1952,11 @@ private final class ColorPickerContent: CombinedComponent {
transition: .immediate
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: 29.0))
.position(CGPoint(x: context.availableSize.width / 2.0, y: 16.0 + 22.0))
)
var contentHeight: CGFloat = 58.0
var contentHeight: CGFloat = 76.0
//backgroundColor: .clear, foregroundColor: UIColor(rgb: 0x6f7075, alpha: 0.6), shadowColor: .black, textColor: UIColor(rgb: 0xffffff), dividerColor: UIColor(rgb: 0x505155, alpha: 0.6)
let modeControl = modeControl.update(
component: SegmentControlComponent(
theme: SegmentControlComponent.Theme(backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.07), legacyBackgroundColor: .clear, foregroundColor: UIColor(rgb: 0x6f7075, alpha: 0.6), textColor: .white, dividerColor: UIColor(rgb: 0x505155, alpha: 0.6)),

View file

@ -25,6 +25,9 @@ import FastBlur
import MediaEditor
import StickerPickerScreen
import ImageObjectSeparation
import GlassBarButtonComponent
import GlassControls
import BundleIconComponent
public struct DrawingResultData {
public let data: Data?
@ -363,15 +366,17 @@ private final class ReferenceContentSource: ContextReferenceContentSource {
private let sourceView: UIView
private let contentArea: CGRect
private let customPosition: CGPoint
private let actionsPosition: ContextControllerReferenceViewInfo.ActionsPosition
init(sourceView: UIView, contentArea: CGRect, customPosition: CGPoint) {
init(sourceView: UIView, contentArea: CGRect, customPosition: CGPoint, actionsPosition: ContextControllerReferenceViewInfo.ActionsPosition = .top) {
self.sourceView = sourceView
self.contentArea = contentArea
self.customPosition = customPosition
self.actionsPosition = actionsPosition
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: self.contentArea, customPosition: self.customPosition, actionsPosition: .top)
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: self.contentArea, customPosition: self.customPosition, actionsPosition: self.actionsPosition)
}
}
@ -467,13 +472,11 @@ private let bottomGradientTag = GenericComponentViewTag()
private let undoButtonTag = GenericComponentViewTag()
private let redoButtonTag = GenericComponentViewTag()
private let clearAllButtonTag = GenericComponentViewTag()
private let topButtonsTag = GenericComponentViewTag()
private let colorButtonTag = GenericComponentViewTag()
private let addButtonTag = GenericComponentViewTag()
private let toolsTag = GenericComponentViewTag()
private let modeTag = GenericComponentViewTag()
private let flipButtonTag = GenericComponentViewTag()
private let fillButtonTag = GenericComponentViewTag()
private let zoomOutButtonTag = GenericComponentViewTag()
private let textSettingsTag = GenericComponentViewTag()
private let sizeSliderTag = GenericComponentViewTag()
private let fontTag = GenericComponentViewTag()
@ -489,6 +492,12 @@ private let colorTags = [color1Tag, color2Tag, color3Tag, color4Tag, color5Tag,
private let cancelButtonTag = GenericComponentViewTag()
private let doneButtonTag = GenericComponentViewTag()
enum DrawingMode: Int {
case drawing
case sticker
case text
}
private final class DrawingScreenComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -616,7 +625,6 @@ private final class DrawingScreenComponent: CombinedComponent {
final class State: ComponentState {
enum ImageKey: Hashable {
case undo
case redo
case done
case add
case fill
@ -633,8 +641,6 @@ private final class DrawingScreenComponent: CombinedComponent {
switch key {
case .undo:
image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Undo"), color: .white)!
case .redo:
image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Redo"), color: .white)!
case .done:
image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Done"), color: .white)!
case .add:
@ -652,13 +658,7 @@ private final class DrawingScreenComponent: CombinedComponent {
return image
}
}
enum Mode {
case drawing
case sticker
case text
}
private let context: AccountContext
private let updateToolState: ActionSlot<DrawingToolState>
private let insertEntity: ActionSlot<DrawingEntity>
@ -675,7 +675,7 @@ private final class DrawingScreenComponent: CombinedComponent {
private let entityViewForEntity: (DrawingEntity) -> DrawingEntityView?
private let present: (ViewController) -> Void
var currentMode: Mode
var currentMode: DrawingMode
var drawingState: DrawingState
var drawingViewState: DrawingView.NavigationState
var currentColor: DrawingColor
@ -992,7 +992,7 @@ private final class DrawingScreenComponent: CombinedComponent {
self.present(contextController)
}
func updateCurrentMode(_ mode: Mode, update: Bool = true) {
func updateCurrentMode(_ mode: DrawingMode, update: Bool = true) {
self.currentMode = mode
if let selectedEntity = self.selectedEntity {
if selectedEntity is DrawingStickerEntity || selectedEntity is DrawingTextEntity {
@ -1058,15 +1058,13 @@ private final class DrawingScreenComponent: CombinedComponent {
let topGradient = Child(BlurredGradientComponent.self)
let bottomGradient = Child(BlurredGradientComponent.self)
let undoButton = Child(Button.self)
let undoButton = Child(GlassBarButtonComponent.self)
let redoButton = Child(GlassBarButtonComponent.self)
let clearAllButton = Child(GlassBarButtonComponent.self)
let topButtons = Child(GlassControlPanelComponent.self)
let redoButton = Child(Button.self)
let clearAllButton = Child(Button.self)
let zoomOutButton = Child(Button.self)
let tools = Child(ToolsComponent.self)
let modeAndSize = Child(ModeAndSizeComponent.self)
let mode = Child(ModeComponent.self)
let colorButton = Child(ColorSwatchComponent.self)
@ -1082,16 +1080,11 @@ private final class DrawingScreenComponent: CombinedComponent {
let swatch8Button = Child(ColorSwatchComponent.self)
let addButton = Child(Button.self)
let flipButton = Child(Button.self)
let fillButton = Child(Button.self)
let backButton = Child(Button.self)
let doneButton = Child(Button.self)
let backButton = Child(GlassBarButtonComponent.self)
let doneButton = Child(GlassBarButtonComponent.self)
let textSize = Child(TextSizeSliderComponent.self)
let textCancelButton = Child(Button.self)
let textDoneButton = Child(Button.self)
let presetColors: [DrawingColor] = [
DrawingColor(rgb: 0xff453a),
@ -1220,6 +1213,8 @@ private final class DrawingScreenComponent: CombinedComponent {
var additionalBottomInset: CGFloat = 0.0
if component.sourceHint == .storyEditor {
additionalBottomInset = max(0.0, previewBottomInset - environment.safeInsets.bottom - 49.0)
} else {
additionalBottomInset = 8.0
}
if let textEntity = state.selectedEntity as? DrawingTextEntity {
@ -1318,8 +1313,8 @@ private final class DrawingScreenComponent: CombinedComponent {
)
}
let rightButtonPosition = rightEdge - 24.0
var offsetX: CGFloat = leftEdge + 24.0
let rightButtonPosition = rightEdge - 28.0
var offsetX: CGFloat = leftEdge + 28.0
let delta: CGFloat = (rightButtonPosition - offsetX) / 7.0
let applySwatchColor: (DrawingColor) -> Void = { [weak state] color in
@ -1608,6 +1603,7 @@ private final class DrawingScreenComponent: CombinedComponent {
}
var hasTopButtons = false
var centerItems: [GlassControlGroupComponent.Item] = []
if let entity = state.selectedEntity {
var isFilled: Bool?
if let entity = entity as? DrawingSimpleShapeEntity {
@ -1625,12 +1621,13 @@ private final class DrawingScreenComponent: CombinedComponent {
hasTopButtons = isFilled != nil || hasFlip
if let isFilled = isFilled {
let fillButton = fillButton.update(
component: Button(
content: AnyComponent(
Image(image: state.image(isFilled ? .fill : .stroke))
),
if controlsAreVisible {
if let isFilled = isFilled {
centerItems.append(GlassControlGroupComponent.Item(
id: AnyHashable("fill"),
content: .customIcon(id: AnyHashable("fill"), component: AnyComponent(
Image(image: state.image(isFilled ? .fill : .stroke), size: CGSize(width: 30.0, height: 30.0))
), insets: .zero),
action: { [weak state] in
guard let state = state else {
return
@ -1661,25 +1658,15 @@ private final class DrawingScreenComponent: CombinedComponent {
}
state.updated(transition: .easeInOut(duration: 0.2))
}
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(fillButtonTag),
availableSize: CGSize(width: 30.0, height: 30.0),
transition: .immediate
)
context.add(fillButton
.position(CGPoint(x: context.availableSize.width / 2.0 - (hasFlip ? 46.0 : 0.0), y: topInset))
.appear(.default(scale: true))
.disappear(.default(scale: true))
.opacity(!controlsAreVisible ? 0.0 : 1.0)
.shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil)
)
}
if hasFlip {
let flipButton = flipButton.update(
component: Button(
content: AnyComponent(
Image(image: state.image(.flip))
),
))
}
if hasFlip {
centerItems.append(GlassControlGroupComponent.Item(
id: AnyHashable("flip"),
content: .customIcon(id: AnyHashable("flip"), component: AnyComponent(
Image(image: state.image(.flip), size: CGSize(width: 30.0, height: 30.0))
), insets: .zero),
action: { [weak state] in
guard let state = state else {
return
@ -1695,17 +1682,8 @@ private final class DrawingScreenComponent: CombinedComponent {
}
state.updated(transition: .easeInOut(duration: 0.2))
}
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(flipButtonTag),
availableSize: CGSize(width: 30.0, height: 30.0),
transition: .immediate
)
context.add(flipButton
.position(CGPoint(x: context.availableSize.width / 2.0 + (isFilled != nil ? 46.0 : 0.0), y: topInset))
.appear(.default(scale: true))
.disappear(.default(scale: true))
.opacity(!controlsAreVisible ? 0.0 : 1.0)
.shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil)
)
))
}
}
}
@ -1730,28 +1708,24 @@ private final class DrawingScreenComponent: CombinedComponent {
sizeValue = entity.lineWidth
}
}
if state.drawingViewState.canZoomOut && !hasTopButtons {
let zoomOutButton = zoomOutButton.update(
component: Button(
content: AnyComponent(
ZoomOutButtonContent(
title: strings.Paint_ZoomOut,
image: state.image(.zoomOut)
)
),
action: {
dismissEyedropper.invoke(Void())
performAction.invoke(.zoomOut)
}
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(zoomOutButtonTag),
availableSize: CGSize(width: 120.0, height: 33.0),
transition: .immediate
)
context.add(zoomOutButton
.position(CGPoint(x: context.availableSize.width / 2.0, y: environment.safeInsets.top + 32.0 - UIScreenPixel))
.appear(.default(scale: true, alpha: true))
.disappear(.default(scale: true, alpha: true))
)
if state.drawingViewState.canZoomOut && !hasTopButtons && controlsAreVisible {
centerItems.append(GlassControlGroupComponent.Item(
id: AnyHashable("zoomOut"),
content: .customIcon(id: AnyHashable("zoomOut"), component: AnyComponent(
HStack([
AnyComponentWithIdentity(id: "icon", component: AnyComponent(
Image(image: state.image(.zoomOut), size: CGSize(width: 24.0, height: 24.0))
)),
AnyComponentWithIdentity(id: "label", component: AnyComponent(
Text(text: environment.strings.Paint_ZoomOut, font: Font.regular(17.0), color: .white)
)),
], spacing: 2.0)
), insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0)),
action: {
dismissEyedropper.invoke(Void())
performAction.invoke(.zoomOut)
}
))
}
}
if let sizeValue {
@ -1784,111 +1758,170 @@ private final class DrawingScreenComponent: CombinedComponent {
.position(CGPoint(x: textSize.size.width / 2.0, y: topInset + (context.availableSize.height - topInset - bottomInset) / 2.0))
.opacity(sizeSliderVisible && controlsAreVisible ? 1.0 : 0.0)
)
let undoButton = undoButton.update(
component: Button(
content: AnyComponent(
Image(image: state.image(.undo))
),
isEnabled: state.drawingViewState.canUndo,
action: {
var leftItems: [GlassControlGroupComponent.Item] = []
var rightItems: [GlassControlGroupComponent.Item] = []
if !isEditingText && controlsAreVisible {
leftItems.append(GlassControlGroupComponent.Item(
id: "undo",
content: .customIcon(id: "undo", component: AnyComponent(Image(image: state.image(.undo), tintColor: state.drawingViewState.canUndo ? .white : .white.withAlphaComponent(0.4), size: CGSize(width: 30.0, height: 30.0))), insets: .zero),
action: state.drawingViewState.canUndo ? {
dismissEyedropper.invoke(Void())
performAction.invoke(.undo)
}
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(undoButtonTag),
availableSize: CGSize(width: 24.0, height: 24.0),
} : nil
))
if state.drawingViewState.canRedo {
leftItems.append(GlassControlGroupComponent.Item(
id: "redo",
content: .customIcon(id: "redo", component: AnyComponent(Image(image: state.image(.undo), tintColor: state.drawingViewState.canRedo ? .white : .white.withAlphaComponent(0.4), size: CGSize(width: 30.0, height: 30.0), flipHorizontally: true)), insets: .zero),
action: state.drawingViewState.canRedo ? {
dismissEyedropper.invoke(Void())
performAction.invoke(.redo)
} : nil
))
}
}
let undoButton = undoButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: true,
state: .glass,
component: AnyComponentWithIdentity(
id: "icon",
component: AnyComponent(Image(image: state.image(.undo)))
),
action: { _ in
dismissEyedropper.invoke(Void())
performAction.invoke(.undo)
},
tag: undoButtonTag
),
availableSize: CGSize(width: 44.0, height: 44.0),
transition: context.transition
)
context.add(undoButton
.position(CGPoint(x: environment.safeInsets.left + undoButton.size.width / 2.0 + 2.0, y: topInset))
.position(CGPoint(x: environment.safeInsets.left + undoButton.size.width / 2.0 + 16.0, y: topInset))
.scale(isEditingText ? 0.01 : 1.0)
.opacity(isEditingText || !controlsAreVisible ? 0.0 : 1.0)
.shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil)
.opacity(0.0)
//.opacity(isEditingText || !controlsAreVisible ? 0.0 : 1.0)
)
let redoButton = redoButton.update(
component: Button(
content: AnyComponent(
Image(image: state.image(.redo))
component: GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: true,
state: .glass,
component: AnyComponentWithIdentity(
id: "icon",
component: AnyComponent(Image(image: state.image(.undo), flipHorizontally: true))
),
action: {
action: { _ in
dismissEyedropper.invoke(Void())
performAction.invoke(.redo)
}
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(redoButtonTag),
availableSize: CGSize(width: 24.0, height: 24.0),
},
tag: redoButtonTag
),
availableSize: CGSize(width: 44.0, height: 44.0),
transition: context.transition
)
context.add(redoButton
.position(CGPoint(x: environment.safeInsets.left + undoButton.size.width + 2.0 + redoButton.size.width / 2.0, y: topInset))
.scale(state.drawingViewState.canRedo && !isEditingText ? 1.0 : 0.01)
.opacity(state.drawingViewState.canRedo && !isEditingText && controlsAreVisible ? 1.0 : 0.0)
.shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil)
.opacity(0.0)
//.opacity(state.drawingViewState.canRedo && !isEditingText && controlsAreVisible ? 1.0 : 0.0)
)
let clearAllButton = clearAllButton.update(
component: Button(
content: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: strings.Paint_Clear, font: Font.regular(17.0), textColor: .white)),
textShadowColor: component.sourceHint == .storyEditor ? UIColor(rgb: 0x000000, alpha: 0.35) : nil,
textShadowBlur: 2.0
)
component: GlassBarButtonComponent(
size: nil,
backgroundColor: nil,
isDark: true,
state: .glass,
component: AnyComponentWithIdentity(
id: "label",
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: strings.Paint_Clear, font: Font.regular(17.0), textColor: .white))
))
),
isEnabled: state.drawingViewState.canClear,
action: {
action: { _ in
dismissEyedropper.invoke(Void())
performAction.invoke(.clear)
}
).tagged(clearAllButtonTag),
},
tag: clearAllButtonTag
),
availableSize: CGSize(width: 180.0, height: 30.0),
transition: context.transition
)
context.add(clearAllButton
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - clearAllButton.size.width / 2.0 - 13.0, y: topInset))
.scale(isEditingText ? 0.01 : 1.0)
.opacity(isEditingText || !controlsAreVisible ? 0.0 : 1.0)
.opacity(0.0)
//.opacity(isEditingText || !controlsAreVisible ? 0.0 : 1.0)
)
let textCancelButton = textCancelButton.update(
component: Button(
content: AnyComponent(
Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: .white)
),
if !isEditingText && controlsAreVisible {
rightItems.append(GlassControlGroupComponent.Item(
id: "clearAll",
content: .text(strings.Paint_Clear),
action: state.drawingViewState.canClear ? {
dismissEyedropper.invoke(Void())
performAction.invoke(.clear)
} : nil
))
}
if isEditingText {
leftItems.append(GlassControlGroupComponent.Item(
id: "cancel",
content: .text(strings.Common_Cancel),
action: { [weak state] in
if let entity = state?.selectedEntity as? DrawingTextEntity {
endEditingTextEntityView.invoke((entity.uuid, true))
}
}
),
availableSize: CGSize(width: 100.0, height: 30.0),
transition: context.transition
)
context.add(textCancelButton
.position(CGPoint(x: environment.safeInsets.left + textCancelButton.size.width / 2.0 + 13.0, y: topInset))
.scale(isEditingText ? 1.0 : 0.01)
.opacity(isEditingText ? 1.0 : 0.0)
)
let textDoneButton = textDoneButton.update(
component: Button(
content: AnyComponent(
Text(text: environment.strings.Common_Done, font: Font.semibold(17.0), color: .white)
),
))
rightItems.append(GlassControlGroupComponent.Item(
id: "done",
content: .text(strings.Common_Done),
action: { [weak state] in
if let entity = state?.selectedEntity as? DrawingTextEntity {
endEditingTextEntityView.invoke((entity.uuid, false))
}
}
))
}
let topButtons = topButtons.update(
component: GlassControlPanelComponent(
theme: defaultDarkPresentationTheme,
leftItem: leftItems.isEmpty ? nil : GlassControlPanelComponent.Item(
items: leftItems,
background: .panel
),
centralItem: centerItems.isEmpty ? nil : GlassControlPanelComponent.Item(
items: centerItems,
background: .panel
),
rightItem: rightItems.isEmpty ? nil : GlassControlPanelComponent.Item(
items: rightItems,
background: .panel
),
centerAlignmentIfPossible: true,
isDark: true,
tag: topButtonsTag
),
availableSize: CGSize(width: 100.0, height: 30.0),
availableSize: CGSize(width: context.availableSize.width - 32.0, height: 44.0),
transition: context.transition
)
context.add(textDoneButton
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - textDoneButton.size.width / 2.0 - 13.0, y: topInset))
.scale(isEditingText ? 1.0 : 0.01)
.opacity(isEditingText ? 1.0 : 0.0)
context.add(topButtons
.position(CGPoint(x: context.availableSize.width / 2.0, y: topInset))
//.opacity(isEditingText ? 1.0 : 0.0)
)
var color: DrawingColor?
@ -1930,7 +1963,7 @@ private final class DrawingScreenComponent: CombinedComponent {
transition: context.transition
)
context.add(colorButton
.position(CGPoint(x: leftEdge + colorButton.size.width / 2.0 + 2.0, y: context.availableSize.height - environment.safeInsets.bottom - colorButton.size.height / 2.0 - 89.0 - additionalBottomInset))
.position(CGPoint(x: leftEdge + colorButton.size.width / 2.0 + 6.0, y: context.availableSize.height - environment.safeInsets.bottom - colorButton.size.height / 2.0 - 89.0 - additionalBottomInset))
.appear(.default(scale: true))
.disappear(.default(scale: true))
.opacity(controlsAreVisible ? 1.0 : 0.0)
@ -1979,7 +2012,7 @@ private final class DrawingScreenComponent: CombinedComponent {
transition: .immediate
)
context.add(addButton
.position(CGPoint(x: rightEdge - addButton.size.width / 2.0 - 2.0, y: context.availableSize.height - environment.safeInsets.bottom - addButton.size.height / 2.0 - 89.0 - additionalBottomInset))
.position(CGPoint(x: rightEdge - addButton.size.width / 2.0 - 6.0, y: context.availableSize.height - environment.safeInsets.bottom - addButton.size.height / 2.0 - 89.0 - additionalBottomInset))
.appear(.default(scale: true))
.disappear(.default(scale: true))
.cornerRadius(12.0)
@ -1987,27 +2020,38 @@ private final class DrawingScreenComponent: CombinedComponent {
)
let doneButton = doneButton.update(
component: Button(
content: AnyComponent(
Image(image: state.image(.done))
component: GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: UIColor(rgb: 0x0088ff),
isDark: true,
state: .tintedGlass,
component: AnyComponentWithIdentity(
id: "icon",
component: AnyComponent(
BundleIconComponent(name: "Navigation/Done", tintColor: .white)
)
),
action: { [weak state] in
action: { [weak state] _ in
dismissEyedropper.invoke(Void())
state?.saveToolState()
apply.invoke(Void())
}
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(doneButtonTag),
availableSize: CGSize(width: 33.0, height: 33.0),
},
tag: doneButtonTag
),
availableSize: CGSize(width: 44.0, height: 44.0),
transition: .immediate
)
var doneButtonPosition = CGPoint(x: context.availableSize.width - environment.safeInsets.right - doneButton.size.width / 2.0 - 3.0, y: context.availableSize.height - environment.safeInsets.bottom - doneButton.size.height / 2.0 - 2.0 - UIScreenPixel)
var doneButtonPosition = CGPoint(x: context.availableSize.width - environment.safeInsets.right - doneButton.size.width / 2.0 - 14.0, y: context.availableSize.height - environment.safeInsets.bottom - doneButton.size.height / 2.0 - 2.0 - UIScreenPixel)
if component.sourceHint == .storyEditor {
doneButtonPosition.x = doneButtonPosition.x - 2.0
if case .regular = environment.metrics.widthClass {
doneButtonPosition.x -= 20.0
}
doneButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + doneButton.size.height / 2.0) + controlsBottomInset
doneButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + doneButton.size.height / 2.0) + controlsBottomInset + 5.0
} else {
doneButtonPosition.x = doneButtonPosition.x - 12.0
doneButtonPosition.y -= 3.0 - UIScreenPixel
}
context.add(doneButton
.position(doneButtonPosition)
@ -2026,117 +2070,82 @@ private final class DrawingScreenComponent: CombinedComponent {
})
.opacity(controlsAreVisible ? 1.0 : 0.0)
)
let selectedIndex: Int
switch state.currentMode {
case .drawing:
selectedIndex = 0
case .sticker:
selectedIndex = 1
case .text:
selectedIndex = 2
}
var selectedSize: CGFloat = 0.0
if let entity = state.selectedEntity {
selectedSize = entity.lineWidth
} else {
selectedSize = state.drawingState.toolState(for: state.drawingState.selectedTool).size ?? 0.0
}
let modeAndSize = modeAndSize.update(
component: ModeAndSizeComponent(
values: [ strings.Paint_Draw, strings.Paint_Sticker, strings.Paint_Text],
sizeValue: selectedSize,
isEditing: false,
isEnabled: true,
rightInset: modeRightInset - 57.0,
tag: modeTag,
selectedIndex: selectedIndex,
selectionChanged: { [weak state] index in
dismissEyedropper.invoke(Void())
guard let state = state else {
return
}
switch index {
case 1:
state.presentStickerPicker()
case 2:
state.addTextEntity()
default:
state.updateCurrentMode(.drawing)
}
},
sizeUpdated: { [weak state] size in
if let state = state {
let mode = mode.update(
component: ModeComponent(
isTablet: false,
strings: environment.strings,
tintColor: .white,
availableModes: [.drawing, .sticker, .text],
currentMode: state.currentMode,
updatedMode: { [weak state] mode in
if let state {
dismissEyedropper.invoke(Void())
state.updateBrushSize(size)
if state.selectedEntity == nil {
previewBrushSize.invoke(size)
switch mode {
case .drawing:
state.updateCurrentMode(.drawing)
case .sticker:
state.presentStickerPicker()
case .text:
state.addTextEntity()
}
}
},
sizeReleased: {
previewBrushSize.invoke(nil)
}
tag: modeTag
),
availableSize: CGSize(width: availableWidth - 57.0 - modeRightInset, height: context.availableSize.height),
availableSize: CGSize(width: context.availableSize.width - 66.0 * 2.0, height: 44.0),
transition: context.transition
)
var modeAndSizePosition = CGPoint(x: context.availableSize.width / 2.0 - (modeRightInset - 57.0) / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - modeAndSize.size.height / 2.0 - 9.0)
var modePosition = CGPoint(x: context.availableSize.width / 2.0 - (modeRightInset - 57.0) / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - mode.size.height / 2.0 - 9.0)
if component.sourceHint == .storyEditor {
modeAndSizePosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 8.0 + modeAndSize.size.height / 2.0) + controlsBottomInset
modePosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 8.0 + mode.size.height / 2.0) + controlsBottomInset
} else {
modePosition.y += 4.0
}
context.add(modeAndSize
.position(modeAndSizePosition)
context.add(mode
.position(modePosition)
.opacity(controlsAreVisible ? 1.0 : 0.0)
)
var animatingOut = false
if let appearanceTransition = context.transition.userData(DrawingScreenTransition.self), case .animateOut = appearanceTransition {
animatingOut = true
}
if animatingOut && component.sourceHint == .storyEditor {
} else {
let backButton = backButton.update(
component: Button(
content: AnyComponent(
LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: "media_backToCancel",
mode: .animating(loop: false),
range: animatingOut || component.isAvatar ? (0.5, 1.0) : (0.0, 0.5)
),
colors: ["__allcolors__": .white],
size: CGSize(width: 33.0, height: 33.0)
)
),
action: { [weak state] in
if let state = state {
dismissEyedropper.invoke(Void())
state.saveToolState()
dismiss.invoke(Void())
}
let backButton = backButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: true,
state: .glass,
component: AnyComponentWithIdentity(
id: "icon",
component: AnyComponent(
BundleIconComponent(name: "Navigation/Close", tintColor: .white)
)
),
action: { [weak state] _ in
if let state {
dismissEyedropper.invoke(Void())
state.saveToolState()
dismiss.invoke(Void())
}
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(cancelButtonTag),
availableSize: CGSize(width: 33.0, height: 33.0),
transition: .immediate
)
var backButtonPosition = CGPoint(x: environment.safeInsets.left + backButton.size.width / 2.0 + 3.0, y: context.availableSize.height - environment.safeInsets.bottom - backButton.size.height / 2.0 - 2.0 - UIScreenPixel)
if component.sourceHint == .storyEditor {
backButtonPosition.x = backButtonPosition.x + 2.0
if case .regular = environment.metrics.widthClass {
backButtonPosition.x += 20.0
}
backButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + backButton.size.height / 2.0) + controlsBottomInset
},
tag: cancelButtonTag
),
availableSize: CGSize(width: 44.0, height: 44.0),
transition: .immediate
)
var backButtonPosition = CGPoint(x: environment.safeInsets.left + backButton.size.width / 2.0 + 14.0, y: context.availableSize.height - environment.safeInsets.bottom - backButton.size.height / 2.0 - 2.0 - UIScreenPixel)
if component.sourceHint == .storyEditor {
backButtonPosition.x = backButtonPosition.x + 2.0
if case .regular = environment.metrics.widthClass {
backButtonPosition.x += 20.0
}
context.add(backButton
.position(backButtonPosition)
.opacity(controlsAreVisible ? 1.0 : 0.0)
)
backButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + backButton.size.height / 2.0) + controlsBottomInset + 5.0
} else {
backButtonPosition.x = backButtonPosition.x + 12.0
backButtonPosition.y -= 3.0 - UIScreenPixel
}
context.add(backButton
.position(backButtonPosition)
.opacity(controlsAreVisible ? 1.0 : 0.0)
)
return context.availableSize
}
@ -2224,22 +2233,24 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
self.performAction.connect { [weak self] action in
if let self {
if case .clear = action {
let actionSheet = ActionSheetController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme))
actionSheet.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: self.presentationData.strings.Paint_ClearConfirm, color: .destructive, action: { [weak actionSheet, weak self] in
actionSheet?.dismissAnimated()
self?._drawingView?.performAction(action)
})
]),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
self.controller?.present(actionSheet, in: .window(.root))
let sourceView: UIView
if let topButtonsView = self.componentHost.findTaggedView(tag: topButtonsTag) as? GlassControlPanelComponent.View, let rightItemView = topButtonsView.rightItemView, let clearAllView = rightItemView.itemView(id: AnyHashable("clearAll")) {
sourceView = clearAllView
} else {
sourceView = self.view
}
let items: [ContextMenuItem] = [
.action(ContextMenuActionItem(text: self.presentationData.strings.Paint_ClearConfirm, textColor: .destructive, icon: { _ in
return nil
}, action: { [weak self] f in
f.dismissWithResult(.default)
self?._drawingView?.performAction(.clear)
}))
]
let presentationData = self.presentationData.withUpdated(theme: defaultDarkPresentationTheme)
let contextController = makeContextController(presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView, contentArea: UIScreen.main.bounds, customPosition: CGPoint(), actionsPosition: .bottom)), items: .single(ContextController.Items(content: .list(items))))
self.controller?.present(contextController, in: .window(.root))
} else {
self._drawingView?.performAction(action)
}
@ -2403,22 +2414,18 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
self.dismiss.connect { [weak self] _ in
if let strongSelf = self {
if strongSelf.drawingView.canUndo || strongSelf.entitiesView.hasChanges {
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme))
actionSheet.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.PhotoEditor_DiscardChanges, color: .accent, action: { [weak actionSheet, weak self] in
actionSheet?.dismissAnimated()
self?.controller?.requestDismiss()
})
]),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])
])
strongSelf.controller?.present(actionSheet, in: .window(.root))
let sourceView = strongSelf.componentHost.findTaggedView(tag: cancelButtonTag) ?? strongSelf.view
let items: [ContextMenuItem] = [
.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PhotoEditor_DiscardChanges, textColor: .destructive, icon: { _ in
return nil
}, action: { [weak self] f in
f.dismissWithResult(.default)
self?.controller?.requestDismiss()
}))
]
let presentationData = strongSelf.presentationData.withUpdated(theme: defaultDarkPresentationTheme)
let contextController = makeContextController(presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView, contentArea: UIScreen.main.bounds, customPosition: CGPoint())), items: .single(ContextController.Items(content: .list(items))))
strongSelf.controller?.present(contextController, in: .window(.root))
} else {
strongSelf.controller?.requestDismiss()
}
@ -2497,17 +2504,24 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
if let view = self.componentHost.findTaggedView(tag: bottomGradientTag) {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
if let buttonView = self.componentHost.findTaggedView(tag: undoButtonTag) {
buttonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
buttonView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3)
if let topButtonsView = self.componentHost.findTaggedView(tag: topButtonsTag) as? GlassControlPanelComponent.View {
topButtonsView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
if let leftItemView = topButtonsView.leftItemView {
leftItemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
leftItemView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3)
}
if let rightItemView = topButtonsView.rightItemView {
rightItemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
rightItemView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3)
}
}
if let buttonView = self.componentHost.findTaggedView(tag: clearAllButtonTag) {
buttonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
buttonView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3)
if let view = self.componentHost.findTaggedView(tag: modeTag) {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
if let buttonView = self.componentHost.findTaggedView(tag: addButtonTag) {
buttonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
buttonView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3)
if let view = self.componentHost.findTaggedView(tag: addButtonTag) {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3)
}
var delay: Double = 0.0
for tag in colorTags {
@ -2520,6 +2534,15 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
if let view = self.componentHost.findTaggedView(tag: sizeSliderTag) {
view.layer.animatePosition(from: CGPoint(x: -33.0, y: 0.0), to: CGPoint(), duration: 0.3, additive: true)
}
if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3)
}
if let view = self.componentHost.findTaggedView(tag: doneButtonTag) {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3)
}
}
func animateOut(completion: @escaping () -> Void) {
@ -2536,43 +2559,24 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
}
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
}
if let buttonView = self.componentHost.findTaggedView(tag: undoButtonTag) {
buttonView.alpha = 0.0
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
}
if let buttonView = self.componentHost.findTaggedView(tag: redoButtonTag), buttonView.alpha > 0.0 {
buttonView.alpha = 0.0
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
}
if let buttonView = self.componentHost.findTaggedView(tag: clearAllButtonTag) {
buttonView.alpha = 0.0
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
if let topButtonsView = self.componentHost.findTaggedView(tag: topButtonsTag) as? GlassControlPanelComponent.View {
topButtonsView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
if let view = topButtonsView.leftItemView {
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3, removeOnCompletion: false)
}
if let view = topButtonsView.rightItemView {
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3, removeOnCompletion: false)
}
}
if let view = self.componentHost.findTaggedView(tag: colorButtonTag) as? ColorSwatchComponent.View {
view.animateOut()
}
if let buttonView = self.componentHost.findTaggedView(tag: addButtonTag) {
buttonView.alpha = 0.0
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
}
if let buttonView = self.componentHost.findTaggedView(tag: flipButtonTag) {
buttonView.alpha = 0.0
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
}
if let buttonView = self.componentHost.findTaggedView(tag: fillButtonTag) {
buttonView.alpha = 0.0
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
}
if let buttonView = self.componentHost.findTaggedView(tag: zoomOutButtonTag) {
buttonView.alpha = 0.0
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
if let view = self.componentHost.findTaggedView(tag: addButtonTag) {
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3, removeOnCompletion: false)
}
if let view = self.componentHost.findTaggedView(tag: sizeSliderTag) {
view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -33.0, y: 0.0), duration: 0.3, removeOnCompletion: false, additive: true)
@ -2596,12 +2600,18 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
})
}
if let view = self.componentHost.findTaggedView(tag: modeTag) as? ModeAndSizeComponent.View {
view.animateOut()
if let view = self.componentHost.findTaggedView(tag: modeTag) {
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
}
if let buttonView = self.componentHost.findTaggedView(tag: doneButtonTag) {
buttonView.alpha = 0.0
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) {
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3, removeOnCompletion: false)
}
if let view = self.componentHost.findTaggedView(tag: doneButtonTag) {
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
}
}

View file

@ -2,95 +2,123 @@ import Foundation
import UIKit
import Display
import ComponentFlow
import LegacyComponents
import TelegramCore
import SegmentedControlNode
import MultilineTextComponent
import TelegramPresentationData
import GlassBackgroundComponent
import LiquidLens
import TabSelectionRecognizer
private func generateMaskPath(size: CGSize, leftRadius: CGFloat, rightRadius: CGFloat) -> UIBezierPath {
let path = UIBezierPath()
path.addArc(withCenter: CGPoint(x: leftRadius, y: size.height / 2.0), radius: leftRadius, startAngle: .pi * 0.5, endAngle: -.pi * 0.5, clockwise: true)
path.addArc(withCenter: CGPoint(x: size.width - rightRadius, y: size.height / 2.0), radius: rightRadius, startAngle: -.pi * 0.5, endAngle: .pi * 0.5, clockwise: true)
path.close()
return path
private let buttonSize = CGSize(width: 55.0, height: 44.0)
private let tabletButtonSize = CGSize(width: 55.0, height: 44.0)
extension DrawingMode {
func title(strings: PresentationStrings) -> String {
switch self {
case .drawing:
return strings.Paint_Draw
case .sticker:
return strings.Paint_Sticker
case .text:
return strings.Paint_Text
}
}
}
private func generateKnobImage() -> UIImage? {
let side: CGFloat = 28.0
let margin: CGFloat = 10.0
let image = generateImage(CGSize(width: side + margin * 2.0, height: side + margin * 2.0), opaque: false, rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 9.0, color: UIColor(rgb: 0x000000, alpha: 0.3).cgColor)
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: margin, y: margin), size: CGSize(width: side, height: side)))
})
return image?.stretchableImage(withLeftCapWidth: Int(margin + side * 0.5), topCapHeight: Int(margin + side * 0.5))
}
final class ModeAndSizeComponent: Component {
let values: [String]
let sizeValue: CGFloat
let isEditing: Bool
let isEnabled: Bool
let rightInset: CGFloat
final class ModeComponent: Component {
let isTablet: Bool
let strings: PresentationStrings
let tintColor: UIColor
let availableModes: [DrawingMode]
let currentMode: DrawingMode
let updatedMode: (DrawingMode) -> Void
let tag: AnyObject?
let selectedIndex: Int
let selectionChanged: (Int) -> Void
let sizeUpdated: (CGFloat) -> Void
let sizeReleased: () -> Void
init(values: [String], sizeValue: CGFloat, isEditing: Bool, isEnabled: Bool, rightInset: CGFloat, tag: AnyObject?, selectedIndex: Int, selectionChanged: @escaping (Int) -> Void, sizeUpdated: @escaping (CGFloat) -> Void, sizeReleased: @escaping () -> Void) {
self.values = values
self.sizeValue = sizeValue
self.isEditing = isEditing
self.isEnabled = isEnabled
self.rightInset = rightInset
init(
isTablet: Bool,
strings: PresentationStrings,
tintColor: UIColor,
availableModes: [DrawingMode],
currentMode: DrawingMode,
updatedMode: @escaping (DrawingMode) -> Void,
tag: AnyObject?
) {
self.isTablet = isTablet
self.strings = strings
self.tintColor = tintColor
self.availableModes = availableModes
self.currentMode = currentMode
self.updatedMode = updatedMode
self.tag = tag
self.selectedIndex = selectedIndex
self.selectionChanged = selectionChanged
self.sizeUpdated = sizeUpdated
self.sizeReleased = sizeReleased
}
static func ==(lhs: ModeAndSizeComponent, rhs: ModeAndSizeComponent) -> Bool {
if lhs.values != rhs.values {
static func ==(lhs: ModeComponent, rhs: ModeComponent) -> Bool {
if lhs.isTablet != rhs.isTablet {
return false
}
if lhs.sizeValue != rhs.sizeValue {
if lhs.strings !== rhs.strings {
return false
}
if lhs.isEditing != rhs.isEditing {
if lhs.tintColor != rhs.tintColor {
return false
}
if lhs.isEnabled != rhs.isEnabled {
if lhs.availableModes != rhs.availableModes {
return false
}
if lhs.rightInset != rhs.rightInset {
return false
}
if lhs.selectedIndex != rhs.selectedIndex {
if lhs.currentMode != rhs.currentMode {
return false
}
return true
}
final class View: UIView, UIGestureRecognizerDelegate, ComponentTaggedView {
private let backgroundNode: NavigationBackgroundNode
private let node: SegmentedControlNode
final class View: UIView, ComponentTaggedView, UIScrollViewDelegate, UIGestureRecognizerDelegate {
private final class ScrollView: UIScrollView {
override func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
}
private var knob: UIImageView
private struct LayoutData {
var containerSize: CGSize
var selectedFrame: CGRect
var cornerRadius: CGFloat?
var isTablet: Bool
}
private let maskLayer = SimpleShapeLayer()
private var component: ModeComponent?
private var state: EmptyComponentState?
private var isEditing: Bool?
private var isControlEnabled: Bool?
private var sliderWidth: CGFloat = 0.0
final class ItemView: HighlightTrackingButton {
init() {
super.init(frame: .zero)
}
required init(coder: NSCoder) {
preconditionFailure()
}
func update(isTablet: Bool, value: String, selected: Bool, tintColor: UIColor) -> CGSize {
let title = NSMutableAttributedString(string: value, font: Font.with(size: 15.0, design: .regular, weight: .medium), textColor: UIColor(rgb: 0xffffff), paragraphAlignment: .center)
self.setAttributedTitle(title, for: .normal)
self.sizeToFit()
return CGSize(width: self.titleLabel?.bounds.size.width ?? 0.0, height: buttonSize.height)
}
}
fileprivate var updated: (CGFloat) -> Void = { _ in }
fileprivate var released: () -> Void = { }
private var backgroundView = UIView()
private var backgroundContainer = GlassBackgroundContainerView()
private var liquidLensView: LiquidLensView?
private let scrollView = ScrollView()
private let selectedScrollView = UIView()
private var ignoreScrolling = false
private var layoutData: LayoutData?
private var itemViews: [AnyHashable: ItemView] = [:]
private var selectedItemViews: [AnyHashable: ItemView] = [:]
private var tabSelectionRecognizer: TabSelectionRecognizer?
private var selectionGestureState: (startX: CGFloat, currentX: CGFloat, itemId: AnyHashable)?
private var component: ModeAndSizeComponent?
public func matches(tag: Any) -> Bool {
if let component = self.component, let componentTag = component.tag {
let tag = tag as AnyObject
@ -102,164 +130,350 @@ final class ModeAndSizeComponent: Component {
}
init() {
self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x888888, alpha: 0.3))
self.node = SegmentedControlNode(theme: SegmentedControlTheme(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shadowColor: .black, textColor: UIColor(rgb: 0xffffff), dividerColor: UIColor(rgb: 0x505155, alpha: 0.6)), items: [], selectedIndex: 0, cornerRadius: 16.0)
self.knob = UIImageView(image: generateKnobImage())
super.init(frame: CGRect())
self.backgroundView.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.09)
self.backgroundView.layer.cornerRadius = 22.0
self.layer.allowsGroupOpacity = true
self.addSubview(self.backgroundNode.view)
self.addSubview(self.node.view)
self.addSubview(self.knob)
self.backgroundNode.layer.mask = self.maskLayer
self.scrollView.delaysContentTouches = false
self.scrollView.canCancelContentTouches = true
self.scrollView.contentInsetAdjustmentBehavior = .never
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.alwaysBounceVertical = false
self.scrollView.scrollsToTop = false
self.scrollView.clipsToBounds = true
self.scrollView.delegate = self
self.scrollView.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in
guard let self else {
return false
}
return self.scrollView.contentOffset.x > .ulpOfOne
}
let pressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePress(_:)))
pressGestureRecognizer.minimumPressDuration = 0.01
pressGestureRecognizer.delegate = self
self.addGestureRecognizer(pressGestureRecognizer)
self.selectedScrollView.clipsToBounds = true
self.selectedScrollView.isUserInteractionEnabled = false
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
panGestureRecognizer.delegate = self
self.addGestureRecognizer(panGestureRecognizer)
self.addSubview(self.backgroundView)
self.backgroundView.addSubview(self.backgroundContainer)
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
@objc func handlePress(_ gestureRecognizer: UILongPressGestureRecognizer) {
let location = gestureRecognizer.location(in: self).offsetBy(dx: -12.0, dy: 0.0)
guard self.frame.width > 0.0, case .began = gestureRecognizer.state else {
return
}
let value = max(0.0, min(1.0, location.x / (self.frame.width - 24.0)))
self.updated(value)
private var animatedOut = false
func animateOutToEditor(transition: ComponentTransition) {
self.animatedOut = true
transition.setAlpha(view: self.backgroundView, alpha: 0.0)
transition.setSublayerTransform(view: self, transform: CATransform3DMakeTranslation(0.0, -buttonSize.height, 0.0))
}
@objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
switch gestureRecognizer.state {
case .changed:
let location = gestureRecognizer.location(in: self).offsetBy(dx: -12.0, dy: 0.0)
guard self.frame.width > 0.0 else {
return
func animateInFromEditor(transition: ComponentTransition) {
self.animatedOut = false
transition.setAlpha(view: self.backgroundView, alpha: 1.0)
transition.setSublayerTransform(view: self, transform: CATransform3DIdentity)
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return self.backgroundView.frame.contains(point)
}
private func item(at point: CGPoint, in view: UIView) -> AnyHashable? {
var closestItem: (AnyHashable, CGFloat)?
for (id, itemView) in self.itemViews {
let itemFrame = itemView.convert(itemView.bounds, to: view)
if itemFrame.contains(point) {
return id
} else {
let distance = abs(point.x - itemFrame.midX)
if let closestItemValue = closestItem {
if closestItemValue.1 > distance {
closestItem = (id, distance)
}
} else {
closestItem = (id, distance)
}
}
}
return closestItem?.0
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if self.ignoreScrolling {
return
}
self.updateScrolling(transition: .immediate)
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer === self.tabSelectionRecognizer && otherGestureRecognizer === self.scrollView.panGestureRecognizer {
return true
}
if otherGestureRecognizer === self.tabSelectionRecognizer && gestureRecognizer === self.scrollView.panGestureRecognizer {
return true
}
return false
}
@objc private func onTabSelectionGesture(_ recognizer: TabSelectionRecognizer) {
guard let component = self.component else {
return
}
let location = recognizer.location(in: self)
switch recognizer.state {
case .began:
if let itemId = self.item(at: location, in: self), let itemView = self.itemViews[itemId] {
let startX = itemView.frame.minX - 4.0
self.selectionGestureState = (startX, startX, itemId)
self.state?.updated(transition: .spring(duration: 0.4), isLocal: true)
}
case .changed:
if var selectionGestureState = self.selectionGestureState {
let translation = recognizer.translation(in: self)
if !component.isTablet && self.scrollView.isScrollEnabled && abs(translation.x) > 6.0 && abs(translation.x) > abs(translation.y) {
self.selectionGestureState = nil
recognizer.state = .cancelled
self.state?.updated(transition: .spring(duration: 0.4), isLocal: true)
return
}
selectionGestureState.currentX = selectionGestureState.startX + recognizer.translation(in: self).x
if let itemId = self.item(at: location, in: self) {
selectionGestureState.itemId = itemId
}
self.selectionGestureState = selectionGestureState
self.state?.updated(transition: .immediate, isLocal: true)
}
let value = max(0.0, min(1.0, location.x / (self.frame.width - 24.0)))
self.updated(value)
case .ended, .cancelled:
self.released()
if let selectionGestureState = self.selectionGestureState {
self.selectionGestureState = nil
if case .ended = recognizer.state {
guard let item = component.availableModes.first(where: { AnyHashable($0.rawValue) == selectionGestureState.itemId }) else {
return
}
component.updatedMode(item)
}
self.state?.updated(transition: .spring(duration: 0.4), isLocal: true)
}
default:
break
}
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let isEditing = self.isEditing, let isControlEnabled = self.isControlEnabled {
return isEditing && isControlEnabled
private func updateScrolling(transition: ComponentTransition) {
guard let component = self.component, let liquidLensView = self.liquidLensView, let layoutData = self.layoutData else {
return
}
let contentOffsetX = layoutData.isTablet ? 0.0 : self.scrollView.bounds.minX
var lensSelection = (origin: layoutData.selectedFrame.origin, size: layoutData.selectedFrame.size)
if let selectionGestureState = self.selectionGestureState, !layoutData.isTablet {
lensSelection.origin = CGPoint(x: selectionGestureState.currentX, y: 0.0)
}
if layoutData.isTablet {
lensSelection.size.width = layoutData.containerSize.width
} else {
return false
lensSelection.origin.x -= contentOffsetX
lensSelection.origin.y = 0.0
lensSelection.size.height = layoutData.containerSize.height
}
let maxSelectionOriginX = max(0.0, layoutData.containerSize.width - lensSelection.size.width)
transition.setFrame(view: self.selectedScrollView, frame: CGRect(origin: .zero, size: layoutData.containerSize))
transition.setBounds(view: self.selectedScrollView, bounds: CGRect(origin: CGPoint(x: contentOffsetX, y: 0.0), size: layoutData.containerSize))
liquidLensView.update(size: layoutData.containerSize, cornerRadius: layoutData.cornerRadius, selectionOrigin: CGPoint(x: max(0.0, min(lensSelection.origin.x, maxSelectionOriginX)), y: lensSelection.origin.y), selectionSize: lensSelection.size, inset: 3.0, isDark: true, isLifted: self.selectionGestureState != nil && !layoutData.isTablet, isCollapsed: false, transition: transition)
self.backgroundContainer.update(size: layoutData.containerSize, isDark: true, transition: .immediate)
self.scrollView.isScrollEnabled = !component.isTablet && self.scrollView.contentSize.width > self.scrollView.bounds.width + .ulpOfOne
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func animateIn() {
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
func animateOut() {
self.node.alpha = 0.0
self.node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
self.backgroundNode.alpha = 0.0
self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
}
func update(component: ModeAndSizeComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.component = component
self.updated = component.sizeUpdated
self.released = component.sizeReleased
let previousIsEditing = self.isEditing
self.isEditing = component.isEditing
self.isControlEnabled = component.isEnabled
if component.isEditing {
self.sliderWidth = availableSize.width
}
self.node.items = component.values.map { SegmentedControlItem(title: $0) }
self.node.setSelectedIndex(component.selectedIndex, animated: !transition.animation.isImmediate)
let selectionChanged = component.selectionChanged
self.node.selectedIndexChanged = { [weak self] index in
self?.window?.endEditing(true)
selectionChanged(index)
}
let nodeSize = self.node.updateLayout(.stretchToFill(width: availableSize.width + component.rightInset), transition: transition.containedViewLayoutTransition)
let size = CGSize(width: availableSize.width, height: nodeSize.height)
transition.setFrame(view: self.node.view, frame: CGRect(origin: CGPoint(), size: nodeSize))
var isDismissingEditing = false
if component.isEditing != previousIsEditing && !component.isEditing {
isDismissingEditing = true
}
self.knob.alpha = component.isEditing ? 1.0 : 0.0
if !isDismissingEditing {
self.knob.frame = CGRect(origin: CGPoint(x: -12.0 + floorToScreenPixels((self.sliderWidth + 24.0 - self.knob.frame.size.width) * component.sizeValue), y: floorToScreenPixels((size.height - self.knob.frame.size.height) / 2.0)), size: self.knob.frame.size)
}
if component.isEditing != previousIsEditing {
let containedTransition = transition.containedViewLayoutTransition
let maskPath: UIBezierPath
if component.isEditing {
maskPath = generateMaskPath(size: size, leftRadius: 2.0, rightRadius: 11.5)
let selectionFrame = self.node.animateSelection(to: self.knob.center, transition: containedTransition)
containedTransition.animateFrame(layer: self.knob.layer, from: selectionFrame.insetBy(dx: -9.0, dy: -9.0))
self.knob.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
func update(component: ModeComponent, availableSize: CGSize, state: EmptyComponentState, transition: ComponentTransition) -> CGSize {
let previousComponent = self.component
self.component = component
self.state = state
let isTablet = component.isTablet
let liquidLensView: LiquidLensView
if let current = self.liquidLensView {
liquidLensView = current
} else {
liquidLensView = LiquidLensView(kind: isTablet ? .noContainer : .externalContainer)
self.liquidLensView = liquidLensView
self.backgroundContainer.contentView.addSubview(liquidLensView)
liquidLensView.contentView.addSubview(self.scrollView)
liquidLensView.selectedContentView.addSubview(self.selectedScrollView)
let tabSelectionRecognizer = TabSelectionRecognizer(target: self, action: #selector(self.onTabSelectionGesture(_:)))
tabSelectionRecognizer.delegate = self
tabSelectionRecognizer.cancelsTouchesInView = false
self.tabSelectionRecognizer = tabSelectionRecognizer
liquidLensView.addGestureRecognizer(tabSelectionRecognizer)
}
if self.scrollView.superview == nil {
liquidLensView.contentView.addSubview(self.scrollView)
}
if self.selectedScrollView.superview == nil {
liquidLensView.selectedContentView.addSubview(self.selectedScrollView)
}
self.backgroundView.backgroundColor = component.isTablet ? .clear : UIColor(rgb: 0xffffff, alpha: 0.11)
var inset: CGFloat = 23.0
let spacing: CGFloat
if isTablet {
spacing = 9.0
} else {
if availableSize.width < 200.0 {
inset = 20.0
spacing = 24.0
} else {
maskPath = generateMaskPath(size: size, leftRadius: 16.0, rightRadius: 16.0)
if previousIsEditing != nil {
let selectionFrame = self.node.animateSelection(from: self.knob.center, transition: containedTransition)
containedTransition.animateFrame(layer: self.knob.layer, from: self.knob.frame, to: selectionFrame.insetBy(dx: -9.0, dy: -9.0))
self.knob.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
spacing = 30.0
}
}
var i = 0
var itemFrame = CGRect(origin: isTablet ? .zero : CGPoint(x: inset, y: 0.0), size: buttonSize)
var selectedFrame = itemFrame
var validKeys: Set<AnyHashable> = Set()
for mode in component.availableModes {
let id = mode.rawValue
validKeys.insert(id)
let itemView: ItemView
let selectedItemView: ItemView
if let current = self.itemViews[id], let currentSelected = self.selectedItemViews[id] {
itemView = current
selectedItemView = currentSelected
} else {
itemView = ItemView()
itemView.isUserInteractionEnabled = false
self.itemViews[id] = itemView
selectedItemView = ItemView()
selectedItemView.isUserInteractionEnabled = false
self.selectedItemViews[id] = selectedItemView
}
if itemView.superview !== self.scrollView {
self.scrollView.addSubview(itemView)
}
if selectedItemView.superview !== self.selectedScrollView {
self.selectedScrollView.addSubview(selectedItemView)
}
let itemSize = itemView.update(isTablet: component.isTablet, value: mode.title(strings: component.strings), selected: false, tintColor: component.tintColor)
itemView.bounds = CGRect(origin: .zero, size: itemSize)
let _ = selectedItemView.update(isTablet: component.isTablet, value: mode.title(strings: component.strings), selected: true, tintColor: component.tintColor)
selectedItemView.bounds = CGRect(origin: .zero, size: itemSize)
itemFrame = CGRect(origin: itemFrame.origin, size: itemSize)
if mode == component.currentMode {
selectedFrame = itemFrame
}
if isTablet {
itemView.center = CGPoint(x: availableSize.width / 2.0, y: itemFrame.midY)
selectedItemView.center = itemView.center
itemFrame = itemFrame.offsetBy(dx: 0.0, dy: tabletButtonSize.height + spacing)
} else {
itemView.center = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
selectedItemView.center = itemView.center
itemFrame = itemFrame.offsetBy(dx: itemFrame.width + spacing, dy: 0.0)
}
i += 1
}
var removeKeys: [AnyHashable] = []
for (id, itemView) in self.itemViews {
if !validKeys.contains(id) {
removeKeys.append(id)
transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in
itemView.removeFromSuperview()
})
if let selectedItemView = self.selectedItemViews[id] {
transition.setAlpha(view: selectedItemView, alpha: 0.0, completion: { _ in
selectedItemView.removeFromSuperview()
})
}
}
transition.setShapeLayerPath(layer: self.maskLayer, path: maskPath.cgPath)
}
transition.setFrame(layer: self.maskLayer, frame: CGRect(origin: .zero, size: nodeSize))
for id in removeKeys {
self.itemViews.removeValue(forKey: id)
self.selectedItemViews.removeValue(forKey: id)
}
transition.setFrame(view: self.backgroundNode.view, frame: CGRect(origin: CGPoint(), size: size))
self.backgroundNode.update(size: size, transition: transition.containedViewLayoutTransition)
let totalSize: CGSize
let size: CGSize
let contentSize: CGSize
var cornerRadius: CGFloat?
if isTablet {
totalSize = CGSize(width: availableSize.width, height: tabletButtonSize.height * CGFloat(component.availableModes.count) + spacing * CGFloat(component.availableModes.count - 1))
size = CGSize(width: availableSize.width, height: availableSize.height)
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: totalSize))
contentSize = totalSize
cornerRadius = 20.0
} else {
size = CGSize(width: availableSize.width, height: buttonSize.height)
totalSize = CGSize(width: itemFrame.minX - spacing + inset, height: buttonSize.height)
let visibleSize = CGSize(width: min(availableSize.width, totalSize.width), height: totalSize.height)
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - visibleSize.width) / 2.0), y: 0.0), size: visibleSize))
contentSize = totalSize
}
if let screenTransition = transition.userData(DrawingScreenTransition.self) {
switch screenTransition {
case .animateIn:
self.animateIn()
case .animateOut:
self.animateOut()
let containerFrame = CGRect(origin: .zero, size: self.backgroundView.frame.size)
transition.setFrame(view: self.backgroundContainer, frame: containerFrame)
transition.setFrame(view: liquidLensView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: containerFrame.size))
let scrollViewFrame = CGRect(origin: .zero, size: containerFrame.size)
transition.setFrame(view: self.scrollView, frame: scrollViewFrame)
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
self.scrollView.isScrollEnabled = !isTablet && contentSize.width > scrollViewFrame.width + .ulpOfOne
self.layoutData = LayoutData(containerSize: containerFrame.size, selectedFrame: selectedFrame.insetBy(dx: -inset, dy: 3.0), cornerRadius: cornerRadius, isTablet: isTablet)
self.ignoreScrolling = true
var scrollViewBounds = CGRect(origin: self.scrollView.bounds.origin, size: scrollViewFrame.size)
let maxContentOffsetX = max(0.0, contentSize.width - scrollViewFrame.width)
let shouldFocusOnSelectedItem = previousComponent?.currentMode != component.currentMode || previousComponent?.availableModes != component.availableModes || self.scrollView.bounds.size != scrollViewFrame.size
if self.scrollView.isScrollEnabled && shouldFocusOnSelectedItem {
let scrollLookahead = min(60.0, scrollViewBounds.width * 0.25)
if scrollViewBounds.minX + scrollViewBounds.width - scrollLookahead < selectedFrame.maxX {
scrollViewBounds.origin.x = selectedFrame.maxX - scrollViewBounds.width + scrollLookahead
}
if scrollViewBounds.minX > selectedFrame.minX - scrollLookahead {
scrollViewBounds.origin.x = selectedFrame.minX - scrollLookahead
}
}
scrollViewBounds.origin.x = max(0.0, min(scrollViewBounds.origin.x, maxContentOffsetX))
transition.setBounds(view: self.scrollView, bounds: scrollViewBounds)
self.ignoreScrolling = false
self.updateScrolling(transition: transition)
return size
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
return view.update(component: self, availableSize: availableSize, state: state, transition: transition)
}
}

View file

@ -675,6 +675,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
super.didLoad()
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
self.scrollNode.view.showsVerticalScrollIndicator = false
self.scrollNode.view.scrollsToTop = false
let backwardLongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.seekBackwardLongPress(_:)))
backwardLongPressGestureRecognizer.minimumPressDuration = 0.3
@ -1356,7 +1357,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
speed: settingsButtonState.speed,
quality: settingsButtonState.quality,
isOpen: false
))),
)), insets: .zero),
action: { [weak self] in
guard let self, let buttonPanelView = self.buttonPanel.view as? GlassControlPanelComponent.View else {
return
@ -1819,12 +1820,12 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
if availableOpenInOptions(context: strongSelf.context, item: item).count > 1 {
preferredAction = .custom(action: ShareControllerAction(title: presentationData.strings.Conversation_FileOpenIn, action: { [weak self] in
if let strongSelf = self {
let openInController = OpenInActionSheetController(context: strongSelf.context, forceTheme: defaultDarkColorPresentationTheme, item: item, additionalAction: nil, openUrl: { [weak self] url in
let openInController = OpenInOptionsScreen(context: strongSelf.context, forceTheme: defaultDarkColorPresentationTheme, item: item, additionalAction: nil, openUrl: { [weak self] url in
if let strongSelf = self {
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
}
})
strongSelf.controllerInteraction?.presentController(openInController, nil)
strongSelf.controllerInteraction?.pushController(openInController)
}
}))
} else {
@ -2124,12 +2125,12 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
if availableOpenInOptions(context: self.context, item: item).count > 1 {
preferredAction = .custom(action: ShareControllerAction(title: presentationData.strings.Conversation_FileOpenIn, action: { [weak self] in
if let strongSelf = self {
let openInController = OpenInActionSheetController(context: strongSelf.context, forceTheme: forceTheme, item: item, additionalAction: nil, openUrl: { [weak self] url in
let openInController = OpenInOptionsScreen(context: strongSelf.context, forceTheme: forceTheme, item: item, additionalAction: nil, openUrl: { [weak self] url in
if let strongSelf = self {
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
}
})
strongSelf.controllerInteraction?.presentController(openInController, nil)
strongSelf.controllerInteraction?.pushController(openInController)
}
}))
} else {

View file

@ -83,6 +83,7 @@ public final class GalleryThumbnailContainerNode: ASDisplayNode, ASScrollViewDel
self.scrollNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.showsVerticalScrollIndicator = false
self.scrollNode.view.scrollsToTop = false
self.addSubnode(self.scrollNode)
}

View file

@ -419,6 +419,9 @@ private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentN
adView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
adView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 64.0), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { _ in
adView.removeFromSuperview()
if self.adView.view === adView {
self.adView = ComponentView<Empty>()
}
Queue.mainQueue().after(0.1) {
adView.layer.removeAllAnimations()
}
@ -444,7 +447,7 @@ private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentN
return result
}
}
if let adView = self.adView.view, adView.frame.contains(point) {
if let adView = self.adView.view, adView.superview === self.view, !self.isAnimatingOut, adView.frame.contains(point) {
return super.hitTest(point, with: event)
}
return nil
@ -3892,12 +3895,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
if !presentationData.theme.overallDarkAppearance {
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
}
let actionSheet = OpenInActionSheetController(context: strongSelf.context, forceTheme: presentationData.theme, item: item, openUrl: { [weak self] url in
let actionSheet = OpenInOptionsScreen(context: strongSelf.context, forceTheme: presentationData.theme, item: item, openUrl: { [weak self] url in
if let strongSelf = self {
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: strongSelf.baseNavigationController(), dismissInput: {})
}
})
controller.present(actionSheet, in: .window(.root))
controller.push(actionSheet)
}
})))
break

View file

@ -110,6 +110,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, ASScroll
self.wrappingScrollNode.view.alwaysBounceVertical = true
self.wrappingScrollNode.view.delaysContentTouches = false
self.wrappingScrollNode.view.canCancelContentTouches = true
self.wrappingScrollNode.view.scrollsToTop = false
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)

View file

@ -140,6 +140,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
self.addSubnode(self.navigationBar)
self.scrollNode.view.delaysContentTouches = false
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
self.scrollNode.view.scrollsToTop = false
self.navigationBar.back = navigateBack
self.navigationBar.share = { [weak self] in
@ -1789,12 +1790,12 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
private func openUrlIn(_ url: InstantPageUrlItem) {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = OpenInActionSheetController(context: self.context, item: .url(url: url.url), openUrl: { [weak self] url in
let actionSheet = OpenInOptionsScreen(context: self.context, item: .url(url: url.url), openUrl: { [weak self] url in
if let strongSelf = self, let navigationController = strongSelf.getNavigationController() {
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
}
})
self.present(actionSheet, nil)
self.pushController(actionSheet)
}
private func mediasFromItems(_ items: [InstantPageItem]) -> [InstantPageMedia] {

View file

@ -52,6 +52,7 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, ASScrollVie
self.wrappingScrollNode.view.alwaysBounceVertical = true
self.wrappingScrollNode.view.delaysContentTouches = false
self.wrappingScrollNode.view.canCancelContentTouches = true
self.wrappingScrollNode.view.scrollsToTop = false
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)

View file

@ -410,14 +410,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
let leftInset: CGFloat = 65.0 + params.leftInset
let verticalInset: CGFloat
switch item.systemStyle {
case .glass:
verticalInset = 13.0
case .legacy:
verticalInset = 11.0
}
let verticalInset: CGFloat = 11.0
let titleSpacing: CGFloat = 2.0
let separatorHeight = UIScreenPixel

View file

@ -35,6 +35,7 @@ swift_library(
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/TelegramUI/Components/ListItemComponentAdaptor",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/TelegramUI/Components/GlassControls",
"//submodules/TelegramUI/Components/HorizontalTabsComponent",
],
visibility = [

View file

@ -689,6 +689,16 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable
}
}
public func itemNode(forTag tag: ItemListItemTag) -> ListViewItemNode? {
var result: ListViewItemNode?
self.forEachItemNode { itemNode in
if result == nil, let taggedItemNode = itemNode as? ItemListItemNode, let itemTag = taggedItemNode.tag, itemTag.isEqual(to: tag) {
result = itemNode
}
}
return result
}
public func ensureItemNodeVisible(_ itemNode: ListViewItemNode, animated: Bool = true, overflow: CGFloat = 0.0, atTop: Bool = false, curve: ListViewAnimationCurve = .Default(duration: 0.25)) {
self.controllerNode.listNode.ensureItemNodeVisible(itemNode, animated: animated, overflow: overflow, atTop: atTop, curve: curve)
}

View file

@ -6,6 +6,8 @@ import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import MergeLists
import ComponentFlow
import GlassControls
public protocol ItemListHeaderItemNode: AnyObject {
func updateTheme(theme: PresentationTheme)
@ -255,7 +257,7 @@ open class ItemListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
private var emptyStateItem: ItemListControllerEmptyStateItem?
private var emptyStateNode: ItemListControllerEmptyStateItemNode?
private var toolbarNode: ToolbarNode?
private var toolbar: ComponentView<Empty>?
private var searchItem: ItemListControllerSearch?
private var searchNode: ItemListControllerSearchNode?
@ -654,7 +656,7 @@ open class ItemListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
insets.bottom = max(insets.bottom, additionalInsets.bottom)
let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0))
if layout.size.width >= 375.0 {
if layout.size.width >= 320.0 {
insets.left += inset
insets.right += inset
}
@ -666,60 +668,165 @@ open class ItemListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
self.listNodeContainer.insertSubnode(self.leftOverlayNode, aboveSubnode: self.listNode)
}
if let toolbarItem = self.toolbarItem {
var tabBarHeight: CGFloat
let bottomInset: CGFloat = insets.bottom
if !layout.safeInsets.left.isZero {
tabBarHeight = 34.0 + bottomInset
insets.bottom += 34.0
if let toolbarData = self.toolbarItem, let theme = self.theme {
var panelsBottomInset: CGFloat = layout.insets(options: []).bottom
if layout.metrics.widthClass == .regular, let inputHeight = layout.inputHeight, inputHeight != 0.0 {
panelsBottomInset = inputHeight + 8.0
}
if panelsBottomInset == 0.0 {
panelsBottomInset = 8.0
} else {
tabBarHeight = 49.0 + bottomInset
insets.bottom += 49.0
panelsBottomInset = max(panelsBottomInset, 8.0)
}
let toolbarFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - tabBarHeight), size: CGSize(width: layout.size.width, height: tabBarHeight))
let sideInset: CGFloat = 20.0
let toolbarHeight = 44.0
let toolbarFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - panelsBottomInset - toolbarHeight), size: CGSize(width: layout.size.width - sideInset * 2.0, height: toolbarHeight))
if let toolbarNode = self.toolbarNode {
transition.updateFrame(node: toolbarNode, frame: toolbarFrame)
toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: layout.intrinsicInsets.bottom, toolbar: toolbarItem.toolbar, transition: transition)
} else if let theme = self.theme {
let toolbarNode = ToolbarNode(theme: ToolbarTheme(rootControllerTheme: theme), displaySeparator: true)
toolbarNode.frame = toolbarFrame
toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: layout.intrinsicInsets.bottom, toolbar: toolbarItem.toolbar, transition: .immediate)
self.addSubnode(toolbarNode)
self.toolbarNode = toolbarNode
if case let .animated(duration, curve) = transition {
toolbarNode.layer.animatePosition(from: CGPoint(x: 0.0, y: toolbarFrame.height), to: CGPoint(), duration: duration, mediaTimingFunction: curve.mediaTimingFunction, additive: true)
let toolbar: ComponentView<Empty>
var toolbarTransition = ComponentTransition(transition)
if let current = self.toolbar {
toolbar = current
} else {
toolbar = ComponentView()
self.toolbar = toolbar
toolbarTransition = .immediate
}
let _ = toolbar.update(
transition: toolbarTransition,
component: AnyComponent(GlassControlPanelComponent(
theme: theme,
leftItem: toolbarData.toolbar.leftAction.flatMap { value in
return GlassControlPanelComponent.Item(
items: [GlassControlGroupComponent.Item(
id: "left_" + value.title,
content: .text(value.title),
action: value.isEnabled ? { [weak self] in
guard let self, let toolbarData = self.toolbarItem else {
return
}
toolbarData.actions[0].action()
} : nil
)],
background: .panel
)
},
centralItem: toolbarData.toolbar.middleAction.flatMap { value in
return GlassControlPanelComponent.Item(
items: [GlassControlGroupComponent.Item(
id: "right_" + value.title,
content: .text(value.title),
action: value.isEnabled ? { [weak self] in
guard let self, let toolbarData = self.toolbarItem else {
return
}
if toolbarData.actions.count == 1 {
toolbarData.actions[0].action()
} else if toolbarData.actions.count == 3 {
toolbarData.actions[1].action()
}
} : nil
)],
background: .panel
)
},
rightItem: toolbarData.toolbar.rightAction.flatMap { value in
return GlassControlPanelComponent.Item(
items: [GlassControlGroupComponent.Item(
id: "right_" + value.title,
content: .text(value.title),
action: value.isEnabled ? { [weak self] in
guard let self, let toolbarData = self.toolbarItem else {
return
}
if toolbarData.actions.count == 2 {
toolbarData.actions[1].action()
} else if toolbarData.actions.count == 3 {
toolbarData.actions[2].action()
}
} : nil
)],
background: .panel
)
},
centerAlignmentIfPossible: true
)),
environment: {},
containerSize: toolbarFrame.size
)
if let toolbarView = toolbar.view {
if toolbarView.superview == nil {
self.view.addSubview(toolbarView)
toolbarView.alpha = 0.0
}
toolbarTransition.setFrame(view: toolbarView, frame: toolbarFrame)
ComponentTransition(transition).setAlpha(view: toolbarView, alpha: 1.0)
}
self.toolbarNode?.left = {
toolbarItem.actions[0].action()
}
self.toolbarNode?.right = {
if toolbarItem.actions.count == 2 {
toolbarItem.actions[1].action()
} else if toolbarItem.actions.count == 3 {
toolbarItem.actions[2].action()
}
}
self.toolbarNode?.middle = {
if toolbarItem.actions.count == 1 {
toolbarItem.actions[0].action()
} else if toolbarItem.actions.count == 3 {
toolbarItem.actions[1].action()
}
}
} else if let toolbarNode = self.toolbarNode {
self.toolbarNode = nil
if case let .animated(duration, curve) = transition {
toolbarNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: toolbarNode.frame.size.height), duration: duration, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: false, additive: true, completion: { [weak toolbarNode] _ in
toolbarNode?.removeFromSupernode()
} else if let toolbar = self.toolbar {
self.toolbar = nil
if let toolbarView = toolbar.view {
ComponentTransition(transition).setAlpha(view: toolbarView, alpha: 0.0, completion: { [weak toolbarView] _ in
toolbarView?.removeFromSuperview()
})
} else {
toolbarNode.removeFromSupernode()
}
}
// if let toolbarItem = self.toolbarItem {
// var tabBarHeight: CGFloat
// let bottomInset: CGFloat = insets.bottom
// if !layout.safeInsets.left.isZero {
// tabBarHeight = 34.0 + bottomInset
// insets.bottom += 34.0
// } else {
// tabBarHeight = 49.0 + bottomInset
// insets.bottom += 49.0
// }
//
// let toolbarFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - tabBarHeight), size: CGSize(width: layout.size.width, height: tabBarHeight))
//
// if let toolbarNode = self.toolbarNode {
// transition.updateFrame(node: toolbarNode, frame: toolbarFrame)
// toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: layout.intrinsicInsets.bottom, toolbar: toolbarItem.toolbar, transition: transition)
// } else if let theme = self.theme {
// let toolbarNode = ToolbarNode(theme: ToolbarTheme(rootControllerTheme: theme), displaySeparator: true)
// toolbarNode.frame = toolbarFrame
// toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: layout.intrinsicInsets.bottom, toolbar: toolbarItem.toolbar, transition: .immediate)
// self.addSubnode(toolbarNode)
// self.toolbarNode = toolbarNode
// if case let .animated(duration, curve) = transition {
// toolbarNode.layer.animatePosition(from: CGPoint(x: 0.0, y: toolbarFrame.height), to: CGPoint(), duration: duration, mediaTimingFunction: curve.mediaTimingFunction, additive: true)
// }
// }
//
// self.toolbarNode?.left = {
// toolbarItem.actions[0].action()
// }
// self.toolbarNode?.right = {
// if toolbarItem.actions.count == 2 {
// toolbarItem.actions[1].action()
// } else if toolbarItem.actions.count == 3 {
// toolbarItem.actions[2].action()
// }
// }
// self.toolbarNode?.middle = {
// if toolbarItem.actions.count == 1 {
// toolbarItem.actions[0].action()
// } else if toolbarItem.actions.count == 3 {
// toolbarItem.actions[1].action()
// }
// }
// } else if let toolbarNode = self.toolbarNode {
// self.toolbarNode = nil
// if case let .animated(duration, curve) = transition {
// toolbarNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: toolbarNode.frame.size.height), duration: duration, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: false, additive: true, completion: { [weak toolbarNode] _ in
// toolbarNode?.removeFromSupernode()
// })
// } else {
// toolbarNode.removeFromSupernode()
// }
// }
if let headerItemNode = self.headerItemNode {
let headerHeight = headerItemNode.updateLayout(layout: layout, transition: transition)
@ -977,7 +1084,7 @@ open class ItemListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
insets.bottom = footerHeight
let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0))
if layout.size.width >= 375.0 {
if layout.size.width >= 320.0 {
insets.left += inset
insets.right += inset
}

View file

@ -164,7 +164,7 @@ public func itemListNeighborsGroupedInsets(_ neighbors: ItemListNeighbors, _ par
}
public func itemListHasRoundedBlockLayout(_ params: ListViewItemLayoutParams) -> Bool {
return params.width >= 350.0
return params.width >= 320.0
}
public final class ItemListPresentationData: Equatable {

View file

@ -74,14 +74,20 @@ public struct ItemListRevealOption: Equatable {
}
private let titleFont = Font.regular(11.0)
private let iconlessTitleFont = Font.regular(13.0)
private let optionSpacing: CGFloat = 10.0
private let optionEdgeInset: CGFloat = 10.0
private let optionTitleSpacing: CGFloat = 4.0
private let optionRevealStartOverlap: CGFloat = 12.0
private let optionRevealEndDistance: CGFloat = 10.0
private let optionExpandedActivationWidthFactor: CGFloat = 5.0
private let optionExpandedActivationWidthFactor: CGFloat = 3.0
private let optionExpandedTransitionDistance: CGFloat = 16.0
private let optionIconlessTitleHorizontalInset: CGFloat = 10.0
private let optionIconAnimationResponse: CGFloat = 18.0
private let optionIconAnimationSnapDistance: CGFloat = 0.5
private let optionIconAnimationSnapSize: CGFloat = 0.5
private let optionIconAnimationSnapAlpha: CGFloat = 0.01
private struct ItemListRevealOptionLayoutMetrics {
let shapeSize: CGSize
@ -122,6 +128,10 @@ private func clampToUnitInterval(_ value: CGFloat) -> CGFloat {
return max(0.0, min(1.0, value))
}
private func frameCenter(_ frame: CGRect) -> CGPoint {
return CGPoint(x: frame.midX, y: frame.midY)
}
private final class ItemListRevealOptionNode: ASDisplayNode {
private let contentContainerNode: ASDisplayNode
private let backgroundNode: ASDisplayNode
@ -131,10 +141,21 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
private let animationNode: SimpleAnimationNode?
private let enableAnimations: Bool
private let displaysTitleInsidePill: Bool
private var animationScale: CGFloat = 1.0
private var animationNodeOffset: CGFloat = 0.0
private var animationNodeFlip = false
private var iconAnimationLink: SharedDisplayLinkDriver.Link?
private weak var manuallyAnimatedIconNode: ASDisplayNode?
private var currentIconCenter: CGPoint?
private var targetIconCenter: CGPoint?
private var currentIconSize: CGSize?
private var targetIconSize: CGSize?
private var currentTitleAlpha: CGFloat?
private var targetTitleAlpha: CGFloat?
private var didApplyLayout = false
var isExpanded: Bool = false
@ -150,7 +171,15 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
self.titleNode = ASTextNode()
self.titleNode.maximumNumberOfLines = 1
self.titleNode.truncationMode = .byTruncatingTail
self.titleNode.attributedText = NSAttributedString(string: title, font: titleFont, textColor: textColor)
let displaysTitleInsidePill: Bool
if case .none = icon {
displaysTitleInsidePill = true
} else {
displaysTitleInsidePill = false
}
self.displaysTitleInsidePill = displaysTitleInsidePill
self.titleNode.attributedText = NSAttributedString(string: title, font: displaysTitleInsidePill ? iconlessTitleFont : titleFont, textColor: displaysTitleInsidePill ? iconColor : textColor)
self.enableAnimations = enableAnimations
@ -201,6 +230,10 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
self.highlightNode.backgroundColor = color.withMultipliedBrightnessBy(0.9)
}
deinit {
self.stopManualIconAnimation()
}
func setHighlighted(_ highlighted: Bool) {
if highlighted {
self.contentContainerNode.insertSubnode(self.highlightNode, aboveSubnode: self.backgroundNode)
@ -214,25 +247,150 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
func resetAnimation() {
self.animationNode?.reset()
self.stopManualIconAnimation()
}
private func currentIconPresentationFrame(iconNode: ASDisplayNode) -> CGRect {
return iconNode.layer.presentation()?.frame ?? iconNode.frame
}
private func currentTitlePresentationAlpha() -> CGFloat {
if let presentation = self.titleNode.layer.presentation() {
return CGFloat(presentation.opacity)
} else {
return self.titleNode.alpha
}
}
private func applyManualIconState(iconNode: ASDisplayNode, center: CGPoint, size: CGSize, titleAlpha: CGFloat) {
iconNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(center.x - size.width / 2.0), y: floorToScreenPixels(center.y - size.height / 2.0)), size: size)
self.titleNode.alpha = titleAlpha
}
private func isManualIconAnimationAtTarget(center: CGPoint, size: CGSize, titleAlpha: CGFloat) -> Bool {
guard let targetIconCenter = self.targetIconCenter, let targetIconSize = self.targetIconSize, let targetTitleAlpha = self.targetTitleAlpha else {
return true
}
let centerDeltaX = targetIconCenter.x - center.x
let centerDeltaY = targetIconCenter.y - center.y
let centerDistance = sqrt(centerDeltaX * centerDeltaX + centerDeltaY * centerDeltaY)
let sizeDistance = max(abs(targetIconSize.width - size.width), abs(targetIconSize.height - size.height))
let alphaDistance = abs(targetTitleAlpha - titleAlpha)
return centerDistance <= optionIconAnimationSnapDistance && sizeDistance <= optionIconAnimationSnapSize && alphaDistance <= optionIconAnimationSnapAlpha
}
private func stopManualIconAnimation() {
self.iconAnimationLink?.isPaused = true
self.iconAnimationLink?.invalidate()
self.iconAnimationLink = nil
self.manuallyAnimatedIconNode = nil
self.currentIconCenter = nil
self.targetIconCenter = nil
self.currentIconSize = nil
self.targetIconSize = nil
self.currentTitleAlpha = nil
self.targetTitleAlpha = nil
}
private func updateManualIconAnimation(iconNode: ASDisplayNode, targetFrame: CGRect, targetTitleAlpha: CGFloat, forceImmediate: Bool) {
iconNode.layer.removeAnimation(forKey: "position")
iconNode.layer.removeAnimation(forKey: "bounds")
self.titleNode.layer.removeAnimation(forKey: "opacity")
let targetCenter = frameCenter(targetFrame)
let targetSize = targetFrame.size
if self.manuallyAnimatedIconNode !== iconNode || self.currentIconCenter == nil || self.currentIconSize == nil || self.currentTitleAlpha == nil {
let currentFrame = self.currentIconPresentationFrame(iconNode: iconNode)
self.currentIconCenter = frameCenter(currentFrame)
self.currentIconSize = currentFrame.size
self.currentTitleAlpha = self.currentTitlePresentationAlpha()
self.manuallyAnimatedIconNode = iconNode
}
self.targetIconCenter = targetCenter
self.targetIconSize = targetSize
self.targetTitleAlpha = targetTitleAlpha
if forceImmediate {
self.applyManualIconState(iconNode: iconNode, center: targetCenter, size: targetSize, titleAlpha: targetTitleAlpha)
self.stopManualIconAnimation()
return
}
if let currentIconCenter = self.currentIconCenter, let currentIconSize = self.currentIconSize, let currentTitleAlpha = self.currentTitleAlpha, self.isManualIconAnimationAtTarget(center: currentIconCenter, size: currentIconSize, titleAlpha: currentTitleAlpha) {
self.applyManualIconState(iconNode: iconNode, center: targetCenter, size: targetSize, titleAlpha: targetTitleAlpha)
self.stopManualIconAnimation()
return
}
if self.iconAnimationLink == nil {
self.iconAnimationLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] deltaTime in
self?.tickManualIconAnimation(deltaTime: deltaTime)
})
self.iconAnimationLink?.isPaused = false
}
}
private func tickManualIconAnimation(deltaTime: CGFloat) {
guard let iconNode = self.manuallyAnimatedIconNode, let currentIconCenter = self.currentIconCenter, let targetIconCenter = self.targetIconCenter, let currentIconSize = self.currentIconSize, let targetIconSize = self.targetIconSize, let currentTitleAlpha = self.currentTitleAlpha, let targetTitleAlpha = self.targetTitleAlpha else {
self.stopManualIconAnimation()
return
}
let clampedDeltaTime = min(0.05, max(0.0, deltaTime))
let progress = 1.0 - exp(-clampedDeltaTime * optionIconAnimationResponse)
let updatedCenter = CGPoint(
x: currentIconCenter.x + (targetIconCenter.x - currentIconCenter.x) * progress,
y: currentIconCenter.y + (targetIconCenter.y - currentIconCenter.y) * progress
)
let updatedSize = CGSize(
width: currentIconSize.width + (targetIconSize.width - currentIconSize.width) * progress,
height: currentIconSize.height + (targetIconSize.height - currentIconSize.height) * progress
)
let updatedTitleAlpha = currentTitleAlpha + (targetTitleAlpha - currentTitleAlpha) * progress
if self.isManualIconAnimationAtTarget(center: updatedCenter, size: updatedSize, titleAlpha: updatedTitleAlpha) {
self.applyManualIconState(iconNode: iconNode, center: targetIconCenter, size: targetIconSize, titleAlpha: targetTitleAlpha)
self.stopManualIconAnimation()
} else {
self.currentIconCenter = updatedCenter
self.currentIconSize = updatedSize
self.currentTitleAlpha = updatedTitleAlpha
self.applyManualIconState(iconNode: iconNode, center: updatedCenter, size: updatedSize, titleAlpha: updatedTitleAlpha)
}
}
func updateLayout(isLeft: Bool, isPrimary: Bool, metrics: ItemListRevealOptionLayoutMetrics, revealProgress: CGFloat, overswipeProgress: CGFloat, expandedProgress: CGFloat, isStretched: Bool, isExpanded: Bool, transition: ContainedViewLayoutTransition) {
let didApplyLayout = self.didApplyLayout
let bounds = CGRect(origin: CGPoint(), size: self.bounds.size)
transition.updateFrame(node: self.contentContainerNode, frame: bounds)
let contentHeight = metrics.contentHeight
let shapeY = floor((bounds.height - contentHeight) / 2.0)
let titleSize = self.titleNode.measure(CGSize(width: metrics.titleWidth, height: CGFloat.greatestFiniteMagnitude))
let pillSize: CGSize
if self.displaysTitleInsidePill {
let pillWidth = max(metrics.shapeSize.width, min(metrics.slotWidth, titleSize.width + optionIconlessTitleHorizontalInset * 2.0))
pillSize = CGSize(width: pillWidth, height: metrics.shapeSize.height)
} else {
pillSize = metrics.shapeSize
}
let shapeY: CGFloat
if self.displaysTitleInsidePill {
shapeY = floor((bounds.height - pillSize.height) / 2.0)
} else {
shapeY = floor((bounds.height - metrics.contentHeight) / 2.0)
}
let shapeFrameX: CGFloat
if isStretched {
shapeFrameX = isLeft ? 0.0 : bounds.width - metrics.shapeSize.width
shapeFrameX = isLeft ? 0.0 : bounds.width - pillSize.width
} else {
shapeFrameX = metrics.slotShapeInset
shapeFrameX = floor((metrics.slotWidth - pillSize.width) / 2.0)
}
let shapeFrame = CGRect(origin: CGPoint(x: shapeFrameX, y: shapeY), size: metrics.shapeSize)
let shapeFrame = CGRect(origin: CGPoint(x: shapeFrameX, y: shapeY), size: pillSize)
let backgroundFrame: CGRect
if isStretched {
backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: shapeY), size: CGSize(width: bounds.width, height: metrics.shapeSize.height))
backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: shapeY), size: CGSize(width: bounds.width, height: pillSize.height))
} else {
backgroundFrame = shapeFrame
}
@ -244,7 +402,6 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
self.isExpanded = isExpanded
self.didApplyLayout = true
let titleSize = self.titleNode.measure(CGSize(width: metrics.titleWidth, height: CGFloat.greatestFiniteMagnitude))
let contentAlpha: CGFloat
if isPrimary {
contentAlpha = revealProgress
@ -255,8 +412,8 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
transition.updateAlpha(node: self.contentContainerNode, alpha: contentAlpha)
transition.updateTransform(node: self.contentContainerNode, transform: CGAffineTransform(scaleX: contentScale, y: contentScale))
let titleAlpha: CGFloat = isPrimary ? (1.0 - expandedProgress) : 1.0
transition.updateAlpha(node: self.titleNode, alpha: titleAlpha)
let titleAlpha: CGFloat = isPrimary && !self.displaysTitleInsidePill ? (1.0 - expandedProgress) : 1.0
var didApplyManualIconAnimation = false
let centeredIconCenterX = isPrimary ? backgroundFrame.midX : shapeFrame.midX
let iconCenterX: CGFloat
@ -281,7 +438,13 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
imageSize = CGSize(width: floorToScreenPixels(imageSize.width * imageScale), height: floorToScreenPixels(imageSize.height * imageScale))
}
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(iconCenterX - imageSize.width / 2.0), y: floorToScreenPixels(iconCenterY - imageSize.height / 2.0) + 6.0 + self.animationNodeOffset), size: imageSize)
transition.updateFrame(node: animationNode, frame: iconFrame)
if isPrimary {
didApplyManualIconAnimation = true
self.updateManualIconAnimation(iconNode: animationNode, targetFrame: iconFrame, targetTitleAlpha: titleAlpha, forceImmediate: !didApplyLayout || revealProgress < CGFloat.ulpOfOne)
} else {
transition.updateFrame(node: animationNode, frame: iconFrame)
}
if self.enableAnimations {
if revealProgress >= 0.4 {
animationNode.play()
@ -297,11 +460,26 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
fittedSize = CGSize(width: floorToScreenPixels(fittedSize.width * imageScale), height: floorToScreenPixels(fittedSize.height * imageScale))
}
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(iconCenterX - fittedSize.width / 2.0), y: floorToScreenPixels(iconCenterY - fittedSize.height / 2.0)), size: fittedSize)
transition.updateFrame(node: iconNode, frame: iconFrame)
if isPrimary {
didApplyManualIconAnimation = true
self.updateManualIconAnimation(iconNode: iconNode, targetFrame: iconFrame, targetTitleAlpha: titleAlpha, forceImmediate: !didApplyLayout || revealProgress < CGFloat.ulpOfOne)
} else {
transition.updateFrame(node: iconNode, frame: iconFrame)
}
}
let titleCenterX = isPrimary ? backgroundFrame.midX : shapeFrame.midX
let titleFrame = CGRect(origin: CGPoint(x: floor(titleCenterX - titleSize.width / 2.0), y: shapeFrame.maxY + optionTitleSpacing), size: titleSize)
if !didApplyManualIconAnimation {
self.stopManualIconAnimation()
transition.updateAlpha(node: self.titleNode, alpha: titleAlpha)
}
let titleFrame: CGRect
if self.displaysTitleInsidePill {
titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - titleSize.width / 2.0), y: floorToScreenPixels(backgroundFrame.midY - titleSize.height / 2.0)), size: titleSize)
} else {
let titleCenterX = isPrimary ? backgroundFrame.midX : shapeFrame.midX
titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(titleCenterX - titleSize.width / 2.0), y: shapeFrame.maxY + optionTitleSpacing), size: titleSize)
}
transition.updateFrame(node: self.titleNode, frame: titleFrame)
}
@ -315,6 +493,8 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
public final class ItemListRevealOptionsNode: ASDisplayNode {
private let optionSelected: (ItemListRevealOption) -> Void
private let tapticAction: () -> Void
private let clippingContainerNode: ASDisplayNode
private let optionsContainerNode: ASDisplayNode
private var options: [ItemListRevealOption] = []
private var isLeft: Bool = false
@ -326,8 +506,14 @@ public final class ItemListRevealOptionsNode: ASDisplayNode {
public init(optionSelected: @escaping (ItemListRevealOption) -> Void, tapticAction: @escaping () -> Void) {
self.optionSelected = optionSelected
self.tapticAction = tapticAction
self.clippingContainerNode = ASDisplayNode()
self.optionsContainerNode = ASDisplayNode()
super.init()
self.clippingContainerNode.clipsToBounds = true
self.addSubnode(self.clippingContainerNode)
self.clippingContainerNode.addSubnode(self.optionsContainerNode)
}
override public func didLoad() {
@ -363,11 +549,11 @@ public final class ItemListRevealOptionsNode: ASDisplayNode {
}
if isLeft {
for node in self.optionNodes.reversed() {
self.addSubnode(node)
self.optionsContainerNode.addSubnode(node)
}
} else {
for node in self.optionNodes {
self.addSubnode(node)
self.optionsContainerNode.addSubnode(node)
}
}
self.invalidateCalculatedLayout()
@ -402,6 +588,16 @@ public final class ItemListRevealOptionsNode: ASDisplayNode {
let primaryIndex = self.isLeft ? 0 : self.optionNodes.count - 1
let stride = metrics.shapeSize.width + optionSpacing
let clippingFrameX: CGFloat
if self.isLeft {
clippingFrameX = max(0.0, size.width - revealedDistance)
} else {
clippingFrameX = 0.0
}
let clippingFrame = CGRect(origin: CGPoint(x: clippingFrameX, y: 0.0), size: CGSize(width: revealedDistance, height: size.height))
transition.updateFrame(node: self.clippingContainerNode, frame: clippingFrame)
transition.updateFrame(node: self.optionsContainerNode, frame: CGRect(origin: CGPoint(x: -clippingFrameX, y: 0.0), size: CGSize(width: max(size.width, revealedDistance), height: size.height)))
let animated = transition.isAnimated
var completionCount = self.optionNodes.count
let intermediateCompletion = {
@ -419,12 +615,8 @@ public final class ItemListRevealOptionsNode: ASDisplayNode {
let isStretched = isPrimary && overswipeDistance > CGFloat.ulpOfOne
let isExpanded = isPrimary && overswipeDistance > expandedActivationDistance
let expandedProgress: CGFloat = isExpanded ? 1.0 : 0.0
var nodeTransition = transition
if node.hasAppliedLayout && node.isExpanded != isExpanded {
nodeTransition = transition.isAnimated ? transition : .animated(duration: 0.2, curve: .easeInOut)
if !transition.isAnimated {
self.tapticAction()
}
if node.hasAppliedLayout && node.isExpanded != isExpanded && !transition.isAnimated {
self.tapticAction()
}
let baseCircleFrame: CGRect
@ -477,7 +669,7 @@ public final class ItemListRevealOptionsNode: ASDisplayNode {
intermediateCompletion()
})
node.updateLayout(isLeft: self.isLeft, isPrimary: isPrimary, metrics: metrics, revealProgress: revealProgress, overswipeProgress: overswipeProgress, expandedProgress: expandedProgress, isStretched: isStretched, isExpanded: isExpanded, transition: nodeTransition)
node.updateLayout(isLeft: self.isLeft, isPrimary: isPrimary, metrics: metrics, revealProgress: revealProgress, overswipeProgress: overswipeProgress, expandedProgress: expandedProgress, isStretched: isStretched, isExpanded: isExpanded, transition: transition)
if self.isLeft {
i -= 1

View file

@ -74,7 +74,33 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem, ListItemCompone
public let tag: ItemListItemTag?
public let shimmeringIndex: Int?
public init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .legacy, icon: UIImage? = nil, context: AccountContext? = nil, iconPeer: EnginePeer? = nil, title: String, attributedTitle: NSAttributedString? = nil, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, titleFont: ItemListDisclosureItemTitleFont = .regular, titleIcon: UIImage? = nil, titleBadge: String? = nil, label: String, attributedLabel: NSAttributedString? = nil, labelStyle: ItemListDisclosureLabelStyle = .text, additionalDetailLabel: String? = nil, additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor = .generic, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, noInsets: Bool = false, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) {
public init(
presentationData: ItemListPresentationData,
systemStyle: ItemListSystemStyle = .legacy,
icon: UIImage? = nil,
context: AccountContext? = nil,
iconPeer: EnginePeer? = nil,
title: String,
attributedTitle: NSAttributedString? = nil,
enabled: Bool = true,
titleColor: ItemListDisclosureItemTitleColor = .primary,
titleFont: ItemListDisclosureItemTitleFont = .regular,
titleIcon: UIImage? = nil,
titleBadge: String? = nil,
label: String,
attributedLabel: NSAttributedString? = nil,
labelStyle: ItemListDisclosureLabelStyle = .text,
additionalDetailLabel: String? = nil,
additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor = .generic,
sectionId: ItemListSectionId,
style: ItemListStyle,
disclosureStyle: ItemListDisclosureStyle = .arrow,
noInsets: Bool = false,
action: (() -> Void)?,
clearHighlightAutomatically: Bool = true,
tag: ItemListItemTag? = nil,
shimmeringIndex: Int? = nil
) {
self.presentationData = presentationData
self.systemStyle = systemStyle
self.icon = icon
@ -709,7 +735,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
centralContentHeight += additionalDetailLabelInfo.0.size.height
}
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - centralContentHeight) / 2.0)), size: titleLayout.size)
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((height - centralContentHeight) / 2.0) + 1.0), size: titleLayout.size)
strongSelf.titleNode.textNode.frame = titleFrame
if let updateBadgeImage = updatedLabelBadgeImage {
@ -739,7 +765,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
case .detailText, .multilineDetailText:
labelFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: labelLayout.size)
default:
labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: floor((height - labelLayout.size.height) / 2.0)), size: labelLayout.size)
labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: floorToScreenPixels((height - labelLayout.size.height) / 2.0) + 1.0), size: labelLayout.size)
}
strongSelf.labelNode.frame = labelFrame

View file

@ -67,6 +67,7 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD
private var enableAnimations: Bool = true
private var initialRevealOffset: CGFloat = 0.0
private var hasActiveRevealGestureOffset: Bool = false
public private(set) var revealOffset: CGFloat = 0.0
private var recognizer: ItemListRevealOptionsGestureRecognizer?
@ -190,17 +191,22 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD
@objc private func revealTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring))
self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.6, curve: self.revealSpringCurve(initialVelocity: 0.0)))
self.revealOptionsInteractivelyClosed()
}
}
private func revealSpringCurve(initialVelocity: CGFloat = 0.0) -> ContainedViewLayoutTransitionCurve {
return .customSpring(mass: 2.0, stiffness: 200.0, damping: 100.0, initialVelocity: initialVelocity)
}
@objc private func revealGesture(_ recognizer: ItemListRevealOptionsGestureRecognizer) {
guard let (size, _, _) = self.validLayout else {
return
}
switch recognizer.state {
case .began:
self.hasActiveRevealGestureOffset = !self.revealOffset.isZero
if let leftRevealNode = self.leftRevealNode {
let revealSize = leftRevealNode.bounds.size
let location = recognizer.location(in: self.view)
@ -232,16 +238,26 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD
if self.leftRevealNode == nil && CGFloat(0.0).isLess(than: translation.x) {
self.setupAndAddLeftRevealNode()
self.revealOptionsInteractivelyOpened()
self.hasActiveRevealGestureOffset = true
} else if self.rightRevealNode == nil && translation.x.isLess(than: 0.0) {
self.setupAndAddRightRevealNode()
self.revealOptionsInteractivelyOpened()
self.hasActiveRevealGestureOffset = true
}
if !translation.x.isZero {
self.hasActiveRevealGestureOffset = true
}
self.updateRevealOffsetInternal(offset: translation.x, transition: .immediate)
if self.leftRevealNode == nil && self.rightRevealNode == nil {
if self.leftRevealNode == nil && self.rightRevealNode == nil && !self.hasActiveRevealGestureOffset {
self.revealOptionsInteractivelyClosed()
}
case .ended, .cancelled:
let hasActiveRevealGestureOffset = self.hasActiveRevealGestureOffset
self.hasActiveRevealGestureOffset = false
guard let recognizer = self.recognizer else {
if hasActiveRevealGestureOffset {
self.revealOptionsInteractivelyClosed()
}
break
}
@ -270,7 +286,7 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD
reveal = false
selectedOption = self.revealOptions.left.first
} else {
self.updateRevealOffsetInternal(offset: reveal ?revealSize.width : 0.0, transition: .animated(duration: 0.3, curve: .spring))
self.updateRevealOffsetInternal(offset: reveal ?revealSize.width : 0.0, transition: .animated(duration: 0.6, curve: self.revealSpringCurve(initialVelocity: 0.0)))
}
if let selectedOption = selectedOption {
@ -305,7 +321,7 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD
reveal = false
selectedOption = self.revealOptions.right.last
} else {
self.updateRevealOffsetInternal(offset: reveal ? -revealSize.width : 0.0, transition: .animated(duration: 0.3, curve: .spring))
self.updateRevealOffsetInternal(offset: reveal ? -revealSize.width : 0.0, transition: .animated(duration: 0.6, curve: self.revealSpringCurve(initialVelocity: 0.0)))
}
if let selectedOption = selectedOption {
@ -315,6 +331,8 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD
self.revealOptionsInteractivelyClosed()
}
}
} else if hasActiveRevealGestureOffset {
self.revealOptionsInteractivelyClosed()
}
default:
break
@ -473,7 +491,7 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD
}
let transition: ContainedViewLayoutTransition
if animated {
transition = .animated(duration: 0.3, curve: .spring)
transition = .animated(duration: 0.6, curve: self.revealSpringCurve(initialVelocity: 0.0))
} else {
transition = .immediate
}
@ -495,7 +513,7 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD
open func animateRevealOptionsFill(completion: (() -> Void)? = nil) {
if let validLayout = self.validLayout {
self.layer.allowsGroupOpacity = true
self.updateRevealOffsetInternal(offset: -validLayout.0.width - 74.0, transition: .animated(duration: 0.3, curve: .spring), completion: {
self.updateRevealOffsetInternal(offset: -validLayout.0.width - 74.0, transition: .animated(duration: 0.6, curve: self.revealSpringCurve(initialVelocity: 0.0)), completion: {
self.layer.allowsGroupOpacity = false
completion?()
})

View file

@ -174,12 +174,15 @@ public class InfoItemNode: ListViewItemNode {
self.labelNode = TextNode()
self.labelNode.isUserInteractionEnabled = false
self.labelNode.displaysAsynchronously = false
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = false
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = false
self.activateArea = AccessibilityAreaNode()
self.activateArea.accessibilityTraits = .staticText

View file

@ -171,6 +171,10 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
public var tag: ItemListItemTag? {
return self.item?.tag
}
public var contextSourceView: UIView {
return self.textNode?.view ?? self.switchNode.view
}
public init(type: ItemListSwitchItemNodeType) {
self.backgroundNode = ASDisplayNode()
@ -251,6 +255,20 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
})
})
}
public func updateHasContextMenu(hasContextMenu: Bool) {
let transition: ContainedViewLayoutTransition
if hasContextMenu {
transition = .immediate
} else {
transition = .animated(duration: 0.3, curve: .easeInOut)
}
if let textNode = self.textNode {
transition.updateAlpha(node: textNode, alpha: hasContextMenu ? 0.5 : 1.0)
} else {
transition.updateAlpha(node: self.switchNode, alpha: hasContextMenu ? 0.5 : 1.0)
}
}
func asyncLayout() -> (_ item: ItemListSwitchItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
@ -488,7 +506,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width - params.rightInset - bottomStripeInset - separatorRightInset, height: separatorHeight)))
}
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: topInset), size: titleLayout.size)
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: topInset + 1.0), size: titleLayout.size)
transition.updatePosition(node: strongSelf.titleNode, position: titleFrame.origin)
strongSelf.titleNode.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)

View file

@ -81,6 +81,7 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, ASScrollVi
self.wrappingScrollNode.view.alwaysBounceVertical = true
self.wrappingScrollNode.view.delaysContentTouches = false
self.wrappingScrollNode.view.canCancelContentTouches = true
self.wrappingScrollNode.view.scrollsToTop = false
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)

View file

@ -133,6 +133,7 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer
self.descriptionNode.textAlignment = .center
self.peersScrollNode = ASScrollNode()
self.peersScrollNode.view.showsHorizontalScrollIndicator = false
self.peersScrollNode.view.scrollsToTop = false
self.actionButtonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: theme), height: 52.0, cornerRadius: 11.0)

View file

@ -76,6 +76,7 @@ final class LanguageLinkPreviewControllerNode: ViewControllerTracingNode, ASScro
self.wrappingScrollNode.view.alwaysBounceVertical = true
self.wrappingScrollNode.view.delaysContentTouches = false
self.wrappingScrollNode.view.canCancelContentTouches = true
self.wrappingScrollNode.view.scrollsToTop = false
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)

View file

@ -14,7 +14,7 @@
@property (nonatomic, copy) void (^captionSet)(id<TGModernGalleryItem>, NSAttributedString *);
@property (nonatomic, copy) void (^donePressed)(id<TGModernGalleryItem>);
@property (nonatomic, copy) void (^doneLongPressed)(id<TGModernGalleryItem>);
@property (nonatomic, copy) void (^doneLongPressed)(id<TGModernGalleryItem>, UIView *);
@property (nonatomic, copy) void (^photoStripItemSelected)(NSInteger index);

View file

@ -1,6 +1,7 @@
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <CoreMedia/CoreMedia.h>
#import <LegacyComponents/TGPhotoToolbarViewProtocol.h>
@class TGPaintingData;
@class TGStickerMaskDescription;
@ -142,6 +143,8 @@
@property (nonatomic, copy) id<TGCaptionPanelView> _Nullable(^ _Nullable captionPanelView)(void);
@property (nonatomic, copy) id<TGLivePhotoButton> _Nullable(^ _Nullable livePhotoButton)(void);
@property (nonatomic, copy) UIView<TGPhotoToolbarViewProtocol> *_Nullable(^ _Nullable photoToolbarView)(TGPhotoEditorBackButton backButton, TGPhotoEditorDoneButton doneButton, bool solidBackground, bool hasSendStarsButton);
@property (nonatomic, copy) bool (^ _Nullable presentMediaPickerSendActionMenu)(UIView * _Nonnull sourceView, bool canSendSilently, bool canSendWhenOnline, bool canSchedule, bool reminder, bool hasTimer, void (^ _Nonnull sendSilently)(void), void (^ _Nonnull sendWhenOnline)(void), void (^ _Nonnull schedule)(void), void (^ _Nonnull sendWithTimer)(void));
@property (nonatomic, copy) void (^ _Nullable editCover)(CGSize dimensions, void(^_Nonnull completion)(UIImage * _Nonnull));

View file

@ -1,44 +1,14 @@
#import <LegacyComponents/TGPhotoEditorButton.h>
#import <LegacyComponents/TGPhotoToolbarViewProtocol.h>
#import <LegacyComponents/LegacyComponentsContext.h>
@protocol TGPhotoPaintStickersContext;
typedef NS_OPTIONS(NSUInteger, TGPhotoEditorTab) {
TGPhotoEditorNoneTab = 0,
TGPhotoEditorCropTab = 1 << 0,
TGPhotoEditorRotateTab = 1 << 1,
TGPhotoEditorMirrorTab = 1 << 2,
TGPhotoEditorPaintTab = 1 << 3,
TGPhotoEditorEraserTab = 1 << 4,
TGPhotoEditorStickerTab = 1 << 5,
TGPhotoEditorTextTab = 1 << 6,
TGPhotoEditorToolsTab = 1 << 7,
TGPhotoEditorQualityTab = 1 << 8,
TGPhotoEditorTimerTab = 1 << 9,
TGPhotoEditorAspectRatioTab = 1 << 10,
TGPhotoEditorTintTab = 1 << 11,
TGPhotoEditorBlurTab = 1 << 12,
TGPhotoEditorCurvesTab = 1 << 13
};
typedef enum
{
TGPhotoEditorBackButtonBack,
TGPhotoEditorBackButtonCancel
} TGPhotoEditorBackButton;
typedef enum
{
TGPhotoEditorDoneButtonSend,
TGPhotoEditorDoneButtonCheck,
TGPhotoEditorDoneButtonDone,
TGPhotoEditorDoneButtonSchedule
} TGPhotoEditorDoneButton;
@interface TGPhotoToolbarView : UIView
@interface TGPhotoToolbarView : UIView <TGPhotoToolbarViewProtocol>
@property (nonatomic, assign) UIInterfaceOrientation interfaceOrientation;
@property (nonatomic, assign) CGFloat bottomInset;
@property (nonatomic, readonly) UIButton *doneButton;
@ -79,8 +49,12 @@ typedef enum
- (void)setActiveTab:(TGPhotoEditorTab)tab;
- (void)setQualityButtonIsPhoto:(bool)isPhoto highQuality:(bool)highQuality videoPreset:(NSInteger)videoPreset;
- (void)setTimerButtonValue:(NSInteger)value;
- (void)setInfoString:(NSString *)string;
- (UIView *)viewForTab:(TGPhotoEditorTab)tab;
- (TGPhotoEditorButton *)buttonForTab:(TGPhotoEditorTab)tab;
@end

View file

@ -0,0 +1,83 @@
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
typedef NS_OPTIONS(NSUInteger, TGPhotoEditorTab) {
TGPhotoEditorNoneTab = 0,
TGPhotoEditorCropTab = 1 << 0,
TGPhotoEditorRotateTab = 1 << 1,
TGPhotoEditorMirrorTab = 1 << 2,
TGPhotoEditorPaintTab = 1 << 3,
TGPhotoEditorEraserTab = 1 << 4,
TGPhotoEditorStickerTab = 1 << 5,
TGPhotoEditorTextTab = 1 << 6,
TGPhotoEditorToolsTab = 1 << 7,
TGPhotoEditorQualityTab = 1 << 8,
TGPhotoEditorTimerTab = 1 << 9,
TGPhotoEditorAspectRatioTab = 1 << 10,
TGPhotoEditorTintTab = 1 << 11,
TGPhotoEditorBlurTab = 1 << 12,
TGPhotoEditorCurvesTab = 1 << 13
};
typedef enum
{
TGPhotoEditorBackButtonBack,
TGPhotoEditorBackButtonCancel
} TGPhotoEditorBackButton;
typedef enum
{
TGPhotoEditorDoneButtonSend,
TGPhotoEditorDoneButtonCheck,
TGPhotoEditorDoneButtonDone,
TGPhotoEditorDoneButtonSchedule
} TGPhotoEditorDoneButton;
@protocol TGPhotoToolbarViewProtocol <NSObject>
@property (nonatomic, assign) UIInterfaceOrientation interfaceOrientation;
@property (nonatomic, assign) CGFloat bottomInset;
@property (nonatomic, readonly) UIView *doneButton;
@property (nonatomic, copy) void(^cancelPressed)(void);
@property (nonatomic, copy) void(^donePressed)(void);
@property (nonatomic, copy) void(^doneLongPressed)(id sender);
@property (nonatomic, copy) void(^tabPressed)(TGPhotoEditorTab tab);
@property (nonatomic, readonly) CGRect cancelButtonFrame;
@property (nonatomic, readonly) CGRect doneButtonFrame;
@property (nonatomic, assign) TGPhotoEditorBackButton backButtonType;
@property (nonatomic, assign) TGPhotoEditorDoneButton doneButtonType;
@property (nonatomic, assign) int64_t sendPaidMessageStars;
@property (nonatomic, readonly) TGPhotoEditorTab currentTabs;
- (void)transitionInAnimated:(bool)animated;
- (void)transitionInAnimated:(bool)animated transparent:(bool)transparent;
- (void)transitionOutAnimated:(bool)animated;
- (void)transitionOutAnimated:(bool)animated transparent:(bool)transparent hideOnCompletion:(bool)hideOnCompletion;
- (void)setDoneButtonEnabled:(bool)enabled animated:(bool)animated;
- (void)setEditButtonsEnabled:(bool)enabled animated:(bool)animated;
- (void)setEditButtonsHidden:(bool)hidden animated:(bool)animated;
- (void)setEditButtonsHighlighted:(TGPhotoEditorTab)buttons;
- (void)setEditButtonsDisabled:(TGPhotoEditorTab)buttons;
- (void)setCenterButtonsHidden:(bool)hidden animated:(bool)animated;
- (void)setAllButtonsHidden:(bool)hidden animated:(bool)animated;
- (void)setCancelDoneButtonsHidden:(bool)hidden animated:(bool)animated;
- (void)setToolbarTabs:(TGPhotoEditorTab)tabs animated:(bool)animated;
- (void)setActiveTab:(TGPhotoEditorTab)tab;
- (void)setQualityButtonIsPhoto:(bool)isPhoto highQuality:(bool)highQuality videoPreset:(NSInteger)videoPreset;
- (void)setTimerButtonValue:(NSInteger)value;
- (void)setInfoString:(NSString *)string;
- (UIView *)viewForTab:(TGPhotoEditorTab)tab;
@end

View file

@ -1548,7 +1548,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
__weak TGModernGalleryController *weakGalleryController = galleryController;
__weak TGMediaPickerGalleryModel *weakModel = model;
model.interfaceView.doneLongPressed = ^(TGMediaPickerGalleryItem *item) {
model.interfaceView.doneLongPressed = ^(TGMediaPickerGalleryItem *item, UIView *sourceView) {
__strong TGCameraController *strongSelf = weakSelf;
__strong TGMediaPickerGalleryModel *strongModel = weakModel;
if (strongSelf == nil || !(strongSelf.hasSilentPosting || strongSelf.hasSchedule) || strongSelf->_shortcut)
@ -1720,6 +1720,21 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
[strongSelf _dismissTransitionForResultController:strongController];
});
};
if (sourceView != nil && strongSelf.stickersContext.presentMediaPickerSendActionMenu != nil && strongSelf.stickersContext.presentMediaPickerSendActionMenu(sourceView, strongSelf->_hasSilentPosting, effectiveHasSchedule, effectiveHasSchedule, strongSelf->_reminder, strongSelf->_hasTimer, ^{
if (controller.sendSilently != nil)
controller.sendSilently();
}, ^{
if (controller.sendWhenOnline != nil)
controller.sendWhenOnline();
}, ^{
if (controller.schedule != nil)
controller.schedule();
}, ^{
if (controller.sendWithTimer != nil)
controller.sendWithTimer();
})) {
return;
}
id<LegacyComponentsOverlayWindowManager> windowManager = nil;
windowManager = [strongSelf->_context makeOverlayWindowManager];

View file

@ -42,6 +42,19 @@
#import <LegacyComponents/TGTooltipView.h>
#import <LegacyComponents/TGPhotoCaptionInputMixin.h>
#import <LegacyComponents/TGPhotoPaintStickersContext.h>
static UIView<TGPhotoToolbarViewProtocol> *TGMediaPickerCreatePhotoToolbarView(id<LegacyComponentsContext> context, TGPhotoEditorBackButton backButton, TGPhotoEditorDoneButton doneButton, bool solidBackground, id<TGPhotoPaintStickersContext> stickersContext, bool hasSendStarsButton)
{
if (stickersContext.photoToolbarView != nil)
{
UIView<TGPhotoToolbarViewProtocol> *toolbarView = stickersContext.photoToolbarView(backButton, doneButton, solidBackground, hasSendStarsButton);
if (toolbarView != nil)
return toolbarView;
}
return [[TGPhotoToolbarView alloc] initWithContext:context backButton:backButton doneButton:doneButton solidBackground:solidBackground stickersContext:hasSendStarsButton ? stickersContext : nil];
}
static TGMediaAsset *TGMediaPickerGalleryLivePhotoAsset(id<TGMediaEditableItem> editableMediaItem)
{
@ -101,8 +114,8 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
UIView *_wrapperView;
UIView *_headerWrapperView;
TGPhotoToolbarView *_portraitToolbarView;
TGPhotoToolbarView *_landscapeToolbarView;
UIView<TGPhotoToolbarViewProtocol> *_portraitToolbarView;
UIView<TGPhotoToolbarViewProtocol> *_landscapeToolbarView;
UIImageView *_arrowView;
UILabel *_recipientLabel;
@ -224,8 +237,10 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
return;
[strongSelf.window endEditing:true];
if (strongSelf->_doneLongPressed != nil)
strongSelf->_doneLongPressed(strongSelf->_currentItem);
if (strongSelf->_doneLongPressed != nil) {
UIView *sourceView = [sender isKindOfClass:[UIView class]] ? (UIView *)sender : nil;
strongSelf->_doneLongPressed(strongSelf->_currentItem, sourceView);
}
[[NSUserDefaults standardUserDefaults] setObject:@(3) forKey:@"TG_displayedMediaTimerTooltip_v3"];
};
@ -471,13 +486,13 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
TGPhotoEditorDoneButton doneButton = isScheduledMessages ? TGPhotoEditorDoneButtonSchedule : TGPhotoEditorDoneButtonSend;
_portraitToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:doneButton solidBackground:false stickersContext:editingContext.sendPaidMessageStars > 0 ? stickersContext : nil];
_portraitToolbarView = TGMediaPickerCreatePhotoToolbarView(_context, TGPhotoEditorBackButtonBack, doneButton, false, stickersContext, editingContext.sendPaidMessageStars > 0);
_portraitToolbarView.cancelPressed = toolbarCancelPressed;
_portraitToolbarView.donePressed = toolbarDonePressed;
_portraitToolbarView.doneLongPressed = toolbarDoneLongPressed;
[_wrapperView addSubview:_portraitToolbarView];
_landscapeToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:doneButton solidBackground:false stickersContext:nil];
_landscapeToolbarView = TGMediaPickerCreatePhotoToolbarView(_context, TGPhotoEditorBackButtonBack, doneButton, false, stickersContext, false);
_landscapeToolbarView.cancelPressed = toolbarCancelPressed;
_landscapeToolbarView.donePressed = toolbarDonePressed;
_landscapeToolbarView.doneLongPressed = toolbarDoneLongPressed;
@ -641,17 +656,17 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
- (UIView *)timerButton
{
if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation))
return [_portraitToolbarView buttonForTab:TGPhotoEditorTimerTab];
return [_portraitToolbarView viewForTab:TGPhotoEditorTimerTab];
else
return [_landscapeToolbarView buttonForTab:TGPhotoEditorTimerTab];
return [_landscapeToolbarView viewForTab:TGPhotoEditorTimerTab];
}
- (UIView *)qualityButton
{
if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation))
return [_portraitToolbarView buttonForTab:TGPhotoEditorQualityTab];
return [_portraitToolbarView viewForTab:TGPhotoEditorQualityTab];
else
return [_landscapeToolbarView buttonForTab:TGPhotoEditorQualityTab];
return [_landscapeToolbarView viewForTab:TGPhotoEditorQualityTab];
}
- (void)setSelectedItemsModel:(TGMediaPickerGallerySelectedItemsModel *)selectedItemsModel
@ -1188,19 +1203,15 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
_muteButton.selected = adjustments.sendAsGif;
TGPhotoEditorButton *qualityButton = [_portraitToolbarView buttonForTab:TGPhotoEditorQualityTab];
UIView *qualityButton = [_portraitToolbarView viewForTab:TGPhotoEditorQualityTab];
if (qualityButton != nil)
{
bool isPhoto = [_currentItemView isKindOfClass:[TGMediaPickerGalleryPhotoItemView class]] || [_currentItem isKindOfClass:[TGCameraCapturedPhoto class]];
bool isHd = false;
TGMediaVideoConversionPreset preset = TGMediaVideoConversionPresetCompressedMedium;
if (isPhoto) {
bool isHd = _editingContext.isHighQualityPhoto;
UIImage *icon = [TGPhotoEditorInterfaceAssets qualityIconForHighQuality:isHd filled: false];
qualityButton.iconImage = icon;
qualityButton = [_landscapeToolbarView buttonForTab:TGPhotoEditorQualityTab];
qualityButton.iconImage = icon;
isHd = _editingContext.isHighQualityPhoto;
} else {
TGMediaVideoConversionPreset preset = 0;
TGMediaVideoConversionPreset adjustmentsPreset = TGMediaVideoConversionPresetCompressedDefault;
if ([adjustments isKindOfClass:[TGMediaVideoEditAdjustments class]])
adjustmentsPreset = ((TGMediaVideoEditAdjustments *)adjustments).preset;
@ -1221,28 +1232,19 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
TGMediaVideoConversionPreset bestPreset = [TGMediaVideoConverter bestAvailablePresetForDimensions:dimensions];
if (preset > bestPreset)
preset = bestPreset;
UIImage *icon = [TGPhotoEditorInterfaceAssets qualityIconForPreset:preset];
qualityButton.iconImage = icon;
qualityButton = [_landscapeToolbarView buttonForTab:TGPhotoEditorQualityTab];
qualityButton.iconImage = icon;
}
[_portraitToolbarView setQualityButtonIsPhoto:isPhoto highQuality:isHd videoPreset:preset];
[_landscapeToolbarView setQualityButtonIsPhoto:isPhoto highQuality:isHd videoPreset:preset];
}
TGPhotoEditorButton *timerButton = [_portraitToolbarView buttonForTab:TGPhotoEditorTimerTab];
UIView *timerButton = [_portraitToolbarView viewForTab:TGPhotoEditorTimerTab];
if (timerButton != nil)
{
NSInteger value = [timer integerValue];
UIImage *defaultIcon = [TGPhotoEditorInterfaceAssets timerIconForValue:0];
UIImage *icon = [TGPhotoEditorInterfaceAssets timerIconForValue:value];
[timerButton setIconImage:defaultIcon activeIconImage:icon];
TGPhotoEditorButton *landscapeTimerButton = [_landscapeToolbarView buttonForTab:TGPhotoEditorTimerTab];
timerButton = landscapeTimerButton;
[timerButton setIconImage:defaultIcon activeIconImage:icon];
[_portraitToolbarView setTimerButtonValue:value];
[_landscapeToolbarView setTimerButtonValue:value];
if (value > 0)
highlightedButtons |= TGPhotoEditorTimerTab;
@ -1413,6 +1415,7 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
_coverButton.alpha = alpha;
_arrowView.alpha = alpha * 0.6f;
_recipientLabel.alpha = alpha * 0.6;
_captionMixin.livePhotoButtonView.alpha = alpha;
} completion:^(BOOL finished)
{
if (finished)
@ -1420,6 +1423,7 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
_checkButton.userInteractionEnabled = !hidden;
_muteButton.userInteractionEnabled = !hidden;
_coverButton.userInteractionEnabled = !hidden;
_captionMixin.livePhotoButtonView.userInteractionEnabled = !hidden;
}
}];
@ -1448,6 +1452,9 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
_arrowView.alpha = alpha * 0.6f;
_recipientLabel.alpha = alpha * 0.6;
_captionMixin.livePhotoButtonView.alpha = alpha;
_captionMixin.livePhotoButtonView.userInteractionEnabled = !hidden;
}
if (hidden)
@ -1735,11 +1742,11 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
{
[self setSelectionInterfaceHidden:true animated:true];
[UIView animateWithDuration:0.2 animations:^
[UIView animateWithDuration:0.3 animations:^
{
_captionMixin.inputPanelView.alpha = 0.0f;
_portraitToolbarView.doneButton.alpha = 0.0f;
_landscapeToolbarView.doneButton.alpha = 0.0f;
_portraitToolbarView.alpha = 0.0f;
_landscapeToolbarView.alpha = 0.0f;
}];
}
@ -1747,11 +1754,11 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
{
[self setSelectionInterfaceHidden:false animated:true];
[UIView animateWithDuration:0.3 animations:^
[UIView animateWithDuration:0.2 animations:^
{
_captionMixin.inputPanelView.alpha = 1.0f;
_portraitToolbarView.doneButton.alpha = 1.0f;
_landscapeToolbarView.doneButton.alpha = 1.0f;
_portraitToolbarView.alpha = 1.0f;
_landscapeToolbarView.alpha = 1.0f;
}];
}
@ -1861,7 +1868,7 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
break;
default:
frame = CGRectMake(screenEdges.left + 5, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 26 - _safeAreaInset.bottom - panelInset - (hasHeaderView ? 64.0 : 0.0), _muteButton.frame.size.width, _muteButton.frame.size.height);
frame = CGRectMake(screenEdges.left + 5, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 26 - _safeAreaInset.bottom - panelInset - (hasHeaderView ? 74.0 : 0.0), _muteButton.frame.size.width, _muteButton.frame.size.height);
break;
}
@ -1972,7 +1979,7 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
break;
default:
frame = CGRectMake(screenEdges.right - 46 - _safeAreaInset.right - buttonInset, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 45 - _safeAreaInset.bottom - panelInset - (hasHeaderView ? 64.0 : 0.0), 44, 44);
frame = CGRectMake(screenEdges.right - 46 - _safeAreaInset.right - buttonInset, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 50 - _safeAreaInset.bottom - panelInset - (hasHeaderView ? 64.0 : 0.0), 44, 44);
break;
}
@ -2085,6 +2092,8 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
CGFloat screenSide = MAX(screenSize.width, screenSize.height);
UIEdgeInsets screenEdges = UIEdgeInsetsZero;
_portraitToolbarView.bottomInset = _safeAreaInset.bottom;
if (TGIsPad())
{
_landscapeToolbarView.hidden = true;
@ -2118,7 +2127,7 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
_coverTitleLabel.frame = CGRectMake(screenEdges.left + floor((self.frame.size.width - _coverTitleLabel.frame.size.width) / 2.0), coverTitleTopY + 26, _coverTitleLabel.frame.size.width, _coverTitleLabel.frame.size.height);
UIEdgeInsets captionEdgeInsets = screenEdges;
captionEdgeInsets.bottom = _portraitToolbarView.frame.size.height;
captionEdgeInsets.bottom = _portraitToolbarView.frame.size.height + 10.0;
[_captionMixin updateLayoutWithFrame:self.bounds edgeInsets:captionEdgeInsets animated:false];
switch (orientation)
@ -2157,14 +2166,14 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
{
[UIView performWithoutAnimation:^
{
_photoCounterButton.frame = CGRectMake(screenEdges.right - 56 - _safeAreaInset.right, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 40 - _safeAreaInset.bottom - (hasHeaderView ? 46.0 : 0.0), 64, 38);
_photoCounterButton.frame = CGRectMake(screenEdges.right - 64 - _safeAreaInset.right, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 50 - _safeAreaInset.bottom - (hasHeaderView ? 46.0 : 0.0), 64, 38);
_selectedPhotosView.frame = CGRectMake(screenEdges.left + 4, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - photosViewSize - 54 - _safeAreaInset.bottom - (hasHeaderView ? 46.0 : 0.0), self.frame.size.width - 4 * 2 - _safeAreaInset.right, photosViewSize);
_selectedPhotosView.frame = CGRectMake(screenEdges.left + 4, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - photosViewSize - 64 - _safeAreaInset.bottom - (hasHeaderView ? 46.0 : 0.0), self.frame.size.width - 4 * 2 - _safeAreaInset.right, photosViewSize);
}];
_landscapeToolbarView.frame = CGRectMake(_landscapeToolbarView.frame.origin.x, screenEdges.top, TGPhotoEditorToolbarSize, self.frame.size.height);
_headerWrapperView.frame = CGRectMake(screenEdges.left, _portraitToolbarView.frame.origin.y - 64.0 - [_captionMixin.inputPanel baseHeight], self.frame.size.width, 72.0);
_headerWrapperView.frame = CGRectMake(screenEdges.left, _portraitToolbarView.frame.origin.y - 74.0 - [_captionMixin.inputPanel baseHeight], self.frame.size.width, 72.0);
}
break;
}

View file

@ -142,7 +142,7 @@
strongSelf.completeWithItem(item, false, 0);
};
model.interfaceView.doneLongPressed = ^(TGMediaPickerGalleryItem *item) {
model.interfaceView.doneLongPressed = ^(TGMediaPickerGalleryItem *item, UIView *sourceView) {
__strong TGMediaPickerModernGalleryMixin *strongSelf = weakSelf;
if (strongSelf == nil || !(hasSilentPosting || hasSchedule))
return;
@ -236,6 +236,19 @@
strongSelf.completeWithItem(item, false, 0);
});
};
if (sourceView != nil && strongSelf->_stickersContext.presentMediaPickerSendActionMenu != nil && strongSelf->_stickersContext.presentMediaPickerSendActionMenu(sourceView, hasSilentPosting, effectiveHasSchedule, effectiveHasSchedule, reminder, false, ^{
if (controller.sendSilently != nil)
controller.sendSilently();
}, ^{
if (controller.sendWhenOnline != nil)
controller.sendWhenOnline();
}, ^{
if (controller.schedule != nil)
controller.schedule();
}, ^{
})) {
return;
}
TGOverlayControllerWindow *controllerWindow = [[TGOverlayControllerWindow alloc] initWithManager:[strongSelf->_context makeOverlayWindowManager] parentController:strongSelf->_parentController contentController:controller];
controllerWindow.hidden = false;

View file

@ -21,6 +21,7 @@
- (void)rotate;
- (void)mirror;
- (void)aspectRatioButtonPressed;
- (void)aspectRatioButtonPressedWithSourceView:(UIView *)sourceView;
- (void)setImage:(UIImage *)image;
- (void)setSnapshotImage:(UIImage *)snapshotImage;

View file

@ -22,8 +22,6 @@
#import "TGPhotoCropView.h"
#import <LegacyComponents/TGModernButton.h>
#import <LegacyComponents/TGMenuSheetController.h>
const CGFloat TGPhotoCropButtonsWrapperSize = 61.0f;
const CGSize TGPhotoCropAreaInsetSize = { 9, 9 };
@ -535,6 +533,11 @@ NSString * const TGPhotoCropOriginalAspectRatio = @"original";
}
- (void)aspectRatioButtonPressed
{
[self aspectRatioButtonPressedWithSourceView:nil];
}
- (void)aspectRatioButtonPressedWithSourceView:(UIView *)sourceView
{
if (_cropView.isAnimating)
return;
@ -547,16 +550,11 @@ NSString * const TGPhotoCropOriginalAspectRatio = @"original";
{
[_cropView performConfirmAnimated:true];
TGMenuSheetController *controller = [[TGMenuSheetController alloc] initWithContext:_context dark:false];
controller.dismissesByOutsideTap = true;
controller.hasSwipeGesture = true;
__weak TGMenuSheetController *weakController = controller;
__weak TGPhotoCropController *weakSelf = self;
void (^action)(NSString *) = ^(NSString *ratioString)
{
__strong TGPhotoCropController *strongSelf = weakSelf;
__strong TGMenuSheetController *strongController = weakController;
if (strongSelf == nil)
return;
@ -569,7 +567,7 @@ NSString * const TGPhotoCropOriginalAspectRatio = @"original";
else
{
aspectRatio = [ratioString floatValue];
if (_cropView.cropOrientation == UIImageOrientationLeft || _cropView.cropOrientation == UIImageOrientationRight)
if (strongSelf->_cropView.cropOrientation == UIImageOrientationLeft || strongSelf->_cropView.cropOrientation == UIImageOrientationRight)
aspectRatio = 1.0f / aspectRatio;
}
@ -588,13 +586,11 @@ NSString * const TGPhotoCropOriginalAspectRatio = @"original";
else
TGDispatchAfter(0.1f, dispatch_get_main_queue(), setAspectRatioBlock);
#pragma clang diagnostic pop
[strongController dismissAnimated:true];
};
NSMutableArray *items = [[NSMutableArray alloc] init];
[items addObject:[[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"PhotoEditor.CropAspectRatioOriginal") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^{ action(TGPhotoCropOriginalAspectRatio); }]];
[items addObject:[[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"PhotoEditor.CropAspectRatioSquare") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^{ action(@"1.0"); }]];
NSMutableArray *actions = [[NSMutableArray alloc] init];
[actions addObject:[[LegacyComponentsActionSheetAction alloc] initWithTitle:TGLocalized(@"PhotoEditor.CropAspectRatioOriginal") action:TGPhotoCropOriginalAspectRatio]];
[actions addObject:[[LegacyComponentsActionSheetAction alloc] initWithTitle:TGLocalized(@"PhotoEditor.CropAspectRatioSquare") action:@"1.0"]];
CGSize croppedImageSize = _cropView.cropRect.size;
if (_cropView.cropOrientation == UIImageOrientationLeft || _cropView.cropOrientation == UIImageOrientationRight)
@ -634,26 +630,14 @@ NSString * const TGPhotoCropOriginalAspectRatio = @"original";
ratio = heightComponent / widthComponent;
[items addObject:[[TGMenuSheetButtonItemView alloc] initWithTitle:[NSString stringWithFormat:@"%d:%d", (int)widthComponent, (int)heightComponent] type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^{ action([NSString stringWithFormat:@"%f", ratio]); }]];
[actions addObject:[[LegacyComponentsActionSheetAction alloc] initWithTitle:[NSString stringWithFormat:@"%d:%d", (int)widthComponent, (int)heightComponent] action:[NSString stringWithFormat:@"%f", ratio]]];
}
[items addObject:[[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel fontSize:20.0 action:^
[_context presentActionSheet:actions view:(sourceView ?: self.view) completion:^(LegacyComponentsActionSheetAction *selectedAction)
{
__strong TGMenuSheetController *strongController = weakController;
if (strongController != nil)
[strongController dismissAnimated:true];
}]];
[controller setItemViews:items];
// controller.sourceRect = ^CGRect
// {
// __strong TGPhotoCropController *strongSelf = weakSelf;
// if (strongSelf != nil)
// return [strongSelf.view convertRect:strongSelf->_aspectRatioButton.frame fromView:strongSelf->_aspectRatioButton.superview];
//
// return CGRectZero;
// };
[controller presentInViewController:self.parentViewController sourceView:self.view animated:true];
if (selectedAction != nil)
action(selectedAction.action);
}];
}
[self _updateTabs];

View file

@ -169,25 +169,7 @@
- (void)blurButtonPressed:(TGPhotoEditorBlurTypeButton *)sender
{
// if (sender.tag != 0 && sender.tag == _currentType)
// {
// _editingIntensity = true;
// _startIntensity = [(PGBlurToolValue *)self.value intensity];
//
// PGBlurToolValue *value = [(PGBlurToolValue *)self.value copy];
// value.editingIntensity = true;
//
// _value = value;
//
// if (self.valueChanged != nil)
// self.valueChanged(value);
//
// [self setIntensitySliderHidden:false animated:true];
// }
// else
// {
[self setSelectedBlurType:(PGBlurToolType)sender.tag update:true];
// }
[self setSelectedBlurType:(PGBlurToolType)sender.tag update:true];
}
- (void)setValue:(id)value

View file

@ -27,6 +27,7 @@
#import <LegacyComponents/TGMediaVideoConverter.h>
#import <LegacyComponents/TGPhotoToolbarView.h>
#import <LegacyComponents/TGPhotoPaintStickersContext.h>
#import "TGPhotoEditorPreviewView.h"
#import <LegacyComponents/TGMenuView.h>
@ -46,11 +47,21 @@
#import "TGMediaPickerGalleryVideoScrubber.h"
#import "TGMediaPickerGalleryVideoScrubberThumbnailView.h"
#import <LegacyComponents/TGMenuSheetController.h>
#import <LegacyComponents/AVURLAsset+TGMediaItem.h>
#import <LegacyComponents/TGCameraCapturedVideo.h>
static UIView<TGPhotoToolbarViewProtocol> *TGPhotoEditorCreatePhotoToolbarView(id<LegacyComponentsContext> context, TGPhotoEditorBackButton backButton, TGPhotoEditorDoneButton doneButton, bool solidBackground, id<TGPhotoPaintStickersContext> stickersContext)
{
if (stickersContext.photoToolbarView != nil)
{
UIView<TGPhotoToolbarViewProtocol> *toolbarView = stickersContext.photoToolbarView(backButton, doneButton, solidBackground, false);
if (toolbarView != nil)
return toolbarView;
}
return [[TGPhotoToolbarView alloc] initWithContext:context backButton:backButton doneButton:doneButton solidBackground:solidBackground stickersContext:nil];
}
@interface TGPhotoEditorController () <TGViewControllerNavigationBarAppearance, TGMediaPickerGalleryVideoScrubberDataSource, TGMediaPickerGalleryVideoScrubberDelegate, UIDocumentInteractionControllerDelegate>
{
bool _switchingTab;
@ -64,8 +75,8 @@
UIView *_containerView;
UIView *_wrapperView;
UIView *_transitionWrapperView;
TGPhotoToolbarView *_portraitToolbarView;
TGPhotoToolbarView *_landscapeToolbarView;
UIView<TGPhotoToolbarViewProtocol> *_portraitToolbarView;
UIView<TGPhotoToolbarViewProtocol> *_landscapeToolbarView;
TGPhotoEditorPreviewView *_previewView;
PGPhotoEditorView *_fullPreviewView;
UIView<TGPhotoDrawingEntitiesView> *_fullEntitiesView;
@ -285,16 +296,27 @@
case TGPhotoEditorRotateTab:
case TGPhotoEditorMirrorTab:
case TGPhotoEditorAspectRatioTab:
if ([strongSelf->_currentTabController isKindOfClass:[TGPhotoCropController class]] || [strongSelf->_currentTabController isKindOfClass:[TGPhotoAvatarPreviewController class]])
[strongSelf->_currentTabController handleTabAction:tab];
break;
case TGPhotoEditorAspectRatioTab:
if ([strongSelf->_currentTabController isKindOfClass:[TGPhotoCropController class]])
{
UIView<TGPhotoToolbarViewProtocol> *toolbarView = UIInterfaceOrientationIsPortrait(strongSelf.effectiveOrientation) ? strongSelf->_portraitToolbarView : strongSelf->_landscapeToolbarView;
[(TGPhotoCropController *)strongSelf->_currentTabController aspectRatioButtonPressedWithSourceView:[toolbarView viewForTab:TGPhotoEditorAspectRatioTab]];
}
else if ([strongSelf->_currentTabController isKindOfClass:[TGPhotoAvatarPreviewController class]])
{
[strongSelf->_currentTabController handleTabAction:tab];
}
break;
}
};
TGPhotoEditorBackButton backButton = TGPhotoEditorBackButtonCancel;
TGPhotoEditorDoneButton doneButton = TGPhotoEditorDoneButtonCheck;
_portraitToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:backButton doneButton:doneButton solidBackground:true stickersContext:nil];
_portraitToolbarView = TGPhotoEditorCreatePhotoToolbarView(_context, backButton, doneButton, true, _stickersContext);
[_portraitToolbarView setToolbarTabs:_availableTabs animated:false];
[_portraitToolbarView setActiveTab:_currentTab];
_portraitToolbarView.cancelPressed = toolbarCancelPressed;
@ -303,7 +325,7 @@
_portraitToolbarView.tabPressed = toolbarTabPressed;
[_wrapperView addSubview:_portraitToolbarView];
_landscapeToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:backButton doneButton:doneButton solidBackground:true stickersContext:nil];
_landscapeToolbarView = TGPhotoEditorCreatePhotoToolbarView(_context, backButton, doneButton, true, _stickersContext);
[_landscapeToolbarView setToolbarTabs:_availableTabs animated:false];
[_landscapeToolbarView setActiveTab:_currentTab];
_landscapeToolbarView.cancelPressed = toolbarCancelPressed;
@ -1974,34 +1996,12 @@
if ((_initialAdjustments == nil && (![editorValues isDefaultValuesForAvatar:[self presentedForAvatarCreation]] || editorValues.cropOrientation != UIImageOrientationUp)) || (_initialAdjustments != nil && ![editorValues isEqual:_initialAdjustments]))
{
TGMenuSheetController *controller = [[TGMenuSheetController alloc] initWithContext:_context dark:false];
controller.dismissesByOutsideTap = true;
controller.narrowInLandscape = true;
__weak TGMenuSheetController *weakController = controller;
NSArray *items = @
NSArray *actions = @
[
[[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"PhotoEditor.DiscardChanges") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^
{
__strong TGMenuSheetController *strongController = weakController;
if (strongController == nil)
return;
[strongController dismissAnimated:true manual:false completion:^
{
dismiss();
}];
}],
[[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel fontSize:20.0 action:^
{
__strong TGMenuSheetController *strongController = weakController;
if (strongController != nil)
[strongController dismissAnimated:true];
}]
[[LegacyComponentsActionSheetAction alloc] initWithTitle:TGLocalized(@"PhotoEditor.DiscardChanges") action:@"discard" type:LegacyComponentsActionSheetActionTypeDestructive]
];
[controller setItemViews:items];
controller.sourceRect = ^
[_context presentActionSheet:actions view:self.view sourceRect:^CGRect
{
__strong TGPhotoEditorController *strongSelf = weakSelf;
if (strongSelf == nil)
@ -2011,8 +2011,11 @@
return [strongSelf.view convertRect:strongSelf->_portraitToolbarView.cancelButtonFrame fromView:strongSelf->_portraitToolbarView];
else
return [strongSelf.view convertRect:strongSelf->_landscapeToolbarView.cancelButtonFrame fromView:strongSelf->_landscapeToolbarView];
};
[controller presentInViewController:self sourceView:self.view animated:true];
} completion:^(LegacyComponentsActionSheetAction *selectedAction)
{
if ([selectedAction.action isEqualToString:@"discard"])
dismiss();
}];
}
else
{

View file

@ -30,11 +30,11 @@
+ (UIColor *)accentColor
{
TGMediaAssetsPallete *pallete = nil;
if ([[LegacyComponentsGlobals provider] respondsToSelector:@selector(mediaAssetsPallete)])
pallete = [[LegacyComponentsGlobals provider] mediaAssetsPallete];
return pallete.maybeAccentColor ?: UIColorRGB(0x65b3ff);
// TGMediaAssetsPallete *pallete = nil;
// if ([[LegacyComponentsGlobals provider] respondsToSelector:@selector(mediaAssetsPallete)])
// pallete = [[LegacyComponentsGlobals provider] mediaAssetsPallete];
//
return UIColorRGB(0xffd300); //pallete.maybeAccentColor ?: UIColorRGB(0x65b3ff);
}
+ (UIColor *)panelBackgroundColor
@ -374,7 +374,7 @@
+ (UIFont *)editorItemTitleFont
{
return [TGFont systemFontOfSize:14];
return [TGFont boldSystemFontOfSize:13]; //[TGFont systemFontOfSize:1];
}
+ (UIColor *)filterSelectionColor

View file

@ -31,6 +31,7 @@
bool _animatingCancelDoneButtons;
}
@end
@implementation TGPhotoToolbarView
@ -635,6 +636,34 @@
return nil;
}
- (UIView *)viewForTab:(TGPhotoEditorTab)tab
{
return [self buttonForTab:tab];
}
- (void)setQualityButtonIsPhoto:(bool)isPhoto highQuality:(bool)highQuality videoPreset:(NSInteger)videoPreset
{
TGPhotoEditorButton *qualityButton = [self buttonForTab:TGPhotoEditorQualityTab];
if (qualityButton == nil)
return;
if (isPhoto)
qualityButton.iconImage = [TGPhotoEditorInterfaceAssets qualityIconForHighQuality:highQuality filled:false];
else
qualityButton.iconImage = [TGPhotoEditorInterfaceAssets qualityIconForPreset:(TGMediaVideoConversionPreset)videoPreset];
}
- (void)setTimerButtonValue:(NSInteger)value
{
TGPhotoEditorButton *timerButton = [self buttonForTab:TGPhotoEditorTimerTab];
if (timerButton == nil)
return;
UIImage *defaultIcon = [TGPhotoEditorInterfaceAssets timerIconForValue:0];
UIImage *activeIcon = [TGPhotoEditorInterfaceAssets timerIconForValue:value];
[timerButton setIconImage:defaultIcon activeIconImage:activeIcon];
}
- (void)layoutSubviews
{
CGRect backgroundFrame = self.bounds;

View file

@ -236,7 +236,7 @@
[strongController dismissWhenReadyAnimated:true];
};
model.interfaceView.doneLongPressed = ^(TGMediaPickerGalleryItem *item)
model.interfaceView.doneLongPressed = ^(TGMediaPickerGalleryItem *item, UIView *sourceView)
{
__strong TGModernGalleryController *strongController = weakGalleryController;
__strong TGMediaPickerGalleryModel *strongModel = weakModel;
@ -286,6 +286,19 @@
complete(silentPosting, time);
});
};
if (sourceView != nil && stickersContext.presentMediaPickerSendActionMenu != nil && stickersContext.presentMediaPickerSendActionMenu(sourceView, hasSilentPosting, hasSchedule, hasSchedule, reminder, false, ^{
if (sendController.sendSilently != nil)
sendController.sendSilently();
}, ^{
if (sendController.sendWhenOnline != nil)
sendController.sendWhenOnline();
}, ^{
if (sendController.schedule != nil)
sendController.schedule();
}, ^{
})) {
return;
}
[strongController presentViewController:sendController animated:false completion:nil];
};

View file

@ -20,6 +20,7 @@ swift_library(
"//submodules/AccountContext:AccountContext",
"//submodules/LegacyComponents:LegacyComponents",
"//submodules/LegacyUI:LegacyUI",
"//submodules/ContextUI:ContextUI",
"//submodules/MimeTypes:MimeTypes",
"//submodules/LocalMediaResources:LocalMediaResources",
"//submodules/SearchPeerMembers:SearchPeerMembers",

View file

@ -166,6 +166,7 @@ public func legacyMediaEditor(
snapshots: [UIView],
transitionCompletion: (() -> Void)?,
getCaptionPanelView: @escaping () -> TGCaptionPanelView?,
photoToolbarView: ((TGPhotoEditorBackButton, TGPhotoEditorDoneButton, Bool, Bool) -> (UIView & TGPhotoToolbarViewProtocol)?)? = nil,
hasSilentPosting: Bool = false,
hasSchedule: Bool = false,
reminder: Bool = false,
@ -191,6 +192,7 @@ public func legacyMediaEditor(
paintStickersContext.captionPanelView = {
return getCaptionPanelView()
}
paintStickersContext.photoToolbarView = photoToolbarView
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let recipientName: String
@ -208,15 +210,28 @@ public func legacyMediaEditor(
legacyController.blocksBackgroundWhenInOverlay = true
legacyController.acceptsFocusWhenInOverlay = true
legacyController.statusBar.statusBarStyle = .Ignore
paintStickersContext.presentMediaPickerSendActionMenu = makeLegacyMediaPickerSendActionMenuPresenter(context: context, presentationData: presentationData, presentInGlobalOverlay: { [weak legacyController] controller in
if let legacyController {
legacyController.presentInGlobalOverlay(controller)
} else if let mainWindow = context.sharedContext.mainWindow {
mainWindow.presentInGlobalOverlay(controller)
} else {
context.sharedContext.presentGlobalController(controller, nil)
}
})
legacyController.controllerLoaded = { [weak legacyController] in
legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true
}
legacyController.presentationCompleted = {
Queue.mainQueue().after(0.1) {
transitionCompletion?()
}
}
let schedulePicker: (Bool, @escaping (Int32, Bool) -> Void) -> Void = { media, done in
presentSchedulePicker(media, done)
}
let appeared: () -> Void = {
transitionCompletion?()
}
let completion: (TGMediaEditableItem, TGMediaEditingContext, Bool, Int32) -> Void = { result, editingContext, silentPosting, scheduleTime in
let nativeGenerator = legacyAssetPickerItemGenerator()
@ -235,64 +250,30 @@ public func legacyMediaEditor(
legacyController.enableSizeClassSignal = true
if isGif {
let galleryController = TGPhotoVideoEditor.controller(
with: legacyController.context,
caption: initialCaption,
withItem: item,
paint: mode == .draw,
adjustments: mode == .adjustments,
recipientName: recipientName,
stickersContext: paintStickersContext,
from: .zero,
mainSnapshot: nil,
snapshots: snapshots as [Any],
immediate: transitionCompletion != nil,
activateInput: mode == .caption,
isGif: true,
hasSilentPosting: hasSilentPosting,
hasSchedule: hasSchedule,
reminder: reminder,
presentSchedulePicker: schedulePicker,
appeared: appeared,
completion: completion,
dismissed: dismissed
)
legacyController.bind(controller: galleryController)
present(legacyController, nil)
} else {
let emptyController = LegacyEmptyController(context: legacyController.context)!
emptyController.navigationBarShouldBeHidden = true
let navigationController = makeLegacyNavigationController(rootController: emptyController)
navigationController.setNavigationBarHidden(true, animated: false)
legacyController.bind(controller: navigationController)
present(legacyController, nil)
TGPhotoVideoEditor.present(
with: legacyController.context,
controller: emptyController,
caption: initialCaption,
withItem: item,
paint: mode == .draw,
adjustments: mode == .adjustments,
recipientName: recipientName,
stickersContext: paintStickersContext,
from: .zero,
mainSnapshot: nil,
snapshots: snapshots as [Any],
immediate: transitionCompletion != nil,
activateInput: mode == .caption,
isGif: false,
hasSilentPosting: hasSilentPosting,
hasSchedule: hasSchedule,
reminder: reminder,
presentSchedulePicker: schedulePicker,
appeared: appeared,
completion: completion,
dismissed: dismissed
)
}
let galleryController = TGPhotoVideoEditor.controller(
with: legacyController.context,
caption: initialCaption,
withItem: item,
paint: mode == .draw,
adjustments: mode == .adjustments,
recipientName: recipientName,
stickersContext: paintStickersContext,
from: .zero,
mainSnapshot: nil,
snapshots: snapshots as [Any],
immediate: transitionCompletion != nil,
activateInput: mode == .caption,
isGif: isGif,
hasSilentPosting: hasSilentPosting,
hasSchedule: hasSchedule,
reminder: reminder,
presentSchedulePicker: schedulePicker,
appeared: appeared,
completion: completion,
dismissed: dismissed
)
legacyController.bind(controller: galleryController)
present(legacyController, nil)
})
}
@ -389,6 +370,15 @@ public func legacyAttachmentMenu(
}
let paintStickersContext = LegacyPaintStickersContext(context: context)
paintStickersContext.presentMediaPickerSendActionMenu = makeLegacyMediaPickerSendActionMenuPresenter(context: context, presentationData: updatedPresentationData.initial, presentInGlobalOverlay: { [weak parentController] controller in
if let parentController {
parentController.presentInGlobalOverlay(controller)
} else if let mainWindow = context.sharedContext.mainWindow {
mainWindow.presentInGlobalOverlay(controller)
} else {
context.sharedContext.presentGlobalController(controller, nil)
}
})
paintStickersContext.captionPanelView = {
return getCaptionPanelView()
}

View file

@ -21,6 +21,7 @@ public func guessMimeTypeByFileExtension(_ ext: String) -> String {
public func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, context: AccountContext, peer: EngineRawPeer, chatLocation: ChatLocation, captionsEnabled: Bool = true, storeCreatedAssets: Bool = true, showFileTooltip: Bool = false, initialCaption: NSAttributedString, hasSchedule: Bool, presentWebSearch: (() -> Void)?, presentSelectionLimitExceeded: @escaping () -> Void, presentSchedulePicker: @escaping (Bool, @escaping (Int32, Bool) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?) {
let paintStickersContext = LegacyPaintStickersContext(context: context)
paintStickersContext.presentMediaPickerSendActionMenu = makeLegacyMediaPickerSendActionMenuPresenter(context: context)
paintStickersContext.captionPanelView = {
return getCaptionPanelView()
}

View file

@ -13,6 +13,7 @@ import SolidRoundedButtonNode
import MediaEditor
import DrawingUI
import TelegramPresentationData
import ContextUI
import AnimatedCountLabelNode
import CoreMedia
@ -571,15 +572,118 @@ public final class LegacyPaintEntityRenderer: NSObject, TGPhotoPaintEntityRender
}
}
public typealias LegacyMediaPickerSendActionMenuPresenter = (
UIView,
Bool,
Bool,
Bool,
Bool,
Bool,
@escaping () -> Void,
@escaping () -> Void,
@escaping () -> Void,
@escaping () -> Void
) -> Bool
private final class LegacyMediaPickerSendActionMenuReferenceContentSource: ContextReferenceContentSource {
private weak var sourceView: UIView?
init(sourceView: UIView) {
self.sourceView = sourceView
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
guard let sourceView = self.sourceView else {
return nil
}
return ContextControllerReferenceViewInfo(referenceView: sourceView, contentAreaInScreenSpace: UIScreen.main.bounds)
}
}
public func makeLegacyMediaPickerSendActionMenuPresenter(
context: AccountContext,
presentationData: PresentationData? = nil,
presentInGlobalOverlay: ((ViewController) -> Void)? = nil
) -> LegacyMediaPickerSendActionMenuPresenter {
return { sourceView, canSendSilently, canSendWhenOnline, canSchedule, reminder, hasTimer, sendSilently, sendWhenOnline, schedule, sendWithTimer in
guard sourceView.window != nil else {
return false
}
let presentationData = (presentationData ?? context.sharedContext.currentPresentationData.with { $0 }).withUpdated(theme: defaultDarkPresentationTheme)
var items: [ContextMenuItem] = []
if canSendSilently {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendSilently, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.default)
sendSilently()
})))
}
if canSendWhenOnline {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendWhenOnline, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/WhenOnlineIcon"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.default)
sendWhenOnline()
})))
}
if canSchedule {
items.append(.action(ContextMenuActionItem(text: reminder ? presentationData.strings.Conversation_SendMessage_SetReminder : presentationData.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.default)
schedule()
})))
}
if hasTimer {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_Timer_Send, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Timer"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.default)
sendWithTimer()
})))
}
guard !items.isEmpty else {
return false
}
let contextController = makeContextController(
presentationData: presentationData,
source: .reference(LegacyMediaPickerSendActionMenuReferenceContentSource(sourceView: sourceView)),
items: .single(ContextController.Items(content: .list(items))),
gesture: nil
)
if let presentInGlobalOverlay {
presentInGlobalOverlay(contextController)
} else if let mainWindow = context.sharedContext.mainWindow {
mainWindow.presentInGlobalOverlay(contextController)
} else {
context.sharedContext.presentGlobalController(contextController, nil)
}
return true
}
}
public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersContext {
public var captionPanelView: (() -> TGCaptionPanelView?)?
public var livePhotoButton: (() -> TGLivePhotoButton?)?
public var photoToolbarView: ((TGPhotoEditorBackButton, TGPhotoEditorDoneButton, Bool, Bool) -> (UIView & TGPhotoToolbarViewProtocol)?)?
public var presentMediaPickerSendActionMenu: LegacyMediaPickerSendActionMenuPresenter?
public var editCover: ((CGSize, @escaping (UIImage) -> Void) -> Void)?
private let context: AccountContext
public init(context: AccountContext) {
self.context = context
super.init()
self.presentMediaPickerSendActionMenu = makeLegacyMediaPickerSendActionMenuPresenter(context: context)
}
class LegacyDrawingAdapter: NSObject, TGPhotoDrawingAdapter {

View file

@ -14,6 +14,7 @@ swift_library(
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/TelegramCore:TelegramCore",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/ContextUI:ContextUI",
"//submodules/Display:Display",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramAudio:TelegramAudio",

View file

@ -1,5 +1,6 @@
import Foundation
import UIKit
import ContextUI
import Display
import SSignalKit
import SwiftSignalKit
@ -28,6 +29,18 @@ private func passControllerAppearanceAnimated(in: Bool, presentation: LegacyCont
}
}
private final class LegacyActionSheetContextReferenceContentSource: ContextReferenceContentSource {
private let sourceView: UIView
init(sourceView: UIView) {
self.sourceView = sourceView
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: .top)
}
}
private final class LegacyComponentsOverlayWindowManagerImpl: NSObject, LegacyComponentsOverlayWindowManager {
private weak var contentController: UIViewController?
private weak var parentController: ViewController?
@ -260,11 +273,80 @@ public final class LegacyControllerContext: NSObject, LegacyComponentsContext {
}
public func presentActionSheet(_ actions: [LegacyComponentsActionSheetAction]!, view: UIView!, completion: ((LegacyComponentsActionSheetAction?) -> Void)!) {
self.presentActionSheet(actions, view: view, sourceRect: nil, completion: completion)
}
public func presentActionSheet(_ actions: [LegacyComponentsActionSheetAction]!, view: UIView!, sourceRect: (() -> CGRect)!, completion: ((LegacyComponentsActionSheetAction?) -> Void)!) {
guard let controller = self.controller, let view = view else {
completion?(nil)
return
}
let presentationData: PresentationData
if let context = legacyContextGet() {
presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme)
} else {
presentationData = defaultPresentationData().withUpdated(theme: defaultDarkColorPresentationTheme)
}
let anchorView: UIView?
let referenceView: UIView
if let sourceRect = sourceRect {
let anchor = UIView(frame: sourceRect())
anchor.isUserInteractionEnabled = false
anchor.backgroundColor = .clear
view.addSubview(anchor)
anchorView = anchor
referenceView = anchor
} else {
anchorView = nil
referenceView = view
}
var didSelectAction = false
var items: [ContextMenuItem] = []
for legacyAction in actions ?? [] {
if legacyAction.type == LegacyComponentsActionSheetActionTypeCancel {
continue
}
guard let title = legacyAction.title else {
continue
}
let textColor: ContextMenuActionItemTextColor = legacyAction.type == LegacyComponentsActionSheetActionTypeDestructive ? .destructive : .primary
items.append(.action(ContextMenuActionItem(text: title, textColor: textColor, icon: { _ in
return nil
}, action: { actionContext in
didSelectAction = true
if let contextController = actionContext.controller {
contextController.dismiss(result: .default, completion: {
completion?(legacyAction)
})
} else {
anchorView?.removeFromSuperview()
completion?(legacyAction)
}
})))
}
if items.isEmpty {
anchorView?.removeFromSuperview()
completion?(nil)
return
}
let contextController = makeContextController(
context: legacyContextGet(),
presentationData: presentationData,
source: .reference(LegacyActionSheetContextReferenceContentSource(sourceView: referenceView)),
items: .single(ContextController.Items(content: .list(items)))
)
contextController.dismissed = { [weak anchorView] in
anchorView?.removeFromSuperview()
if !didSelectAction {
completion?(nil)
}
}
controller.present(contextController, in: .window(.root))
}
public func presentTooltip(_ text: String!, icon: UIImage!, sourceRect: CGRect) {

View file

@ -4,6 +4,10 @@ import AsyncDisplayKit
import Display
import TelegramPresentationData
private let compactInfinityFont = Font.with(size: 14.0, design: .round, weight: .bold)
private let compactTextFont = Font.with(size: 12.0, design: .round, weight: .bold)
private let compactSmallTextFont = Font.with(size: 10.0, design: .round, weight: .bold)
private let infinityFont = Font.with(size: 15.0, design: .round, weight: .bold)
private let textFont = Font.with(size: 13.0, design: .round, weight: .bold)
private let smallTextFont = Font.with(size: 11.0, design: .round, weight: .bold)
@ -134,11 +138,11 @@ public final class ChatMessageLiveLocationTimerNode: ASDisplayNode {
let font: UIFont
if parameters.string == "" {
font = infinityFont
font = bounds.width < 28.0 ? compactInfinityFont : infinityFont
} else if parameters.string.count > 2 {
font = smallTextFont
font = bounds.width < 28.0 ? compactSmallTextFont : smallTextFont
} else {
font = textFont
font = bounds.width < 28.0 ? compactTextFont : textFont
}
let attributes: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: parameters.foregroundColor]
@ -147,7 +151,7 @@ public final class ChatMessageLiveLocationTimerNode: ASDisplayNode {
var offset = CGPoint()
if parameters.string == "" {
offset = CGPoint(x: 1.0, y: -1.0)
offset = bounds.width < 28.0 ? CGPoint(x: 1.0 - UIScreenPixel, y: 0.0) : CGPoint(x: 1.0, y: -1.0)
} else if parameters.string.count > 2 {
offset = CGPoint(x: 0.0, y: UIScreenPixel)
}

View file

@ -16,6 +16,7 @@ swift_library(
"//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/LegacyComponents:LegacyComponents",
"//submodules/ContextUI:ContextUI",
"//submodules/ShareController:ShareController",
"//submodules/AccountContext:AccountContext",
"//submodules/OpenInExternalAppUI:OpenInExternalAppUI",
@ -41,14 +42,20 @@ swift_library(
"//submodules/TelegramNotices:TelegramNotices",
"//submodules/TooltipUI:TooltipUI",
"//submodules/UndoUI:UndoUI",
"//submodules/Weather",
"//submodules/AttachmentUI:AttachmentUI",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/ComponentFlow",
"//submodules/Components/BundleIconComponent",
"//submodules/Components/SheetComponent",
"//submodules/Components/ViewControllerComponent",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/TelegramUI/Components/GlassControls",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/LottieComponentResourceContent",
"//submodules/TelegramUI/Components/EdgeEffect",
"//submodules/TelegramUI/Components/SearchInputPanelComponent",
"//submodules/TelegramUI/Components/ButtonComponent",

View file

@ -141,16 +141,18 @@ final class LocationActionListItem: ListViewItem {
let title: String
let subtitle: String
let icon: LocationActionListItemIcon
let isOpaque: Bool
let beginTimeAndTimeout: (Double, Double)?
let action: () -> Void
let highlighted: (Bool) -> Void
public init(presentationData: ItemListPresentationData, engine: TelegramEngine, title: String, subtitle: String, icon: LocationActionListItemIcon, beginTimeAndTimeout: (Double, Double)?, action: @escaping () -> Void, highlighted: @escaping (Bool) -> Void = { _ in }) {
public init(presentationData: ItemListPresentationData, engine: TelegramEngine, title: String, subtitle: String, icon: LocationActionListItemIcon, isOpaque: Bool = true, beginTimeAndTimeout: (Double, Double)?, action: @escaping () -> Void, highlighted: @escaping (Bool) -> Void = { _ in }) {
self.presentationData = presentationData
self.engine = engine
self.title = title
self.subtitle = subtitle
self.icon = icon
self.isOpaque = isOpaque
self.beginTimeAndTimeout = beginTimeAndTimeout
self.action = action
self.highlighted = highlighted
@ -216,6 +218,7 @@ final class LocationActionListItemNode: ListViewItemNode {
self.separatorNode.isLayerBacked = true
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.clipsToBounds = true
self.highlightedBackgroundNode.isLayerBacked = true
self.iconNode = ASImageNode()
@ -232,6 +235,21 @@ final class LocationActionListItemNode: ListViewItemNode {
self.addSubnode(self.venueIconNode)
}
func liveLocationContextSourceView(extend: Bool) -> UIView? {
guard let icon = self.item?.icon else {
return nil
}
switch icon {
case .liveLocation:
return extend ? nil : self.view
case .extendLiveLocation:
return extend ? self.view : nil
default:
return nil
}
}
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
if let item = self.item {
let makeLayout = self.asyncLayout()
@ -278,7 +296,7 @@ final class LocationActionListItemNode: ListViewItemNode {
let iconLayout = self.venueIconNode.asyncLayout()
return { [weak self] item, params, hasSeparator in
let leftInset: CGFloat = 65.0 + params.leftInset
let leftInset: CGFloat = (item.isOpaque ? 65.0 : 72.0 ) + params.leftInset
let rightInset: CGFloat = params.rightInset
let verticalInset: CGFloat = 8.0
let iconSize: CGFloat = 40.0
@ -292,7 +310,7 @@ final class LocationActionListItemNode: ListViewItemNode {
let subtitleAttributedString = NSAttributedString(string: item.subtitle, font: subtitleFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 15.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let titleSpacing: CGFloat = 1.0
let titleSpacing: CGFloat = 0.0
let bottomInset: CGFloat = hasSeparator ? 0.0 : 4.0
var contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height + bottomInset)
if hasSeparator {
@ -300,6 +318,8 @@ final class LocationActionListItemNode: ListViewItemNode {
}
let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets())
var hasSeparator = hasSeparator
return (nodeLayout, { [weak self] in
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
@ -315,11 +335,15 @@ final class LocationActionListItemNode: ListViewItemNode {
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
if let _ = updatedTheme {
strongSelf.separatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
if item.isOpaque {
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
} else {
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.contextMenu.itemHighlightedBackgroundColor
}
}
var arguments: TransformImageCustomArguments?
@ -394,15 +418,34 @@ final class LocationActionListItemNode: ListViewItemNode {
let topHighlightInset: CGFloat = separatorHeight
let separatorRightInset: CGFloat = 16.0
let iconNodeFrame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: floorToScreenPixels((contentSize.height - bottomInset - iconSize) / 2.0)), size: CGSize(width: iconSize, height: iconSize))
let contentLeftInset: CGFloat = item.isOpaque ? 0.0 : 7.0
let iconNodeFrame = CGRect(origin: CGPoint(x: params.leftInset + 15.0 + contentLeftInset, y: floorToScreenPixels((contentSize.height - bottomInset - iconSize) / 2.0)), size: CGSize(width: iconSize, height: iconSize))
strongSelf.iconNode.frame = iconNodeFrame
strongSelf.venueIconNode.frame = iconNodeFrame
strongSelf.wavesNode?.frame = CGRect(origin: CGPoint(x: params.leftInset + 11.0, y: floorToScreenPixels((contentSize.height - bottomInset - iconSize) / 2.0) - 4.0), size: CGSize(width: 48.0, height: 48.0))
strongSelf.wavesNode?.frame = CGRect(origin: CGPoint(x: params.leftInset + 11.0 + contentLeftInset, y: floorToScreenPixels((contentSize.height - bottomInset - iconSize) / 2.0) - 4.0), size: CGSize(width: 48.0, height: 48.0))
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentSize.width, height: contentSize.height))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: contentSize.width, height: contentSize.height + topHighlightInset))
let highlightFrame: CGRect
let highlightCornerRadius: CGFloat
if item.isOpaque {
highlightFrame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: contentSize.width, height: contentSize.height + topHighlightInset))
highlightCornerRadius = 0.0
} else {
highlightFrame = CGRect(origin: CGPoint(x: 14.0, y: 2.0), size: CGSize(width: contentSize.width - 14.0 * 2.0, height: 52.0))
highlightCornerRadius = highlightFrame.height * 0.5
}
strongSelf.highlightedBackgroundNode.frame = highlightFrame
strongSelf.highlightedBackgroundNode.cornerRadius = highlightCornerRadius
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width - leftInset - params.rightInset - separatorRightInset, height: separatorHeight))
if !item.isOpaque {
hasSeparator = false
}
strongSelf.backgroundNode.isHidden = !item.isOpaque
strongSelf.separatorNode.isHidden = !hasSeparator
if let (beginTimestamp, timeout) = item.beginTimeAndTimeout {
@ -414,9 +457,9 @@ final class LocationActionListItemNode: ListViewItemNode {
strongSelf.addSubnode(timerNode)
strongSelf.timerNode = timerNode
}
let timerSize = CGSize(width: 28.0, height: 28.0)
let timerSize = CGSize(width: 24.0, height: 24.0)
timerNode.update(backgroundColor: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.4), foregroundColor: item.presentationData.theme.list.itemAccentColor, textColor: item.presentationData.theme.list.itemAccentColor, beginTimestamp: beginTimestamp, timeout: Int32(timeout) == liveLocationIndefinitePeriod ? -1.0 : timeout, strings: item.presentationData.strings)
timerNode.frame = CGRect(origin: CGPoint(x: contentSize.width - 16.0 - timerSize.width, y: floorToScreenPixels((contentSize.height - timerSize.height) / 2.0) - 2.0), size: timerSize)
timerNode.frame = CGRect(origin: CGPoint(x: contentSize.width - 15.0 - contentLeftInset - timerSize.width, y: floorToScreenPixels((contentSize.height - timerSize.height) / 2.0) - 2.0), size: timerSize)
} else if let timerNode = strongSelf.timerNode {
strongSelf.timerNode = nil
timerNode.removeFromSupernode()

View file

@ -177,6 +177,7 @@ public class LocationPinAnnotationView: MKAnnotationView {
var hasPulse = false
var headingKvoToken: NSKeyValueObservation?
private var mapHeading: CGFloat = 0.0
override public class var layerClass: AnyClass {
return LocationPinAnnotationLayer.self
@ -291,12 +292,10 @@ public class LocationPinAnnotationView: MKAnnotationView {
headingKvoToken.invalidate()
}
self.headingKvoToken = annotation.observe(\.heading, options: .new) { [weak self] (_, change) in
guard let heading = change.newValue else {
return
}
self?.updateHeading(heading)
self.headingKvoToken = annotation.observe(\.heading, options: .new) { [weak self] (_, _) in
self?.updateHeading()
}
self.updateHeading()
}
else if let peer = annotation.peer {
self.iconNode.isHidden = true
@ -310,7 +309,7 @@ public class LocationPinAnnotationView: MKAnnotationView {
self.headingKvoToken = nil
headingKvoToken.invalidate()
}
self.updateHeading(nil)
self.updateHeading()
} else if let location = annotation.location {
let venueType = location.venue?.type ?? ""
let color = venueType.isEmpty ? annotation.theme.list.itemAccentColor : venueIconColor(type: venueType)
@ -347,16 +346,23 @@ public class LocationPinAnnotationView: MKAnnotationView {
self.headingKvoToken = nil
headingKvoToken.invalidate()
}
self.updateHeading(nil)
self.updateHeading()
}
}
}
}
private func updateHeading(_ heading: NSNumber?) {
if let heading = heading?.int32Value {
func updateMapHeading(_ mapHeading: CGFloat) {
if self.mapHeading != mapHeading {
self.mapHeading = mapHeading
self.updateHeading()
}
}
private func updateHeading() {
if let heading = (self.annotation as? LocationPinAnnotation)?.heading?.doubleValue {
self.arrowNode.isHidden = false
self.arrowNode.transform = CATransform3DMakeRotation(CGFloat(heading) / 180.0 * CGFloat.pi, 0.0, 0.0, 1.0)
self.arrowNode.transform = CATransform3DMakeRotation((CGFloat(heading) - self.mapHeading) / 180.0 * CGFloat.pi, 0.0, 0.0, 1.0)
} else {
self.arrowNode.isHidden = true
self.arrowNode.transform = CATransform3DIdentity

View file

@ -7,9 +7,10 @@ import TelegramCore
import TelegramPresentationData
import ItemListUI
import LocationResources
import AppBundle
import SolidRoundedButtonNode
import ShimmerEffect
import ComponentFlow
import ButtonComponent
import BundleIconComponent
public final class LocationInfoListItem: ListViewItem {
let presentationData: ItemListPresentationData
@ -18,27 +19,35 @@ public final class LocationInfoListItem: ListViewItem {
let address: String?
let distance: String?
let drivingTime: ExpectedTravelTime
let transitTime: ExpectedTravelTime
let walkingTime: ExpectedTravelTime
let hasEta: Bool
let action: () -> Void
let drivingAction: () -> Void
let transitAction: () -> Void
let walkingAction: () -> Void
public init(presentationData: ItemListPresentationData, engine: TelegramEngine, location: TelegramMediaMap, address: String?, distance: String?, drivingTime: ExpectedTravelTime, transitTime: ExpectedTravelTime, walkingTime: ExpectedTravelTime, hasEta: Bool, action: @escaping () -> Void, drivingAction: @escaping () -> Void, transitAction: @escaping () -> Void, walkingAction: @escaping () -> Void) {
public init(
presentationData: ItemListPresentationData,
engine: TelegramEngine,
location: TelegramMediaMap,
address: String?,
distance: String?,
drivingTime: ExpectedTravelTime,
walkingTime: ExpectedTravelTime,
hasEta: Bool,
action: @escaping () -> Void,
drivingAction: @escaping () -> Void,
walkingAction: @escaping () -> Void
) {
self.presentationData = presentationData
self.engine = engine
self.location = location
self.address = address
self.distance = distance
self.drivingTime = drivingTime
self.transitTime = transitTime
self.walkingTime = walkingTime
self.hasEta = hasEta
self.action = action
self.drivingAction = drivingAction
self.transitAction = transitAction
self.walkingAction = walkingAction
}
@ -76,31 +85,26 @@ public final class LocationInfoListItem: ListViewItem {
}
public final class LocationInfoListItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private var titleNode: TextNode?
private var subtitleNode: TextNode?
private let venueIconNode: TransformImageNode
private let buttonNode: HighlightableButtonNode
private var placeholderNode: ShimmerEffectNode?
private var drivingButtonNode: SolidRoundedButtonNode?
private var transitButtonNode: SolidRoundedButtonNode?
private var walkingButtonNode: SolidRoundedButtonNode?
private let drivingButton = ComponentView<Empty>()
private let walkingButton = ComponentView<Empty>()
private var item: LocationInfoListItem?
private var layoutParams: ListViewItemLayoutParams?
private var absoluteLocation: (CGRect, CGSize)?
required public init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.buttonNode = HighlightableButtonNode()
self.venueIconNode = TransformImageNode()
self.venueIconNode.isUserInteractionEnabled = false
super.init(layerBacked: false, rotated: false, seeThrough: false)
//self.addSubnode(self.backgroundNode)
self.addSubnode(self.buttonNode)
self.addSubnode(self.venueIconNode)
@ -145,14 +149,15 @@ public final class LocationInfoListItemNode: ListViewItemNode {
let iconLayout = self.venueIconNode.asyncLayout()
return { [weak self] item, params in
let leftInset: CGFloat = 75.0 + params.leftInset
let leftInset: CGFloat = 78.0 + params.leftInset
let rightInset: CGFloat = params.rightInset
let verticalInset: CGFloat = 14.0
let iconSize: CGFloat = 48.0
let inset: CGFloat = 15.0
let iconSize: CGFloat = 40.0
let directionsButtonHeight: CGFloat = 52.0
let directionsTopInset: CGFloat = 18.0
let titleFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize)
let subtitleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0))
let titleFont = Font.semibold(item.presentationData.fontSize.itemListBaseFontSize)
let subtitleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
let title: String
let subtitle: String
@ -179,10 +184,11 @@ public final class LocationInfoListItemNode: ListViewItemNode {
let subtitleAttributedString = NSAttributedString(string: subtitle, font: subtitleFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 15.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let titleSpacing: CGFloat = 1.0
let bottomInset: CGFloat = 4.0
let titleSpacing: CGFloat = 0.0
let bottomInset: CGFloat = 16.0
let textContentSize = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height + bottomInset
let contentSize = CGSize(width: params.width, height: item.hasEta ? max(100.0, textContentSize) : textContentSize)
let etaContentSize = verticalInset + titleLayout.size.height + titleSpacing + subtitleLayout.size.height + directionsTopInset + directionsButtonHeight + bottomInset
let contentSize = CGSize(width: params.width, height: item.hasEta ? max(etaContentSize, textContentSize) : textContentSize)
let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets())
return (nodeLayout, { [weak self] in
@ -200,13 +206,7 @@ public final class LocationInfoListItemNode: ListViewItemNode {
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
if let _ = updatedTheme {
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor
}
strongSelf.backgroundNode.isHidden = params.isStandalone
let arguments = VenueIconArguments(defaultBackgroundColor: item.presentationData.theme.chat.inputPanel.actionControlFillColor, defaultForegroundColor: item.presentationData.theme.chat.inputPanel.actionControlForegroundColor)
if let updatedLocation = updatedLocation {
strongSelf.venueIconNode.setSignal(venueIcon(engine: item.engine, type: updatedLocation.venue?.type ?? "", background: true))
@ -229,107 +229,126 @@ public final class LocationInfoListItemNode: ListViewItemNode {
strongSelf.addSubnode(subtitleNode)
}
let buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme)
if strongSelf.drivingButtonNode == nil {
strongSelf.drivingButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsDriving"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0)
strongSelf.drivingButtonNode?.iconSpacing = 5.0
strongSelf.drivingButtonNode?.alpha = 0.0
strongSelf.drivingButtonNode?.allowsGroupOpacity = true
strongSelf.drivingButtonNode?.pressed = { [weak self] in
if let item = self?.item {
item.drivingAction()
}
}
strongSelf.drivingButtonNode.flatMap { strongSelf.addSubnode($0) }
strongSelf.transitButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsTransit"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0)
strongSelf.transitButtonNode?.iconSpacing = 2.0
strongSelf.transitButtonNode?.alpha = 0.0
strongSelf.transitButtonNode?.allowsGroupOpacity = true
strongSelf.transitButtonNode?.pressed = { [weak self] in
if let item = self?.item {
item.transitAction()
}
}
strongSelf.transitButtonNode.flatMap { strongSelf.addSubnode($0) }
strongSelf.walkingButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsWalking"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0)
strongSelf.walkingButtonNode?.iconSpacing = 2.0
strongSelf.walkingButtonNode?.alpha = 0.0
strongSelf.walkingButtonNode?.allowsGroupOpacity = true
strongSelf.walkingButtonNode?.pressed = { [weak self] in
if let item = self?.item {
item.walkingAction()
}
}
strongSelf.walkingButtonNode.flatMap { strongSelf.addSubnode($0) }
} else if let _ = updatedTheme {
strongSelf.drivingButtonNode?.updateTheme(buttonTheme)
strongSelf.drivingButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsDriving"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor)
strongSelf.transitButtonNode?.updateTheme(buttonTheme)
strongSelf.transitButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsTransit"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor)
strongSelf.walkingButtonNode?.updateTheme(buttonTheme)
strongSelf.walkingButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsWalking"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor)
}
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size)
titleNode.frame = titleFrame
let subtitleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset + titleLayout.size.height + titleSpacing), size: subtitleLayout.size)
subtitleNode.frame = subtitleFrame
let iconNodeFrame = CGRect(origin: CGPoint(x: params.leftInset + inset, y: 10.0), size: CGSize(width: iconSize, height: iconSize))
let iconNodeFrame = CGRect(origin: CGPoint(x: params.leftInset + 26.0, y: 14.0), size: CGSize(width: iconSize, height: iconSize))
strongSelf.venueIconNode.frame = iconNodeFrame
var directionsWidth: CGFloat = 93.0
let glassInset: CGFloat = 6.0
let buttonSideInset: CGFloat = 30.0
let buttonSpacing: CGFloat = 10.0
var directionsWidth: CGFloat = floorToScreenPixels((params.width - glassInset * 2.0 - buttonSideInset * 2.0 - buttonSpacing) / 2.0)
if item.hasEta {
if item.drivingTime == .unknown && item.transitTime == .unknown && item.walkingTime == .unknown {
strongSelf.drivingButtonNode?.icon = nil
strongSelf.drivingButtonNode?.title = item.presentationData.strings.Map_GetDirections
if let drivingButtonNode = strongSelf.drivingButtonNode {
let buttonSize = drivingButtonNode.sizeThatFits(contentSize)
directionsWidth = buttonSize.width
}
let buttonBackground = ButtonComponent.Background(
style: .glass,
color: item.presentationData.theme.list.itemCheckColors.fillColor,
foreground: item.presentationData.theme.list.itemCheckColors.foregroundColor,
pressedColor: item.presentationData.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
)
let foregroundColor = item.presentationData.theme.list.itemCheckColors.foregroundColor
var drivingButtonTitle = ""
var walkingButtonTitle = ""
var drivingButtonHasIcon = true
var drivingButtonVisible = false
var walkingButtonVisible = false
if item.drivingTime == .unknown && item.walkingTime == .unknown {
drivingButtonHasIcon = false
drivingButtonTitle = item.presentationData.strings.Map_GetDirections
drivingButtonVisible = true
if let previousDrivingTime = currentItem?.drivingTime, case .calculating = previousDrivingTime {
strongSelf.drivingButtonNode?.alpha = 1.0
strongSelf.drivingButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
strongSelf.drivingButton.view?.alpha = 1.0
strongSelf.drivingButton.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
} else {
if case let .ready(drivingTime) = item.drivingTime {
strongSelf.drivingButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: drivingTime, format: { $0 })
drivingButtonTitle = stringForEstimatedDuration(strings: item.presentationData.strings, time: drivingTime, format: { $0 }) ?? ""
drivingButtonVisible = true
if let previousDrivingTime = currentItem?.drivingTime, case .calculating = previousDrivingTime {
strongSelf.drivingButtonNode?.alpha = 1.0
strongSelf.drivingButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
if case let .ready(transitTime) = item.transitTime {
strongSelf.transitButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: transitTime, format: { $0 })
if let previousTransitTime = currentItem?.transitTime, case .calculating = previousTransitTime {
strongSelf.transitButtonNode?.alpha = 1.0
strongSelf.transitButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
strongSelf.drivingButton.view?.alpha = 1.0
strongSelf.drivingButton.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
if case let .ready(walkingTime) = item.walkingTime {
strongSelf.walkingButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: walkingTime, format: { $0 })
walkingButtonTitle = stringForEstimatedDuration(strings: item.presentationData.strings, time: walkingTime, format: { $0 }) ?? ""
walkingButtonVisible = true
if let previousWalkingTime = currentItem?.walkingTime, case .calculating = previousWalkingTime {
strongSelf.walkingButtonNode?.alpha = 1.0
strongSelf.walkingButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
strongSelf.walkingButton.view?.alpha = 1.0
strongSelf.walkingButton.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
}
let drivingButtonContent: AnyComponent<Empty>
if drivingButtonHasIcon {
drivingButtonContent = AnyComponent(
HStack([
AnyComponentWithIdentity(id: "icon", component: AnyComponent(BundleIconComponent(name: "Location/DirectionsDriving", tintColor: foregroundColor))),
AnyComponentWithIdentity(id: "title", component: AnyComponent(Text(text: drivingButtonTitle, font: Font.semibold(17.0), color: foregroundColor)))
], spacing: 5.0)
)
} else {
drivingButtonContent = AnyComponent(Text(text: drivingButtonTitle, font: Font.semibold(17.0), color: foregroundColor))
}
let directionsSpacing: CGFloat = 8.0
let drivingButtonSize = strongSelf.drivingButton.update(
transition: .immediate,
component: AnyComponent(ButtonComponent(
background: buttonBackground,
content: AnyComponentWithIdentity(
id: AnyHashable("driving-\(drivingButtonHasIcon)-\(drivingButtonTitle)"),
component: drivingButtonContent
),
action: { [weak self] in
if let item = self?.item {
item.drivingAction()
}
}
)),
environment: {},
containerSize: CGSize(width: drivingButtonHasIcon ? directionsWidth : contentSize.width - glassInset * 2.0 - buttonSideInset * 2.0, height: directionsButtonHeight)
)
if !drivingButtonHasIcon {
directionsWidth = drivingButtonSize.width
}
if case .calculating = item.drivingTime, case .calculating = item.transitTime, case .calculating = item.walkingTime {
let walkingButtonSize = strongSelf.walkingButton.update(
transition: .immediate,
component: AnyComponent(ButtonComponent(
background: buttonBackground,
content: AnyComponentWithIdentity(
id: AnyHashable("walking-\(walkingButtonTitle)"),
component: AnyComponent(
HStack([
AnyComponentWithIdentity(id: "icon", component: AnyComponent(BundleIconComponent(name: "Location/DirectionsWalking", tintColor: foregroundColor))),
AnyComponentWithIdentity(id: "title", component: AnyComponent(Text(text: walkingButtonTitle, font: Font.semibold(17.0), color: foregroundColor)))
], spacing: 2.0)
)
),
contentInsets: UIEdgeInsets(),
action: { [weak self] in
if let item = self?.item {
item.walkingAction()
}
}
)),
environment: {},
containerSize: CGSize(width: directionsWidth, height: directionsButtonHeight)
)
var buttonOrigin = glassInset + buttonSideInset
if case .calculating = item.drivingTime, case .calculating = item.walkingTime {
let shimmerNode: ShimmerEffectNode
if let current = strongSelf.placeholderNode {
shimmerNode = current
@ -338,17 +357,17 @@ public final class LocationInfoListItemNode: ListViewItemNode {
strongSelf.placeholderNode = shimmerNode
strongSelf.addSubnode(shimmerNode)
}
shimmerNode.frame = CGRect(origin: CGPoint(x: leftInset, y: subtitleFrame.maxY + 12.0), size: CGSize(width: contentSize.width - leftInset, height: 32.0))
shimmerNode.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + directionsTopInset), size: CGSize(width: contentSize.width - buttonOrigin * 2.0, height: directionsButtonHeight))
if let (rect, size) = strongSelf.absoluteLocation {
shimmerNode.updateAbsoluteRect(rect, within: size)
}
var shapes: [ShimmerEffectNode.Shape] = []
shapes.append(.roundedRectLine(startPoint: CGPoint(x: 0.0, y: 0.0), width: directionsWidth, diameter: 32.0))
shapes.append(.roundedRectLine(startPoint: CGPoint(x: directionsWidth + directionsSpacing, y: 0.0), width: directionsWidth, diameter: 32.0))
shapes.append(.roundedRectLine(startPoint: CGPoint(x: directionsWidth + directionsSpacing + directionsWidth + directionsSpacing, y: 0.0), width: directionsWidth, diameter: 32.0))
shapes.append(.roundedRectLine(startPoint: CGPoint(x: 0.0, y: 0.0), width: directionsWidth, diameter: directionsButtonHeight))
shapes.append(.roundedRectLine(startPoint: CGPoint(x: directionsWidth + buttonSpacing, y: 0.0), width: directionsWidth, diameter: directionsButtonHeight))
shapes.append(.roundedRectLine(startPoint: CGPoint(x: directionsWidth + buttonSpacing + directionsWidth + buttonSpacing, y: 0.0), width: directionsWidth, diameter: directionsButtonHeight))
shimmerNode.update(backgroundColor: item.presentationData.theme.list.plainBackgroundColor, foregroundColor: item.presentationData.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: shimmerNode.frame.size)
shimmerNode.update(backgroundColor: .clear, foregroundColor: item.presentationData.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: shimmerNode.frame.size, mask: true)
} else if let shimmerNode = strongSelf.placeholderNode {
strongSelf.placeholderNode = nil
shimmerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak shimmerNode] _ in
@ -356,30 +375,42 @@ public final class LocationInfoListItemNode: ListViewItemNode {
})
}
let drivingHeight = strongSelf.drivingButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0
let transitHeight = strongSelf.transitButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0
let walkingHeight = strongSelf.walkingButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0
var buttonOrigin = leftInset
strongSelf.drivingButtonNode?.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: drivingHeight))
let drivingButtonFrame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + directionsTopInset), size: CGSize(width: directionsWidth, height: drivingButtonSize.height))
if let drivingButtonView = strongSelf.drivingButton.view {
if drivingButtonView.superview == nil {
strongSelf.view.addSubview(drivingButtonView)
}
drivingButtonView.frame = drivingButtonFrame
if drivingButtonView.layer.animation(forKey: "opacity") == nil {
drivingButtonView.alpha = drivingButtonVisible ? 1.0 : 0.0
}
}
if case .ready = item.drivingTime {
buttonOrigin += directionsWidth + directionsSpacing
buttonOrigin += directionsWidth + buttonSpacing
}
strongSelf.transitButtonNode?.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: transitHeight))
if case .ready = item.transitTime {
buttonOrigin += directionsWidth + directionsSpacing
let walkingButtonFrame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + directionsTopInset), size: CGSize(width: directionsWidth, height: walkingButtonSize.height))
if let walkingButtonView = strongSelf.walkingButton.view {
if walkingButtonView.superview == nil {
strongSelf.view.addSubview(walkingButtonView)
}
walkingButtonView.frame = walkingButtonFrame
if walkingButtonView.layer.animation(forKey: "opacity") == nil {
walkingButtonView.alpha = walkingButtonVisible ? 1.0 : 0.0
}
}
strongSelf.walkingButtonNode?.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: walkingHeight))
} else {
strongSelf.drivingButton.view?.alpha = 0.0
strongSelf.walkingButton.view?.alpha = 0.0
if let shimmerNode = strongSelf.placeholderNode {
strongSelf.placeholderNode = nil
shimmerNode.removeFromSupernode()
}
}
strongSelf.buttonNode.frame = CGRect(x: 0.0, y: 0.0, width: contentSize.width, height: 72.0)
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentSize.width, height: contentSize.height))
}
})
})

View file

@ -10,10 +10,11 @@ import TelegramUIPreferences
import TelegramStringFormatting
import ItemListUI
import LocationResources
import AppBundle
import AvatarNode
import LiveLocationTimerNode
import SolidRoundedButtonNode
import ComponentFlow
import ButtonComponent
import BundleIconComponent
final class LocationLiveListItem: ListViewItem {
let presentationData: ItemListPresentationData
@ -24,17 +25,15 @@ final class LocationLiveListItem: ListViewItem {
let distance: Double?
let drivingTime: ExpectedTravelTime
let transitTime: ExpectedTravelTime
let walkingTime: ExpectedTravelTime
let action: () -> Void
let longTapAction: () -> Void
let drivingAction: () -> Void
let transitAction: () -> Void
let walkingAction: () -> Void
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, message: EngineMessage, distance: Double?, drivingTime: ExpectedTravelTime, transitTime: ExpectedTravelTime, walkingTime: ExpectedTravelTime, action: @escaping () -> Void, longTapAction: @escaping () -> Void = { }, drivingAction: @escaping () -> Void, transitAction: @escaping () -> Void, walkingAction: @escaping () -> Void) {
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, message: EngineMessage, distance: Double?, drivingTime: ExpectedTravelTime, walkingTime: ExpectedTravelTime, action: @escaping () -> Void, longTapAction: @escaping () -> Void = { }, drivingAction: @escaping () -> Void, walkingAction: @escaping () -> Void) {
self.presentationData = presentationData
self.dateTimeFormat = dateTimeFormat
self.nameDisplayOrder = nameDisplayOrder
@ -42,12 +41,10 @@ final class LocationLiveListItem: ListViewItem {
self.message = message
self.distance = distance
self.drivingTime = drivingTime
self.transitTime = transitTime
self.walkingTime = walkingTime
self.action = action
self.longTapAction = longTapAction
self.drivingAction = drivingAction
self.transitAction = transitAction
self.walkingAction = walkingAction
}
@ -91,28 +88,19 @@ final class LocationLiveListItem: ListViewItem {
private let avatarFont = avatarPlaceholderFont(size: floor(40.0 * 16.0 / 37.0))
final class LocationLiveListItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let separatorNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private var titleNode: TextNode?
private var subtitleNode: TextNode?
private let avatarNode: AvatarNode
private var timerNode: ChatMessageLiveLocationTimerNode?
private var drivingButtonNode: SolidRoundedButtonNode?
private var transitButtonNode: SolidRoundedButtonNode?
private var walkingButtonNode: SolidRoundedButtonNode?
private let drivingButton = ComponentView<Empty>()
private let walkingButton = ComponentView<Empty>()
private var item: LocationLiveListItem?
private var layoutParams: ListViewItemLayoutParams?
required init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.separatorNode = ASDisplayNode()
self.separatorNode.isLayerBacked = true
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
@ -121,8 +109,6 @@ final class LocationLiveListItemNode: ListViewItemNode {
super.init(layerBacked: false, rotated: false, seeThrough: false)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.separatorNode)
self.addSubnode(self.avatarNode)
}
@ -142,7 +128,7 @@ final class LocationLiveListItemNode: ListViewItemNode {
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode)
self.insertSubnode(self.highlightedBackgroundNode, at: 0)
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
@ -169,7 +155,7 @@ final class LocationLiveListItemNode: ListViewItemNode {
let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode)
return { [weak self] item, params, hasSeparator in
let leftInset: CGFloat = 65.0 + params.leftInset
let leftInset: CGFloat = 72.0 + params.leftInset
let rightInset: CGFloat = params.rightInset
let verticalInset: CGFloat = 8.0
@ -203,18 +189,17 @@ final class LocationLiveListItemNode: ListViewItemNode {
let subtitleAttributedString = NSAttributedString(string: subtitle, font: subtitleFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 54.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let titleSpacing: CGFloat = 1.0
let titleSpacing: CGFloat = 0.0
var contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height)
let hasEta: Bool
var hasEta: Bool
if case .ready = item.drivingTime {
hasEta = true
} else if case .ready = item.transitTime {
hasEta = true
} else if case .ready = item.walkingTime {
hasEta = true
} else {
hasEta = false
}
hasEta = true
if hasEta {
contentSize.height += 46.0
}
@ -232,9 +217,7 @@ final class LocationLiveListItemNode: ListViewItemNode {
strongSelf.layoutParams = params
if let _ = updatedTheme {
strongSelf.separatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.contextMenu.itemHighlightedBackgroundColor
}
let titleNode = titleApply()
@ -249,72 +232,23 @@ final class LocationLiveListItemNode: ListViewItemNode {
strongSelf.addSubnode(subtitleNode)
}
let buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme)
if strongSelf.drivingButtonNode == nil {
strongSelf.drivingButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsDriving"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0)
strongSelf.drivingButtonNode?.alpha = 0.0
strongSelf.drivingButtonNode?.iconSpacing = 5.0
strongSelf.drivingButtonNode?.allowsGroupOpacity = true
strongSelf.drivingButtonNode?.pressed = { [weak self] in
if let item = self?.item {
item.drivingAction()
}
}
strongSelf.drivingButtonNode.flatMap { strongSelf.addSubnode($0) }
strongSelf.transitButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsTransit"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0)
strongSelf.transitButtonNode?.alpha = 0.0
strongSelf.transitButtonNode?.iconSpacing = 2.0
strongSelf.transitButtonNode?.allowsGroupOpacity = true
strongSelf.transitButtonNode?.pressed = { [weak self] in
if let item = self?.item {
item.transitAction()
}
}
strongSelf.transitButtonNode.flatMap { strongSelf.addSubnode($0) }
strongSelf.walkingButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsWalking"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0)
strongSelf.walkingButtonNode?.alpha = 0.0
strongSelf.walkingButtonNode?.iconSpacing = 2.0
strongSelf.walkingButtonNode?.allowsGroupOpacity = true
strongSelf.walkingButtonNode?.pressed = { [weak self] in
if let item = self?.item {
item.walkingAction()
}
}
strongSelf.walkingButtonNode.flatMap { strongSelf.addSubnode($0) }
} else if let _ = updatedTheme {
strongSelf.drivingButtonNode?.updateTheme(buttonTheme)
strongSelf.drivingButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsDriving"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor)
strongSelf.transitButtonNode?.updateTheme(buttonTheme)
strongSelf.transitButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsTransit"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor)
strongSelf.walkingButtonNode?.updateTheme(buttonTheme)
strongSelf.walkingButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsWalking"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor)
}
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size)
titleNode.frame = titleFrame
let subtitleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset + titleLayout.size.height + titleSpacing), size: subtitleLayout.size)
subtitleNode.frame = subtitleFrame
let separatorHeight = UIScreenPixel
let topHighlightInset: CGFloat = separatorHeight
let separatorRightInset: CGFloat = 16.0
let avatarSize: CGFloat = 40.0
if let peer = item.message.author {
strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: nil, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: false)
}
strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 8.0), size: CGSize(width: avatarSize, height: avatarSize))
strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 22.0, y: 8.0), size: CGSize(width: avatarSize, height: avatarSize))
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentSize.width, height: contentSize.height))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: contentSize.width, height: contentSize.height + topHighlightInset))
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width - leftInset - params.rightInset - separatorRightInset, height: separatorHeight))
strongSelf.separatorNode.isHidden = !hasSeparator
let highlightFrame = CGRect(origin: CGPoint(x: 14.0, y: 2.0), size: CGSize(width: contentSize.width - 14.0 * 2.0, height: 52.0))
let highlightCornerRadius = highlightFrame.height * 0.5
strongSelf.highlightedBackgroundNode.frame = highlightFrame
strongSelf.highlightedBackgroundNode.cornerRadius = highlightCornerRadius
var liveBroadcastingTimeout: Int32 = 0
if let location = getLocation(from: item.message), let timeout = location.liveBroadcastingTimeout {
@ -338,61 +272,134 @@ final class LocationLiveListItemNode: ListViewItemNode {
strongSelf.addSubnode(timerNode)
strongSelf.timerNode = timerNode
}
let timerSize = CGSize(width: 28.0, height: 28.0)
let timerSize = CGSize(width: 24.0, height: 24.0)
timerNode.update(backgroundColor: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.4), foregroundColor: item.presentationData.theme.list.itemAccentColor, textColor: item.presentationData.theme.list.itemAccentColor, beginTimestamp: Double(item.message.timestamp), timeout: Int32(liveBroadcastingTimeout) == liveLocationIndefinitePeriod ? -1.0 : Double(liveBroadcastingTimeout), strings: item.presentationData.strings)
timerNode.frame = CGRect(origin: CGPoint(x: contentSize.width - 16.0 - timerSize.width, y: 14.0), size: timerSize)
timerNode.frame = CGRect(origin: CGPoint(x: contentSize.width - 26.0 - timerSize.width, y: floorToScreenPixels((56.0 - timerSize.height) / 2.0)), size: timerSize)
} else if let timerNode = strongSelf.timerNode {
strongSelf.timerNode = nil
timerNode.removeFromSupernode()
}
let buttonBackground = ButtonComponent.Background(
style: .glass,
color: item.presentationData.theme.list.itemCheckColors.fillColor,
foreground: item.presentationData.theme.list.itemCheckColors.foregroundColor,
pressedColor: item.presentationData.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
)
let foregroundColor = item.presentationData.theme.list.itemCheckColors.foregroundColor
var directionsSize = CGSize(width: 96.0, height: 36.0)
let directionsSpacing: CGFloat = 8.0
var drivingButtonTitle = ""
var drivingButtonHasIcon = true
var walkingButtonTitle = ""
var drivingButtonVisible = false
var walkingButtonVisible = false
if case let .ready(drivingTime) = item.drivingTime {
strongSelf.drivingButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: drivingTime, format: { $0 })
drivingButtonTitle = stringForEstimatedDuration(strings: item.presentationData.strings, time: drivingTime, format: { $0 }) ?? ""
drivingButtonVisible = true
if let previousDrivingTime = currentItem?.drivingTime, case .calculating = previousDrivingTime {
strongSelf.drivingButtonNode?.alpha = 1.0
strongSelf.drivingButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
strongSelf.drivingButton.view?.alpha = 1.0
strongSelf.drivingButton.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
if case let .ready(transitTime) = item.transitTime {
strongSelf.transitButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: transitTime, format: { $0 })
if let previousTransitTime = currentItem?.transitTime, case .calculating = previousTransitTime {
strongSelf.transitButtonNode?.alpha = 1.0
strongSelf.transitButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
} else {
drivingButtonVisible = true
if case .unknown = item.walkingTime {
drivingButtonHasIcon = false
drivingButtonTitle = item.presentationData.strings.Map_GetDirections
directionsSize.width = contentSize.width - leftInset * 2.0
}
}
if case let .ready(walkingTime) = item.walkingTime {
strongSelf.walkingButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: walkingTime, format: { $0 })
walkingButtonTitle = stringForEstimatedDuration(strings: item.presentationData.strings, time: walkingTime, format: { $0 }) ?? ""
walkingButtonVisible = true
if let previousWalkingTime = currentItem?.walkingTime, case .calculating = previousWalkingTime {
strongSelf.walkingButtonNode?.alpha = 1.0
strongSelf.walkingButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
strongSelf.walkingButton.view?.alpha = 1.0
strongSelf.walkingButton.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
let directionsWidth: CGFloat = 93.0
let directionsSpacing: CGFloat = 8.0
let drivingHeight = strongSelf.drivingButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0
let transitHeight = strongSelf.transitButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0
let walkingHeight = strongSelf.walkingButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0
var drivingButtonContent: [AnyComponentWithIdentity<Empty>] = []
if drivingButtonHasIcon {
drivingButtonContent.append(AnyComponentWithIdentity(id: "icon", component: AnyComponent(BundleIconComponent(name: "Location/DirectionsDriving", tintColor: foregroundColor))))
}
drivingButtonContent.append(AnyComponentWithIdentity(id: "title", component: AnyComponent(Text(text: drivingButtonTitle, font: Font.semibold(14.0), color: foregroundColor))))
let drivingButtonSize = strongSelf.drivingButton.update(
transition: .immediate,
component: AnyComponent(ButtonComponent(
background: buttonBackground,
content: AnyComponentWithIdentity(
id: AnyHashable("driving-\(drivingButtonTitle)"),
component: AnyComponent(
HStack(drivingButtonContent, spacing: 2.0)
)
),
contentInsets: UIEdgeInsets(),
action: { [weak self] in
if let item = self?.item {
item.drivingAction()
}
}
)),
environment: {},
containerSize: directionsSize
)
let walkingButtonSize = strongSelf.walkingButton.update(
transition: .immediate,
component: AnyComponent(ButtonComponent(
background: buttonBackground,
content: AnyComponentWithIdentity(
id: AnyHashable("walking-\(walkingButtonTitle)"),
component: AnyComponent(
HStack([
AnyComponentWithIdentity(id: "icon", component: AnyComponent(BundleIconComponent(name: "Location/DirectionsWalking", tintColor: foregroundColor))),
AnyComponentWithIdentity(id: "title", component: AnyComponent(Text(text: walkingButtonTitle, font: Font.semibold(14.0), color: foregroundColor)))
], spacing: 0.0)
)
),
contentInsets: UIEdgeInsets(),
action: { [weak self] in
if let item = self?.item {
item.walkingAction()
}
}
)),
environment: {},
containerSize: directionsSize
)
var buttonOrigin = leftInset
strongSelf.drivingButtonNode?.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: drivingHeight))
let drivingButtonFrame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: drivingButtonSize)
if let drivingButtonView = strongSelf.drivingButton.view {
if drivingButtonView.superview == nil {
strongSelf.view.addSubview(drivingButtonView)
}
drivingButtonView.frame = drivingButtonFrame
if drivingButtonView.layer.animation(forKey: "opacity") == nil {
drivingButtonView.alpha = drivingButtonVisible ? 1.0 : 0.0
}
}
if case .ready = item.drivingTime {
buttonOrigin += directionsWidth + directionsSpacing
buttonOrigin += directionsSize.width + directionsSpacing
}
strongSelf.transitButtonNode?.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: transitHeight))
if case .ready = item.transitTime {
buttonOrigin += directionsWidth + directionsSpacing
let walkingButtonFrame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: walkingButtonSize)
if let walkingButtonView = strongSelf.walkingButton.view {
if walkingButtonView.superview == nil {
strongSelf.view.addSubview(walkingButtonView)
}
walkingButtonView.frame = walkingButtonFrame
if walkingButtonView.layer.animation(forKey: "opacity") == nil {
walkingButtonView.alpha = walkingButtonVisible ? 1.0 : 0.0
}
}
strongSelf.walkingButtonNode?.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: walkingHeight))
}
})
})

Some files were not shown because too many files have changed in this diff Show more