mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-06-08 02:53:56 +02:00
chore: migrate to new version + fixed several critical bugs
- Migrated project to latest Telegram iOS base (v12.3.2+) - Fixed circular dependency between GhostModeManager and MiscSettingsManager - Fixed multiple Bazel build configuration errors (select() default conditions) - Fixed duplicate type definitions in PeerInfoScreen - Fixed swiftmodule directory resolution in build scripts - Added Ghostgram Settings tab in main Settings menu with all 5 features - Cleared sensitive credentials from config.json (template-only now) - Excluded bazel-cache from version control
This commit is contained in:
@@ -49,7 +49,7 @@ swift_library(
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//third-party/recaptcha:RecaptchaEnterprise",
|
||||
"//third-party/recaptcha:RecaptchaEnterpriseSDK",
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/SSignalKit/SSignalKit:SSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
@@ -153,7 +153,6 @@ swift_library(
|
||||
"//submodules/ChatListSearchItemHeader:ChatListSearchItemHeader",
|
||||
"//submodules/ItemListPeerItem:ItemListPeerItem",
|
||||
"//submodules/ContactsPeerItem:ContactsPeerItem",
|
||||
"//submodules/ChatListSearchItemNode:ChatListSearchItemNode",
|
||||
"//submodules/TelegramPermissionsUI:TelegramPermissionsUI",
|
||||
"//submodules/PeersNearbyIconNode:PeersNearbyIconNode",
|
||||
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
|
||||
@@ -498,12 +497,26 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/AttachmentFileController",
|
||||
"//submodules/TelegramUI/Components/Contacts/NewContactScreen",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu",
|
||||
"//submodules/TelegramUI/Components/NavigationBarImpl",
|
||||
"//submodules/TelegramUI/Components/GlobalControlPanelsContext",
|
||||
"//submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent",
|
||||
"//submodules/TelegramUI/Components/LiveLocationHeaderPanelComponent",
|
||||
"//submodules/TelegramUI/Components/TranslateHeaderPanelComponent",
|
||||
"//submodules/TelegramUI/Components/AdPanelHeaderPanelComponent",
|
||||
"//submodules/TelegramUI/Components/MessageFeeHeaderPanelComponent",
|
||||
"//submodules/TelegramUI/Components/LegacyChatHeaderPanelComponent",
|
||||
"//submodules/TelegramUI/Components/GroupCallHeaderPanelComponent",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatSearchNavigationContentNode",
|
||||
"//submodules/TelegramUI/Components/Settings/PasskeysScreen",
|
||||
"//submodules/TelegramUI/Components/Gifts/GiftDemoScreen",
|
||||
"//submodules/TelegramUI/Components/EmojiGameStakeScreen",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatAgeRestrictionAlertController",
|
||||
"//submodules/TelegramUI/Components/CocoonInfoScreen",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
"//build-system:ios_sim_arm64": [],
|
||||
"@build_bazel_rules_apple//apple:ios_x86_64": [],
|
||||
"//conditions:default": [],
|
||||
}),
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "AdPanelHeaderPanelComponent",
|
||||
module_name = "AdPanelHeaderPanelComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/TelegramUIPreferences",
|
||||
"//submodules/StickerResources",
|
||||
"//submodules/PhotoResources",
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/AnimatedCountLabelNode",
|
||||
"//submodules/AnimatedNavigationStripeNode",
|
||||
"//submodules/ContextUI",
|
||||
"//submodules/RadialStatusNode",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/TelegramUI/Components/TextNodeWithEntities",
|
||||
"//submodules/TranslateUI",
|
||||
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import ComponentFlow
|
||||
import ComponentDisplayAdapters
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import PresentationDataUtils
|
||||
import ContextUI
|
||||
import AsyncDisplayKit
|
||||
|
||||
public final class AdPanelHeaderPanelComponent: Component {
|
||||
public struct Info: Equatable {
|
||||
public let message: EngineMessage
|
||||
|
||||
public init(message: EngineMessage) {
|
||||
self.message = message
|
||||
}
|
||||
}
|
||||
|
||||
public let context: AccountContext
|
||||
public let theme: PresentationTheme
|
||||
public let strings: PresentationStrings
|
||||
public let info: Info
|
||||
public let action: (EngineMessage) -> Void
|
||||
public let contextAction: (EngineMessage, ASDisplayNode, ContextGesture?) -> Void
|
||||
public let close: () -> Void
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
info: Info,
|
||||
action: @escaping (EngineMessage) -> Void,
|
||||
contextAction: @escaping (EngineMessage, ASDisplayNode, ContextGesture?) -> Void,
|
||||
close: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.info = info
|
||||
self.action = action
|
||||
self.contextAction = contextAction
|
||||
self.close = close
|
||||
}
|
||||
|
||||
public static func ==(lhs: AdPanelHeaderPanelComponent, rhs: AdPanelHeaderPanelComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.info != rhs.info {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private var panel: ChatAdPanelNode?
|
||||
|
||||
private var component: AdPanelHeaderPanelComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
public var message: EngineMessage? {
|
||||
return self.component?.info.message
|
||||
}
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
func update(component: AdPanelHeaderPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let panel: ChatAdPanelNode
|
||||
if let current = self.panel {
|
||||
panel = current
|
||||
} else {
|
||||
panel = ChatAdPanelNode(
|
||||
context: component.context,
|
||||
action: component.action,
|
||||
contextAction: component.contextAction,
|
||||
close: component.close
|
||||
)
|
||||
self.panel = panel
|
||||
self.addSubview(panel.view)
|
||||
}
|
||||
|
||||
let height = panel.updateLayout(
|
||||
width: availableSize.width,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
info: component.info,
|
||||
transition: transition.containedViewLayoutTransition
|
||||
)
|
||||
let size = CGSize(width: availableSize.width, height: height)
|
||||
let panelFrame = CGRect(origin: CGPoint(), size: size)
|
||||
transition.setFrame(view: panel.view, frame: panelFrame)
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
+523
@@ -0,0 +1,523 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import StickerResources
|
||||
import PhotoResources
|
||||
import TelegramStringFormatting
|
||||
import AnimatedCountLabelNode
|
||||
import AnimatedNavigationStripeNode
|
||||
import ContextUI
|
||||
import RadialStatusNode
|
||||
import TextFormat
|
||||
import TextNodeWithEntities
|
||||
import TranslateUI
|
||||
|
||||
private enum PinnedMessageAnimation {
|
||||
case slideToTop
|
||||
case slideToBottom
|
||||
}
|
||||
|
||||
final class ChatAdPanelNode: ASDisplayNode {
|
||||
private let context: AccountContext
|
||||
private let action: (EngineMessage) -> Void
|
||||
private let contextAction: (EngineMessage, ASDisplayNode, ContextGesture?) -> Void
|
||||
private let close: () -> Void
|
||||
|
||||
private(set) var message: EngineMessage?
|
||||
|
||||
private let tapButton: HighlightTrackingButtonNode
|
||||
|
||||
private let contextContainer: ContextControllerSourceNode
|
||||
private let clippingContainer: ASDisplayNode
|
||||
private let contentContainer: ASDisplayNode
|
||||
private let contentTextContainer: ASDisplayNode
|
||||
private let adNode: TextNode
|
||||
private let titleNode: TextNode
|
||||
private let textNode: TextNodeWithEntities
|
||||
|
||||
private let removeButtonNode: HighlightTrackingButtonNode
|
||||
private let removeBackgroundNode: ASImageNode
|
||||
private let removeTextNode: ImmediateTextNode
|
||||
|
||||
private let closeButton: HighlightableButtonNode
|
||||
|
||||
private let imageNode: TransformImageNode
|
||||
private let imageNodeContainer: ASDisplayNode
|
||||
|
||||
private var currentLayout: (CGFloat, CGFloat, CGFloat)?
|
||||
private var previousMediaReference: AnyMediaReference?
|
||||
|
||||
private let fetchDisposable = MetaDisposable()
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
action: @escaping (EngineMessage) -> Void,
|
||||
contextAction: @escaping (EngineMessage, ASDisplayNode, ContextGesture?) -> Void,
|
||||
close: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.action = action
|
||||
self.contextAction = contextAction
|
||||
self.close = close
|
||||
|
||||
self.tapButton = HighlightTrackingButtonNode()
|
||||
|
||||
self.contextContainer = ContextControllerSourceNode()
|
||||
|
||||
self.clippingContainer = ASDisplayNode()
|
||||
self.clippingContainer.clipsToBounds = true
|
||||
|
||||
self.contentContainer = ASDisplayNode()
|
||||
self.contentTextContainer = ASDisplayNode()
|
||||
|
||||
self.adNode = TextNode()
|
||||
self.adNode.displaysAsynchronously = false
|
||||
self.adNode.isUserInteractionEnabled = false
|
||||
|
||||
self.removeButtonNode = HighlightTrackingButtonNode()
|
||||
self.removeBackgroundNode = ASImageNode()
|
||||
|
||||
self.removeTextNode = ImmediateTextNode()
|
||||
self.removeTextNode.displaysAsynchronously = false
|
||||
self.removeTextNode.isUserInteractionEnabled = false
|
||||
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.displaysAsynchronously = false
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
|
||||
self.textNode = TextNodeWithEntities()
|
||||
self.textNode.textNode.displaysAsynchronously = false
|
||||
self.textNode.textNode.isUserInteractionEnabled = false
|
||||
|
||||
self.imageNode = TransformImageNode()
|
||||
self.imageNode.contentAnimations = [.subsequentUpdates]
|
||||
|
||||
self.imageNodeContainer = ASDisplayNode()
|
||||
|
||||
self.closeButton = HighlightableButtonNode()
|
||||
self.closeButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0)
|
||||
self.closeButton.displaysAsynchronously = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.contextContainer)
|
||||
|
||||
self.contextContainer.addSubnode(self.clippingContainer)
|
||||
self.clippingContainer.addSubnode(self.contentContainer)
|
||||
self.contentTextContainer.addSubnode(self.titleNode)
|
||||
|
||||
self.contentTextContainer.addSubnode(self.adNode)
|
||||
|
||||
self.contentTextContainer.addSubnode(self.textNode.textNode)
|
||||
self.contentContainer.addSubnode(self.contentTextContainer)
|
||||
|
||||
self.imageNodeContainer.addSubnode(self.imageNode)
|
||||
self.contentContainer.addSubnode(self.imageNodeContainer)
|
||||
|
||||
self.tapButton.addTarget(self, action: #selector(self.tapped), forControlEvents: [.touchUpInside])
|
||||
self.tapButton.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.adNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.adNode.alpha = 0.4
|
||||
strongSelf.titleNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.titleNode.alpha = 0.4
|
||||
strongSelf.textNode.textNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.textNode.textNode.alpha = 0.4
|
||||
strongSelf.imageNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.imageNode.alpha = 0.4
|
||||
strongSelf.removeTextNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.removeTextNode.alpha = 0.4
|
||||
strongSelf.removeBackgroundNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.removeBackgroundNode.alpha = 0.4
|
||||
} else {
|
||||
strongSelf.adNode.alpha = 1.0
|
||||
strongSelf.adNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
strongSelf.titleNode.alpha = 1.0
|
||||
strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
strongSelf.textNode.textNode.alpha = 1.0
|
||||
strongSelf.textNode.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
strongSelf.imageNode.alpha = 1.0
|
||||
strongSelf.imageNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
strongSelf.removeTextNode.alpha = 1.0
|
||||
strongSelf.removeTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
strongSelf.removeBackgroundNode.alpha = 1.0
|
||||
strongSelf.removeBackgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.contextContainer.addSubnode(self.tapButton)
|
||||
|
||||
self.contextContainer.addSubnode(self.removeBackgroundNode)
|
||||
self.contextContainer.addSubnode(self.removeTextNode)
|
||||
self.contextContainer.addSubnode(self.removeButtonNode)
|
||||
|
||||
self.removeButtonNode.addTarget(self, action: #selector(self.removePressed), forControlEvents: [.touchUpInside])
|
||||
self.removeButtonNode.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.removeTextNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.removeTextNode.alpha = 0.4
|
||||
strongSelf.removeBackgroundNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.removeBackgroundNode.alpha = 0.4
|
||||
} else {
|
||||
strongSelf.removeTextNode.alpha = 1.0
|
||||
strongSelf.removeTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
strongSelf.removeBackgroundNode.alpha = 1.0
|
||||
strongSelf.removeBackgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.contextContainer.activated = { [weak self] gesture, _ in
|
||||
guard let self, let message = self.message else {
|
||||
return
|
||||
}
|
||||
self.contextAction(message, self.contextContainer, gesture)
|
||||
}
|
||||
|
||||
self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside])
|
||||
self.addSubnode(self.closeButton)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.fetchDisposable.dispose()
|
||||
}
|
||||
|
||||
private var theme: PresentationTheme?
|
||||
|
||||
@objc private func closePressed() {
|
||||
/*if self.context.isPremium, let adAttribute = self.message?.adAttribute {
|
||||
self.controllerInteraction?.removeAd(adAttribute.opaqueId)
|
||||
} else {
|
||||
self.controllerInteraction?.openNoAdsDemo()
|
||||
}*/
|
||||
self.close()
|
||||
}
|
||||
|
||||
func updateLayout(width: CGFloat, theme: PresentationTheme, strings: PresentationStrings, info: AdPanelHeaderPanelComponent.Info, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
let leftInset: CGFloat = 0.0
|
||||
let rightInset: CGFloat = 0.0
|
||||
|
||||
self.message = info.message
|
||||
|
||||
if self.theme !== theme {
|
||||
self.theme = theme
|
||||
self.removeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 15.0, color: theme.chat.inputPanel.panelControlColor.withMultipliedAlpha(0.1))
|
||||
self.removeTextNode.attributedText = NSAttributedString(string: strings.Chat_BotAd_WhatIsThis, font: Font.regular(11.0), textColor: theme.chat.inputPanel.panelControlColor)
|
||||
self.closeButton.setImage(generateImage(CGSize(width: 12.0, height: 12.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setStrokeColor(theme.chat.inputPanel.panelControlColor.cgColor)
|
||||
context.setLineWidth(1.33)
|
||||
context.setLineCap(.round)
|
||||
context.move(to: CGPoint(x: 1.0, y: 1.0))
|
||||
context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - 1.0))
|
||||
context.strokePath()
|
||||
context.move(to: CGPoint(x: size.width - 1.0, y: 1.0))
|
||||
context.addLine(to: CGPoint(x: 1.0, y: size.height - 1.0))
|
||||
context.strokePath()
|
||||
}), for: [])
|
||||
}
|
||||
|
||||
self.contextContainer.isGestureEnabled = false
|
||||
|
||||
let panelHeight: CGFloat
|
||||
var hasCloseButton = true
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 })
|
||||
panelHeight = self.enqueueTransition(width: width, leftInset: leftInset, rightInset: rightInset, transition: .immediate, animation: nil, message: info.message, theme: theme, strings: strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: self.context.account.peerId, firstTime: false, isReplyThread: false, translateToLanguage: nil)
|
||||
hasCloseButton = info.message.media.isEmpty
|
||||
|
||||
self.contextContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))
|
||||
|
||||
self.tapButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))
|
||||
|
||||
self.clippingContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))
|
||||
self.contentContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))
|
||||
|
||||
let contentRightInset: CGFloat = 14.0 + rightInset
|
||||
let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0))
|
||||
self.closeButton.frame = CGRect(origin: CGPoint(x: width - contentRightInset - closeButtonSize.width, y: floorToScreenPixels((panelHeight - closeButtonSize.height) / 2.0)), size: closeButtonSize)
|
||||
|
||||
self.closeButton.isHidden = !hasCloseButton
|
||||
|
||||
self.currentLayout = (width, leftInset, rightInset)
|
||||
|
||||
return panelHeight
|
||||
}
|
||||
|
||||
private func enqueueTransition(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, animation: PinnedMessageAnimation?, message: EngineMessage, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: PeerId, firstTime: Bool, isReplyThread: Bool, translateToLanguage: String?) -> CGFloat {
|
||||
var animationTransition: ContainedViewLayoutTransition = .immediate
|
||||
|
||||
if let animation = animation {
|
||||
animationTransition = .animated(duration: 0.2, curve: .easeInOut)
|
||||
|
||||
if let copyView = self.textNode.textNode.view.snapshotView(afterScreenUpdates: false) {
|
||||
let offset: CGFloat
|
||||
switch animation {
|
||||
case .slideToTop:
|
||||
offset = -10.0
|
||||
case .slideToBottom:
|
||||
offset = 10.0
|
||||
}
|
||||
|
||||
copyView.frame = self.textNode.textNode.frame
|
||||
self.textNode.textNode.view.superview?.addSubview(copyView)
|
||||
copyView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: offset), duration: 0.2, removeOnCompletion: false, additive: true)
|
||||
copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak copyView] _ in
|
||||
copyView?.removeFromSuperview()
|
||||
})
|
||||
self.textNode.textNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -offset), to: CGPoint(), duration: 0.2, additive: true)
|
||||
self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
let makeAdLayout = TextNode.asyncLayout(self.adNode)
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let makeTextLayout = TextNodeWithEntities.asyncLayout(self.textNode)
|
||||
let imageNodeLayout = self.imageNode.asyncLayout()
|
||||
|
||||
let previousMediaReference = self.previousMediaReference
|
||||
let context = self.context
|
||||
|
||||
let contentLeftInset: CGFloat = leftInset + 18.0
|
||||
let contentRightInset: CGFloat = rightInset + 9.0
|
||||
|
||||
var textRightInset: CGFloat = 0.0
|
||||
|
||||
var updatedMediaReference: AnyMediaReference?
|
||||
var imageDimensions: CGSize?
|
||||
|
||||
if !message._asMessage().containsSecretMedia {
|
||||
for media in message.media {
|
||||
if let image = media as? TelegramMediaImage {
|
||||
updatedMediaReference = .message(message: MessageReference(message._asMessage()), media: image)
|
||||
if let representation = largestRepresentationForPhoto(image) {
|
||||
imageDimensions = representation.dimensions.cgSize
|
||||
}
|
||||
break
|
||||
} else if let file = media as? TelegramMediaFile {
|
||||
updatedMediaReference = .message(message: MessageReference(message._asMessage()), media: file)
|
||||
if !file.isInstantVideo && !file.isSticker, let representation = largestImageRepresentation(file.previewRepresentations) {
|
||||
imageDimensions = representation.dimensions.cgSize
|
||||
} else if file.isAnimated, let dimensions = file.dimensions {
|
||||
imageDimensions = dimensions.cgSize
|
||||
}
|
||||
break
|
||||
} else if let paidContent = media as? TelegramMediaPaidContent, let firstMedia = paidContent.extendedMedia.first {
|
||||
switch firstMedia {
|
||||
case let .preview(dimensions, immediateThumbnailData, _):
|
||||
let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
|
||||
if let dimensions {
|
||||
imageDimensions = dimensions.cgSize
|
||||
}
|
||||
updatedMediaReference = .standalone(media: thumbnailMedia)
|
||||
case let .full(fullMedia):
|
||||
updatedMediaReference = .message(message: MessageReference(message._asMessage()), media: fullMedia)
|
||||
if let image = fullMedia as? TelegramMediaImage {
|
||||
if let representation = largestRepresentationForPhoto(image) {
|
||||
imageDimensions = representation.dimensions.cgSize
|
||||
}
|
||||
break
|
||||
} else if let file = fullMedia as? TelegramMediaFile {
|
||||
if let dimensions = file.dimensions {
|
||||
imageDimensions = dimensions.cgSize
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let imageBoundingSize = CGSize(width: 48.0, height: 48.0)
|
||||
var applyImage: (() -> Void)?
|
||||
if let imageDimensions {
|
||||
applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: 10.0), imageSize: imageDimensions.aspectFilled(imageBoundingSize), boundingSize: imageBoundingSize, intrinsicInsets: UIEdgeInsets()))
|
||||
textRightInset += imageBoundingSize.width + 18.0
|
||||
} else {
|
||||
textRightInset = 27.0
|
||||
}
|
||||
|
||||
var mediaUpdated = false
|
||||
if let updatedMediaReference = updatedMediaReference, let previousMediaReference = previousMediaReference {
|
||||
mediaUpdated = !updatedMediaReference.media.isEqual(to: previousMediaReference.media)
|
||||
} else if (updatedMediaReference != nil) != (previousMediaReference != nil) {
|
||||
mediaUpdated = true
|
||||
}
|
||||
|
||||
var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
|
||||
var updatedFetchMediaSignal: Signal<FetchResourceSourceType, FetchResourceError>?
|
||||
if mediaUpdated {
|
||||
if let updatedMediaReference = updatedMediaReference, imageDimensions != nil {
|
||||
if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) {
|
||||
if imageReference.media.representations.isEmpty {
|
||||
updateImageSignal = chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, ignoreFullSize: true, synchronousLoad: true)
|
||||
} else {
|
||||
updateImageSignal = chatMessagePhotoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, blurred: false)
|
||||
}
|
||||
} else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) {
|
||||
if fileReference.media.isAnimatedSticker {
|
||||
let dimensions = fileReference.media.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
updateImageSignal = chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), file: fileReference.media, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)))
|
||||
updatedFetchMediaSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(message.id.peerId), userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(fileReference.media.resource))
|
||||
} else if fileReference.media.isVideo || fileReference.media.isAnimated {
|
||||
updateImageSignal = chatMessageVideoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), fileReference: fileReference, blurred: false)
|
||||
} else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) {
|
||||
updateImageSignal = chatWebpageSnippetFile(account: context.account, userLocation: .peer(message.id.peerId), mediaReference: fileReference.abstract, representation: iconImageRepresentation)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updateImageSignal = .single({ _ in return nil })
|
||||
}
|
||||
}
|
||||
|
||||
let (adLayout, adApply) = makeAdLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Chat_BotAd_Title, font: Font.semibold(14.0), textColor: theme.chat.inputPanel.panelControlColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width, height: .greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: .zero))
|
||||
|
||||
let titleConstrainedSize = CGSize(width: width - contentLeftInset - contentRightInset - textRightInset - adLayout.size.width - 90.0, height: CGFloat.greatestFiniteMagnitude)
|
||||
let textConstrainedSize = CGSize(width: width - contentLeftInset - contentRightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude)
|
||||
|
||||
var titleText: String = ""
|
||||
if let author = message.author {
|
||||
titleText = author.compactDisplayTitle
|
||||
}
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleText, font: Font.semibold(14.0), textColor: theme.chat.inputPanel.primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: titleConstrainedSize, alignment: .natural, cutout: nil, insets: .zero))
|
||||
|
||||
let (textString, _, isText) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId)
|
||||
|
||||
let messageText: NSAttributedString
|
||||
let textFont = Font.regular(14.0)
|
||||
if isText {
|
||||
var text = message.text
|
||||
var messageEntities = message._asMessage().textEntitiesAttribute?.entities ?? []
|
||||
|
||||
if let translateToLanguage = translateToLanguage, !text.isEmpty {
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage {
|
||||
text = attribute.text
|
||||
messageEntities = attribute.entities
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let entities = messageEntities.filter { entity in
|
||||
switch entity.type {
|
||||
case .CustomEmoji:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
let textColor = theme.chat.inputPanel.primaryTextColor
|
||||
if entities.count > 0 {
|
||||
messageText = stringWithAppliedEntities(trimToLineCount(text, lineCount: 1), entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message._asMessage())
|
||||
} else {
|
||||
messageText = NSAttributedString(string: foldLineBreaks(text), font: textFont, textColor: textColor)
|
||||
}
|
||||
} else {
|
||||
messageText = NSAttributedString(string: foldLineBreaks(textString.string), font: textFont, textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor)
|
||||
}
|
||||
|
||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: .zero))
|
||||
|
||||
var panelHeight: CGFloat = 0.0
|
||||
if let _ = imageDimensions {
|
||||
panelHeight = 9.0 + imageBoundingSize.height + 9.0
|
||||
}
|
||||
|
||||
var textHeight: CGFloat
|
||||
var titleOnSeparateLine = false
|
||||
if textLayout.numberOfLines == 1 || contentLeftInset + adLayout.size.width + 2.0 + titleLayout.size.width > width - contentRightInset - textRightInset {
|
||||
textHeight = adLayout.size.height + titleLayout.size.height + textLayout.size.height + 15.0
|
||||
titleOnSeparateLine = true
|
||||
} else {
|
||||
textHeight = titleLayout.size.height + textLayout.size.height + 15.0
|
||||
}
|
||||
|
||||
panelHeight = max(panelHeight, textHeight)
|
||||
|
||||
Queue.mainQueue().async {
|
||||
let _ = adApply()
|
||||
let _ = titleApply()
|
||||
|
||||
let textArguments = TextNodeWithEntities.Arguments(
|
||||
context: self.context,
|
||||
cache: self.context.animationCache,
|
||||
renderer: self.context.animationRenderer,
|
||||
placeholderColor: theme.list.mediaPlaceholderColor,
|
||||
attemptSynchronous: false
|
||||
)
|
||||
let _ = textApply(textArguments)
|
||||
|
||||
self.previousMediaReference = updatedMediaReference
|
||||
|
||||
let textContainerFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: 0.0), size: CGSize(width: width, height: panelHeight))
|
||||
animationTransition.updateFrameAdditive(node: self.contentTextContainer, frame: textContainerFrame)
|
||||
|
||||
let removeTextSize = self.removeTextNode.updateLayout(CGSize(width: width, height: .greatestFiniteMagnitude))
|
||||
|
||||
if titleOnSeparateLine {
|
||||
self.adNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 9.0), size: adLayout.size)
|
||||
self.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 26.0), size: titleLayout.size)
|
||||
self.textNode.textNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 43.0), size: textLayout.size)
|
||||
|
||||
self.removeTextNode.frame = CGRect(origin: CGPoint(x: contentLeftInset + adLayout.size.width + 8.0, y: 11.0 - UIScreenPixel), size: removeTextSize)
|
||||
self.removeBackgroundNode.frame = self.removeTextNode.frame.insetBy(dx: -5.0, dy: -1.0)
|
||||
self.removeButtonNode.frame = self.removeBackgroundNode.frame
|
||||
} else {
|
||||
self.adNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 9.0), size: adLayout.size)
|
||||
self.titleNode.frame = CGRect(origin: CGPoint(x: adLayout.size.width + 2.0, y: 9.0), size: titleLayout.size)
|
||||
self.textNode.textNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 26.0), size: textLayout.size)
|
||||
|
||||
self.removeTextNode.frame = CGRect(origin: CGPoint(x: contentLeftInset + adLayout.size.width + 2.0 + titleLayout.size.width + 8.0, y: 11.0 - UIScreenPixel), size: removeTextSize)
|
||||
self.removeBackgroundNode.frame = self.removeTextNode.frame.insetBy(dx: -5.0, dy: -1.0)
|
||||
self.removeButtonNode.frame = self.removeBackgroundNode.frame
|
||||
}
|
||||
|
||||
self.textNode.visibilityRect = CGRect.infinite
|
||||
|
||||
self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: width - contentRightInset - imageBoundingSize.width, y: 9.0), size: imageBoundingSize)
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(), size: imageBoundingSize)
|
||||
|
||||
if let applyImage = applyImage {
|
||||
applyImage()
|
||||
|
||||
animationTransition.updateSublayerTransformScale(node: self.imageNodeContainer, scale: 1.0)
|
||||
animationTransition.updateAlpha(node: self.imageNodeContainer, alpha: 1.0, beginWithCurrentState: true)
|
||||
} else {
|
||||
animationTransition.updateSublayerTransformScale(node: self.imageNodeContainer, scale: 0.1)
|
||||
animationTransition.updateAlpha(node: self.imageNodeContainer, alpha: 0.0, beginWithCurrentState: true)
|
||||
}
|
||||
|
||||
if let updateImageSignal = updateImageSignal {
|
||||
self.imageNode.setSignal(updateImageSignal)
|
||||
}
|
||||
if let updatedFetchMediaSignal = updatedFetchMediaSignal {
|
||||
self.fetchDisposable.set(updatedFetchMediaSignal.startStrict())
|
||||
}
|
||||
}
|
||||
|
||||
return panelHeight
|
||||
}
|
||||
|
||||
@objc func tapped() {
|
||||
guard let message = self.message else {
|
||||
return
|
||||
}
|
||||
self.action(message)
|
||||
}
|
||||
|
||||
@objc func removePressed() {
|
||||
guard let message = self.message else {
|
||||
return
|
||||
}
|
||||
self.contextAction(message, self.contextContainer, nil)
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -1211,8 +1211,7 @@ private final class AdminUserActionsSheetComponent: Component {
|
||||
return
|
||||
}
|
||||
if !isEnabled {
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: environment.strings.GroupPermission_PermissionDisabledByDefault, actions: [
|
||||
self.environment?.controller()?.present(textAlertController(context: component.context, title: nil, text: environment.strings.GroupPermission_PermissionDisabledByDefault, actions: [
|
||||
TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {
|
||||
})
|
||||
]), in: .window(.root))
|
||||
|
||||
+1
-1
@@ -496,7 +496,7 @@ private final class RecentActionsSettingsSheetComponent: Component {
|
||||
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Navigation/Close",
|
||||
tintColor: environment.theme.rootController.navigationBar.glassBarButtonForegroundColor
|
||||
tintColor: environment.theme.chat.inputPanel.panelControlColor
|
||||
)
|
||||
)),
|
||||
action: { [weak self] _ in
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "AlertCheckComponent",
|
||||
module_name = "AlertCheckComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/Markdown",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
"//submodules/TelegramUI/Components/CheckComponent",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import AlertComponent
|
||||
import PlainButtonComponent
|
||||
import MultilineTextComponent
|
||||
import CheckComponent
|
||||
import TextFormat
|
||||
import Markdown
|
||||
|
||||
public final class AlertCheckComponent: Component {
|
||||
public typealias EnvironmentType = AlertComponentEnvironment
|
||||
|
||||
public class ExternalState {
|
||||
public fileprivate(set) var value: Bool
|
||||
fileprivate var valuePromise = Promise<Bool>()
|
||||
public var valueSignal: Signal<Bool, NoError>
|
||||
|
||||
public init() {
|
||||
self.value = false
|
||||
self.valueSignal = self.valuePromise.get()
|
||||
}
|
||||
}
|
||||
|
||||
let title: String
|
||||
let initialValue: Bool
|
||||
let externalState: ExternalState
|
||||
let linkAction: (() -> Void)?
|
||||
|
||||
public init(
|
||||
title: String,
|
||||
initialValue: Bool,
|
||||
externalState: ExternalState,
|
||||
linkAction: (() -> Void)? = nil
|
||||
) {
|
||||
self.title = title
|
||||
self.initialValue = initialValue
|
||||
self.externalState = externalState
|
||||
self.linkAction = linkAction
|
||||
}
|
||||
|
||||
public static func ==(lhs: AlertCheckComponent, rhs: AlertCheckComponent) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let button = ComponentView<Empty>()
|
||||
|
||||
private var component: AlertCheckComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
private var isUpdating = false
|
||||
|
||||
private var valuePromise = ValuePromise<Bool>(false)
|
||||
|
||||
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
func findTextView(view: UIView?) -> ImmediateTextView? {
|
||||
if let view {
|
||||
if let view = view as? ImmediateTextView {
|
||||
return view
|
||||
}
|
||||
for view in view.subviews {
|
||||
if let result = findTextView(view: view) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
let result = super.hitTest(point, with: event)
|
||||
if let textView = findTextView(view: result) {
|
||||
if let (_, attributes) = textView.attributesAtPoint(self.convert(point, to: textView)) {
|
||||
if attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] != nil {
|
||||
return textView
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func update(component: AlertCheckComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
if self.component == nil {
|
||||
component.externalState.value = component.initialValue
|
||||
component.externalState.valuePromise.set(self.valuePromise.get())
|
||||
}
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let environment = environment[AlertComponentEnvironment.self]
|
||||
|
||||
let checkTheme = CheckComponent.Theme(
|
||||
backgroundColor: environment.theme.list.itemCheckColors.fillColor,
|
||||
strokeColor: environment.theme.list.itemCheckColors.foregroundColor,
|
||||
borderColor: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.15),
|
||||
overlayBorder: false,
|
||||
hasInset: false,
|
||||
hasShadow: false
|
||||
)
|
||||
|
||||
let textFont = Font.regular(15.0)
|
||||
let boldTextFont = Font.semibold(15.0)
|
||||
let textColor = environment.theme.actionSheet.primaryTextColor
|
||||
let linkColor = environment.theme.actionSheet.controlAccentColor
|
||||
let markdownAttributes = MarkdownAttributes(
|
||||
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
|
||||
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
|
||||
link: MarkdownAttributeSet(font: textFont, textColor: linkColor),
|
||||
linkAttribute: { contents in
|
||||
return (TelegramTextAttributes.URL, contents)
|
||||
}
|
||||
)
|
||||
|
||||
let buttonSize = self.button.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(HStack([
|
||||
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(CheckComponent(
|
||||
theme: checkTheme,
|
||||
size: CGSize(width: 18.0, height: 18.0),
|
||||
selected: component.externalState.value
|
||||
))),
|
||||
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
|
||||
text: .markdown(text: component.title, attributes: markdownAttributes),
|
||||
maximumNumberOfLines: 2,
|
||||
highlightColor: linkColor.withAlphaComponent(0.1),
|
||||
highlightAction: { attributes in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
||||
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
tapAction: { attributes, _ in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
|
||||
component.linkAction?()
|
||||
}
|
||||
}
|
||||
)))
|
||||
], spacing: 10.0)),
|
||||
effectAlignment: .center,
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.externalState.value = !component.externalState.value
|
||||
self.valuePromise.set(component.externalState.value)
|
||||
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
},
|
||||
animateAlpha: false,
|
||||
animateScale: false
|
||||
)),
|
||||
environment: {
|
||||
},
|
||||
containerSize: CGSize(width: availableSize.width + 20.0, height: 1000.0)
|
||||
)
|
||||
let buttonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - buttonSize.width) / 2.0), y: 7.0), size: buttonSize)
|
||||
if let buttonView = self.button.view {
|
||||
if buttonView.superview == nil {
|
||||
self.addSubview(buttonView)
|
||||
}
|
||||
transition.setFrame(view: buttonView, frame: buttonFrame)
|
||||
}
|
||||
|
||||
return CGSize(width: availableSize.width, height: buttonSize.height + 7.0)
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "AlertInputFieldComponent",
|
||||
module_name = "AlertInputFieldComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
+353
@@ -0,0 +1,353 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import AlertComponent
|
||||
import MultilineTextComponent
|
||||
import AccountContext
|
||||
import TextFormat
|
||||
import PlainButtonComponent
|
||||
import BundleIconComponent
|
||||
|
||||
public final class AlertInputFieldComponent: Component {
|
||||
public typealias EnvironmentType = AlertComponentEnvironment
|
||||
|
||||
public class ExternalState {
|
||||
public fileprivate(set) var value: String = ""
|
||||
public fileprivate(set) var animateError: () -> Void = {}
|
||||
public fileprivate(set) var activateInput: () -> Void = {}
|
||||
fileprivate let valuePromise = ValuePromise<String>("")
|
||||
public var valueSignal: Signal<String, NoError> {
|
||||
return self.valuePromise.get()
|
||||
}
|
||||
|
||||
public init() {
|
||||
}
|
||||
}
|
||||
|
||||
let context: AccountContext
|
||||
let initialValue: String?
|
||||
let placeholder: String
|
||||
let characterLimit: Int?
|
||||
let hasClearButton: Bool
|
||||
let isSecureTextEntry: Bool
|
||||
let returnKeyType: UIReturnKeyType
|
||||
let keyboardType: UIKeyboardType
|
||||
let autocapitalizationType: UITextAutocapitalizationType
|
||||
let autocorrectionType: UITextAutocorrectionType
|
||||
let isInitiallyFocused: Bool
|
||||
let externalState: ExternalState
|
||||
let shouldChangeText: ((String) -> Bool)?
|
||||
let returnKeyAction: (() -> Void)?
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
initialValue: String? = nil,
|
||||
placeholder: String,
|
||||
characterLimit: Int? = nil,
|
||||
hasClearButton: Bool = false,
|
||||
isSecureTextEntry: Bool = false,
|
||||
returnKeyType: UIReturnKeyType = .done,
|
||||
keyboardType: UIKeyboardType = .default,
|
||||
autocapitalizationType: UITextAutocapitalizationType = .sentences,
|
||||
autocorrectionType: UITextAutocorrectionType = .default,
|
||||
isInitiallyFocused: Bool = false,
|
||||
externalState: ExternalState,
|
||||
shouldChangeText: ((String) -> Bool)? = nil,
|
||||
returnKeyAction: (() -> Void)? = nil
|
||||
) {
|
||||
self.context = context
|
||||
self.initialValue = initialValue
|
||||
self.placeholder = placeholder
|
||||
self.characterLimit = characterLimit
|
||||
self.hasClearButton = hasClearButton
|
||||
self.isSecureTextEntry = isSecureTextEntry
|
||||
self.returnKeyType = returnKeyType
|
||||
self.keyboardType = keyboardType
|
||||
self.autocapitalizationType = autocapitalizationType
|
||||
self.autocorrectionType = autocorrectionType
|
||||
self.isInitiallyFocused = isInitiallyFocused
|
||||
self.externalState = externalState
|
||||
self.shouldChangeText = shouldChangeText
|
||||
self.returnKeyAction = returnKeyAction
|
||||
}
|
||||
|
||||
public static func ==(lhs: AlertInputFieldComponent, rhs: AlertInputFieldComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.initialValue != rhs.initialValue {
|
||||
return false
|
||||
}
|
||||
if lhs.placeholder != rhs.placeholder {
|
||||
return false
|
||||
}
|
||||
if lhs.characterLimit != rhs.characterLimit {
|
||||
return false
|
||||
}
|
||||
if lhs.hasClearButton != rhs.hasClearButton {
|
||||
return false
|
||||
}
|
||||
if lhs.isSecureTextEntry != rhs.isSecureTextEntry {
|
||||
return false
|
||||
}
|
||||
if lhs.returnKeyType != rhs.returnKeyType {
|
||||
return false
|
||||
}
|
||||
if lhs.keyboardType != rhs.keyboardType {
|
||||
return false
|
||||
}
|
||||
if lhs.autocapitalizationType != rhs.autocapitalizationType {
|
||||
return false
|
||||
}
|
||||
if lhs.autocorrectionType != rhs.autocorrectionType {
|
||||
return false
|
||||
}
|
||||
if lhs.isInitiallyFocused != rhs.isInitiallyFocused {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private final class TextField: UITextField {
|
||||
var sideInset: CGFloat = 0.0
|
||||
|
||||
override func textRect(forBounds bounds: CGRect) -> CGRect {
|
||||
return CGRect(origin: CGPoint(x: self.sideInset, y: 0.0), size: CGSize(width: bounds.width - self.sideInset * 2.0, height: bounds.height))
|
||||
}
|
||||
|
||||
override func editingRect(forBounds bounds: CGRect) -> CGRect {
|
||||
return CGRect(origin: CGPoint(x: self.sideInset, y: 0.0), size: CGSize(width: bounds.width - self.sideInset * 2.0, height: bounds.height))
|
||||
}
|
||||
}
|
||||
|
||||
public final class View: UIView, UITextFieldDelegate {
|
||||
private let background = ComponentView<Empty>()
|
||||
private let textField = TextField()
|
||||
private let placeholder = ComponentView<Empty>()
|
||||
private let clearButton = ComponentView<Empty>()
|
||||
|
||||
private var component: AlertInputFieldComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
private var isUpdating = false
|
||||
|
||||
var currentText: String {
|
||||
return self.textField.text ?? ""
|
||||
}
|
||||
|
||||
private var clearOnce: Bool = false
|
||||
|
||||
func activateInput() {
|
||||
self.textField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
func animateError() {
|
||||
if let component = self.component, component.isInitiallyFocused {
|
||||
self.clearOnce = true
|
||||
}
|
||||
self.textField.layer.addShakeAnimation()
|
||||
|
||||
HapticFeedback().error()
|
||||
}
|
||||
|
||||
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
self.component?.returnKeyAction?()
|
||||
return false
|
||||
}
|
||||
|
||||
@objc private func textDidChange() {
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
public func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
self.clearButton.view?.isHidden = self.currentText.isEmpty
|
||||
}
|
||||
|
||||
public func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
self.clearButton.view?.isHidden = true
|
||||
}
|
||||
|
||||
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
guard let component = self.component else {
|
||||
return true
|
||||
}
|
||||
|
||||
if self.clearOnce {
|
||||
self.clearOnce = false
|
||||
if range.length > string.count {
|
||||
textField.text = ""
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
let updatedText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
|
||||
if let shouldChangeText = component.shouldChangeText {
|
||||
return shouldChangeText(updatedText)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public func setText(text: String) {
|
||||
self.textField.text = text
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .immediate, isLocal: true)
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: AlertInputFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
var resetText: String?
|
||||
if self.component == nil {
|
||||
resetText = component.initialValue
|
||||
component.externalState.animateError = { [weak self] in
|
||||
self?.animateError()
|
||||
}
|
||||
component.externalState.activateInput = { [weak self] in
|
||||
self?.activateInput()
|
||||
}
|
||||
}
|
||||
|
||||
let isFirstTime = self.component == nil
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let environment = environment[AlertComponentEnvironment.self]
|
||||
|
||||
let topInset: CGFloat = 15.0
|
||||
|
||||
if self.textField.superview == nil {
|
||||
self.addSubview(self.textField)
|
||||
self.textField.delegate = self
|
||||
self.textField.addTarget(self, action: #selector(self.textDidChange), for: .editingChanged)
|
||||
}
|
||||
if self.textField.autocapitalizationType != component.autocapitalizationType {
|
||||
self.textField.autocapitalizationType = component.autocapitalizationType
|
||||
}
|
||||
if self.textField.autocorrectionType != component.autocorrectionType {
|
||||
self.textField.autocorrectionType = component.autocorrectionType
|
||||
}
|
||||
if self.textField.isSecureTextEntry != component.isSecureTextEntry {
|
||||
self.textField.isSecureTextEntry = component.isSecureTextEntry
|
||||
}
|
||||
if self.textField.returnKeyType != component.returnKeyType {
|
||||
self.textField.returnKeyType = component.returnKeyType
|
||||
}
|
||||
self.textField.keyboardAppearance = environment.theme.overallDarkAppearance ? .dark : .light
|
||||
if let resetText {
|
||||
self.textField.text = resetText
|
||||
}
|
||||
|
||||
self.textField.font = Font.regular(17.0)
|
||||
self.textField.textColor = environment.theme.actionSheet.primaryTextColor
|
||||
self.textField.tintColor = environment.theme.actionSheet.controlAccentColor
|
||||
self.textField.sideInset = 16.0
|
||||
|
||||
let backgroundPadding: CGFloat = 14.0
|
||||
let size = CGSize(width: availableSize.width, height: 50.0)
|
||||
|
||||
let backgroundSize = self.background.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
FilledRoundedRectangleComponent(color: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1), cornerRadius: .value(25.0), smoothCorners: false)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width + backgroundPadding * 2.0, height: size.height)
|
||||
)
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - backgroundSize.width) / 2.0), y: topInset ), size: backgroundSize)
|
||||
if let backgroundView = self.background.view {
|
||||
if backgroundView.superview == nil {
|
||||
self.addSubview(backgroundView)
|
||||
}
|
||||
transition.setFrame(view: backgroundView, frame: backgroundFrame)
|
||||
}
|
||||
|
||||
let textFieldSize = CGSize(width: availableSize.width - 24.0, height: 50.0)
|
||||
let textFieldFrame = CGRect(origin: CGPoint(x: -12.0, y: topInset), size: textFieldSize)
|
||||
transition.setFrame(view: self.textField, frame: textFieldFrame)
|
||||
|
||||
let placeholderSize = self.placeholder.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(text: .plain(NSAttributedString(
|
||||
string: component.placeholder,
|
||||
font: Font.regular(17.0),
|
||||
textColor: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.4)
|
||||
)))
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width, height: 50.0)
|
||||
)
|
||||
let placeholderFrame = CGRect(origin: CGPoint(x: 4.0, y: floorToScreenPixels(textFieldFrame.midY - placeholderSize.height / 2.0)), size: placeholderSize)
|
||||
if let placeholderView = self.placeholder.view {
|
||||
if placeholderView.superview == nil {
|
||||
placeholderView.isUserInteractionEnabled = false
|
||||
self.addSubview(placeholderView)
|
||||
}
|
||||
placeholderView.frame = placeholderFrame
|
||||
placeholderView.isHidden = !self.currentText.isEmpty
|
||||
}
|
||||
|
||||
if component.hasClearButton {
|
||||
let clearButtonSize = self.clearButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(BundleIconComponent(
|
||||
name: "Components/Search Bar/Clear",
|
||||
tintColor: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.4)
|
||||
)),
|
||||
effectAlignment: .center,
|
||||
minSize: CGSize(width: 44.0, height: 44.0),
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.setText(text: "")
|
||||
},
|
||||
animateAlpha: false,
|
||||
animateScale: true
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 44.0, height: 44.0)
|
||||
)
|
||||
if let clearButtonView = self.clearButton.view {
|
||||
if clearButtonView.superview == nil {
|
||||
self.addSubview(clearButtonView)
|
||||
}
|
||||
transition.setFrame(view: clearButtonView, frame: CGRect(origin: CGPoint(x: availableSize.width - clearButtonSize.width + 11.0, y: topInset + floor((size.height - clearButtonSize.height) * 0.5)), size: clearButtonSize))
|
||||
clearButtonView.isHidden = self.currentText.isEmpty || !self.textField.isFirstResponder
|
||||
}
|
||||
} else if let clearButtonView = self.clearButton.view, clearButtonView.superview != nil {
|
||||
clearButtonView.removeFromSuperview()
|
||||
}
|
||||
|
||||
if isFirstTime && component.isInitiallyFocused {
|
||||
self.activateInput()
|
||||
}
|
||||
|
||||
component.externalState.value = self.currentText
|
||||
component.externalState.valuePromise.set(self.currentText)
|
||||
|
||||
return CGSize(width: availableSize.width, height: size.height + topInset)
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "AlertMultilineInputFieldComponent",
|
||||
module_name = "AlertMultilineInputFieldComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
"//submodules/TelegramUI/Components/TextFieldComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
+361
@@ -0,0 +1,361 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import AlertComponent
|
||||
import TextFieldComponent
|
||||
import MultilineTextComponent
|
||||
import AccountContext
|
||||
import TextFormat
|
||||
|
||||
public final class AlertMultilineInputFieldComponent: Component {
|
||||
public typealias EnvironmentType = AlertComponentEnvironment
|
||||
|
||||
public class ExternalState {
|
||||
public fileprivate(set) var value: NSAttributedString = NSAttributedString()
|
||||
public fileprivate(set) var animateError: () -> Void = {}
|
||||
public fileprivate(set) var activateInput: () -> Void = {}
|
||||
fileprivate let valuePromise = ValuePromise<NSAttributedString>(NSAttributedString())
|
||||
public var valueSignal: Signal<NSAttributedString, NoError> {
|
||||
return self.valuePromise.get()
|
||||
}
|
||||
|
||||
public var textAndEntities: (String, [MessageTextEntity]) {
|
||||
let text = self.value.string
|
||||
let entities = generateChatInputTextEntities(self.value)
|
||||
return (text, entities)
|
||||
}
|
||||
|
||||
public init() {
|
||||
}
|
||||
}
|
||||
|
||||
public enum FormatMenuAvailability: Equatable {
|
||||
public enum Action: CaseIterable {
|
||||
case bold
|
||||
case italic
|
||||
case monospace
|
||||
case link
|
||||
case strikethrough
|
||||
case underline
|
||||
case spoiler
|
||||
case quote
|
||||
case code
|
||||
|
||||
public static var all: [Action] = [
|
||||
.bold,
|
||||
.italic,
|
||||
.monospace,
|
||||
.link,
|
||||
.strikethrough,
|
||||
.underline,
|
||||
.spoiler,
|
||||
.quote,
|
||||
.code
|
||||
]
|
||||
|
||||
var textFieldValue: TextFieldComponent.FormatMenuAvailability.Action {
|
||||
switch self {
|
||||
case .bold:
|
||||
return .bold
|
||||
case .italic:
|
||||
return .italic
|
||||
case .monospace:
|
||||
return .monospace
|
||||
case .link:
|
||||
return .link
|
||||
case .strikethrough:
|
||||
return .strikethrough
|
||||
case .underline:
|
||||
return .underline
|
||||
case .spoiler:
|
||||
return .spoiler
|
||||
case .quote:
|
||||
return .quote
|
||||
case .code:
|
||||
return .code
|
||||
}
|
||||
}
|
||||
}
|
||||
case available([Action])
|
||||
case none
|
||||
|
||||
var textFieldValue: TextFieldComponent.FormatMenuAvailability {
|
||||
switch self {
|
||||
case let .available(actions):
|
||||
return .available(actions.map { $0.textFieldValue })
|
||||
case .none:
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum EmptyLineHandling {
|
||||
case allowed
|
||||
case oneConsecutive
|
||||
case notAllowed
|
||||
|
||||
var textFieldValue: TextFieldComponent.EmptyLineHandling {
|
||||
switch self {
|
||||
case .allowed:
|
||||
return .allowed
|
||||
case .oneConsecutive:
|
||||
return .oneConsecutive
|
||||
case .notAllowed:
|
||||
return .notAllowed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let context: AccountContext
|
||||
let initialValue: NSAttributedString?
|
||||
let placeholder: String
|
||||
let prefix: NSAttributedString?
|
||||
let characterLimit: Int?
|
||||
let returnKeyType: UIReturnKeyType
|
||||
let keyboardType: UIKeyboardType
|
||||
let autocapitalizationType: UITextAutocapitalizationType
|
||||
let autocorrectionType: UITextAutocorrectionType
|
||||
let formatMenuAvailability: FormatMenuAvailability
|
||||
let emptyLineHandling: EmptyLineHandling
|
||||
let isInitiallyFocused: Bool
|
||||
let externalState: ExternalState
|
||||
let present: (ViewController) -> Void
|
||||
let returnKeyAction: (() -> Void)?
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
initialValue: NSAttributedString? = nil,
|
||||
placeholder: String,
|
||||
prefix: NSAttributedString? = nil,
|
||||
characterLimit: Int? = nil,
|
||||
returnKeyType: UIReturnKeyType = .default,
|
||||
keyboardType: UIKeyboardType = .default,
|
||||
autocapitalizationType: UITextAutocapitalizationType = .sentences,
|
||||
autocorrectionType: UITextAutocorrectionType = .default,
|
||||
formatMenuAvailability: FormatMenuAvailability = .none,
|
||||
emptyLineHandling: EmptyLineHandling = .allowed,
|
||||
isInitiallyFocused: Bool = false,
|
||||
externalState: ExternalState,
|
||||
present: @escaping (ViewController) -> Void = { _ in },
|
||||
returnKeyAction: (() -> Void)? = nil
|
||||
) {
|
||||
self.context = context
|
||||
self.initialValue = initialValue
|
||||
self.placeholder = placeholder
|
||||
self.prefix = prefix
|
||||
self.characterLimit = characterLimit
|
||||
self.returnKeyType = returnKeyType
|
||||
self.keyboardType = keyboardType
|
||||
self.autocapitalizationType = autocapitalizationType
|
||||
self.autocorrectionType = autocorrectionType
|
||||
self.formatMenuAvailability = formatMenuAvailability
|
||||
self.emptyLineHandling = emptyLineHandling
|
||||
self.isInitiallyFocused = isInitiallyFocused
|
||||
self.externalState = externalState
|
||||
self.present = present
|
||||
self.returnKeyAction = returnKeyAction
|
||||
}
|
||||
|
||||
public static func ==(lhs: AlertMultilineInputFieldComponent, rhs: AlertMultilineInputFieldComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.initialValue != rhs.initialValue {
|
||||
return false
|
||||
}
|
||||
if lhs.placeholder != rhs.placeholder {
|
||||
return false
|
||||
}
|
||||
if lhs.prefix != rhs.prefix {
|
||||
return false
|
||||
}
|
||||
if lhs.returnKeyType != rhs.returnKeyType {
|
||||
return false
|
||||
}
|
||||
if lhs.characterLimit != rhs.characterLimit {
|
||||
return false
|
||||
}
|
||||
if lhs.keyboardType != rhs.keyboardType {
|
||||
return false
|
||||
}
|
||||
if lhs.autocapitalizationType != rhs.autocapitalizationType {
|
||||
return false
|
||||
}
|
||||
if lhs.autocorrectionType != rhs.autocorrectionType {
|
||||
return false
|
||||
}
|
||||
if lhs.formatMenuAvailability != rhs.formatMenuAvailability {
|
||||
return false
|
||||
}
|
||||
if lhs.emptyLineHandling != rhs.emptyLineHandling {
|
||||
return false
|
||||
}
|
||||
if lhs.isInitiallyFocused != rhs.isInitiallyFocused {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let background = ComponentView<Empty>()
|
||||
private let textField = ComponentView<Empty>()
|
||||
private let textFieldExternalState = TextFieldComponent.ExternalState()
|
||||
private let placeholder = ComponentView<Empty>()
|
||||
|
||||
private var component: AlertMultilineInputFieldComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
func activateInput() {
|
||||
if let textFieldView = self.textField.view as? TextFieldComponent.View {
|
||||
textFieldView.activateInput()
|
||||
}
|
||||
}
|
||||
|
||||
func animateError() {
|
||||
if let textFieldView = self.textField.view {
|
||||
textFieldView.layer.addShakeAnimation()
|
||||
}
|
||||
HapticFeedback().error()
|
||||
}
|
||||
|
||||
func update(component: AlertMultilineInputFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
var resetText: NSAttributedString?
|
||||
if self.component == nil {
|
||||
resetText = component.initialValue
|
||||
component.externalState.animateError = { [weak self] in
|
||||
self?.animateError()
|
||||
}
|
||||
component.externalState.activateInput = { [weak self] in
|
||||
self?.activateInput()
|
||||
}
|
||||
}
|
||||
|
||||
let isFirstTime = self.component == nil
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let environment = environment[AlertComponentEnvironment.self]
|
||||
|
||||
let topInset: CGFloat = 15.0
|
||||
let horizontalInset: CGFloat = 4.0
|
||||
let verticalInset: CGFloat = 11.0 - UIScreenPixel
|
||||
|
||||
let textFieldSize = self.textField.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(TextFieldComponent(
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
externalState: self.textFieldExternalState,
|
||||
fontSize: 17.0,
|
||||
textColor: environment.theme.actionSheet.primaryTextColor,
|
||||
accentColor: environment.theme.actionSheet.controlAccentColor,
|
||||
insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0),
|
||||
hideKeyboard: false,
|
||||
customInputView: nil,
|
||||
resetText: resetText,
|
||||
isOneLineWhenUnfocused: false,
|
||||
characterLimit: component.characterLimit,
|
||||
emptyLineHandling: component.emptyLineHandling.textFieldValue,
|
||||
formatMenuAvailability: component.formatMenuAvailability.textFieldValue,
|
||||
returnKeyType: component.returnKeyType,
|
||||
keyboardType: component.keyboardType,
|
||||
autocapitalizationType: component.autocapitalizationType,
|
||||
autocorrectionType: component.autocorrectionType,
|
||||
lockedFormatAction: {
|
||||
},
|
||||
present: { [weak self] c in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.present(c)
|
||||
},
|
||||
paste: { _ in
|
||||
},
|
||||
returnKeyAction: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.returnKeyAction?()
|
||||
},
|
||||
backspaceKeyAction: {
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width + horizontalInset * 2.0, height: availableSize.height)
|
||||
)
|
||||
component.externalState.value = self.textFieldExternalState.text
|
||||
component.externalState.valuePromise.set(component.externalState.value)
|
||||
|
||||
let backgroundPadding: CGFloat = 14.0
|
||||
let size = CGSize(width: availableSize.width, height: max(50.0, floor(textFieldSize.height + verticalInset * 2.0)))
|
||||
|
||||
let backgroundSize = self.background.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
FilledRoundedRectangleComponent(color: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1), cornerRadius: .value(25.0), smoothCorners: false)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width + backgroundPadding * 2.0, height: size.height)
|
||||
)
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - backgroundSize.width) / 2.0), y: topInset ), size: backgroundSize)
|
||||
if let backgroundView = self.background.view {
|
||||
if backgroundView.superview == nil {
|
||||
self.addSubview(backgroundView)
|
||||
}
|
||||
transition.setFrame(view: backgroundView, frame: backgroundFrame)
|
||||
}
|
||||
|
||||
let textFieldFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - textFieldSize.width) / 2.0), y: topInset + 11.0 - UIScreenPixel), size: textFieldSize)
|
||||
if let textFieldView = self.textField.view {
|
||||
if textFieldView.superview == nil {
|
||||
self.addSubview(textFieldView)
|
||||
self.textField.parentState = state
|
||||
}
|
||||
transition.setFrame(view: textFieldView, frame: textFieldFrame)
|
||||
}
|
||||
|
||||
let placeholderSize = self.placeholder.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(text: .plain(NSAttributedString(
|
||||
string: component.placeholder,
|
||||
font: Font.regular(17.0),
|
||||
textColor: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.4)
|
||||
)))
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width, height: 50.0)
|
||||
)
|
||||
let placeholderFrame = CGRect(origin: CGPoint(x: 4.0, y: floorToScreenPixels(textFieldFrame.midY - placeholderSize.height / 2.0)), size: placeholderSize)
|
||||
if let placeholderView = self.placeholder.view {
|
||||
if placeholderView.superview == nil {
|
||||
placeholderView.isUserInteractionEnabled = false
|
||||
self.addSubview(placeholderView)
|
||||
}
|
||||
placeholderView.frame = placeholderFrame
|
||||
placeholderView.isHidden = self.textFieldExternalState.hasText
|
||||
}
|
||||
|
||||
if isFirstTime && component.isInitiallyFocused {
|
||||
self.activateInput()
|
||||
}
|
||||
|
||||
return CGSize(width: availableSize.width, height: size.height + topInset)
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "AlertTableComponent",
|
||||
module_name = "AlertTableComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
"//submodules/TelegramUI/Components/Gifts/TableComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramPresentationData
|
||||
import AlertComponent
|
||||
import TableComponent
|
||||
|
||||
public final class AlertTableComponent: Component {
|
||||
public typealias EnvironmentType = AlertComponentEnvironment
|
||||
|
||||
let items: [TableComponent.Item]
|
||||
|
||||
public init(
|
||||
items: [TableComponent.Item]
|
||||
) {
|
||||
self.items = items
|
||||
}
|
||||
|
||||
public static func ==(lhs: AlertTableComponent, rhs: AlertTableComponent) -> Bool {
|
||||
if lhs.items != rhs.items {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let table = ComponentView<Empty>()
|
||||
|
||||
private var component: AlertTableComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
func update(component: AlertTableComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let environment = environment[AlertComponentEnvironment.self]
|
||||
|
||||
let tableSize = self.table.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
TableComponent(
|
||||
theme: environment.theme,
|
||||
items: component.items,
|
||||
semiTransparent: true
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width + 20.0, height: availableSize.height)
|
||||
)
|
||||
let tableFrame = CGRect(origin: CGPoint(x: -10.0, y: 5.0), size: tableSize)
|
||||
if let tableView = self.table.view {
|
||||
if tableView.superview == nil {
|
||||
self.addSubview(tableView)
|
||||
}
|
||||
transition.setFrame(view: tableView, frame: tableFrame)
|
||||
}
|
||||
return CGSize(width: availableSize.width, height: tableSize.height + 10.0)
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "AlertTransferHeaderComponent",
|
||||
module_name = "AlertTransferHeaderComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramPresentationData
|
||||
import AlertComponent
|
||||
import BundleIconComponent
|
||||
|
||||
public final class AlertTransferHeaderComponent: Component {
|
||||
public typealias EnvironmentType = AlertComponentEnvironment
|
||||
|
||||
public enum IconType {
|
||||
case transfer
|
||||
case take
|
||||
}
|
||||
|
||||
let fromComponent: AnyComponentWithIdentity<Empty>
|
||||
let toComponent: AnyComponentWithIdentity<Empty>
|
||||
let type: IconType
|
||||
|
||||
public init(
|
||||
fromComponent: AnyComponentWithIdentity<Empty>,
|
||||
toComponent: AnyComponentWithIdentity<Empty>,
|
||||
type: IconType
|
||||
) {
|
||||
self.fromComponent = fromComponent
|
||||
self.toComponent = toComponent
|
||||
self.type = type
|
||||
}
|
||||
|
||||
public static func ==(lhs: AlertTransferHeaderComponent, rhs: AlertTransferHeaderComponent) -> Bool {
|
||||
if lhs.fromComponent != rhs.fromComponent {
|
||||
return false
|
||||
}
|
||||
if lhs.toComponent != rhs.toComponent {
|
||||
return false
|
||||
}
|
||||
if lhs.type != rhs.type {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let from = ComponentView<Empty>()
|
||||
private let to = ComponentView<Empty>()
|
||||
private let arrow = ComponentView<Empty>()
|
||||
|
||||
private var component: AlertTransferHeaderComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
func update(component: AlertTransferHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let environment = environment[AlertComponentEnvironment.self]
|
||||
|
||||
let size: CGSize
|
||||
let iconName: String
|
||||
switch component.type {
|
||||
case .transfer:
|
||||
iconName = "Peer Info/AlertArrow"
|
||||
size = CGSize(width: 148.0, height: 60.0)
|
||||
case .take:
|
||||
iconName = "Media Editor/CutoutUndo"
|
||||
size = CGSize(width: 154.0, height: 60.0)
|
||||
}
|
||||
let sideInset = floorToScreenPixels((availableSize.width - size.width) / 2.0)
|
||||
|
||||
let fromSize = self.from.update(
|
||||
transition: transition,
|
||||
component: component.fromComponent.component,
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 60.0, height: 60.0)
|
||||
)
|
||||
let fromFrame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: fromSize)
|
||||
if let fromView = self.from.view {
|
||||
if fromView.superview == nil {
|
||||
self.addSubview(fromView)
|
||||
}
|
||||
transition.setFrame(view: fromView, frame: fromFrame)
|
||||
}
|
||||
|
||||
let arrowSize = self.arrow.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
BundleIconComponent(name: iconName, tintColor: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.2))
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - arrowSize.width) / 2.0), y: floorToScreenPixels((size.height - arrowSize.height) / 2.0)), size: arrowSize)
|
||||
if let arrowView = self.arrow.view {
|
||||
if arrowView.superview == nil {
|
||||
self.addSubview(arrowView)
|
||||
}
|
||||
transition.setFrame(view: arrowView, frame: arrowFrame)
|
||||
}
|
||||
|
||||
let toSize = self.to.update(
|
||||
transition: transition,
|
||||
component: component.toComponent.component,
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 60.0, height: 60.0)
|
||||
)
|
||||
let toFrame = CGRect(origin: CGPoint(x: availableSize.width - toSize.width - sideInset, y: 0.0), size: toSize)
|
||||
if let toView = self.to.view {
|
||||
if toView.superview == nil {
|
||||
self.addSubview(toView)
|
||||
}
|
||||
transition.setFrame(view: toView, frame: toFrame)
|
||||
}
|
||||
|
||||
return CGSize(width: availableSize.width, height: size.height + 11.0)
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,19 @@ swift_library(
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/Markdown",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/Components/ViewControllerComponent",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/MultilineTextWithEntitiesComponent",
|
||||
"//submodules/Components/ActivityIndicatorComponent",
|
||||
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import AccountContext
|
||||
import TelegramPresentationData
|
||||
import MultilineTextComponent
|
||||
import GlassBackgroundComponent
|
||||
import ActivityIndicatorComponent
|
||||
|
||||
private let titleFont = Font.medium(17.0)
|
||||
private let boldTitleFont = Font.semibold(17.0)
|
||||
|
||||
final class AlertActionComponent: Component {
|
||||
typealias EnvironmentType = AlertComponentEnvironment
|
||||
|
||||
static let actionHeight: CGFloat = 48.0
|
||||
|
||||
struct Theme: Equatable {
|
||||
enum Font {
|
||||
case regular
|
||||
case bold
|
||||
}
|
||||
|
||||
let background: UIColor
|
||||
let foreground: UIColor
|
||||
let secondary: UIColor
|
||||
let font: Font
|
||||
}
|
||||
|
||||
let theme: Theme
|
||||
let title: String
|
||||
let isHighlighted: Bool
|
||||
let isEnabled: Signal<Bool, NoError>
|
||||
let progress: Signal<Bool, NoError>
|
||||
|
||||
init(
|
||||
theme: Theme,
|
||||
title: String,
|
||||
isHighlighted: Bool,
|
||||
isEnabled: Signal<Bool, NoError>,
|
||||
progress: Signal<Bool, NoError>
|
||||
) {
|
||||
self.theme = theme
|
||||
self.title = title
|
||||
self.isHighlighted = isHighlighted
|
||||
self.isEnabled = isEnabled
|
||||
self.progress = progress
|
||||
}
|
||||
|
||||
static func ==(lhs: AlertActionComponent, rhs: AlertActionComponent) -> Bool {
|
||||
if lhs.theme != rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.isHighlighted != rhs.isHighlighted {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let backgroundView = UIView()
|
||||
private let title = ComponentView<Empty>()
|
||||
private var activity: ComponentView<Empty>?
|
||||
|
||||
private var component: AlertActionComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
private var isEnabledDisposable: Disposable?
|
||||
private var isEnabled = true
|
||||
|
||||
private var progressDisposable: Disposable?
|
||||
private var hasProgress = false
|
||||
|
||||
private var isUpdating = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.backgroundView.clipsToBounds = true
|
||||
self.addSubview(self.backgroundView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.isEnabledDisposable?.dispose()
|
||||
self.progressDisposable?.dispose()
|
||||
}
|
||||
|
||||
func update(component: AlertActionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
if self.component == nil {
|
||||
self.isEnabledDisposable = (component.isEnabled
|
||||
|> deliverOnMainQueue).start(next: { [weak self] isEnabled in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.isEnabled = isEnabled
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.25))
|
||||
}
|
||||
})
|
||||
|
||||
self.progressDisposable = (component.progress
|
||||
|> deliverOnMainQueue).start(next: { [weak self] hasProgress in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.hasProgress = hasProgress
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.25))
|
||||
}
|
||||
})
|
||||
}
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let attributedString = NSMutableAttributedString(string: component.title, font: component.theme.font == .bold ? boldTitleFont : titleFont, textColor: .white, paragraphAlignment: .center)
|
||||
if let range = attributedString.string.range(of: "$") {
|
||||
attributedString.addAttribute(.attachment, value: UIImage(bundleImageName: "Item List/PremiumIcon")!, range: NSRange(range, in: attributedString.string))
|
||||
attributedString.addAttribute(.foregroundColor, value: UIColor.white, range: NSRange(range, in: attributedString.string))
|
||||
attributedString.addAttribute(.baselineOffset, value: 2.0, range: NSRange(range, in: attributedString.string))
|
||||
}
|
||||
|
||||
let titlePadding: CGFloat = 16.0
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(attributedString),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 1,
|
||||
tintColor: component.theme.foreground
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - titlePadding * 2.0, height: availableSize.height)
|
||||
)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
titleView.bounds = CGRect(origin: .zero, size: titleSize)
|
||||
transition.setAlpha(view: titleView, alpha: self.hasProgress ? 0.0 : 1.0)
|
||||
}
|
||||
|
||||
if self.hasProgress {
|
||||
let activity: ComponentView<Empty>
|
||||
if let current = self.activity {
|
||||
activity = current
|
||||
} else {
|
||||
activity = ComponentView()
|
||||
self.activity = activity
|
||||
}
|
||||
let activitySize = CGSize(width: 18.0, height: 18.0)
|
||||
let _ = activity.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ActivityIndicatorComponent(color: component.theme.secondary)),
|
||||
environment: {},
|
||||
containerSize: activitySize
|
||||
)
|
||||
if let activityView = activity.view {
|
||||
activityView.bounds = CGRect(origin: .zero, size: activitySize)
|
||||
}
|
||||
} else if let activity = self.activity {
|
||||
self.activity = nil
|
||||
if let activityView = activity.view {
|
||||
transition.setAlpha(view: activityView, alpha: 0.0, completion: { _ in
|
||||
activityView.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let buttonAlpha: CGFloat
|
||||
if self.isEnabled {
|
||||
buttonAlpha = component.isHighlighted ? 0.35 : 1.0
|
||||
} else {
|
||||
buttonAlpha = 0.2
|
||||
}
|
||||
|
||||
transition.setBackgroundColor(view: self.backgroundView, color: component.theme.background)
|
||||
transition.setAlpha(view: self.backgroundView, alpha: buttonAlpha)
|
||||
self.backgroundView.layer.cornerRadius = availableSize.height * 0.5
|
||||
|
||||
return CGSize(width: titleSize.width + titlePadding * 2.0, height: availableSize.height)
|
||||
}
|
||||
|
||||
func applySize(size: CGSize, transition: ComponentTransition) {
|
||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: size))
|
||||
|
||||
if let titleView = self.title.view {
|
||||
let titleSize = titleView.bounds.size
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize)
|
||||
transition.setFrame(view: titleView, frame: titleFrame)
|
||||
}
|
||||
|
||||
if let activityView = self.activity?.view {
|
||||
var activityTransition = transition
|
||||
if activityView.superview == nil {
|
||||
self.addSubview(activityView)
|
||||
transition.animateAlpha(view: activityView, from: 0.0, to: 1.0)
|
||||
activityTransition = .immediate
|
||||
}
|
||||
let activitySize = activityView.bounds.size
|
||||
let activityFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - activitySize.width) / 2.0), y: floorToScreenPixels((size.height - activitySize.height) / 2.0)), size: activitySize)
|
||||
activityTransition.setPosition(view: activityView, position: activityFrame.center)
|
||||
activityView.transform = CGAffineTransformMakeScale(0.7, 0.7)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,366 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import MultilineTextComponent
|
||||
import MultilineTextWithEntitiesComponent
|
||||
import Markdown
|
||||
import TextFormat
|
||||
import AccountContext
|
||||
|
||||
private let titleFont = Font.bold(17.0)
|
||||
private let defaultTextFont = Font.regular(15.0)
|
||||
private let defaultBoldTextFont = Font.semibold(15.0)
|
||||
private let defaultItalicTextFont = Font.italic(15.0)
|
||||
private let defaultBoldItalicTextFont = Font.with(size: 15.0, weight: .semibold, traits: [.italic])
|
||||
private let defaultFixedTextFont = Font.monospace(15.0)
|
||||
private let smallTextFont = Font.regular(14.0)
|
||||
private let smallBoldTextFont = Font.semibold(14.0)
|
||||
private let smallItalicTextFont = Font.italic(14.0)
|
||||
private let smallBoldItalicTextFont = Font.with(size: 14.0, weight: .semibold, traits: [.italic])
|
||||
private let smallFixedTextFont = Font.monospace(14.0)
|
||||
private let backgroundInset: CGFloat = 8.0
|
||||
|
||||
public final class AlertTitleComponent: Component {
|
||||
public typealias EnvironmentType = AlertComponentEnvironment
|
||||
|
||||
public enum Alignment {
|
||||
case `default`
|
||||
case center
|
||||
}
|
||||
|
||||
let title: String
|
||||
let alignment: Alignment
|
||||
|
||||
public init(
|
||||
title: String,
|
||||
alignment: Alignment = .default
|
||||
) {
|
||||
self.title = title
|
||||
self.alignment = alignment
|
||||
}
|
||||
|
||||
public static func ==(lhs: AlertTitleComponent, rhs: AlertTitleComponent) -> Bool {
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.alignment != rhs.alignment {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let title = ComponentView<Empty>()
|
||||
|
||||
private var component: AlertTitleComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
func update(component: AlertTitleComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let environment = environment[AlertComponentEnvironment.self]
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: component.title,
|
||||
font: titleFont,
|
||||
textColor: environment.theme.actionSheet.primaryTextColor
|
||||
)),
|
||||
horizontalAlignment: component.alignment == .center ? .center : .natural,
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
|
||||
let titleOriginX: CGFloat
|
||||
switch component.alignment {
|
||||
case .default:
|
||||
titleOriginX = 0.0
|
||||
case .center:
|
||||
titleOriginX = floorToScreenPixels((availableSize.width - titleSize.width) / 2.0)
|
||||
}
|
||||
let titleFrame = CGRect(origin: CGPoint(x: titleOriginX, y: 0.0), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
transition.setFrame(view: titleView, frame: titleFrame)
|
||||
}
|
||||
return CGSize(width: availableSize.width, height: titleSize.height)
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class AlertTextComponent: Component {
|
||||
public typealias EnvironmentType = AlertComponentEnvironment
|
||||
|
||||
public enum Content: Equatable {
|
||||
case plain(String)
|
||||
case attributed(NSAttributedString)
|
||||
case textWithEntities(AccountContext, String, [MessageTextEntity])
|
||||
|
||||
public static func ==(lhs: Content, rhs: Content) -> Bool {
|
||||
switch lhs {
|
||||
case let .plain(text):
|
||||
if case .plain(text) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .attributed(text):
|
||||
if case .attributed(text) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .textWithEntities(_, lhsText, lhsEntities):
|
||||
if case let .textWithEntities(_, rhsText, rhsEntities) = rhs {
|
||||
return lhsText == rhsText && lhsEntities == rhsEntities
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum Alignment: Equatable {
|
||||
case `default`
|
||||
case center
|
||||
}
|
||||
|
||||
public enum Color: Equatable {
|
||||
case primary
|
||||
case secondary
|
||||
case destructive
|
||||
}
|
||||
|
||||
public enum TextStyle: Equatable {
|
||||
case `default`
|
||||
case small
|
||||
case bold
|
||||
}
|
||||
|
||||
public enum Style: Equatable {
|
||||
case plain(TextStyle)
|
||||
case background(TextStyle)
|
||||
}
|
||||
|
||||
let content: Content
|
||||
let alignment: Alignment
|
||||
let color: Color
|
||||
let style: Style
|
||||
let insets: UIEdgeInsets
|
||||
let action: ([NSAttributedString.Key: Any]) -> Void
|
||||
|
||||
public init(
|
||||
content: Content,
|
||||
alignment: Alignment = .default,
|
||||
color: Color = .primary,
|
||||
style: Style = .plain(.default),
|
||||
insets: UIEdgeInsets = .zero,
|
||||
action: @escaping ([NSAttributedString.Key: Any]) -> Void = { _ in }
|
||||
) {
|
||||
self.content = content
|
||||
self.alignment = alignment
|
||||
self.color = color
|
||||
self.style = style
|
||||
self.insets = insets
|
||||
self.action = action
|
||||
}
|
||||
|
||||
public static func ==(lhs: AlertTextComponent, rhs: AlertTextComponent) -> Bool {
|
||||
if lhs.content != rhs.content {
|
||||
return false
|
||||
}
|
||||
if lhs.alignment != rhs.alignment {
|
||||
return false
|
||||
}
|
||||
if lhs.color != rhs.color {
|
||||
return false
|
||||
}
|
||||
if lhs.style != rhs.style {
|
||||
return false
|
||||
}
|
||||
if lhs.insets != rhs.insets {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let background = ComponentView<Empty>()
|
||||
private let text = ComponentView<Empty>()
|
||||
|
||||
private var component: AlertTextComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
func update(component: AlertTextComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let environment = environment[AlertComponentEnvironment.self]
|
||||
|
||||
let textColor: UIColor
|
||||
switch component.color {
|
||||
case .primary:
|
||||
textColor = environment.theme.actionSheet.primaryTextColor
|
||||
case .secondary:
|
||||
textColor = environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.35)
|
||||
case .destructive:
|
||||
textColor = environment.theme.actionSheet.destructiveActionTextColor
|
||||
}
|
||||
let linkColor = environment.theme.actionSheet.controlAccentColor
|
||||
|
||||
let textFont: UIFont
|
||||
let boldTextFont: UIFont
|
||||
let italicTextFont: UIFont
|
||||
let fixedTextFont: UIFont
|
||||
switch component.style {
|
||||
case let .plain(textStyle), let .background(textStyle):
|
||||
switch textStyle {
|
||||
case .default:
|
||||
textFont = defaultTextFont
|
||||
boldTextFont = defaultBoldTextFont
|
||||
italicTextFont = defaultItalicTextFont
|
||||
fixedTextFont = defaultFixedTextFont
|
||||
case .small:
|
||||
textFont = smallTextFont
|
||||
boldTextFont = smallBoldTextFont
|
||||
italicTextFont = smallItalicTextFont
|
||||
fixedTextFont = smallFixedTextFont
|
||||
case .bold:
|
||||
textFont = defaultBoldTextFont
|
||||
boldTextFont = defaultBoldTextFont
|
||||
italicTextFont = defaultBoldItalicTextFont
|
||||
fixedTextFont = defaultFixedTextFont
|
||||
}
|
||||
}
|
||||
|
||||
var finalText: NSAttributedString
|
||||
var context: AccountContext?
|
||||
switch component.content {
|
||||
case let .plain(text):
|
||||
let markdownAttributes = MarkdownAttributes(
|
||||
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
|
||||
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
|
||||
link: MarkdownAttributeSet(font: textFont, textColor: linkColor),
|
||||
linkAttribute: { contents in
|
||||
return (TelegramTextAttributes.URL, contents)
|
||||
}
|
||||
)
|
||||
finalText = parseMarkdownIntoAttributedString(text, attributes: markdownAttributes)
|
||||
case let .attributed(attributedText):
|
||||
finalText = attributedText
|
||||
case let .textWithEntities(accountContext, text, entities):
|
||||
context = accountContext
|
||||
finalText = stringWithAppliedEntities(text, entities: entities, baseColor: textColor, linkColor: linkColor, baseFont: textFont, linkFont: textFont, boldFont: boldTextFont, italicFont: italicTextFont, boldItalicFont: italicTextFont, fixedFont: fixedTextFont, blockQuoteFont: textFont, message: nil)
|
||||
}
|
||||
|
||||
var hasCenterAlignment = component.alignment == .center
|
||||
switch component.style {
|
||||
case .background:
|
||||
hasCenterAlignment = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
let textConstrainedSize = CGSize(width: availableSize.width, height: availableSize.height)
|
||||
|
||||
let textSize = self.text.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
MultilineTextWithEntitiesComponent(
|
||||
context: context,
|
||||
animationCache: context?.animationCache,
|
||||
animationRenderer: context?.animationRenderer,
|
||||
placeholderColor: textColor.withMultipliedAlpha(0.1),
|
||||
text: .plain(finalText),
|
||||
horizontalAlignment: hasCenterAlignment ? .center : .natural,
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.2,
|
||||
spoilerColor: textColor,
|
||||
highlightColor: linkColor.withAlphaComponent(0.2),
|
||||
manualVisibilityControl: true,
|
||||
resetAnimationsOnVisibilityChange: true,
|
||||
highlightAction: { attributes in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
||||
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
tapAction: { attributes, _ in
|
||||
component.action(attributes)
|
||||
}
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: textConstrainedSize
|
||||
)
|
||||
|
||||
var textOffset = CGPoint()
|
||||
if hasCenterAlignment {
|
||||
textOffset.x = floorToScreenPixels((availableSize.width - textSize.width) / 2.0)
|
||||
}
|
||||
var size = CGSize(width: availableSize.width, height: textSize.height)
|
||||
if case .background = component.style {
|
||||
let backgroundSize = CGSize(width: availableSize.width + 20.0, height: textSize.height + backgroundInset * 2.0)
|
||||
size = backgroundSize
|
||||
textOffset = CGPoint(x: textOffset.x, y: backgroundInset)
|
||||
|
||||
let _ = self.background.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
FilledRoundedRectangleComponent(
|
||||
color: textColor.withMultipliedAlpha(0.1),
|
||||
cornerRadius: .value(10.0),
|
||||
smoothCorners: true
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: backgroundSize
|
||||
)
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: -10.0, y: component.insets.top), size: backgroundSize)
|
||||
if let backgroundView = self.background.view {
|
||||
if backgroundView.superview == nil {
|
||||
self.addSubview(backgroundView)
|
||||
}
|
||||
transition.setFrame(view: backgroundView, frame: backgroundFrame)
|
||||
}
|
||||
}
|
||||
|
||||
let textFrame = CGRect(origin: textOffset.offsetBy(dx: 0.0, dy: component.insets.top), size: textSize)
|
||||
if let textView = self.text.view {
|
||||
if textView.superview == nil {
|
||||
self.addSubview(textView)
|
||||
}
|
||||
transition.setFrame(view: textView, frame: textFrame)
|
||||
}
|
||||
return CGSize(width: size.width, height: size.height + component.insets.top + component.insets.bottom)
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ swift_library(
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
+50
-11
@@ -4,6 +4,7 @@ import Display
|
||||
import ComponentFlow
|
||||
import TelegramPresentationData
|
||||
import BundleIconComponent
|
||||
import MultilineTextComponent
|
||||
|
||||
extension ComponentTransition {
|
||||
func animateBlur(layer: CALayer, from: CGFloat, to: CGFloat, delay: Double = 0.0, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
|
||||
@@ -46,6 +47,7 @@ public final class AnimatedTextComponent: Component {
|
||||
public let items: [Item]
|
||||
public let noDelay: Bool
|
||||
public let animateScale: Bool
|
||||
public let animateSlide: Bool
|
||||
public let preferredDirectionIsDown: Bool
|
||||
public let blur: Bool
|
||||
|
||||
@@ -55,6 +57,7 @@ public final class AnimatedTextComponent: Component {
|
||||
items: [Item],
|
||||
noDelay: Bool = false,
|
||||
animateScale: Bool = true,
|
||||
animateSlide: Bool = true,
|
||||
preferredDirectionIsDown: Bool = false,
|
||||
blur: Bool = false
|
||||
) {
|
||||
@@ -63,6 +66,7 @@ public final class AnimatedTextComponent: Component {
|
||||
self.items = items
|
||||
self.noDelay = noDelay
|
||||
self.animateScale = animateScale
|
||||
self.animateSlide = animateSlide
|
||||
self.preferredDirectionIsDown = preferredDirectionIsDown
|
||||
self.blur = blur
|
||||
}
|
||||
@@ -83,6 +87,9 @@ public final class AnimatedTextComponent: Component {
|
||||
if lhs.animateScale != rhs.animateScale {
|
||||
return false
|
||||
}
|
||||
if lhs.animateSlide != rhs.animateSlide {
|
||||
return false
|
||||
}
|
||||
if lhs.preferredDirectionIsDown != rhs.preferredDirectionIsDown {
|
||||
return false
|
||||
}
|
||||
@@ -101,6 +108,8 @@ public final class AnimatedTextComponent: Component {
|
||||
public final class View: UIView {
|
||||
private var characters: [CharacterKey: ComponentView<Empty>] = [:]
|
||||
|
||||
private var spaceSize: CGSize?
|
||||
|
||||
private var component: AnimatedTextComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
@@ -113,6 +122,15 @@ public final class AnimatedTextComponent: Component {
|
||||
}
|
||||
|
||||
func update(component: AnimatedTextComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
let spaceSize: CGSize
|
||||
if let current = self.spaceSize, self.component?.font == component.font {
|
||||
spaceSize = current
|
||||
} else {
|
||||
let spaceSizeValue = NSAttributedString(string: " ", font: component.font, textColor: .black).boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil).size
|
||||
spaceSize = CGSize(width: ceil(spaceSizeValue.width), height: ceil(spaceSizeValue.height))
|
||||
self.spaceSize = spaceSize
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
@@ -220,7 +238,7 @@ public final class AnimatedTextComponent: Component {
|
||||
itemText = [.icon(iconName, tint, offset)]
|
||||
}
|
||||
var index = 0
|
||||
for character in itemText {
|
||||
characterLoop: for character in itemText {
|
||||
let characterKey = CharacterKey(itemId: item.id, index: index, value: character.value)
|
||||
index += 1
|
||||
|
||||
@@ -236,13 +254,23 @@ public final class AnimatedTextComponent: Component {
|
||||
|
||||
let characterComponent: AnyComponent<Empty>
|
||||
var characterOffset: CGPoint = .zero
|
||||
var addTrailingSpace = false
|
||||
switch character {
|
||||
case let .text(text):
|
||||
characterComponent = AnyComponent(Text(
|
||||
text: String(text),
|
||||
font: component.font,
|
||||
color: component.color
|
||||
))
|
||||
if text == " " {
|
||||
size.height = max(size.height, ceil(spaceSize.height))
|
||||
size.width += max(0.0, ceil(spaceSize.width))
|
||||
|
||||
continue characterLoop
|
||||
} else {
|
||||
if text.hasSuffix(" ") {
|
||||
addTrailingSpace = true
|
||||
}
|
||||
|
||||
characterComponent = AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: text, font: component.font, textColor: component.color))
|
||||
))
|
||||
}
|
||||
case let .icon(iconName, tint, offset):
|
||||
characterComponent = AnyComponent(BundleIconComponent(
|
||||
name: iconName,
|
||||
@@ -263,6 +291,7 @@ public final class AnimatedTextComponent: Component {
|
||||
if characterComponentView.superview == nil {
|
||||
characterComponentView.layer.rasterizationScale = UIScreenScale
|
||||
self.addSubview(characterComponentView)
|
||||
characterComponentView.layer.anchorPoint = CGPoint()
|
||||
animateIn = true
|
||||
}
|
||||
|
||||
@@ -282,8 +311,9 @@ public final class AnimatedTextComponent: Component {
|
||||
}
|
||||
|
||||
characterComponentView.bounds = CGRect(origin: CGPoint(), size: characterFrame.size)
|
||||
let deltaPosition = CGPoint(x: characterFrame.midX - characterComponentView.frame.midX, y: characterFrame.midY - characterComponentView.frame.midY)
|
||||
characterComponentView.center = characterFrame.center
|
||||
|
||||
let deltaPosition = CGPoint(x: characterFrame.minX - characterComponentView.frame.minX, y: characterFrame.minY - characterComponentView.frame.minY)
|
||||
characterComponentView.center = characterFrame.origin
|
||||
characterComponentView.layer.animatePosition(from: CGPoint(x: -deltaPosition.x, y: -deltaPosition.y), to: CGPoint(), duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
}
|
||||
}
|
||||
@@ -305,13 +335,20 @@ public final class AnimatedTextComponent: Component {
|
||||
if component.blur {
|
||||
ComponentTransition.easeInOut(duration: 0.2).animateBlur(layer: characterComponentView.layer, from: transitionBlurRadius, to: 0.0, delay: delayNorm * delayWidth)
|
||||
}
|
||||
characterComponentView.layer.animatePosition(from: CGPoint(x: 0.0, y: characterSize.height * offsetNorm), to: CGPoint(), duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
if component.animateSlide {
|
||||
characterComponentView.layer.animatePosition(from: CGPoint(x: 0.0, y: characterSize.height * offsetNorm), to: CGPoint(), duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
}
|
||||
characterComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18, delay: delayNorm * delayWidth)
|
||||
}
|
||||
}
|
||||
|
||||
size.height = max(size.height, characterSize.height)
|
||||
size.width += max(0.0, characterSize.width - UIScreenPixel * 2.0)
|
||||
size.width += max(0.0, characterSize.width - UIScreenPixel)
|
||||
|
||||
if addTrailingSpace {
|
||||
size.height = max(size.height, ceil(spaceSize.height))
|
||||
size.width += max(0.0, ceil(spaceSize.width))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,7 +379,9 @@ public final class AnimatedTextComponent: Component {
|
||||
} else {
|
||||
targetY = characterComponentView.center.y - characterComponentView.bounds.height * offsetNorm
|
||||
}
|
||||
outScaleTransition.setPosition(view: characterComponentView, position: CGPoint(x: characterComponentView.center.x, y: targetY), delay: delayNorm * delayWidth)
|
||||
if component.animateSlide {
|
||||
outScaleTransition.setPosition(view: characterComponentView, position: CGPoint(x: characterComponentView.center.x, y: targetY), delay: delayNorm * delayWidth)
|
||||
}
|
||||
outAlphaTransition.setAlpha(view: characterComponentView, alpha: 0.0, delay: delayNorm * delayWidth, completion: { [weak characterComponentView] _ in
|
||||
characterComponentView?.removeFromSuperview()
|
||||
})
|
||||
|
||||
+1
-1
@@ -363,7 +363,7 @@ public final class AsyncListComponent: Component {
|
||||
init() {
|
||||
self.contentContainer = UIView()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
|
||||
super.init(layerBacked: false, rotated: false, seeThrough: false)
|
||||
|
||||
self.view.addSubview(self.contentContainer)
|
||||
|
||||
|
||||
+12
-10
@@ -212,7 +212,7 @@ private func attachmentFileControllerEntries(presentationData: PresentationData,
|
||||
|
||||
if case .audio = mode {
|
||||
if let savedMusic, savedMusic.count > 0 {
|
||||
entries.append(.savedHeader(presentationData.theme, "SAVED MUSIC".uppercased()))
|
||||
entries.append(.savedHeader(presentationData.theme, presentationData.strings.MediaEditor_Audio_SavedMusic.uppercased()))
|
||||
var savedMusic = savedMusic
|
||||
var showMore = false
|
||||
if savedMusic.count > 4 && !state.savedMusicExpanded {
|
||||
@@ -225,7 +225,7 @@ private func attachmentFileControllerEntries(presentationData: PresentationData,
|
||||
i += 1
|
||||
}
|
||||
if showMore {
|
||||
entries.append(.showMore(presentationData.theme, "Show More"))
|
||||
entries.append(.showMore(presentationData.theme, presentationData.strings.MediaEditor_Audio_ShowMore))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -459,16 +459,17 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre
|
||||
let updatedTheme = presentationData.theme.withModalBlocksBackground()
|
||||
presentationData = presentationData.withUpdated(theme: updatedTheme)
|
||||
|
||||
let barButtonSize = CGSize(width: 40.0, height: 40.0)
|
||||
let barButtonSize = CGSize(width: 44.0, height: 44.0)
|
||||
let closeButton = GlassBarButtonComponent(
|
||||
size: barButtonSize,
|
||||
backgroundColor: presentationData.theme.rootController.navigationBar.glassBarButtonBackgroundColor,
|
||||
backgroundColor: nil,
|
||||
isDark: presentationData.theme.overallDarkAppearance,
|
||||
state: .generic,
|
||||
animateScale: false,
|
||||
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Navigation/Close",
|
||||
tintColor: presentationData.theme.rootController.navigationBar.glassBarButtonForegroundColor
|
||||
tintColor: presentationData.theme.chat.inputPanel.panelControlColor
|
||||
)
|
||||
)),
|
||||
action: { _ in
|
||||
@@ -489,13 +490,14 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre
|
||||
|
||||
let searchButton = GlassBarButtonComponent(
|
||||
size: barButtonSize,
|
||||
backgroundColor: presentationData.theme.rootController.navigationBar.glassBarButtonBackgroundColor,
|
||||
backgroundColor: nil,
|
||||
isDark: presentationData.theme.overallDarkAppearance,
|
||||
state: .generic,
|
||||
animateScale: false,
|
||||
component: AnyComponentWithIdentity(id: "search", component: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Navigation/Search",
|
||||
tintColor: presentationData.theme.rootController.navigationBar.glassBarButtonForegroundColor
|
||||
tintColor: presentationData.theme.chat.inputPanel.panelControlColor
|
||||
)
|
||||
)),
|
||||
action: { _ in
|
||||
@@ -509,7 +511,7 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre
|
||||
}
|
||||
)
|
||||
let searchButtonComponent = state.searching ? nil : AnyComponentWithIdentity(id: "search", component: AnyComponent(searchButton))
|
||||
let searchButtonNode = existingSearchButton.modify { current in
|
||||
let searchButtonNode: BarComponentHostNode? = !state.searching ? existingSearchButton.modify { current in
|
||||
let buttonNode: BarComponentHostNode
|
||||
if let current {
|
||||
buttonNode = current
|
||||
@@ -518,7 +520,7 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre
|
||||
buttonNode = BarComponentHostNode(component: searchButtonComponent, size: barButtonSize)
|
||||
}
|
||||
return buttonNode
|
||||
}
|
||||
} : nil
|
||||
|
||||
let previousRecentDocuments = previousRecentDocuments.swap(recentDocuments)
|
||||
let crossfade = previousRecentDocuments == nil && recentDocuments != nil
|
||||
@@ -542,7 +544,7 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre
|
||||
case .recent:
|
||||
title = presentationData.strings.Attachment_File
|
||||
case .audio:
|
||||
title = "Audio"
|
||||
title = presentationData.strings.MediaEditor_Audio_Title
|
||||
}
|
||||
|
||||
let controllerState = ItemListControllerState(
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "AvatarComponent",
|
||||
module_name = "AvatarComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/AccountContext",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,116 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import AvatarNode
|
||||
import AccountContext
|
||||
|
||||
public final class AvatarComponent: Component {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let peer: EnginePeer
|
||||
let icon: AnyComponent<Empty>?
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
peer: EnginePeer,
|
||||
icon: AnyComponent<Empty>? = nil
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.peer = peer
|
||||
self.icon = icon
|
||||
}
|
||||
|
||||
public static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.icon != rhs.icon {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let avatarNode: AvatarNode
|
||||
private var icon: ComponentView<Empty>?
|
||||
|
||||
private var component: AvatarComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 42.0))
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubnode(self.avatarNode)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: AvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
if self.component == nil {
|
||||
self.avatarNode.font = avatarPlaceholderFont(size: ceil(42.0 * availableSize.width / 100.0))
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
var cutoutRect: CGRect?
|
||||
if let icon = component.icon {
|
||||
let iconView: ComponentView<Empty>
|
||||
if let current = self.icon {
|
||||
iconView = current
|
||||
} else {
|
||||
iconView = ComponentView()
|
||||
self.icon = iconView
|
||||
}
|
||||
let iconSize = iconView.update(
|
||||
transition: .immediate,
|
||||
component: icon,
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
let iconFrame = CGRect(origin: CGPoint(x: availableSize.width - iconSize.width + 2.0, y: availableSize.height - iconSize.height + 2.0), size: iconSize)
|
||||
if let iconView = iconView.view {
|
||||
if iconView.superview == nil {
|
||||
self.addSubview(iconView)
|
||||
}
|
||||
iconView.frame = iconFrame
|
||||
}
|
||||
cutoutRect = CGRect(origin: CGPoint(x: iconFrame.minX, y: availableSize.height - iconFrame.maxY), size: iconFrame.size).insetBy(dx: -2.0 + UIScreenPixel, dy: -2.0 + UIScreenPixel)
|
||||
}
|
||||
|
||||
self.avatarNode.frame = CGRect(origin: .zero, size: availableSize)
|
||||
self.avatarNode.setPeer(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
peer: component.peer,
|
||||
synchronousLoad: true,
|
||||
cutoutRect: cutoutRect
|
||||
)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/MediaEditor",
|
||||
"//submodules/TelegramUI/Components/AvatarBackground",
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
|
||||
"//submodules/TelegramUI/Components/PremiumAlertController",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@@ -27,6 +27,7 @@ import MediaEditor
|
||||
import AvatarBackground
|
||||
import LottieComponent
|
||||
import UndoUI
|
||||
import PremiumAlertController
|
||||
|
||||
public struct AvatarKeyboardInputData: Equatable {
|
||||
var emoji: EmojiPagerContentComponent
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ public final class BackButtonComponent: Component {
|
||||
self.component = component
|
||||
|
||||
if self.arrowView.image == nil {
|
||||
self.arrowView.image = NavigationBar.backArrowImage(color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||
self.arrowView.image = navigationBarBackArrowImage(color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
self.arrowView.tintColor = component.color
|
||||
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ final class BackButtonView: HighlightableButton {
|
||||
var pressAction: (() -> Void)?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.iconView = UIImageView(image: NavigationBar.backArrowImage(color: .white))
|
||||
self.iconView = UIImageView(image: navigationBarBackArrowImage(color: .white))
|
||||
self.iconView.isUserInteractionEnabled = false
|
||||
|
||||
self.textView = TextView()
|
||||
|
||||
@@ -95,6 +95,8 @@ swift_library(
|
||||
"//submodules/TelegramCallsUI",
|
||||
"//submodules/TelegramUI/Components/Stories/StoryContainerScreen",
|
||||
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
|
||||
"//submodules/TelegramUI/Components/LiquidLens",
|
||||
"//submodules/TelegramUI/Components/TabSelectionRecognizer",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@@ -2097,7 +2097,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
if !isSticker, case .none = component.cameraState.recording, component.cameraState.isStreaming == .none && !state.isTransitioning && hasAllRequiredAccess && component.cameraState.collageProgress < 1.0 - .ulpOfOne {
|
||||
let availableModeControlSize: CGSize
|
||||
if isTablet {
|
||||
availableModeControlSize = CGSize(width: panelWidth, height: 120.0)
|
||||
availableModeControlSize = CGSize(width: floor(panelWidth), height: 120.0)
|
||||
} else {
|
||||
availableModeControlSize = availableSize
|
||||
}
|
||||
@@ -2131,7 +2131,6 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
modeControlPosition = CGPoint(x: availableSize.width / 2.0, y: availableSize.height - environment.safeInsets.bottom + modeControl.size.height / 2.0 + controlsBottomInset + 16.0)
|
||||
}
|
||||
context.add(modeControl
|
||||
.clipsToBounds(true)
|
||||
.position(modeControlPosition)
|
||||
.appear(.default(alpha: true))
|
||||
.disappear(.default(alpha: true))
|
||||
|
||||
@@ -5,6 +5,8 @@ import ComponentFlow
|
||||
import MultilineTextComponent
|
||||
import TelegramPresentationData
|
||||
import GlassBackgroundComponent
|
||||
import LiquidLens
|
||||
import TabSelectionRecognizer
|
||||
|
||||
extension CameraMode {
|
||||
func title(strings: PresentationStrings) -> String {
|
||||
@@ -70,28 +72,17 @@ final class ModeComponent: Component {
|
||||
|
||||
final class View: UIView, ComponentTaggedView {
|
||||
private var component: ModeComponent?
|
||||
private var state: EmptyComponentState?
|
||||
|
||||
final class ItemView: HighlightTrackingButton {
|
||||
var pressed: () -> Void = {
|
||||
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
|
||||
self.isExclusiveTouch = true
|
||||
|
||||
self.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
@objc func buttonPressed() {
|
||||
self.pressed()
|
||||
}
|
||||
|
||||
func update(isTablet: Bool, value: String, selected: Bool, tintColor: UIColor) -> CGSize {
|
||||
let accentColor: UIColor
|
||||
let normalColor: UIColor
|
||||
@@ -113,9 +104,15 @@ final class ModeComponent: Component {
|
||||
}
|
||||
|
||||
private var backgroundView = UIView()
|
||||
private var glassContainerView = GlassBackgroundContainerView()
|
||||
private var selectionView = GlassBackgroundView()
|
||||
private var itemViews: [Int32: ItemView] = [:]
|
||||
private var backgroundContainer = GlassBackgroundContainerView()
|
||||
|
||||
private var liquidLensView: LiquidLensView?
|
||||
|
||||
private var itemViews: [AnyHashable: ItemView] = [:]
|
||||
private var selectedItemViews: [AnyHashable: ItemView] = [:]
|
||||
|
||||
private var tabSelectionRecognizer: TabSelectionRecognizer?
|
||||
private var selectionGestureState: (startX: CGFloat, currentX: CGFloat, itemId: AnyHashable)?
|
||||
|
||||
public func matches(tag: Any) -> Bool {
|
||||
if let component = self.component, let componentTag = component.tag {
|
||||
@@ -132,12 +129,11 @@ final class ModeComponent: Component {
|
||||
|
||||
self.backgroundView.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.11)
|
||||
self.backgroundView.layer.cornerRadius = 24.0
|
||||
|
||||
|
||||
self.layer.allowsGroupOpacity = true
|
||||
|
||||
self.addSubview(self.backgroundView)
|
||||
self.backgroundView.addSubview(self.glassContainerView)
|
||||
self.glassContainerView.contentView.addSubview(self.selectionView)
|
||||
self.backgroundView.addSubview(self.backgroundContainer)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
@@ -162,14 +158,82 @@ final class ModeComponent: Component {
|
||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
return self.backgroundView.frame.contains(point)
|
||||
}
|
||||
|
||||
func update(component: ModeComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
let isTablet = component.isTablet
|
||||
let updatedMode = component.updatedMode
|
||||
private func item(at point: CGPoint) -> AnyHashable? {
|
||||
var closestItem: (AnyHashable, CGFloat)?
|
||||
for (id, itemView) in self.itemViews {
|
||||
if itemView.frame.contains(point) {
|
||||
return id
|
||||
} else {
|
||||
let distance = abs(point.x - itemView.center.x)
|
||||
if let closestItemValue = closestItem {
|
||||
if closestItemValue.1 > distance {
|
||||
closestItem = (id, distance)
|
||||
}
|
||||
} else {
|
||||
closestItem = (id, distance)
|
||||
}
|
||||
}
|
||||
}
|
||||
return closestItem?.0
|
||||
}
|
||||
|
||||
@objc private func onTabSelectionGesture(_ recognizer: TabSelectionRecognizer) {
|
||||
guard let component = self.component, let liquidLensView = self.liquidLensView else {
|
||||
return
|
||||
}
|
||||
let location = recognizer.location(in: liquidLensView.contentView)
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
if let itemId = self.item(at: location), 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 {
|
||||
selectionGestureState.currentX = selectionGestureState.startX + recognizer.translation(in: self).x
|
||||
if let itemId = self.item(at: location) {
|
||||
selectionGestureState.itemId = itemId
|
||||
}
|
||||
self.selectionGestureState = selectionGestureState
|
||||
self.state?.updated(transition: .immediate, isLocal: true)
|
||||
}
|
||||
case .ended, .cancelled:
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: ModeComponent, availableSize: CGSize, state: EmptyComponentState, transition: ComponentTransition) -> CGSize {
|
||||
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)
|
||||
|
||||
let tabSelectionRecognizer = TabSelectionRecognizer(target: self, action: #selector(self.onTabSelectionGesture(_:)))
|
||||
self.tabSelectionRecognizer = tabSelectionRecognizer
|
||||
liquidLensView.addGestureRecognizer(tabSelectionRecognizer)
|
||||
}
|
||||
|
||||
self.glassContainerView.isHidden = component.isTablet
|
||||
self.backgroundView.backgroundColor = component.isTablet ? .clear : UIColor(rgb: 0xffffff, alpha: 0.11)
|
||||
|
||||
let inset: CGFloat = 23.0
|
||||
@@ -177,28 +241,36 @@ final class ModeComponent: Component {
|
||||
|
||||
var i = 0
|
||||
var itemFrame = CGRect(origin: isTablet ? .zero : CGPoint(x: inset, y: 0.0), size: buttonSize)
|
||||
var selectedCenter = itemFrame.minX
|
||||
var selectedFrame = itemFrame
|
||||
|
||||
var validKeys: Set<Int32> = Set()
|
||||
var validKeys: Set<AnyHashable> = Set()
|
||||
for mode in component.availableModes.reversed() {
|
||||
let id = mode.rawValue
|
||||
validKeys.insert(id)
|
||||
|
||||
let itemView: ItemView
|
||||
if let current = self.itemViews[id] {
|
||||
let selectedItemView: ItemView
|
||||
if let current = self.itemViews[id], let currentSelected = self.selectedItemViews[id] {
|
||||
itemView = current
|
||||
selectedItemView = currentSelected
|
||||
} else {
|
||||
itemView = ItemView()
|
||||
self.backgroundView.addSubview(itemView)
|
||||
itemView.isUserInteractionEnabled = false
|
||||
self.itemViews[id] = itemView
|
||||
}
|
||||
itemView.pressed = {
|
||||
updatedMode(mode)
|
||||
liquidLensView.contentView.addSubview(itemView)
|
||||
|
||||
selectedItemView = ItemView()
|
||||
selectedItemView.isUserInteractionEnabled = false
|
||||
self.selectedItemViews[id] = selectedItemView
|
||||
liquidLensView.selectedContentView.addSubview(selectedItemView)
|
||||
}
|
||||
|
||||
let itemSize = itemView.update(isTablet: component.isTablet, value: mode.title(strings: component.strings), selected: mode == component.currentMode, tintColor: component.tintColor)
|
||||
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 {
|
||||
@@ -207,21 +279,17 @@ final class ModeComponent: Component {
|
||||
|
||||
if isTablet {
|
||||
itemView.center = CGPoint(x: availableSize.width / 2.0, y: itemFrame.midY)
|
||||
if mode == component.currentMode {
|
||||
selectedCenter = 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)
|
||||
if mode == component.currentMode {
|
||||
selectedCenter = itemFrame.midX
|
||||
}
|
||||
selectedItemView.center = itemView.center
|
||||
itemFrame = itemFrame.offsetBy(dx: itemFrame.width + spacing, dy: 0.0)
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
|
||||
var removeKeys: [Int32] = []
|
||||
var removeKeys: [AnyHashable] = []
|
||||
for (id, itemView) in self.itemViews {
|
||||
if !validKeys.contains(id) {
|
||||
removeKeys.append(id)
|
||||
@@ -237,10 +305,12 @@ final class ModeComponent: Component {
|
||||
|
||||
let totalSize: CGSize
|
||||
let size: 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: CGPoint(x: 0.0, y: availableSize.height / 2.0 - selectedCenter), size: totalSize))
|
||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: totalSize))
|
||||
cornerRadius = 20.0
|
||||
} else {
|
||||
size = CGSize(width: availableSize.width, height: buttonSize.height)
|
||||
totalSize = CGSize(width: itemFrame.minX - spacing + inset, height: buttonSize.height)
|
||||
@@ -248,12 +318,26 @@ final class ModeComponent: Component {
|
||||
}
|
||||
|
||||
let containerFrame = CGRect(origin: .zero, size: self.backgroundView.frame.size)
|
||||
transition.setFrame(view: self.glassContainerView, frame: containerFrame)
|
||||
transition.setFrame(view: self.backgroundContainer, frame: containerFrame)
|
||||
|
||||
let selectionFrame = selectedFrame.insetBy(dx: -20.0, dy: 3.0)
|
||||
self.glassContainerView.update(size: containerFrame.size, isDark: true, transition: .immediate)
|
||||
self.selectionView.update(size: selectionFrame.size, cornerRadius: selectionFrame.height * 0.5, isDark: true, tintColor: .init(kind: .custom, color: UIColor(rgb: 0xffffff, alpha: 0.16)), transition: transition)
|
||||
transition.setFrame(view: self.selectionView, frame: selectionFrame)
|
||||
let selectionFrame = selectedFrame.insetBy(dx: -23.0, dy: 3.0)
|
||||
var lensSelection: (origin: CGPoint, size: CGSize)
|
||||
if let selectionGestureState = self.selectionGestureState, !isTablet {
|
||||
lensSelection = (CGPoint(x: selectionGestureState.currentX, y: 0.0), selectionFrame.size)
|
||||
} else {
|
||||
lensSelection = (CGPoint(x: selectionFrame.minX, y: selectionFrame.minY), selectionFrame.size)
|
||||
}
|
||||
|
||||
if isTablet {
|
||||
lensSelection.size.width = size.width
|
||||
} else {
|
||||
lensSelection.size.height = containerFrame.size.height
|
||||
lensSelection.origin.y = 0.0
|
||||
}
|
||||
|
||||
transition.setFrame(view: liquidLensView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: containerFrame.size))
|
||||
liquidLensView.update(size: containerFrame.size, cornerRadius: cornerRadius, selectionOrigin: CGPoint(x: max(0.0, min(lensSelection.origin.x, containerFrame.size.width - lensSelection.size.width)), y: lensSelection.origin.y), selectionSize: lensSelection.size, inset: 3.0, isDark: true, isLifted: self.selectionGestureState != nil && !isTablet, isCollapsed: false, transition: transition)
|
||||
self.backgroundContainer.update(size: containerFrame.size, isDark: true, transition: .immediate)
|
||||
|
||||
return size
|
||||
}
|
||||
@@ -264,7 +348,7 @@ final class ModeComponent: Component {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ChatAgeRestrictionAlertController",
|
||||
module_name = "ChatAgeRestrictionAlertController",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent/AlertCheckComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import ComponentFlow
|
||||
import AlertComponent
|
||||
import AlertCheckComponent
|
||||
|
||||
public func chatAgeRestrictionAlertController(
|
||||
context: AccountContext,
|
||||
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
|
||||
parentController: ViewController,
|
||||
completion: @escaping (Bool) -> Void
|
||||
) -> ViewController {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let strings = presentationData.strings
|
||||
|
||||
let checkState = AlertCheckComponent.ExternalState()
|
||||
|
||||
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "title",
|
||||
component: AnyComponent(
|
||||
AlertTitleComponent(title: strings.SensitiveContent_Title)
|
||||
)
|
||||
))
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "text",
|
||||
component: AnyComponent(
|
||||
AlertTextComponent(content: .plain(strings.SensitiveContent_Text))
|
||||
)
|
||||
))
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "check",
|
||||
component: AnyComponent(
|
||||
AlertCheckComponent(title: strings.SensitiveContent_ShowAlways, initialValue: false, externalState: checkState)
|
||||
)
|
||||
))
|
||||
|
||||
var effectiveUpdatedPresentationData: (PresentationData, Signal<PresentationData, NoError>)
|
||||
if let updatedPresentationData {
|
||||
effectiveUpdatedPresentationData = updatedPresentationData
|
||||
} else {
|
||||
effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData)
|
||||
}
|
||||
|
||||
let alertController = AlertScreen(
|
||||
configuration: AlertScreen.Configuration(actionAlignment: .vertical),
|
||||
content: content,
|
||||
actions: [
|
||||
.init(title: strings.SensitiveContent_ViewAnyway, type: .default, action: {
|
||||
completion(checkState.value)
|
||||
}),
|
||||
.init(title: strings.Common_Cancel)
|
||||
],
|
||||
updatedPresentationData: effectiveUpdatedPresentationData
|
||||
)
|
||||
return alertController
|
||||
}
|
||||
+3
-7
@@ -75,12 +75,8 @@ public final class ChatAvatarNavigationNode: ASDisplayNode {
|
||||
strongSelf.contextAction?(strongSelf.containerNode, gesture)
|
||||
}
|
||||
|
||||
self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 37.0, height: 37.0)).offsetBy(dx: 10.0, dy: 1.0)
|
||||
self.avatarNode.frame = self.containerNode.bounds
|
||||
|
||||
#if DEBUG
|
||||
//self.hasUnseenStories = true
|
||||
#endif
|
||||
self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 44.0, height: 44.0))
|
||||
self.avatarNode.frame = self.containerNode.bounds.insetBy(dx: 3.0, dy: 3.0)
|
||||
}
|
||||
|
||||
deinit {
|
||||
@@ -265,7 +261,7 @@ public final class ChatAvatarNavigationNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
||||
return CGSize(width: 37.0, height: 37.0)
|
||||
return CGSize(width: 44.0, height: 44.0)
|
||||
}
|
||||
|
||||
public func onLayout() {
|
||||
|
||||
@@ -118,7 +118,7 @@ public final class ChatBotInfoItemNode: ListViewItemNode {
|
||||
self.textNode = TextNode()
|
||||
self.titleNode = TextNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: true, rotated: true)
|
||||
super.init(layerBacked: false, rotated: true)
|
||||
|
||||
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
|
||||
|
||||
|
||||
+2
-44
@@ -177,51 +177,8 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
|
||||
private var layoutData: (CGFloat, CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, CGFloat, Bool, LayoutMetrics)?
|
||||
|
||||
public override init() {
|
||||
/*self.button = HighlightableButton()
|
||||
self.buttonBackgroundView = GlassBackgroundView()
|
||||
self.buttonBackgroundView.isUserInteractionEnabled = false
|
||||
self.buttonTitle = ImmediateTextNode()
|
||||
self.buttonTitle.isUserInteractionEnabled = false
|
||||
self.buttonTintTitle = ImmediateTextNode()
|
||||
self.buttonBackgroundView.contentView.addSubview(self.buttonTitle.view)
|
||||
self.buttonBackgroundView.maskContentView.addSubview(self.buttonTintTitle.view)
|
||||
self.buttonBackgroundView.contentView.addSubview(self.button)
|
||||
|
||||
self.helpButton = HighlightableButton()
|
||||
self.helpButtonBackgroundView = GlassBackgroundView()
|
||||
self.helpButtonBackgroundView.isUserInteractionEnabled = false
|
||||
self.helpButtonIconView = GlassBackgroundView.ContentImageView()
|
||||
self.helpButtonBackgroundView.contentView.addSubview(self.helpButtonIconView)
|
||||
self.helpButtonBackgroundView.contentView.addSubview(self.helpButton)
|
||||
self.helpButtonBackgroundView.isHidden = true
|
||||
|
||||
self.giftButton = HighlightableButton()
|
||||
self.giftButtonBackgroundView = GlassBackgroundView()
|
||||
self.giftButtonBackgroundView.isUserInteractionEnabled = false
|
||||
self.giftButtonIconView = GlassBackgroundView.ContentImageView()
|
||||
self.giftButtonBackgroundView.contentView.addSubview(self.giftButtonIconView)
|
||||
self.giftButtonBackgroundView.contentView.addSubview(self.giftButton)
|
||||
self.giftButtonBackgroundView.isHidden = true
|
||||
|
||||
self.suggestedPostButton = HighlightableButton()
|
||||
self.suggestedPostButtonBackgroundView = GlassBackgroundView()
|
||||
self.suggestedPostButtonBackgroundView.isUserInteractionEnabled = false
|
||||
self.suggestedPostButtonIconView = GlassBackgroundView.ContentImageView()
|
||||
self.suggestedPostButtonBackgroundView.contentView.addSubview(self.suggestedPostButtonIconView)
|
||||
self.suggestedPostButtonBackgroundView.contentView.addSubview(self.suggestedPostButton)
|
||||
self.suggestedPostButtonBackgroundView.isHidden = true*/
|
||||
|
||||
super.init()
|
||||
|
||||
/*self.view.addSubview(self.buttonBackgroundView)
|
||||
self.view.addSubview(self.helpButtonBackgroundView)
|
||||
self.view.addSubview(self.giftButtonBackgroundView)
|
||||
self.view.addSubview(self.suggestedPostButtonBackgroundView)
|
||||
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
|
||||
self.helpButton.addTarget(self, action: #selector(self.helpPressed), for: .touchUpInside)
|
||||
self.giftButton.addTarget(self, action: #selector(self.giftPressed), for: .touchUpInside)
|
||||
self.suggestedPostButton.addTarget(self, action: #selector(self.suggestedPostPressed), for: .touchUpInside)*/
|
||||
|
||||
self.view.addSubview(self.panelContainer)
|
||||
}
|
||||
|
||||
@@ -495,7 +452,8 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
|
||||
self?.buttonPressed()
|
||||
}
|
||||
)],
|
||||
background: centerAction.isAccent ? .activeTint : .panel
|
||||
background: centerAction.isAccent ? .activeTint : .panel,
|
||||
keepWide: true
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+37
-45
@@ -56,53 +56,53 @@ public enum ChatHistoryEntry: Identifiable, Comparable {
|
||||
case UnreadEntry(MessageIndex, ChatPresentationData)
|
||||
case ReplyCountEntry(MessageIndex, Bool, Int, ChatPresentationData)
|
||||
case ChatInfoEntry(ChatInfoData, ChatPresentationData)
|
||||
case SearchEntry(PresentationTheme, PresentationStrings)
|
||||
|
||||
public var stableId: UInt64 {
|
||||
switch self {
|
||||
case let .MessageEntry(message, _, _, _, _, attributes):
|
||||
let type: UInt64
|
||||
switch attributes.contentTypeHint {
|
||||
case .generic:
|
||||
type = 2
|
||||
case .largeEmoji:
|
||||
type = 3
|
||||
case .animatedEmoji:
|
||||
type = 4
|
||||
}
|
||||
return UInt64(message.stableId) | ((type << 40))
|
||||
case let .MessageGroupEntry(groupInfo, _, _):
|
||||
return UInt64(bitPattern: groupInfo) | ((UInt64(2) << 40))
|
||||
case .UnreadEntry:
|
||||
return UInt64(4) << 40
|
||||
case .ReplyCountEntry:
|
||||
return UInt64(5) << 40
|
||||
case .ChatInfoEntry:
|
||||
return UInt64(6) << 40
|
||||
case .SearchEntry:
|
||||
case let .MessageEntry(message, _, _, _, _, attributes):
|
||||
let type: UInt64
|
||||
switch attributes.contentTypeHint {
|
||||
case .generic:
|
||||
type = 2
|
||||
case .largeEmoji:
|
||||
type = 3
|
||||
case .animatedEmoji:
|
||||
type = 4
|
||||
}
|
||||
return UInt64(message.stableId) | ((type << 40))
|
||||
case let .MessageGroupEntry(groupInfo, _, _):
|
||||
return UInt64(bitPattern: groupInfo) | ((UInt64(2) << 40))
|
||||
case .UnreadEntry:
|
||||
return UInt64(4) << 40
|
||||
case .ReplyCountEntry:
|
||||
return UInt64(5) << 40
|
||||
case let .ChatInfoEntry(infoData, _):
|
||||
switch infoData {
|
||||
case .newThreadInfo:
|
||||
return UInt64(7) << 40
|
||||
default:
|
||||
return UInt64(6) << 40
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var index: MessageIndex {
|
||||
switch self {
|
||||
case let .MessageEntry(message, _, _, _, _, _):
|
||||
return message.index
|
||||
case let .MessageGroupEntry(_, messages, _):
|
||||
return messages[messages.count - 1].0.index
|
||||
case let .UnreadEntry(index, _):
|
||||
return index
|
||||
case let .ReplyCountEntry(index, _, _, _):
|
||||
return index
|
||||
case let .ChatInfoEntry(infoData, _):
|
||||
switch infoData {
|
||||
case .newThreadInfo:
|
||||
return MessageIndex.absoluteUpperBound()
|
||||
default:
|
||||
return MessageIndex.absoluteLowerBound()
|
||||
}
|
||||
case .SearchEntry:
|
||||
case let .MessageEntry(message, _, _, _, _, _):
|
||||
return message.index
|
||||
case let .MessageGroupEntry(_, messages, _):
|
||||
return messages[messages.count - 1].0.index
|
||||
case let .UnreadEntry(index, _):
|
||||
return index
|
||||
case let .ReplyCountEntry(index, _, _, _):
|
||||
return index
|
||||
case let .ChatInfoEntry(infoData, _):
|
||||
switch infoData {
|
||||
case .newThreadInfo:
|
||||
return MessageIndex.absoluteUpperBound()
|
||||
default:
|
||||
return MessageIndex.absoluteLowerBound()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,8 +123,6 @@ public enum ChatHistoryEntry: Identifiable, Comparable {
|
||||
default:
|
||||
return MessageIndex.absoluteLowerBound()
|
||||
}
|
||||
case .SearchEntry:
|
||||
return MessageIndex.absoluteLowerBound()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,12 +295,6 @@ public enum ChatHistoryEntry: Identifiable, Comparable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .SearchEntry(lhsTheme, lhsStrings):
|
||||
if case let .SearchEntry(rhsTheme, rhsStrings) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -158,7 +158,7 @@ public final class ChatHistorySearchContainerNode: SearchDisplayControllerConten
|
||||
self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings, self.presentationData.dateTimeFormat, self.presentationData.listsFontSize))
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5)
|
||||
self.dimNode.backgroundColor = .clear
|
||||
self.listNode = ListView()
|
||||
self.listNode.accessibilityPageScrolledString = { row, count in
|
||||
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
|
||||
|
||||
-2
@@ -901,8 +901,6 @@ public final class ChatInlineSearchResultsListComponent: Component {
|
||||
},
|
||||
openStarsTopup: { _ in
|
||||
},
|
||||
dismissNotice: { _ in
|
||||
},
|
||||
editPeer: { _ in
|
||||
},
|
||||
openWebApp: { _ in
|
||||
|
||||
+1
@@ -440,6 +440,7 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
|
||||
if node.iconNode == nil {
|
||||
let iconNode = ASImageNode()
|
||||
iconNode.contentMode = .center
|
||||
iconNode.isUserInteractionEnabled = false
|
||||
node.iconNode = iconNode
|
||||
node.addSubnode(iconNode)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ swift_library(
|
||||
"//submodules/WallpaperBackgroundNode",
|
||||
"//submodules/LocalMediaResources",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/ChatPresentationInterfaceState",
|
||||
"//submodules/TelegramUI/Components/TextNodeWithEntities",
|
||||
"//submodules/TelegramUI/Components/ChatControllerInteraction",
|
||||
|
||||
+124
@@ -47,6 +47,7 @@ import ManagedDiceAnimationNode
|
||||
import MessageHaptics
|
||||
import ChatMessageTransitionNode
|
||||
import ChatMessageSuggestedPostInfoNode
|
||||
import TelegramStringFormatting
|
||||
|
||||
private let nameFont = Font.medium(14.0)
|
||||
private let inlineBotPrefixFont = Font.regular(14.0)
|
||||
@@ -99,6 +100,11 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
private var swipeToReplyNode: ChatMessageSwipeToReplyNode?
|
||||
private var swipeToReplyFeedback: HapticFeedback?
|
||||
|
||||
private let labelNode: TextNodeWithEntities
|
||||
private var labelBackgroundNode: WallpaperBubbleBackgroundNode?
|
||||
private let labelBackgroundMaskNode: ASImageNode
|
||||
private var cachedMaskLabelBackgroundImage: (CGPoint, UIImage, [CGRect])?
|
||||
|
||||
private var selectionNode: ChatMessageSelectionNode?
|
||||
private var deliveryFailedNode: ChatMessageDeliveryFailedNode?
|
||||
private var shareButtonNode: ChatMessageShareButton?
|
||||
@@ -161,6 +167,12 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
self.textNode.textNode.displaysAsynchronously = false
|
||||
self.textNode.textNode.isUserInteractionEnabled = false
|
||||
|
||||
self.labelNode = TextNodeWithEntities()
|
||||
self.labelNode.textNode.isUserInteractionEnabled = false
|
||||
self.labelNode.textNode.displaysAsynchronously = false
|
||||
|
||||
self.labelBackgroundMaskNode = ASImageNode()
|
||||
|
||||
super.init(rotated: rotated)
|
||||
|
||||
self.containerNode.shouldBegin = { [weak self] location in
|
||||
@@ -469,9 +481,24 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
} else if let telegramDice = self.telegramDice, let diceNode = self.animationNode as? ManagedDiceAnimationNode {
|
||||
if let value = telegramDice.value {
|
||||
let wasRolling = diceNode.isRolling
|
||||
diceNode.setState(value == 0 ? .rolling : .value(value, true))
|
||||
|
||||
if wasRolling && !diceNode.isRolling {
|
||||
Queue.mainQueue().after(3.0, {
|
||||
self.labelNode.textNode.alpha = 1.0
|
||||
self.labelBackgroundNode?.alpha = 1.0
|
||||
self.labelNode.textNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25)
|
||||
self.labelBackgroundNode?.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25)
|
||||
self.labelNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
self.labelBackgroundNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
diceNode.setState(.rolling)
|
||||
|
||||
self.labelNode.textNode.alpha = 0.0
|
||||
self.labelBackgroundNode?.alpha = 0.0
|
||||
}
|
||||
} else if self.telegramFile == nil && self.telegramDice == nil {
|
||||
let (emoji, fitz) = item.message.text.basicEmoji
|
||||
@@ -816,6 +843,9 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode)
|
||||
let reactionButtonsLayout = ChatMessageReactionButtonsNode.asyncLayout(self.reactionButtonsNode)
|
||||
|
||||
let makeLabelLayout = TextNodeWithEntities.asyncLayout(self.labelNode)
|
||||
let cachedMaskLabelBackgroundImage = self.cachedMaskLabelBackgroundImage
|
||||
|
||||
let makeForwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode)
|
||||
|
||||
let viaBotLayout = TextNode.asyncLayout(self.viaBotNode)
|
||||
@@ -850,6 +880,44 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
let avatarInset: CGFloat
|
||||
var hasAvatar = false
|
||||
|
||||
let labelAttributedText = universalServiceMessageString(presentationData: (item.presentationData.theme.theme, item.presentationData.theme.wallpaper), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: EngineMessage(item.message), accountPeerId: item.context.account.peerId, forChatList: false, forForumOverview: false, forAdditionalServiceMessage: true)
|
||||
|
||||
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: labelAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
var labelRects = labelLayout.linesRects()
|
||||
if labelRects.count > 1 {
|
||||
let sortedIndices = (0 ..< labelRects.count).sorted(by: { labelRects[$0].width > labelRects[$1].width })
|
||||
for i in 0 ..< sortedIndices.count {
|
||||
let index = sortedIndices[i]
|
||||
for j in -1 ... 1 {
|
||||
if j != 0 && index + j >= 0 && index + j < sortedIndices.count {
|
||||
if abs(labelRects[index + j].width - labelRects[index].width) < 40.0 {
|
||||
labelRects[index + j].size.width = max(labelRects[index + j].width, labelRects[index].width)
|
||||
labelRects[index].size.width = labelRects[index + j].size.width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for i in 0 ..< labelRects.count {
|
||||
labelRects[i] = labelRects[i].insetBy(dx: -7.0, dy: floor((labelRects[i].height - 22.0) / 2.0))
|
||||
labelRects[i].size.height = 22.0
|
||||
labelRects[i].origin.x = floor((labelLayout.size.width - labelRects[i].width) / 2.0)
|
||||
}
|
||||
|
||||
let backgroundMaskImage: (CGPoint, UIImage)?
|
||||
var backgroundMaskUpdated = false
|
||||
if labelLayout.size.height > 0.0 {
|
||||
if let (currentOffset, currentImage, currentRects) = cachedMaskLabelBackgroundImage, currentRects == labelRects {
|
||||
backgroundMaskImage = (currentOffset, currentImage)
|
||||
} else {
|
||||
backgroundMaskImage = LinkHighlightingNode.generateImage(color: .black, inset: 0.0, innerRadius: 11.0, outerRadius: 11.0, rects: labelRects, useModernPathCalculation: false)
|
||||
backgroundMaskUpdated = true
|
||||
}
|
||||
} else {
|
||||
backgroundMaskImage = nil
|
||||
}
|
||||
|
||||
switch item.chatLocation {
|
||||
case let .peer(peerId):
|
||||
if peerId != item.context.account.peerId {
|
||||
@@ -1061,6 +1129,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
var viewCount: Int? = nil
|
||||
var dateReplies = 0
|
||||
var starsCount: Int64?
|
||||
var tonAmount: Int64?
|
||||
var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message)
|
||||
if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) {
|
||||
dateReactionsAndPeers = ([], [])
|
||||
@@ -1079,6 +1148,10 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
}
|
||||
|
||||
if let stakeTonAmount = telegramDice?.tonAmount {
|
||||
tonAmount = stakeTonAmount
|
||||
}
|
||||
|
||||
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .regular, associatedData: item.associatedData)
|
||||
|
||||
var isReplyThread = false
|
||||
@@ -1107,6 +1180,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
messageEffect: messageEffect,
|
||||
replyCount: dateReplies,
|
||||
starsCount: starsCount,
|
||||
tonAmount: tonAmount,
|
||||
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
|
||||
hasAutoremove: item.message.isSelfExpiring,
|
||||
canViewReactionList: canViewMessageReactionList(message: item.message),
|
||||
@@ -1211,6 +1285,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
quote: replyQuote,
|
||||
todoItemId: replyTodoItemId,
|
||||
story: replyStory,
|
||||
isSummarized: false,
|
||||
parentMessage: item.message,
|
||||
constrainedSize: CGSize(width: availableContentWidth, height: CGFloat.greatestFiniteMagnitude),
|
||||
animationCache: item.controllerInteraction.presentationContext.animationCache,
|
||||
@@ -1501,6 +1576,10 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
} else {
|
||||
updatedImageFrame = imageFrame.offsetBy(dx: 0.0, dy: floor((contentHeight - imageSize.height) / 2.0))
|
||||
contextContentFrame = updatedImageFrame
|
||||
|
||||
if let telegramDice, let _ = telegramDice.tonAmount {
|
||||
updatedImageFrame = updatedImageFrame.offsetBy(dx: 0.0, dy: -30.0)
|
||||
}
|
||||
}
|
||||
var updatedContentFrame = updatedImageFrame
|
||||
if isEmoji && emojiString == nil {
|
||||
@@ -1508,6 +1587,51 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
contextContentFrame = updatedContentFrame
|
||||
}
|
||||
|
||||
let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - labelLayout.size.width) / 2.0), y: updatedImageFrame.maxY + 6.0), size: labelLayout.size)
|
||||
strongSelf.labelNode.textNode.frame = labelFrame
|
||||
if strongSelf.labelNode.textNode.supernode == nil, labelLayout.size.height > 0.0 {
|
||||
strongSelf.addSubnode(strongSelf.labelNode.textNode)
|
||||
}
|
||||
|
||||
let _ = labelApply(TextNodeWithEntities.Arguments(
|
||||
context: item.context,
|
||||
cache: item.controllerInteraction.presentationContext.animationCache,
|
||||
renderer: item.controllerInteraction.presentationContext.animationRenderer,
|
||||
placeholderColor: item.presentationData.theme.theme.chat.message.freeform.withWallpaper.reactionInactiveBackground,
|
||||
attemptSynchronous: synchronousLoads
|
||||
))
|
||||
|
||||
let baseBackgroundFrame = labelFrame.offsetBy(dx: 0.0, dy: -11.0)
|
||||
if let (offset, image) = backgroundMaskImage {
|
||||
if strongSelf.labelBackgroundNode == nil {
|
||||
if let backgroundNode = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
|
||||
backgroundNode.alpha = strongSelf.labelNode.textNode.alpha
|
||||
strongSelf.labelBackgroundNode = backgroundNode
|
||||
strongSelf.insertSubnode(backgroundNode, at: 0)
|
||||
}
|
||||
}
|
||||
|
||||
if backgroundMaskUpdated, let backgroundNode = strongSelf.labelBackgroundNode {
|
||||
if labelRects.count == 1 {
|
||||
backgroundNode.clipsToBounds = true
|
||||
backgroundNode.cornerRadius = labelRects[0].height / 2.0
|
||||
backgroundNode.view.mask = nil
|
||||
} else {
|
||||
backgroundNode.clipsToBounds = false
|
||||
backgroundNode.cornerRadius = 0.0
|
||||
backgroundNode.view.mask = strongSelf.labelBackgroundMaskNode.view
|
||||
}
|
||||
}
|
||||
|
||||
if let backgroundNode = strongSelf.labelBackgroundNode {
|
||||
backgroundNode.layer.frame = CGRect(origin: CGPoint(x: baseBackgroundFrame.minX + offset.x, y: baseBackgroundFrame.minY + offset.y), size: image.size)
|
||||
}
|
||||
strongSelf.labelBackgroundMaskNode.image = image
|
||||
strongSelf.labelBackgroundMaskNode.frame = CGRect(origin: CGPoint(), size: image.size)
|
||||
|
||||
strongSelf.cachedMaskLabelBackgroundImage = (offset, image, labelRects)
|
||||
}
|
||||
|
||||
if let (_, textApply) = textLayoutAndApply {
|
||||
let placeholderColor = bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.stickerPlaceholderColor, wallpaper: item.presentationData.theme.wallpaper)
|
||||
let _ = textApply(TextNodeWithEntities.Arguments(context: item.context, cache: item.controllerInteraction.presentationContext.animationCache, renderer: item.controllerInteraction.presentationContext.animationRenderer, placeholderColor: placeholderColor, attemptSynchronous: synchronousLoads))
|
||||
|
||||
@@ -94,6 +94,7 @@ swift_library(
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageSuggestedPostInfoNode",
|
||||
"//submodules/TelegramUI/Components/PremiumAlertController",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
+160
-2
@@ -83,6 +83,7 @@ import TelegramAnimatedStickerNode
|
||||
import LottieMetal
|
||||
import AvatarNode
|
||||
import ChatMessageSuggestedPostInfoNode
|
||||
import PremiumAlertController
|
||||
|
||||
private struct BubbleItemAttributes {
|
||||
var index: Int?
|
||||
@@ -689,6 +690,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
private var unlockButtonNode: ChatMessageUnlockMediaNode?
|
||||
private var mediaInfoNode: ChatMessageStarsMediaInfoNode?
|
||||
|
||||
private var summarizeButtonNode: ChatMessageShareButton?
|
||||
private var shareButtonNode: ChatMessageShareButton?
|
||||
|
||||
private let messageAccessibilityArea: AccessibilityAreaNode
|
||||
@@ -1222,6 +1224,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
}
|
||||
}
|
||||
|
||||
if let summarizeButtonNode = strongSelf.summarizeButtonNode, summarizeButtonNode.frame.contains(point) {
|
||||
return .fail
|
||||
}
|
||||
|
||||
if let shareButtonNode = strongSelf.shareButtonNode, shareButtonNode.frame.contains(point) {
|
||||
return .fail
|
||||
}
|
||||
@@ -1724,6 +1730,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
let isFailed = item.content.firstMessage.effectivelyFailed(timestamp: item.context.account.network.getApproximateRemoteTimestamp())
|
||||
|
||||
var needsShareButton = false
|
||||
var needsSummarizeButton = false
|
||||
|
||||
if incoming, case let .customChatContents(contents) = item.associatedData.subject, case .hashTagSearch = contents.kind {
|
||||
needsShareButton = true
|
||||
@@ -1751,6 +1758,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
needsShareButton = true
|
||||
}
|
||||
|
||||
if let _ = item.message.attributes.first(where: { $0 is SummarizationMessageAttribute }) {
|
||||
needsSummarizeButton = true
|
||||
}
|
||||
|
||||
if let peer = item.message.peers[item.message.id.peerId] {
|
||||
if let channel = peer as? TelegramChannel {
|
||||
if case .broadcast = channel.info {
|
||||
@@ -1789,6 +1800,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
loop: for media in item.message.media {
|
||||
if media is TelegramMediaAction {
|
||||
needsShareButton = false
|
||||
needsSummarizeButton = false
|
||||
break loop
|
||||
} else if let media = media as? TelegramMediaFile, media.isInstantVideo {
|
||||
mayHaveSeparateCommentsButton = true
|
||||
@@ -1801,12 +1813,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
if mayHaveSeparateCommentsButton && hasCommentButton(item: item) {
|
||||
} else {
|
||||
needsShareButton = false
|
||||
needsSummarizeButton = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isPreview {
|
||||
needsShareButton = false
|
||||
needsSummarizeButton = false
|
||||
}
|
||||
let isAd = item.content.firstMessage.adAttribute != nil
|
||||
if isAd {
|
||||
@@ -1815,13 +1829,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
for attribute in item.content.firstMessage.attributes {
|
||||
if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) != nil {
|
||||
needsShareButton = false
|
||||
needsSummarizeButton = false
|
||||
}
|
||||
}
|
||||
|
||||
if let subject = item.associatedData.subject, case .messageOptions = subject {
|
||||
needsShareButton = false
|
||||
}
|
||||
|
||||
|
||||
var tmpWidth: CGFloat
|
||||
if allowFullWidth {
|
||||
tmpWidth = baseWidth
|
||||
@@ -2312,6 +2327,17 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
}
|
||||
}
|
||||
|
||||
let translateToLanguage = item.associatedData.translateToLanguage
|
||||
var isSummarized = false
|
||||
if item.controllerInteraction.summarizedMessageIds.contains(item.message.id) {
|
||||
for attribute in item.message.attributes {
|
||||
if let attribute = attribute as? SummarizationMessageAttribute, attribute.summaryForLang(translateToLanguage) != nil {
|
||||
initialDisplayHeader = true
|
||||
isSummarized = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var displayHeader = false
|
||||
if initialDisplayHeader {
|
||||
if authorNameString != nil {
|
||||
@@ -2339,6 +2365,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
displayHeader = true
|
||||
}
|
||||
}
|
||||
if isSummarized {
|
||||
displayHeader = true
|
||||
}
|
||||
}
|
||||
|
||||
let firstNodeTopPosition: ChatMessageBubbleRelativePosition
|
||||
@@ -2717,7 +2746,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
hasReply = false
|
||||
}
|
||||
|
||||
if !isInstantVideo, hasReply, (replyMessage != nil || replyForward != nil || replyStory != nil) {
|
||||
if isSummarized {
|
||||
hasReply = true
|
||||
}
|
||||
|
||||
if !isInstantVideo, hasReply, (replyMessage != nil || replyForward != nil || replyStory != nil || isSummarized) {
|
||||
if headerSize.height.isZero {
|
||||
headerSize.height += 11.0
|
||||
} else {
|
||||
@@ -2733,6 +2766,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
quote: replyQuote,
|
||||
todoItemId: replyTodoItemId,
|
||||
story: replyStory,
|
||||
isSummarized: isSummarized,
|
||||
parentMessage: item.message,
|
||||
constrainedSize: CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right - 6.0, height: CGFloat.greatestFiniteMagnitude),
|
||||
animationCache: item.controllerInteraction.presentationContext.animationCache,
|
||||
@@ -3491,6 +3525,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
unlockButtonSizeAndApply: unlockButtonSizeApply,
|
||||
mediaInfoOrigin: mediaInfoOrigin?.offsetBy(dx: 0.0, dy: layoutInsets.top),
|
||||
mediaInfoSizeAndApply: mediaInfoSizeApply,
|
||||
needsSummarizeButton: needsSummarizeButton,
|
||||
needsShareButton: needsShareButton,
|
||||
shareButtonOffset: shareButtonOffset,
|
||||
avatarOffset: avatarOffset,
|
||||
@@ -3556,6 +3591,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
unlockButtonSizeAndApply: (CGSize, (Bool) -> ChatMessageUnlockMediaNode?),
|
||||
mediaInfoOrigin: CGPoint?,
|
||||
mediaInfoSizeAndApply: (CGSize, (Bool) -> ChatMessageStarsMediaInfoNode?),
|
||||
needsSummarizeButton: Bool,
|
||||
needsShareButton: Bool,
|
||||
shareButtonOffset: CGPoint?,
|
||||
avatarOffset: CGFloat?,
|
||||
@@ -4787,6 +4823,20 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
})
|
||||
}
|
||||
|
||||
if needsSummarizeButton {
|
||||
if strongSelf.summarizeButtonNode == nil {
|
||||
let summarizeButtonNode = ChatMessageShareButton()
|
||||
strongSelf.summarizeButtonNode = summarizeButtonNode
|
||||
strongSelf.insertSubnode(summarizeButtonNode, belowSubnode: strongSelf.messageAccessibilityArea)
|
||||
summarizeButtonNode.pressed = { [weak strongSelf] in
|
||||
strongSelf?.toggleSummarization()
|
||||
}
|
||||
}
|
||||
} else if let summarizeButtonNode = strongSelf.summarizeButtonNode {
|
||||
strongSelf.summarizeButtonNode = nil
|
||||
summarizeButtonNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
if needsShareButton {
|
||||
if strongSelf.shareButtonNode == nil {
|
||||
let shareButtonNode = ChatMessageShareButton()
|
||||
@@ -4960,6 +5010,30 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
}
|
||||
strongSelf.messageAccessibilityArea.frame = backgroundFrame
|
||||
}
|
||||
if let summarizeButtonNode = strongSelf.summarizeButtonNode {
|
||||
let buttonSize = summarizeButtonNode.update(presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: disablesComments, isSummarize: true)
|
||||
|
||||
var buttonFrame = CGRect(origin: CGPoint(x: !incoming ? backgroundFrame.minX - buttonSize.width - 8.0 : backgroundFrame.maxX + 8.0, y: backgroundFrame.minY + 1.0), size: buttonSize)
|
||||
|
||||
if let shareButtonOffset = shareButtonOffset {
|
||||
if incoming {
|
||||
buttonFrame.origin.x = shareButtonOffset.x
|
||||
}
|
||||
buttonFrame.origin.y = buttonFrame.origin.y + shareButtonOffset.y - (buttonSize.height - 30.0)
|
||||
} else if !disablesComments {
|
||||
buttonFrame.origin.y = buttonFrame.origin.y - (buttonSize.height - 30.0)
|
||||
}
|
||||
|
||||
if isSidePanelOpen {
|
||||
buttonFrame.origin.x -= buttonFrame.width * 0.5
|
||||
buttonFrame.origin.y += buttonFrame.height * 0.5
|
||||
}
|
||||
|
||||
animation.animator.updatePosition(layer: summarizeButtonNode.layer, position: buttonFrame.center, completion: nil)
|
||||
animation.animator.updateBounds(layer: summarizeButtonNode.layer, bounds: CGRect(origin: CGPoint(), size: buttonFrame.size), completion: nil)
|
||||
animation.animator.updateAlpha(layer: summarizeButtonNode.layer, alpha: (isCurrentlyPlayingMedia || isSidePanelOpen) ? 0.0 : 1.0, completion: nil)
|
||||
animation.animator.updateScale(layer: summarizeButtonNode.layer, scale: (isCurrentlyPlayingMedia || isSidePanelOpen) ? 0.001 : 1.0, completion: nil)
|
||||
}
|
||||
if let shareButtonNode = strongSelf.shareButtonNode {
|
||||
let buttonSize = shareButtonNode.update(presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: disablesComments)
|
||||
|
||||
@@ -4994,6 +5068,30 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
strongSelf.backgroundFrameTransition = nil
|
||||
}*/
|
||||
strongSelf.messageAccessibilityArea.frame = backgroundFrame
|
||||
if let summarizeButtonNode = strongSelf.summarizeButtonNode {
|
||||
let buttonSize = summarizeButtonNode.update(presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: disablesComments, isSummarize: true)
|
||||
|
||||
var buttonFrame = CGRect(origin: CGPoint(x: !incoming ? backgroundFrame.minX - buttonSize.width - 8.0 : backgroundFrame.maxX + 8.0, y: backgroundFrame.minY + 1.0), size: buttonSize)
|
||||
|
||||
if let shareButtonOffset = shareButtonOffset {
|
||||
if incoming {
|
||||
buttonFrame.origin.x = shareButtonOffset.x
|
||||
}
|
||||
buttonFrame.origin.y = buttonFrame.origin.y + shareButtonOffset.y - (buttonSize.height - 30.0)
|
||||
} else if !disablesComments {
|
||||
buttonFrame.origin.y = buttonFrame.origin.y - (buttonSize.height - 30.0)
|
||||
}
|
||||
|
||||
if isSidePanelOpen {
|
||||
buttonFrame.origin.x -= buttonFrame.width * 0.5
|
||||
buttonFrame.origin.y += buttonFrame.height * 0.5
|
||||
}
|
||||
|
||||
animation.animator.updatePosition(layer: summarizeButtonNode.layer, position: buttonFrame.center, completion: nil)
|
||||
animation.animator.updateBounds(layer: summarizeButtonNode.layer, bounds: CGRect(origin: CGPoint(), size: buttonFrame.size), completion: nil)
|
||||
animation.animator.updateAlpha(layer: summarizeButtonNode.layer, alpha: (isCurrentlyPlayingMedia || isSidePanelOpen) ? 0.0 : 1.0, completion: nil)
|
||||
animation.animator.updateScale(layer: summarizeButtonNode.layer, scale: (isCurrentlyPlayingMedia || isSidePanelOpen) ? 0.001 : 1.0, completion: nil)
|
||||
}
|
||||
if let shareButtonNode = strongSelf.shareButtonNode {
|
||||
let buttonSize = shareButtonNode.update(presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: disablesComments)
|
||||
|
||||
@@ -5275,6 +5373,15 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
}
|
||||
} else if let replyInfoNode = self.replyInfoNode, self.item?.controllerInteraction.tapMessage == nil, replyInfoNode.frame.contains(location) {
|
||||
if let item = self.item {
|
||||
if item.controllerInteraction.summarizedMessageIds.contains(item.message.id) {
|
||||
return .action(InternalBubbleTapAction.Action({ [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.toggleSummarization()
|
||||
}))
|
||||
}
|
||||
|
||||
for attribute in item.message.attributes {
|
||||
if let attribute = attribute as? ReplyMessageAttribute {
|
||||
if let threadId = item.message.threadId, Int32(clamping: threadId) == attribute.messageId.id, let quotedReply = item.message.attributes.first(where: { $0 is QuotedReplyMessageAttribute }) as? QuotedReplyMessageAttribute {
|
||||
@@ -5824,6 +5931,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
return boostButtonNode.view
|
||||
}
|
||||
|
||||
if let summarizeButtonNode = self.summarizeButtonNode, summarizeButtonNode.frame.contains(point) {
|
||||
return summarizeButtonNode.view.hitTest(self.view.convert(point, to: summarizeButtonNode.view), with: event)
|
||||
}
|
||||
|
||||
if let shareButtonNode = self.shareButtonNode, shareButtonNode.frame.contains(point) {
|
||||
return shareButtonNode.view.hitTest(self.view.convert(point, to: shareButtonNode.view), with: event)
|
||||
}
|
||||
@@ -6516,6 +6627,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
container.updateAbsoluteRect(containerFrame, within: containerSize)
|
||||
}
|
||||
|
||||
if let summarizeButtonNode = self.summarizeButtonNode {
|
||||
var summarizeButtonNodeFrame = summarizeButtonNode.frame
|
||||
summarizeButtonNodeFrame.origin.x += rect.minX
|
||||
summarizeButtonNodeFrame.origin.y += rect.minY
|
||||
|
||||
summarizeButtonNode.updateAbsoluteRect(summarizeButtonNodeFrame, within: containerSize)
|
||||
}
|
||||
|
||||
if let shareButtonNode = self.shareButtonNode {
|
||||
var shareButtonNodeFrame = shareButtonNode.frame
|
||||
shareButtonNodeFrame.origin.x += rect.minX
|
||||
@@ -6854,6 +6973,45 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
self.updateVisibility(isScroll: false)
|
||||
}
|
||||
|
||||
private func toggleSummarization() {
|
||||
guard let item = self.item else {
|
||||
return
|
||||
}
|
||||
|
||||
if item.controllerInteraction.summarizedMessageIds.contains(item.message.id) {
|
||||
item.controllerInteraction.summarizedMessageIds.remove(item.message.id)
|
||||
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false)
|
||||
} else {
|
||||
item.controllerInteraction.summarizedMessageIds.insert(item.message.id)
|
||||
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false)
|
||||
|
||||
let translateToLanguage = item.associatedData.translateToLanguage
|
||||
var requestSummary = true
|
||||
for attribute in item.message.attributes {
|
||||
if let attribute = attribute as? SummarizationMessageAttribute, attribute.summaryForLang(translateToLanguage) != nil {
|
||||
requestSummary = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if requestSummary {
|
||||
let _ = (item.context.engine.messages.summarizeMessage(messageId: item.message.id, translateToLang: translateToLanguage)
|
||||
|> deliverOnMainQueue).start(error: { error in
|
||||
if case .limitExceededPremium = error, let parentController = item.controllerInteraction.navigationController()?.topViewController as? ViewController {
|
||||
item.controllerInteraction.summarizedMessageIds.remove(item.message.id)
|
||||
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false)
|
||||
let controller = premiumAlertController(
|
||||
context: item.context,
|
||||
parentController: parentController,
|
||||
title: item.presentationData.strings.Conversation_Summary_Limit_Title,
|
||||
text: item.presentationData.strings.Conversation_Summary_Limit_Text
|
||||
)
|
||||
parentController.present(controller, in: .window(.root))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateVisibility(isScroll: Bool) {
|
||||
guard let item = self.item else {
|
||||
return
|
||||
|
||||
+80
-4
@@ -12,6 +12,7 @@ import ReactionButtonListComponent
|
||||
import ReactionImageComponent
|
||||
import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
import TelegramStringFormatting
|
||||
|
||||
private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) {
|
||||
if let _ = layer.animation(forKey: "clockFrameAnimation") {
|
||||
@@ -197,8 +198,10 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
var messageEffect: AvailableMessageEffects.MessageEffect?
|
||||
var replyCount: Int
|
||||
var starsCount: Int64?
|
||||
var tonAmount: Int64?
|
||||
var isPinned: Bool
|
||||
var hasAutoremove: Bool
|
||||
var isDeleted: Bool
|
||||
var canViewReactionList: Bool
|
||||
var animationCache: AnimationCache
|
||||
var animationRenderer: MultiAnimationRenderer
|
||||
@@ -222,8 +225,10 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
messageEffect: AvailableMessageEffects.MessageEffect?,
|
||||
replyCount: Int,
|
||||
starsCount: Int64?,
|
||||
tonAmount: Int64? = nil,
|
||||
isPinned: Bool,
|
||||
hasAutoremove: Bool,
|
||||
isDeleted: Bool = false,
|
||||
canViewReactionList: Bool,
|
||||
animationCache: AnimationCache,
|
||||
animationRenderer: MultiAnimationRenderer
|
||||
@@ -246,8 +251,10 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
self.messageEffect = messageEffect
|
||||
self.replyCount = replyCount
|
||||
self.starsCount = starsCount
|
||||
self.tonAmount = tonAmount
|
||||
self.isPinned = isPinned
|
||||
self.hasAutoremove = hasAutoremove
|
||||
self.isDeleted = isDeleted
|
||||
self.canViewReactionList = canViewReactionList
|
||||
self.animationCache = animationCache
|
||||
self.animationRenderer = animationRenderer
|
||||
@@ -270,6 +277,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
private var replyCountNode: TextNode?
|
||||
private var starsIcon: ASImageNode?
|
||||
private var starsCountNode: TextNode?
|
||||
private var deletedIcon: ASImageNode?
|
||||
|
||||
private var type: ChatMessageDateAndStatusType?
|
||||
private var theme: ChatPresentationThemeData?
|
||||
@@ -417,8 +425,10 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
} else if arguments.isPinned {
|
||||
repliesImage = graphics.incomingDateAndStatusPinnedIcon
|
||||
}
|
||||
if (arguments.starsCount ?? 0) != 0 {
|
||||
if (arguments.starsCount ?? 0) != 0 {
|
||||
starsImage = graphics.incomingDateAndStatusStarsIcon
|
||||
} else if (arguments.tonAmount ?? 0) != 0 {
|
||||
starsImage = graphics.incomingDateAndStatusTonIcon
|
||||
}
|
||||
case let .BubbleOutgoing(status):
|
||||
dateColor = arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor
|
||||
@@ -438,6 +448,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
}
|
||||
if (arguments.starsCount ?? 0) != 0 {
|
||||
starsImage = graphics.outgoingDateAndStatusStarsIcon
|
||||
} else if (arguments.tonAmount ?? 0) != 0 {
|
||||
starsImage = graphics.outgoingDateAndStatusTonIcon
|
||||
}
|
||||
case .ImageIncoming:
|
||||
dateColor = arguments.presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor
|
||||
@@ -457,6 +469,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
}
|
||||
if (arguments.starsCount ?? 0) != 0 {
|
||||
starsImage = graphics.mediaStarsIcon
|
||||
} else if (arguments.tonAmount ?? 0) != 0 {
|
||||
starsImage = graphics.mediaTonIcon
|
||||
}
|
||||
case let .ImageOutgoing(status):
|
||||
dateColor = arguments.presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor
|
||||
@@ -477,6 +491,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
}
|
||||
if (arguments.starsCount ?? 0) != 0 {
|
||||
starsImage = graphics.mediaStarsIcon
|
||||
} else if (arguments.tonAmount ?? 0) != 0 {
|
||||
starsImage = graphics.mediaTonIcon
|
||||
}
|
||||
case .FreeIncoming:
|
||||
let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper)
|
||||
@@ -498,6 +514,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
}
|
||||
if (arguments.starsCount ?? 0) != 0 {
|
||||
starsImage = graphics.freeStarsIcon
|
||||
} else if (arguments.tonAmount ?? 0) != 0 {
|
||||
starsImage = graphics.freeTonIcon
|
||||
}
|
||||
case let .FreeOutgoing(status):
|
||||
let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper)
|
||||
@@ -519,6 +537,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
}
|
||||
if (arguments.starsCount ?? 0) != 0 {
|
||||
starsImage = graphics.freeStarsIcon
|
||||
} else if (arguments.tonAmount ?? 0) != 0 {
|
||||
starsImage = graphics.freeTonIcon
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,6 +606,36 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
currentStarsIcon = nil
|
||||
}
|
||||
|
||||
// ANTIDELETE: Deleted message icon
|
||||
var currentDeletedIcon = self?.deletedIcon
|
||||
var deletedIconSize = CGSize()
|
||||
if arguments.isDeleted {
|
||||
if currentDeletedIcon == nil {
|
||||
let iconNode = ASImageNode()
|
||||
iconNode.isLayerBacked = true
|
||||
iconNode.displayWithoutProcessing = true
|
||||
iconNode.displaysAsynchronously = false
|
||||
currentDeletedIcon = iconNode
|
||||
}
|
||||
// Use trash icon from bundle or create simple one
|
||||
let deletedImage = UIImage(bundleImageName: "Chat/Message/DeletedIcon") ?? generateTintedImage(image: UIImage(systemName: "trash"), color: dateColor)
|
||||
deletedIconSize = deletedImage?.size ?? CGSize(width: 10, height: 10)
|
||||
// Scale down the image
|
||||
if let img = deletedImage {
|
||||
let scaledSize = CGSize(width: 10, height: 10)
|
||||
UIGraphicsBeginImageContextWithOptions(scaledSize, false, 0.0)
|
||||
img.draw(in: CGRect(origin: .zero, size: scaledSize))
|
||||
let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
currentDeletedIcon?.image = scaledImage
|
||||
deletedIconSize = scaledSize
|
||||
} else {
|
||||
currentDeletedIcon?.image = deletedImage
|
||||
}
|
||||
} else {
|
||||
currentDeletedIcon = nil
|
||||
}
|
||||
|
||||
if let outgoingStatus = outgoingStatus {
|
||||
switch outgoingStatus {
|
||||
case .Sending:
|
||||
@@ -717,7 +767,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
|
||||
let layoutAndApply = makeReplyCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0)))
|
||||
reactionInset += 14.0 + layoutAndApply.0.size.width + 4.0
|
||||
if arguments.starsCount != nil {
|
||||
if arguments.starsCount != nil || arguments.tonAmount != nil {
|
||||
reactionInset += 3.0
|
||||
}
|
||||
replyCountLayoutAndApply = layoutAndApply
|
||||
@@ -735,6 +785,11 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
countString = "\(starsCount)"
|
||||
}
|
||||
|
||||
let layoutAndApply = makeStarsCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0)))
|
||||
reactionInset += 14.0 + layoutAndApply.0.size.width + 4.0
|
||||
starsCountLayoutAndApply = layoutAndApply
|
||||
} else if let tonAmount = arguments.tonAmount, tonAmount > 0 {
|
||||
let countString = formatTonAmountText(tonAmount, dateTimeFormat: arguments.presentationData.dateTimeFormat)
|
||||
let layoutAndApply = makeStarsCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0)))
|
||||
reactionInset += 14.0 + layoutAndApply.0.size.width + 4.0
|
||||
starsCountLayoutAndApply = layoutAndApply
|
||||
@@ -744,9 +799,15 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
reactionInset += 13.0
|
||||
}
|
||||
|
||||
// ANTIDELETE: Add deleted icon space
|
||||
var deletedIconWidth: CGFloat = 0.0
|
||||
if arguments.isDeleted {
|
||||
deletedIconWidth = deletedIconSize.width + 3.0
|
||||
}
|
||||
|
||||
leftInset += reactionInset
|
||||
|
||||
let layoutSize = CGSize(width: leftInset + impressionWidth + date.size.width + statusWidth + backgroundInsets.left + backgroundInsets.right, height: date.size.height + backgroundInsets.top + backgroundInsets.bottom)
|
||||
let layoutSize = CGSize(width: leftInset + deletedIconWidth + impressionWidth + date.size.width + statusWidth + backgroundInsets.left + backgroundInsets.right, height: date.size.height + backgroundInsets.top + backgroundInsets.bottom)
|
||||
|
||||
let verticalReactionsInset: CGFloat
|
||||
let verticalInset: CGFloat
|
||||
@@ -1089,7 +1150,22 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
strongSelf.impressionIcon = nil
|
||||
}
|
||||
|
||||
animation.animator.updateFrame(layer: strongSelf.dateNode.layer, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left + impressionWidth, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: date.size), completion: nil)
|
||||
// ANTIDELETE: Position deleted icon
|
||||
if let currentDeletedIcon = currentDeletedIcon {
|
||||
let deletedIconFrame = CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left + impressionWidth, y: backgroundInsets.top + 1.0 + offset + verticalInset + floor((date.size.height - deletedIconSize.height) / 2.0)), size: deletedIconSize)
|
||||
if currentDeletedIcon.supernode == nil {
|
||||
strongSelf.deletedIcon = currentDeletedIcon
|
||||
strongSelf.addSubnode(currentDeletedIcon)
|
||||
currentDeletedIcon.frame = deletedIconFrame
|
||||
} else {
|
||||
animation.animator.updateFrame(layer: currentDeletedIcon.layer, frame: deletedIconFrame, completion: nil)
|
||||
}
|
||||
} else if let deletedIcon = strongSelf.deletedIcon {
|
||||
deletedIcon.removeFromSupernode()
|
||||
strongSelf.deletedIcon = nil
|
||||
}
|
||||
|
||||
animation.animator.updateFrame(layer: strongSelf.dateNode.layer, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left + impressionWidth + deletedIconWidth, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: date.size), completion: nil)
|
||||
|
||||
if let clockFrameNode = clockFrameNode {
|
||||
let clockPosition = CGPoint(x: leftOffset + backgroundInsets.left + clockPosition.x + reactionInset, y: backgroundInsets.top + clockPosition.y + verticalInset)
|
||||
|
||||
+5
-2
@@ -455,7 +455,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
switch action.action {
|
||||
case let .giftPremium(_, _, daysValue, _, _, giftText, giftEntities):
|
||||
months = max(3, Int32(round(Float(daysValue) / 30.0)))
|
||||
if months == 12 {
|
||||
if daysValue < 30 {
|
||||
title = item.presentationData.strings.Notification_PremiumGift_DaysTitle(daysValue)
|
||||
} else if months == 12 {
|
||||
title = item.presentationData.strings.Notification_PremiumGift_YearsTitle(1)
|
||||
} else {
|
||||
title = item.presentationData.strings.Notification_PremiumGift_MonthsTitle(months)
|
||||
@@ -513,7 +515,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
title = item.presentationData.strings.Notification_StarsGiveaway_Title
|
||||
let starsString = item.presentationData.strings.Notification_StarsGiveaway_Subtitle_Stars(Int32(clamping: count)).replacingOccurrences(of: " ", with: "\u{00A0}")
|
||||
text = item.presentationData.strings.Notification_StarsGiveaway_Subtitle(peerName, starsString).string
|
||||
case let .giftCode(_, fromGiveaway, unclaimed, channelId, monthsValue, _, _, _, _, giftText, giftEntities):
|
||||
case let .giftCode(_, fromGiveaway, unclaimed, channelId, daysValue, _, _, _, _, giftText, giftEntities):
|
||||
let monthsValue = max(3, Int32(round(Float(daysValue) / 30.0)))
|
||||
if channelId == nil {
|
||||
months = monthsValue
|
||||
if months == 12 {
|
||||
|
||||
+1
-1
@@ -131,7 +131,7 @@ public class ChatMessageGiftOfferBubbleContentNode: ChatMessageBubbleContentNode
|
||||
case .stars:
|
||||
priceString = item.presentationData.strings.Notification_StarGiftOffer_Offer_Stars(Int32(clamping: amount.amount.value))
|
||||
case .ton:
|
||||
priceString = "\(amount.amount) TON"
|
||||
priceString = formatTonAmountText(amount.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat) + " TON"
|
||||
}
|
||||
|
||||
let peerName = item.message.peers[item.message.id.peerId].flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
|
||||
|
||||
+1
@@ -530,6 +530,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco
|
||||
quote: replyQuote,
|
||||
todoItemId: replyTodoItemId,
|
||||
story: replyStory,
|
||||
isSummarized: false,
|
||||
parentMessage: item.message,
|
||||
constrainedSize: CGSize(width: max(0, availableWidth), height: CGFloat.greatestFiniteMagnitude),
|
||||
animationCache: item.controllerInteraction.presentationContext.animationCache,
|
||||
|
||||
+14
-61
@@ -349,7 +349,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
private func transcribe() {
|
||||
guard let arguments = self.arguments, let context = self.context, let message = self.message else {
|
||||
guard let _ = self.arguments, let context = self.context, let message = self.message else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -358,43 +358,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: arguments.context.currentAppConfiguration.with { $0 })
|
||||
|
||||
let transcriptionText = self.forcedAudioTranscriptionText ?? transcribedText(message: message)
|
||||
if transcriptionText == nil && !arguments.associatedData.alwaysDisplayTranscribeButton.providedByGroupBoost {
|
||||
if premiumConfiguration.audioTransciptionTrialCount > 0 {
|
||||
if !arguments.associatedData.isPremium {
|
||||
if self.presentAudioTranscriptionTooltip(finished: false) {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
guard arguments.associatedData.isPremium else {
|
||||
if self.hapticFeedback == nil {
|
||||
self.hapticFeedback = HapticFeedback()
|
||||
}
|
||||
self.hapticFeedback?.impact(.medium)
|
||||
|
||||
let tipController = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_voiceToText", scale: 0.065, colors: [:], title: nil, text: presentationData.strings.Message_AudioTranscription_SubscribeToPremium, customUndoText: presentationData.strings.Message_AudioTranscription_SubscribeToPremiumAction, timeout: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { action in
|
||||
if case .undo = action {
|
||||
var replaceImpl: ((ViewController) -> Void)?
|
||||
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, forceDark: false, action: {
|
||||
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil)
|
||||
replaceImpl?(controller)
|
||||
}, dismissed: nil)
|
||||
replaceImpl = { [weak controller] c in
|
||||
controller?.replace(with: c)
|
||||
}
|
||||
arguments.controllerInteraction.navigationController()?.pushViewController(controller, animated: true)
|
||||
|
||||
let _ = ApplicationSpecificNotice.incrementAudioTranscriptionSuggestion(accountManager: context.sharedContext.accountManager).startStandalone()
|
||||
}
|
||||
return false })
|
||||
arguments.controllerInteraction.presentControllerInCurrent(tipController, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
// GHOSTGRAM: Premium check removed - local transcription is free!
|
||||
|
||||
var shouldBeginTranscription = false
|
||||
var shouldExpandNow = false
|
||||
@@ -420,7 +384,8 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
self.audioTranscriptionState = .inProgress
|
||||
self.requestUpdateLayout(true)
|
||||
|
||||
if context.sharedContext.immediateExperimentalUISettings.localTranscription {
|
||||
// GHOSTGRAM: Always use local transcription (free, private, on-device!)
|
||||
if true {
|
||||
let appLocale = presentationData.strings.baseLanguageCode
|
||||
|
||||
let signal: Signal<LocallyTranscribedAudio?, NoError> = context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: message.id))
|
||||
@@ -640,8 +605,6 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
var audioWaveform: AudioWaveform?
|
||||
var isVoice = false
|
||||
var audioDuration: Int32 = 0
|
||||
var isConsumed: Bool?
|
||||
|
||||
var consumableContentIcon: UIImage?
|
||||
for attribute in arguments.message.attributes {
|
||||
if let attribute = attribute as? ConsumableContentMessageAttribute {
|
||||
@@ -652,7 +615,6 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
consumableContentIcon = PresentationResourcesChat.chatBubbleConsumableContentOutgoingIcon(arguments.presentationData.theme.theme)
|
||||
}
|
||||
}
|
||||
isConsumed = attribute.consumed
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -771,24 +733,8 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
if Namespaces.Message.allNonRegular.contains(arguments.message.id.namespace) {
|
||||
displayTranscribe = false
|
||||
} else if arguments.message.id.peerId.namespace != Namespaces.Peer.SecretChat && !isViewOnceMessage && !arguments.presentationData.isPreview {
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: arguments.context.currentAppConfiguration.with { $0 })
|
||||
if arguments.associatedData.isPremium {
|
||||
displayTranscribe = true
|
||||
} else if premiumConfiguration.audioTransciptionTrialCount > 0 {
|
||||
if arguments.incoming {
|
||||
if audioDuration < premiumConfiguration.audioTransciptionTrialMaxDuration {
|
||||
displayTranscribe = true
|
||||
}
|
||||
}
|
||||
} else if arguments.associatedData.alwaysDisplayTranscribeButton.canBeDisplayed {
|
||||
if audioDuration >= 60 {
|
||||
displayTranscribe = true
|
||||
} else if arguments.incoming && isConsumed == false && arguments.associatedData.alwaysDisplayTranscribeButton.displayForNotConsumed {
|
||||
displayTranscribe = true
|
||||
}
|
||||
} else if arguments.associatedData.alwaysDisplayTranscribeButton.providedByGroupBoost {
|
||||
displayTranscribe = true
|
||||
}
|
||||
// GHOSTGRAM: Always show transcribe button for voice messages
|
||||
displayTranscribe = true
|
||||
}
|
||||
|
||||
let transcribedText = forcedAudioTranscriptionText ?? transcribedText(message: arguments.message)
|
||||
@@ -1564,8 +1510,15 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
|
||||
if isTranslating, !rects.isEmpty {
|
||||
if self.shimmeringNodes.isEmpty {
|
||||
let color: UIColor
|
||||
let isIncoming = arguments.message.effectivelyIncoming(arguments.context.account.peerId)
|
||||
if arguments.presentationData.theme.theme.overallDarkAppearance {
|
||||
color = isIncoming ? arguments.presentationData.theme.theme.chat.message.incoming.primaryTextColor.withAlphaComponent(0.1) : arguments.presentationData.theme.theme.chat.message.outgoing.primaryTextColor.withAlphaComponent(0.1)
|
||||
} else {
|
||||
color = isIncoming ? arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor.withAlphaComponent(0.1) : arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor.withAlphaComponent(0.1)
|
||||
}
|
||||
for rects in rects {
|
||||
let shimmeringNode = ShimmeringLinkNode(color: arguments.message.effectivelyIncoming(arguments.context.account.peerId) ? arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor.withAlphaComponent(0.1) : arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor.withAlphaComponent(0.1))
|
||||
let shimmeringNode = ShimmeringLinkNode(color: color)
|
||||
shimmeringNode.updateRects(rects)
|
||||
shimmeringNode.frame = self.bounds
|
||||
shimmeringNode.updateLayout(self.bounds.size)
|
||||
|
||||
+2
-39
@@ -404,6 +404,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
quote: replyQuote,
|
||||
todoItemId: replyTodoItemId,
|
||||
story: replyStory,
|
||||
isSummarized: false,
|
||||
parentMessage: item.message,
|
||||
constrainedSize: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude),
|
||||
animationCache: item.controllerInteraction.presentationContext.animationCache,
|
||||
@@ -1830,45 +1831,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
return
|
||||
}
|
||||
|
||||
let presentationData = item.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: item.context.currentAppConfiguration.with { $0 })
|
||||
|
||||
let transcriptionText = transcribedText(message: item.message)
|
||||
if transcriptionText == nil && !item.associatedData.alwaysDisplayTranscribeButton.providedByGroupBoost {
|
||||
if premiumConfiguration.audioTransciptionTrialCount > 0 {
|
||||
if !item.associatedData.isPremium {
|
||||
if self.presentAudioTranscriptionTooltip(finished: false) {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
guard item.associatedData.isPremium else {
|
||||
if self.hapticFeedback == nil {
|
||||
self.hapticFeedback = HapticFeedback()
|
||||
}
|
||||
self.hapticFeedback?.impact(.medium)
|
||||
|
||||
let tipController = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_voiceToText", scale: 0.065, colors: [:], title: nil, text: presentationData.strings.Message_AudioTranscription_SubscribeToPremium, customUndoText: presentationData.strings.Message_AudioTranscription_SubscribeToPremiumAction, timeout: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { action in
|
||||
if case .undo = action {
|
||||
let context = item.context
|
||||
var replaceImpl: ((ViewController) -> Void)?
|
||||
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, forceDark: false, action: {
|
||||
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil)
|
||||
replaceImpl?(controller)
|
||||
}, dismissed: nil)
|
||||
replaceImpl = { [weak controller] c in
|
||||
controller?.replace(with: c)
|
||||
}
|
||||
item.controllerInteraction.navigationController()?.pushViewController(controller, animated: true)
|
||||
|
||||
let _ = ApplicationSpecificNotice.incrementAudioTranscriptionSuggestion(accountManager: item.context.sharedContext.accountManager).startStandalone()
|
||||
}
|
||||
return false })
|
||||
item.controllerInteraction.presentControllerInCurrent(tipController, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
// GHOSTGRAM: Premium check removed - local transcription is free!
|
||||
|
||||
var shouldBeginTranscription = false
|
||||
var shouldExpandNow = false
|
||||
|
||||
+2
-2
@@ -645,7 +645,7 @@ public final class ChatMessageDateHeaderNodeImpl: ListViewItemHeaderNode, ChatMe
|
||||
|
||||
let isRotated = controllerInteraction?.chatIsRotated ?? true
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: true, isRotated: isRotated, seeThrough: false)
|
||||
super.init(layerBacked: false, isRotated: isRotated, seeThrough: false)
|
||||
|
||||
if isRotated {
|
||||
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
|
||||
@@ -1012,7 +1012,7 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, Chat
|
||||
|
||||
let isRotated = controllerInteraction?.chatIsRotated ?? true
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: true, isRotated: isRotated, seeThrough: false)
|
||||
super.init(layerBacked: false, isRotated: isRotated, seeThrough: false)
|
||||
|
||||
if isRotated {
|
||||
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ public class ChatReplyCountItemNode: ListViewItemNode {
|
||||
|
||||
self.backgroundColorNode = ASDisplayNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: true, rotated: true)
|
||||
super.init(layerBacked: false, rotated: true)
|
||||
|
||||
self.addSubnode(self.labelNode)
|
||||
|
||||
|
||||
+1
-1
@@ -94,7 +94,7 @@ public class ChatUnreadItemNode: ListViewItemNode {
|
||||
self.activateArea = AccessibilityAreaNode()
|
||||
self.activateArea.accessibilityTraits = .staticText
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: true, rotated: true)
|
||||
super.init(layerBacked: false, rotated: true)
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
|
||||
|
||||
+2
-2
@@ -112,7 +112,7 @@ public final class ChatMessageAccessibilityData {
|
||||
if let chatPeer = message.peers[item.message.id.peerId] {
|
||||
let authorName = message.author.flatMap(EnginePeer.init)?.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
|
||||
|
||||
let (_, _, messageText, _, _) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, contentSettings: item.context.currentContentSettings.with { $0 }, messages: [EngineMessage(message)], chatPeer: EngineRenderedPeer(peer: EnginePeer(chatPeer)), accountPeerId: item.context.account.peerId)
|
||||
let (_, _, messageText, _, _, _) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, contentSettings: item.context.currentContentSettings.with { $0 }, messages: [EngineMessage(message)], chatPeer: EngineRenderedPeer(peer: EnginePeer(chatPeer)), accountPeerId: item.context.account.peerId)
|
||||
|
||||
var text = messageText
|
||||
|
||||
@@ -664,7 +664,7 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol {
|
||||
public var effectAnimationNodes: [ChatMessageTransitionNode.DecorationItemNode] = []
|
||||
|
||||
public required init(rotated: Bool) {
|
||||
super.init(layerBacked: false, dynamicBounce: true, rotated: rotated)
|
||||
super.init(layerBacked: false, rotated: rotated)
|
||||
if rotated {
|
||||
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
|
||||
}
|
||||
|
||||
+7
-7
@@ -105,7 +105,7 @@ final class ChatCallNotificationItemNode: NotificationItemNode {
|
||||
override public func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
self.validLayout = width
|
||||
|
||||
let panelHeight: CGFloat = 66.0
|
||||
let panelHeight: CGFloat = 64.0
|
||||
|
||||
guard let item = self.item else {
|
||||
return panelHeight
|
||||
@@ -113,19 +113,19 @@ final class ChatCallNotificationItemNode: NotificationItemNode {
|
||||
|
||||
let presentationData = item.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let leftInset: CGFloat = 14.0
|
||||
let rightInset: CGFloat = 14.0
|
||||
let avatarSize: CGFloat = 38.0
|
||||
let leftInset: CGFloat = 12.0
|
||||
let rightInset: CGFloat = 12.0
|
||||
let avatarSize: CGFloat = 40.0
|
||||
let avatarTextSpacing: CGFloat = 10.0
|
||||
let buttonSpacing: CGFloat = 14.0
|
||||
let titleTextSpacing: CGFloat = 0.0
|
||||
let titleTextSpacing: CGFloat = 1.0
|
||||
|
||||
let maxTextWidth: CGFloat = width - leftInset - avatarTextSpacing - rightInset - avatarSize * 2.0 - buttonSpacing - avatarTextSpacing
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: item.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.semibold(16.0), textColor: presentationData.theme.list.itemPrimaryTextColor))
|
||||
text: .plain(NSAttributedString(string: item.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.semibold(15.0), textColor: presentationData.theme.list.itemPrimaryTextColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: maxTextWidth, height: 100.0)
|
||||
@@ -134,7 +134,7 @@ final class ChatCallNotificationItemNode: NotificationItemNode {
|
||||
let textSize = self.text.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: item.isVideo ? presentationData.strings.Notification_VideoCallIncoming : presentationData.strings.Notification_CallIncoming, font: Font.regular(13.0), textColor: presentationData.theme.list.itemPrimaryTextColor))
|
||||
text: .plain(NSAttributedString(string: item.isVideo ? presentationData.strings.Notification_VideoCallIncoming : presentationData.strings.Notification_CallIncoming, font: Font.regular(15.0), textColor: presentationData.theme.list.itemPrimaryTextColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: maxTextWidth, height: 100.0)
|
||||
|
||||
+9
-9
@@ -381,11 +381,11 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
|
||||
var applyImage: (() -> Void)?
|
||||
if let imageDimensions = imageDimensions {
|
||||
let boundingSize = CGSize(width: 55.0, height: 55.0)
|
||||
var radius: CGFloat = 6.0
|
||||
var radius: CGFloat = 20.0
|
||||
if isRound {
|
||||
radius = floor(boundingSize.width / 2.0)
|
||||
}
|
||||
applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()))
|
||||
applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: radius, curve: isRound ? .circular : .continuous), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()))
|
||||
}
|
||||
|
||||
var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
|
||||
@@ -424,16 +424,16 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
|
||||
let compact = self.compact ?? false
|
||||
|
||||
let panelHeight: CGFloat = compact ? 64.0 : 74.0
|
||||
let imageSize: CGSize = compact ? CGSize(width: 44.0, height: 44.0) : CGSize(width: 54.0, height: 54.0)
|
||||
let imageSpacing: CGFloat = compact ? 19.0 : 23.0
|
||||
let imageSize: CGSize = compact ? CGSize(width: 40.0, height: 40.0) : CGSize(width: 54.0, height: 54.0)
|
||||
let imageSpacing: CGFloat = compact ? 22.0 : 23.0
|
||||
let leftInset: CGFloat = imageSize.width + imageSpacing
|
||||
var rightInset: CGFloat = 8.0
|
||||
var rightInset: CGFloat = 10.0
|
||||
|
||||
if !self.imageNode.isHidden {
|
||||
rightInset += imageSize.width + 8.0
|
||||
rightInset += imageSize.width + 10.0
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: 10.0, y: (panelHeight - imageSize.height) / 2.0), size: imageSize))
|
||||
transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: 12.0, y: (panelHeight - imageSize.height) / 2.0), size: imageSize))
|
||||
|
||||
var titleInset: CGFloat = 0.0
|
||||
if let image = self.titleIconNode.image {
|
||||
@@ -465,7 +465,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
|
||||
|
||||
let textSpacing: CGFloat = 1.0
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset + titleInset, y: 1.0 + floor((panelHeight - textLayout.size.height - titleLayout.size.height - textSpacing) / 2.0)), size: titleLayout.size)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset + titleInset, y: floor((panelHeight - textLayout.size.height - titleLayout.size.height - textSpacing) / 2.0)), size: titleLayout.size)
|
||||
transition.updateFrame(node: self.titleNode, frame: titleFrame)
|
||||
|
||||
if let image = self.titleIconNode.image {
|
||||
@@ -475,7 +475,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
|
||||
let textFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + textSpacing), size: textLayout.size)
|
||||
transition.updateFrame(node: self.textNode.textNode, frame: textFrame)
|
||||
|
||||
transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(x: width - 10.0 - imageSize.width, y: (panelHeight - imageSize.height) / 2.0), size: imageSize))
|
||||
transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(x: width - 12.0 - imageSize.width, y: (panelHeight - imageSize.height) / 2.0), size: imageSize))
|
||||
|
||||
if !textLayout.spoilers.isEmpty, let item = self.item {
|
||||
let presentationData = item.context.sharedContext.currentPresentationData.with({ $0 })
|
||||
|
||||
@@ -25,6 +25,8 @@ swift_library(
|
||||
"//submodules/CheckNode",
|
||||
"//submodules/TelegramUIPreferences",
|
||||
"//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent/AlertCheckComponent",
|
||||
"//submodules/Markdown",
|
||||
],
|
||||
visibility = [
|
||||
|
||||
+172
-412
@@ -16,309 +16,10 @@ import CheckNode
|
||||
import Markdown
|
||||
import TextFormat
|
||||
import StarsBalanceOverlayComponent
|
||||
import AlertComponent
|
||||
import AlertCheckComponent
|
||||
|
||||
private let textFont = Font.regular(13.0)
|
||||
private let boldTextFont = Font.semibold(13.0)
|
||||
|
||||
private func formattedText(_ text: String, fontSize: CGFloat, color: UIColor, linkColor: UIColor, textAlignment: NSTextAlignment = .natural) -> NSAttributedString {
|
||||
return parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: Font.regular(fontSize), textColor: color), bold: MarkdownAttributeSet(font: Font.semibold(fontSize), textColor: color), link: MarkdownAttributeSet(font: Font.regular(fontSize), textColor: linkColor), linkAttribute: { _ in return (TelegramTextAttributes.URL, "") }), textAlignment: textAlignment)
|
||||
}
|
||||
|
||||
private final class ChatMessagePaymentAlertContentNode: AlertContentNode, ASGestureRecognizerDelegate {
|
||||
private let strings: PresentationStrings
|
||||
private let title: String
|
||||
private let text: String
|
||||
private let optionText: String?
|
||||
private let alignment: TextAlertContentActionLayout
|
||||
|
||||
private let titleNode: ImmediateTextNode
|
||||
private let textNode: ImmediateTextNode
|
||||
|
||||
private let checkNode: InteractiveCheckNode
|
||||
private let checkLabelNode: ImmediateTextNode
|
||||
|
||||
private let actionNodesSeparator: ASDisplayNode
|
||||
private let actionNodes: [TextAlertContentActionNode]
|
||||
private let actionVerticalSeparators: [ASDisplayNode]
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
override var dismissOnOutsideTap: Bool {
|
||||
return self.isUserInteractionEnabled
|
||||
}
|
||||
|
||||
var dontAskAgain: Bool = false {
|
||||
didSet {
|
||||
self.checkNode.setSelected(self.dontAskAgain, animated: true)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var openTerms: () -> Void = {}
|
||||
|
||||
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, title: String, text: String, optionText: String?, actions: [TextAlertAction], alignment: TextAlertContentActionLayout) {
|
||||
self.strings = strings
|
||||
self.title = title
|
||||
self.text = text
|
||||
self.optionText = optionText
|
||||
self.alignment = alignment
|
||||
|
||||
self.titleNode = ImmediateTextNode()
|
||||
self.titleNode.displaysAsynchronously = false
|
||||
self.titleNode.maximumNumberOfLines = 1
|
||||
self.titleNode.textAlignment = .center
|
||||
|
||||
self.textNode = ImmediateTextNode()
|
||||
self.textNode.maximumNumberOfLines = 0
|
||||
self.textNode.displaysAsynchronously = false
|
||||
self.textNode.lineSpacing = 0.1
|
||||
self.textNode.textAlignment = .center
|
||||
|
||||
self.checkNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: theme.accentColor, strokeColor: theme.contrastColor, borderColor: theme.controlBorderColor, overlayBorder: false, hasInset: false, hasShadow: false))
|
||||
self.checkLabelNode = ImmediateTextNode()
|
||||
self.checkLabelNode.maximumNumberOfLines = 4
|
||||
|
||||
self.actionNodesSeparator = ASDisplayNode()
|
||||
self.actionNodesSeparator.isLayerBacked = true
|
||||
|
||||
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
|
||||
return TextAlertContentActionNode(theme: theme, action: action)
|
||||
}
|
||||
|
||||
var actionVerticalSeparators: [ASDisplayNode] = []
|
||||
if actions.count > 1 {
|
||||
for _ in 0 ..< actions.count - 1 {
|
||||
let separatorNode = ASDisplayNode()
|
||||
separatorNode.isLayerBacked = true
|
||||
actionVerticalSeparators.append(separatorNode)
|
||||
}
|
||||
}
|
||||
self.actionVerticalSeparators = actionVerticalSeparators
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.textNode)
|
||||
|
||||
if let _ = optionText {
|
||||
self.addSubnode(self.checkNode)
|
||||
self.addSubnode(self.checkLabelNode)
|
||||
}
|
||||
|
||||
self.addSubnode(self.actionNodesSeparator)
|
||||
|
||||
for actionNode in self.actionNodes {
|
||||
self.addSubnode(actionNode)
|
||||
}
|
||||
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
|
||||
self.checkNode.valueChanged = { [weak self] value in
|
||||
if let strongSelf = self {
|
||||
strongSelf.dontAskAgain = !strongSelf.dontAskAgain
|
||||
}
|
||||
}
|
||||
|
||||
self.checkLabelNode.highlightAttributeAction = { attributes in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
||||
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
self.checkLabelNode.tapAttributeAction = { [weak self] attributes, _ in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
||||
self?.openTerms()
|
||||
}
|
||||
}
|
||||
|
||||
self.updateTheme(theme)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.acceptTap(_:)))
|
||||
tapGesture.delegate = self.wrappedGestureRecognizerDelegate
|
||||
self.view.addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
let location = gestureRecognizer.location(in: self.checkLabelNode.view)
|
||||
if self.checkLabelNode.bounds.contains(location) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if !self.bounds.contains(point) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let (_, attributes) = self.checkLabelNode.attributesAtPoint(self.view.convert(point, to: self.checkLabelNode.view)) {
|
||||
if attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] == nil {
|
||||
return self.view
|
||||
}
|
||||
}
|
||||
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
@objc private func acceptTap(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
self.dontAskAgain = !self.dontAskAgain
|
||||
}
|
||||
|
||||
override func updateTheme(_ theme: AlertControllerTheme) {
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
self.textNode.attributedText = formattedText(self.text, fontSize: 13.0, color: theme.primaryColor, linkColor: theme.accentColor, textAlignment: .center)
|
||||
|
||||
self.checkLabelNode.attributedText = parseMarkdownIntoAttributedString(
|
||||
self.optionText ?? "",
|
||||
attributes: MarkdownAttributes(
|
||||
body: MarkdownAttributeSet(font: textFont, textColor: theme.primaryColor),
|
||||
bold: MarkdownAttributeSet(font: boldTextFont, textColor: theme.primaryColor),
|
||||
link: MarkdownAttributeSet(font: textFont, textColor: theme.primaryColor),
|
||||
linkAttribute: { _ in
|
||||
return nil
|
||||
}
|
||||
)
|
||||
)
|
||||
self.actionNodesSeparator.backgroundColor = theme.separatorColor
|
||||
for actionNode in self.actionNodes {
|
||||
actionNode.updateTheme(theme)
|
||||
}
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
separatorNode.backgroundColor = theme.separatorColor
|
||||
}
|
||||
|
||||
if let size = self.validLayout {
|
||||
_ = self.updateLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var size = size
|
||||
size.width = min(size.width, 270.0)
|
||||
|
||||
self.validLayout = size
|
||||
|
||||
var origin: CGPoint = CGPoint(x: 0.0, y: 17.0)
|
||||
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - 32.0, height: size.height))
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
|
||||
origin.y += titleSize.height + 4.0
|
||||
|
||||
var entriesHeight: CGFloat = 0.0
|
||||
|
||||
let textSize = self.textNode.updateLayout(CGSize(width: size.width - 32.0, height: size.height))
|
||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
|
||||
origin.y += textSize.height
|
||||
|
||||
if self.checkLabelNode.supernode != nil {
|
||||
origin.y += 21.0
|
||||
entriesHeight += 21.0
|
||||
|
||||
let checkSize = CGSize(width: 22.0, height: 22.0)
|
||||
let condensedSize = CGSize(width: size.width - 76.0, height: size.height)
|
||||
|
||||
let spacing: CGFloat = 12.0
|
||||
let acceptTermsSize = self.checkLabelNode.updateLayout(condensedSize)
|
||||
let acceptTermsTotalWidth = checkSize.width + spacing + acceptTermsSize.width
|
||||
let acceptTermsOriginX = floorToScreenPixels((size.width - acceptTermsTotalWidth) / 2.0)
|
||||
|
||||
transition.updateFrame(node: self.checkNode, frame: CGRect(origin: CGPoint(x: acceptTermsOriginX, y: origin.y - 3.0), size: checkSize))
|
||||
transition.updateFrame(node: self.checkLabelNode, frame: CGRect(origin: CGPoint(x: acceptTermsOriginX + checkSize.width + spacing, y: origin.y), size: acceptTermsSize))
|
||||
origin.y += acceptTermsSize.height
|
||||
entriesHeight += acceptTermsSize.height
|
||||
origin.y += 21.0
|
||||
}
|
||||
|
||||
let actionButtonHeight: CGFloat = 44.0
|
||||
var minActionsWidth: CGFloat = 0.0
|
||||
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
|
||||
let actionTitleInsets: CGFloat = 8.0
|
||||
|
||||
var effectiveActionLayout = self.alignment
|
||||
for actionNode in self.actionNodes {
|
||||
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
|
||||
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
|
||||
effectiveActionLayout = .vertical
|
||||
}
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
minActionsWidth += actionTitleSize.width + actionTitleInsets
|
||||
case .vertical:
|
||||
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
|
||||
}
|
||||
}
|
||||
|
||||
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
|
||||
|
||||
let contentWidth = max(size.width, minActionsWidth)
|
||||
|
||||
var actionsHeight: CGFloat = 0.0
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionsHeight = actionButtonHeight
|
||||
case .vertical:
|
||||
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
|
||||
}
|
||||
|
||||
let resultSize = CGSize(width: contentWidth, height: titleSize.height + textSize.height + entriesHeight + actionsHeight + 3.0 + insets.top + insets.bottom)
|
||||
|
||||
transition.updateFrame(node: 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]
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
|
||||
case .vertical:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
}
|
||||
}
|
||||
separatorIndex += 1
|
||||
|
||||
let currentActionWidth: CGFloat
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
if nodeIndex == self.actionNodes.count - 1 {
|
||||
currentActionWidth = resultSize.width - actionOffset
|
||||
} else {
|
||||
currentActionWidth = actionWidth
|
||||
}
|
||||
case .vertical:
|
||||
currentActionWidth = resultSize.width
|
||||
}
|
||||
|
||||
let actionNodeFrame: CGRect
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += currentActionWidth
|
||||
case .vertical:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += actionButtonHeight
|
||||
}
|
||||
|
||||
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
|
||||
|
||||
nodeIndex += 1
|
||||
}
|
||||
|
||||
return resultSize
|
||||
}
|
||||
}
|
||||
|
||||
public class ChatMessagePaymentAlertController: AlertController {
|
||||
public class ChatMessagePaymentAlertController: AlertScreen {
|
||||
private let context: AccountContext?
|
||||
private let presentationData: PresentationData
|
||||
private weak var parentNavigationController: NavigationController?
|
||||
@@ -327,29 +28,26 @@ public class ChatMessagePaymentAlertController: AlertController {
|
||||
private let animateBalanceOverlay: Bool
|
||||
|
||||
private var didUpdateCurrency = false
|
||||
public var currency: CurrencyAmount.Currency {
|
||||
didSet {
|
||||
self.didUpdateCurrency = true
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.25, curve: .easeInOut))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var initialCurrency: CurrencyAmount.Currency?
|
||||
public var currency: CurrencyAmount.Currency?
|
||||
private var currencyDisposable: Disposable?
|
||||
|
||||
private let balance = ComponentView<Empty>()
|
||||
|
||||
private var didAppear = false
|
||||
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
|
||||
public init(
|
||||
context: AccountContext?,
|
||||
presentationData: PresentationData,
|
||||
contentNode: AlertContentNode,
|
||||
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
|
||||
configuration: Configuration = AlertScreen.Configuration(),
|
||||
contentSignal: Signal<[AnyComponentWithIdentity<AlertComponentEnvironment>], NoError>,
|
||||
actionsSignal: Signal<[AlertScreen.Action], NoError>,
|
||||
navigationController: NavigationController?,
|
||||
chatPeerId: EnginePeer.Id,
|
||||
showBalance: Bool = true,
|
||||
currency: CurrencyAmount.Currency = .stars,
|
||||
currencySignal: Signal<CurrencyAmount.Currency, NoError> = .single(.stars),
|
||||
animateBalanceOverlay: Bool = true
|
||||
) {
|
||||
self.context = context
|
||||
@@ -357,31 +55,84 @@ public class ChatMessagePaymentAlertController: AlertController {
|
||||
self.parentNavigationController = navigationController
|
||||
self.chatPeerId = chatPeerId
|
||||
self.showBalance = showBalance
|
||||
self.currency = currency
|
||||
self.animateBalanceOverlay = animateBalanceOverlay
|
||||
|
||||
super.init(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
|
||||
var effectiveUpdatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)
|
||||
if let updatedPresentationData {
|
||||
effectiveUpdatedPresentationData = updatedPresentationData
|
||||
} else {
|
||||
effectiveUpdatedPresentationData = (initial: presentationData, signal: .single(presentationData))
|
||||
}
|
||||
|
||||
super.init(
|
||||
configuration: configuration,
|
||||
contentSignal: contentSignal,
|
||||
actionsSignal: actionsSignal,
|
||||
updatedPresentationData: effectiveUpdatedPresentationData
|
||||
)
|
||||
|
||||
self.willDismiss = { [weak self] in
|
||||
self.currencyDisposable = (currencySignal
|
||||
|> distinctUntilChanged
|
||||
|> deliverOnMainQueue).start(next: { [weak self] currency in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.animateOut()
|
||||
}
|
||||
if self.currency == nil {
|
||||
self.initialCurrency = currency
|
||||
}
|
||||
self.currency = currency
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.25, curve: .easeInOut))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public convenience init(
|
||||
context: AccountContext?,
|
||||
presentationData: PresentationData,
|
||||
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
|
||||
configuration: Configuration = AlertScreen.Configuration(),
|
||||
content: [AnyComponentWithIdentity<AlertComponentEnvironment>],
|
||||
actions: [AlertScreen.Action],
|
||||
navigationController: NavigationController?,
|
||||
chatPeerId: EnginePeer.Id,
|
||||
showBalance: Bool = true,
|
||||
currency: CurrencyAmount.Currency = .stars,
|
||||
animateBalanceOverlay: Bool = true
|
||||
) {
|
||||
self.init(
|
||||
context: context,
|
||||
presentationData: presentationData,
|
||||
updatedPresentationData: updatedPresentationData,
|
||||
configuration: configuration,
|
||||
contentSignal: .single(content),
|
||||
actionsSignal: .single(actions),
|
||||
navigationController: navigationController,
|
||||
chatPeerId: chatPeerId,
|
||||
showBalance: showBalance,
|
||||
currencySignal: .single(currency),
|
||||
animateBalanceOverlay: animateBalanceOverlay
|
||||
)
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
override public func dismiss(completion: (() -> Void)? = nil) {
|
||||
super.dismiss(completion: completion)
|
||||
|
||||
self.animateOut()
|
||||
}
|
||||
|
||||
private func animateOut() {
|
||||
if !self.animateBalanceOverlay {
|
||||
if self.currency == .ton && self.didUpdateCurrency {
|
||||
if case .ton = self.currency, let initialCurrency, initialCurrency != self.currency {
|
||||
self.currency = .stars
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.25, curve: .easeInOut))
|
||||
}
|
||||
}
|
||||
Queue.mainQueue().after(0.39, {
|
||||
|
||||
})
|
||||
} else {
|
||||
if let view = self.balance.view {
|
||||
view.layer.animateScale(from: 1.0, to: 0.8, duration: 0.4, removeOnCompletion: false)
|
||||
@@ -389,18 +140,10 @@ public class ChatMessagePaymentAlertController: AlertController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override func dismissAnimated() {
|
||||
super.dismissAnimated()
|
||||
|
||||
self.animateOut()
|
||||
}
|
||||
|
||||
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.validLayout = layout
|
||||
|
||||
|
||||
if !self.didAppear {
|
||||
self.didAppear = true
|
||||
if !layout.metrics.isTablet && layout.size.width > layout.size.height {
|
||||
@@ -410,7 +153,7 @@ public class ChatMessagePaymentAlertController: AlertController {
|
||||
}
|
||||
}
|
||||
|
||||
if let context = self.context, let _ = self.parentNavigationController, self.showBalance {
|
||||
if let context = self.context, let _ = self.parentNavigationController, self.showBalance, let currency = self.currency {
|
||||
let insets = layout.insets(options: .statusBar)
|
||||
var balanceTransition = ComponentTransition(transition)
|
||||
if self.balance.view == nil {
|
||||
@@ -424,12 +167,12 @@ public class ChatMessagePaymentAlertController: AlertController {
|
||||
context: context,
|
||||
peerId: self.chatPeerId.namespace == Namespaces.Peer.CloudChannel ? self.chatPeerId : context.account.peerId,
|
||||
theme: self.presentationData.theme,
|
||||
currency: self.currency,
|
||||
currency: currency,
|
||||
action: { [weak self] in
|
||||
guard let self, let starsContext = context.starsContext, let navigationController = self.parentNavigationController else {
|
||||
guard let self, let starsContext = context.starsContext, let navigationController = self.parentNavigationController, let currency = self.currency else {
|
||||
return
|
||||
}
|
||||
switch self.currency {
|
||||
switch currency {
|
||||
case .stars:
|
||||
let _ = (context.engine.payments.starsTopUpOptions()
|
||||
|> take(1)
|
||||
@@ -452,7 +195,7 @@ public class ChatMessagePaymentAlertController: AlertController {
|
||||
}
|
||||
context.sharedContext.applicationBindings.openUrl(fragmentUrl)
|
||||
}
|
||||
self.dismissAnimated()
|
||||
self.dismiss(completion: nil)
|
||||
}
|
||||
)
|
||||
),
|
||||
@@ -486,57 +229,66 @@ public func chatMessagePaymentAlertController(
|
||||
hasCheck: Bool = true,
|
||||
navigationController: NavigationController?,
|
||||
completion: @escaping (Bool) -> Void
|
||||
) -> AlertController {
|
||||
let theme = defaultDarkColorPresentationTheme
|
||||
let presentationData = updatedPresentationData?.initial ?? presentationData
|
||||
) -> ViewController {
|
||||
let strings = presentationData.strings
|
||||
|
||||
var completionImpl: (() -> Void)?
|
||||
var dismissImpl: (() -> Void)?
|
||||
|
||||
let title = presentationData.strings.Chat_PaidMessage_Confirm_Title
|
||||
let actionTitle = presentationData.strings.Chat_PaidMessage_Confirm_PayForMessage(count)
|
||||
let messagesString = presentationData.strings.Chat_PaidMessage_Confirm_Text_Messages(count)
|
||||
|
||||
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: actionTitle, action: {
|
||||
completionImpl?()
|
||||
dismissImpl?()
|
||||
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
dismissImpl?()
|
||||
})]
|
||||
|
||||
let messagesString = strings.Chat_PaidMessage_Confirm_Text_Messages(count)
|
||||
let text: String
|
||||
if peers.count == 1, let peer = peers.first {
|
||||
let amountString = presentationData.strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(clamping: amount.value))
|
||||
let totalString = presentationData.strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(clamping: amount.value * Int64(count)))
|
||||
let amountString = strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(clamping: amount.value))
|
||||
let totalString = strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(clamping: amount.value * Int64(count)))
|
||||
if case let .channel(channel) = peer.chatOrMonoforumMainPeer, case .broadcast = channel.info {
|
||||
text = presentationData.strings.Chat_PaidMessage_Confirm_SingleComment_Text(EnginePeer(channel).compactDisplayTitle, amountString, totalString, messagesString).string
|
||||
text = strings.Chat_PaidMessage_Confirm_SingleComment_Text(EnginePeer(channel).compactDisplayTitle, amountString, totalString, messagesString).string
|
||||
} else {
|
||||
text = presentationData.strings.Chat_PaidMessage_Confirm_Single_Text(peer.chatOrMonoforumMainPeer?.compactDisplayTitle ?? " ", amountString, totalString, messagesString).string
|
||||
text = strings.Chat_PaidMessage_Confirm_Single_Text(peer.chatOrMonoforumMainPeer?.compactDisplayTitle ?? " ", amountString, totalString, messagesString).string
|
||||
}
|
||||
} else {
|
||||
let amount = totalAmount ?? amount
|
||||
let usersString = presentationData.strings.Chat_PaidMessage_Confirm_Text_Users(Int32(peers.count))
|
||||
let totalString = presentationData.strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(clamping: amount.value * Int64(count)))
|
||||
text = presentationData.strings.Chat_PaidMessage_Confirm_Multiple_Text(usersString, totalString, messagesString).string
|
||||
let usersString = strings.Chat_PaidMessage_Confirm_Text_Users(Int32(peers.count))
|
||||
let totalString = strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(clamping: amount.value * Int64(count)))
|
||||
text = strings.Chat_PaidMessage_Confirm_Multiple_Text(usersString, totalString, messagesString).string
|
||||
}
|
||||
|
||||
let checkState = AlertCheckComponent.ExternalState()
|
||||
|
||||
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "title",
|
||||
component: AnyComponent(
|
||||
AlertTitleComponent(title: strings.Chat_PaidMessage_Confirm_Title)
|
||||
)
|
||||
))
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "text",
|
||||
component: AnyComponent(
|
||||
AlertTextComponent(content: .plain(text))
|
||||
)
|
||||
))
|
||||
if hasCheck {
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "check",
|
||||
component: AnyComponent(
|
||||
AlertCheckComponent(title: strings.Chat_PaidMessage_Confirm_DontAskAgain, initialValue: false, externalState: checkState)
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
let optionText = hasCheck ? presentationData.strings.Chat_PaidMessage_Confirm_DontAskAgain : nil
|
||||
|
||||
let contentNode = ChatMessagePaymentAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, optionText: optionText, actions: actions, alignment: .vertical)
|
||||
|
||||
completionImpl = { [weak contentNode] in
|
||||
guard let contentNode else {
|
||||
return
|
||||
}
|
||||
completion(contentNode.dontAskAgain)
|
||||
}
|
||||
|
||||
let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode, navigationController: navigationController, chatPeerId: context?.account.peerId ?? peers[0].peerId)
|
||||
dismissImpl = { [weak controller] in
|
||||
controller?.dismissAnimated()
|
||||
}
|
||||
return controller
|
||||
let alertController = ChatMessagePaymentAlertController(
|
||||
context: context,
|
||||
presentationData: presentationData,
|
||||
updatedPresentationData: updatedPresentationData,
|
||||
configuration: AlertScreen.Configuration(actionAlignment: .vertical, allowInputInset: true),
|
||||
content: content,
|
||||
actions: [
|
||||
.init(title: strings.Chat_PaidMessage_Confirm_PayForMessage(count), type: .default, action: {
|
||||
completion(checkState.value)
|
||||
}),
|
||||
.init(title: strings.Common_Cancel)
|
||||
],
|
||||
navigationController: navigationController,
|
||||
chatPeerId: context?.account.peerId ?? peers[0].peerId
|
||||
)
|
||||
return alertController
|
||||
}
|
||||
|
||||
public func chatMessageRemovePaymentAlertController(
|
||||
@@ -548,47 +300,55 @@ public func chatMessageRemovePaymentAlertController(
|
||||
amount: StarsAmount?,
|
||||
navigationController: NavigationController?,
|
||||
completion: @escaping (Bool) -> Void
|
||||
) -> AlertController {
|
||||
let theme = defaultDarkColorPresentationTheme
|
||||
let presentationData = updatedPresentationData?.initial ?? presentationData
|
||||
) -> ViewController {
|
||||
let strings = presentationData.strings
|
||||
|
||||
var completionImpl: (() -> Void)?
|
||||
var dismissImpl: (() -> Void)?
|
||||
|
||||
let actions: [TextAlertAction] = [
|
||||
TextAlertAction(type: .genericAction, title: strings.Common_Cancel, action: {
|
||||
dismissImpl?()
|
||||
}),
|
||||
TextAlertAction(type: .defaultAction, title: strings.Chat_PaidMessage_RemoveFee_Yes, action: {
|
||||
completionImpl?()
|
||||
dismissImpl?()
|
||||
})
|
||||
]
|
||||
|
||||
let title = strings.Chat_PaidMessage_RemoveFee_Title
|
||||
|
||||
let text: String
|
||||
if let context, chatPeer.id != context.account.peerId {
|
||||
if case .user = chatPeer {
|
||||
text = strings.Chat_PaidMessage_RemoveFee_Text(peer.compactDisplayTitle).string
|
||||
} else if let context, chatPeer.id != context.account.peerId {
|
||||
text = strings.Channel_RemoveFeeAlert_Text(peer.compactDisplayTitle).string
|
||||
} else {
|
||||
text = strings.Chat_PaidMessage_RemoveFee_Text(peer.compactDisplayTitle).string
|
||||
}
|
||||
|
||||
let checkState = AlertCheckComponent.ExternalState()
|
||||
|
||||
let optionText = amount.flatMap { strings.Chat_PaidMessage_RemoveFee_Refund(strings.Chat_PaidMessage_RemoveFee_Refund_Stars(Int32(clamping: $0.value))).string }
|
||||
|
||||
let contentNode = ChatMessagePaymentAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, optionText: optionText, actions: actions, alignment: .horizontal)
|
||||
|
||||
completionImpl = { [weak contentNode] in
|
||||
guard let contentNode else {
|
||||
return
|
||||
}
|
||||
completion(contentNode.dontAskAgain)
|
||||
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "title",
|
||||
component: AnyComponent(
|
||||
AlertTitleComponent(title: strings.Chat_PaidMessage_RemoveFee_Title)
|
||||
)
|
||||
))
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "text",
|
||||
component: AnyComponent(
|
||||
AlertTextComponent(content: .plain(text))
|
||||
)
|
||||
))
|
||||
if let amount {
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "check",
|
||||
component: AnyComponent(
|
||||
AlertCheckComponent(title: strings.Chat_PaidMessage_RemoveFee_Refund(strings.Chat_PaidMessage_RemoveFee_Refund_Stars(Int32(clamping: amount.value))).string, initialValue: false, externalState: checkState)
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode, navigationController: navigationController, chatPeerId: chatPeer.id)
|
||||
dismissImpl = { [weak controller] in
|
||||
controller?.dismissAnimated()
|
||||
}
|
||||
return controller
|
||||
let alertController = ChatMessagePaymentAlertController(
|
||||
context: context,
|
||||
presentationData: presentationData,
|
||||
updatedPresentationData: updatedPresentationData,
|
||||
content: content,
|
||||
actions: [
|
||||
.init(title: strings.Common_Cancel),
|
||||
.init(title: strings.Chat_PaidMessage_RemoveFee_Yes, type: .default, action: {
|
||||
completion(checkState.value)
|
||||
})
|
||||
],
|
||||
navigationController: navigationController,
|
||||
chatPeerId: chatPeer.id
|
||||
)
|
||||
return alertController
|
||||
}
|
||||
|
||||
+8
-1
@@ -1678,8 +1678,15 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
|
||||
if isTranslating, !rects.isEmpty {
|
||||
if self.shimmeringNodes.isEmpty {
|
||||
let color: UIColor
|
||||
let isIncoming = item.message.effectivelyIncoming(item.context.account.peerId)
|
||||
if item.presentationData.theme.theme.overallDarkAppearance {
|
||||
color = isIncoming ? item.presentationData.theme.theme.chat.message.incoming.primaryTextColor.withAlphaComponent(0.1) : item.presentationData.theme.theme.chat.message.outgoing.primaryTextColor.withAlphaComponent(0.1)
|
||||
} else {
|
||||
color = isIncoming ? item.presentationData.theme.theme.chat.message.incoming.accentTextColor.withAlphaComponent(0.1) : item.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor.withAlphaComponent(0.1)
|
||||
}
|
||||
for rects in rects {
|
||||
let shimmeringNode = ShimmeringLinkNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.secondaryTextColor.withAlphaComponent(0.1) : item.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor.withAlphaComponent(0.1))
|
||||
let shimmeringNode = ShimmeringLinkNode(color: color)
|
||||
shimmeringNode.updateRects(rects)
|
||||
shimmeringNode.frame = self.bounds
|
||||
shimmeringNode.updateLayout(self.bounds.size)
|
||||
|
||||
+128
-2
@@ -85,6 +85,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
public let quote: (quote: EngineMessageReplyQuote, isQuote: Bool)?
|
||||
public let todoItemId: Int32?
|
||||
public let story: StoryId?
|
||||
public let isSummarized: Bool
|
||||
public let parentMessage: Message
|
||||
public let constrainedSize: CGSize
|
||||
public let animationCache: AnimationCache?
|
||||
@@ -101,6 +102,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
quote: (quote: EngineMessageReplyQuote, isQuote: Bool)?,
|
||||
todoItemId: Int32?,
|
||||
story: StoryId?,
|
||||
isSummarized: Bool,
|
||||
parentMessage: Message,
|
||||
constrainedSize: CGSize,
|
||||
animationCache: AnimationCache?,
|
||||
@@ -116,6 +118,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
self.quote = quote
|
||||
self.todoItemId = todoItemId
|
||||
self.story = story
|
||||
self.isSummarized = isSummarized
|
||||
self.parentMessage = parentMessage
|
||||
self.constrainedSize = constrainedSize
|
||||
self.animationCache = animationCache
|
||||
@@ -133,6 +136,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
private let backgroundView: MessageInlineBlockBackgroundView
|
||||
private var starsView: StarsView?
|
||||
private var quoteIconView: UIImageView?
|
||||
private let contentNode: ASDisplayNode
|
||||
private var titleNode: TextNode?
|
||||
@@ -206,7 +210,6 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
var secondaryColor: UIColor?
|
||||
var tertiaryColor: UIColor?
|
||||
|
||||
|
||||
var authorNameColor: UIColor?
|
||||
var dashSecondaryColor: UIColor?
|
||||
var dashTertiaryColor: UIColor?
|
||||
@@ -239,6 +242,10 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
break
|
||||
}
|
||||
|
||||
if arguments.isSummarized {
|
||||
authorNameColor = nil
|
||||
}
|
||||
|
||||
switch arguments.type {
|
||||
case let .bubble(incoming):
|
||||
titleColor = incoming ? (authorNameColor ?? arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor) : arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor
|
||||
@@ -437,7 +444,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
var textLeftInset: CGFloat = 0.0
|
||||
let messageText: NSAttributedString
|
||||
var messageText: NSAttributedString
|
||||
var todoItemCompleted: Bool?
|
||||
if let todoItemId = arguments.todoItemId, let todo = arguments.message?.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo, let todoItem = todo.items.first(where: { $0.id == todoItemId }) {
|
||||
messageText = stringWithAppliedEntities(todoItem.text, entities: todoItem.entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: nil)
|
||||
@@ -607,6 +614,11 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
}
|
||||
adjustedConstrainedTextSize.width -= textLeftInset
|
||||
|
||||
if arguments.isSummarized {
|
||||
titleString = NSAttributedString(string: arguments.presentationData.strings.Conversation_Summary_Title, font: titleFont, textColor: titleColor)
|
||||
messageText = NSAttributedString(string: arguments.presentationData.strings.Conversation_Summary_Text, font: textFont, textColor: titleColor)
|
||||
}
|
||||
|
||||
let (titleLayout, titleApply) = titleNodeLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: maxTitleNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: contrainedTextSize.width - additionalTitleWidth, height: contrainedTextSize.height), alignment: .natural, cutout: nil, insets: textInsets))
|
||||
if isExpiredStory || isStory {
|
||||
contrainedTextSize.width -= 26.0
|
||||
@@ -687,6 +699,11 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
node = ChatMessageReplyInfoNode()
|
||||
}
|
||||
|
||||
var animation = animation
|
||||
if node.titleNode == nil {
|
||||
animation = .None
|
||||
}
|
||||
|
||||
node.previousMediaReference = updatedMediaReference
|
||||
|
||||
//node.textNode?.textNode.displaysAsynchronously = !arguments.presentationData.isPreview
|
||||
@@ -925,6 +942,22 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
giftEmojiLayer.removeFromSuperlayer()
|
||||
}
|
||||
|
||||
if arguments.isSummarized {
|
||||
let starsView: StarsView
|
||||
if let current = node.starsView {
|
||||
starsView = current
|
||||
} else {
|
||||
starsView = StarsView()
|
||||
node.starsView = starsView
|
||||
node.contentNode.view.insertSubview(starsView, at: 1)
|
||||
}
|
||||
starsView.frame = CGRect(origin: CGPoint(), size: backgroundFrame.size)
|
||||
starsView.update(size: backgroundFrame.size, color: mainColor)
|
||||
} else if let starsView = node.starsView {
|
||||
node.starsView = nil
|
||||
starsView.removeFromSuperview()
|
||||
}
|
||||
|
||||
node.contentNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
return node
|
||||
@@ -1065,3 +1098,96 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private final class StarsView: UIView {
|
||||
private let staticEmitterLayer = CAEmitterLayer()
|
||||
|
||||
private var currentColor: UIColor?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.layer.addSublayer(self.staticEmitterLayer)
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
private func setupEmitter(size: CGSize) {
|
||||
guard let currentColor = self.currentColor else {
|
||||
return
|
||||
}
|
||||
let color = currentColor
|
||||
|
||||
self.staticEmitterLayer.emitterShape = .rectangle
|
||||
self.staticEmitterLayer.emitterSize = size
|
||||
self.staticEmitterLayer.emitterMode = .surface
|
||||
self.layer.addSublayer(self.staticEmitterLayer)
|
||||
|
||||
let staticEmitter = CAEmitterCell()
|
||||
staticEmitter.name = "emitter"
|
||||
staticEmitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage
|
||||
staticEmitter.birthRate = 20.0
|
||||
staticEmitter.lifetime = 3.2
|
||||
staticEmitter.velocity = 18.0
|
||||
staticEmitter.velocityRange = 3
|
||||
staticEmitter.scale = 0.1
|
||||
staticEmitter.scaleRange = 0.08
|
||||
staticEmitter.emissionRange = .pi * 2.0
|
||||
staticEmitter.setValue(3.0, forKey: "mass")
|
||||
staticEmitter.setValue(2.0, forKey: "massRange")
|
||||
|
||||
let staticColors: [Any] = [
|
||||
color.withAlphaComponent(0.0).cgColor,
|
||||
color.cgColor,
|
||||
color.withAlphaComponent(0.0).cgColor,
|
||||
color.withAlphaComponent(0.0).cgColor,
|
||||
color.cgColor,
|
||||
color.withAlphaComponent(0.0).cgColor
|
||||
]
|
||||
let staticColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife")
|
||||
staticColorBehavior.setValue(staticColors, forKey: "colors")
|
||||
staticEmitter.setValue([staticColorBehavior], forKey: "emitterBehaviors")
|
||||
|
||||
let attractor = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor")
|
||||
attractor.setValue("attractor", forKey: "name")
|
||||
attractor.setValue(20, forKey: "falloff")
|
||||
attractor.setValue(35, forKey: "radius")
|
||||
self.staticEmitterLayer.setValue([attractor], forKey: "emitterBehaviors")
|
||||
self.staticEmitterLayer.setValue(4.0, forKeyPath: "emitterBehaviors.attractor.stiffness")
|
||||
self.staticEmitterLayer.setValue(false, forKeyPath: "emitterBehaviors.attractor.enabled")
|
||||
|
||||
self.staticEmitterLayer.emitterCells = [staticEmitter]
|
||||
}
|
||||
|
||||
func update(size: CGSize, color: UIColor) {
|
||||
if self.staticEmitterLayer.emitterCells == nil {
|
||||
self.currentColor = color
|
||||
self.setupEmitter(size: size)
|
||||
} else if self.currentColor != color {
|
||||
self.currentColor = color
|
||||
|
||||
let staticColors: [Any] = [
|
||||
UIColor.white.withAlphaComponent(0.0).cgColor,
|
||||
UIColor.white.withAlphaComponent(0.35).cgColor,
|
||||
color.cgColor,
|
||||
color.cgColor,
|
||||
color.withAlphaComponent(0.0).cgColor
|
||||
]
|
||||
let staticColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife")
|
||||
staticColorBehavior.setValue(staticColors, forKey: "colors")
|
||||
|
||||
for cell in self.staticEmitterLayer.emitterCells ?? [] {
|
||||
cell.setValue([staticColorBehavior], forKey: "emitterBehaviors")
|
||||
}
|
||||
}
|
||||
|
||||
let emitterPosition = CGPoint(x: size.width * 0.5, y: size.height * 0.5)
|
||||
self.staticEmitterLayer.frame = CGRect(origin: .zero, size: size)
|
||||
self.staticEmitterLayer.emitterPosition = emitterPosition
|
||||
self.staticEmitterLayer.setValue(emitterPosition, forKeyPath: "emitterBehaviors.attractor.position")
|
||||
}
|
||||
}
|
||||
|
||||
+6
-5
@@ -121,7 +121,6 @@ private final class GlassButtonView: UIView {
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.backgroundView = GlassBackgroundView()
|
||||
self.backgroundView.isUserInteractionEnabled = false
|
||||
|
||||
self.iconView = GlassBackgroundView.ContentImageView()
|
||||
self.backgroundView.contentView.addSubview(self.iconView)
|
||||
@@ -460,11 +459,13 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
|
||||
override public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat {
|
||||
self.validLayout = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, metrics, isSecondary, isMediaInputExpanded)
|
||||
|
||||
var leftInset = leftInset
|
||||
leftInset += 16.0
|
||||
var leftInset = leftInset + 8.0
|
||||
var rightInset = rightInset + 8.0
|
||||
|
||||
var rightInset = rightInset
|
||||
rightInset += 16.0
|
||||
if bottomInset <= 32.0 {
|
||||
leftInset += 18.0
|
||||
rightInset += 18.0
|
||||
}
|
||||
|
||||
let panelHeight = defaultHeight(metrics: metrics)
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ swift_library(
|
||||
"//submodules/WallpaperBackgroundNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
|
||||
"//submodules/ContextUI",
|
||||
"//submodules/Components/HierarchyTrackingLayer",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
+114
-4
@@ -10,6 +10,7 @@ import Postbox
|
||||
import WallpaperBackgroundNode
|
||||
import ChatMessageItemCommon
|
||||
import ContextUI
|
||||
import HierarchyTrackingLayer
|
||||
|
||||
public class ChatMessageShareButton: ASDisplayNode {
|
||||
private let referenceNode: ContextReferenceContentNode
|
||||
@@ -21,15 +22,18 @@ public class ChatMessageShareButton: ASDisplayNode {
|
||||
private let topButton: HighlightTrackingButtonNode
|
||||
private let topIconNode: ASImageNode
|
||||
private var topIconOffset = CGPoint()
|
||||
|
||||
|
||||
private var bottomButton: HighlightTrackingButtonNode?
|
||||
private var bottomIconNode: ASImageNode?
|
||||
|
||||
private var starsView: StarsView?
|
||||
|
||||
private var separatorNode: ASDisplayNode?
|
||||
|
||||
private var theme: PresentationTheme?
|
||||
private var isReplies: Bool = false
|
||||
private var hasMore: Bool = false
|
||||
private var isExpand: Bool = false
|
||||
|
||||
private var textNode: ImmediateTextNode?
|
||||
|
||||
@@ -103,7 +107,7 @@ public class ChatMessageShareButton: ASDisplayNode {
|
||||
self.morePressed?()
|
||||
}
|
||||
|
||||
public func update(presentationData: ChatPresentationData, controllerInteraction: ChatControllerInteraction, chatLocation: ChatLocation, subject: ChatControllerSubject?, message: Message, account: Account, disableComments: Bool = false) -> CGSize {
|
||||
public func update(presentationData: ChatPresentationData, controllerInteraction: ChatControllerInteraction, chatLocation: ChatLocation, subject: ChatControllerSubject?, message: Message, account: Account, disableComments: Bool = false, isSummarize: Bool = false) -> CGSize {
|
||||
var isReplies = false
|
||||
var isNavigate = false
|
||||
var replyCount = 0
|
||||
@@ -134,15 +138,27 @@ public class ChatMessageShareButton: ASDisplayNode {
|
||||
hasMore = true
|
||||
}
|
||||
|
||||
if self.theme !== presentationData.theme.theme || self.isReplies != isReplies || self.hasMore != hasMore {
|
||||
var isExpand = false
|
||||
if controllerInteraction.summarizedMessageIds.contains(message.id) {
|
||||
isExpand = true
|
||||
}
|
||||
|
||||
if self.theme !== presentationData.theme.theme || self.isReplies != isReplies || self.hasMore != hasMore || self.isExpand != isExpand {
|
||||
self.theme = presentationData.theme.theme
|
||||
self.isReplies = isReplies
|
||||
self.hasMore = hasMore
|
||||
self.isExpand = isExpand
|
||||
|
||||
var updatedIconImage: UIImage?
|
||||
var updatedBottomIconImage: UIImage?
|
||||
var updatedIconOffset = CGPoint()
|
||||
if let _ = message.adAttribute {
|
||||
if isSummarize {
|
||||
if isExpand {
|
||||
updatedIconImage = PresentationResourcesChat.chatFreeExpandButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)
|
||||
} else {
|
||||
updatedIconImage = PresentationResourcesChat.chatFreeCollapseButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)
|
||||
}
|
||||
} else if let _ = message.adAttribute {
|
||||
updatedIconImage = PresentationResourcesChat.chatFreeCloseButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)
|
||||
updatedIconOffset = CGPoint(x: UIScreenPixel, y: UIScreenPixel)
|
||||
|
||||
@@ -167,6 +183,17 @@ public class ChatMessageShareButton: ASDisplayNode {
|
||||
updatedIconImage = PresentationResourcesChat.chatFreeShareButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)
|
||||
}
|
||||
|
||||
if isSummarize {
|
||||
if self.topIconNode.image != nil, let snapshotView = self.topIconNode.view.snapshotContentTree() {
|
||||
self.view.addSubview(snapshotView)
|
||||
|
||||
snapshotView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
snapshotView.removeFromSuperview()
|
||||
})
|
||||
self.topIconNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25)
|
||||
}
|
||||
}
|
||||
|
||||
self.topIconNode.image = updatedIconImage
|
||||
self.topIconOffset = updatedIconOffset
|
||||
|
||||
@@ -309,6 +336,22 @@ public class ChatMessageShareButton: ASDisplayNode {
|
||||
self.backgroundBlurView?.view.isHidden = false
|
||||
}
|
||||
|
||||
if isSummarize {
|
||||
let starsView: StarsView
|
||||
if let current = self.starsView {
|
||||
starsView = current
|
||||
} else {
|
||||
starsView = StarsView()
|
||||
self.starsView = starsView
|
||||
self.view.insertSubview(starsView, belowSubview: self.topIconNode.view)
|
||||
}
|
||||
starsView.frame = CGRect(origin: .zero, size: size)
|
||||
starsView.update(size: size, color: .white)
|
||||
} else if let starsView = self.starsView {
|
||||
self.starsView = nil
|
||||
starsView.removeFromSuperview()
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
@@ -322,3 +365,70 @@ public class ChatMessageShareButton: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class StarsView: UIView {
|
||||
private let hierarchyTrackingLayer: HierarchyTrackingLayer
|
||||
private let topStar = SimpleLayer()
|
||||
private let bottomStar = SimpleLayer()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.clipsToBounds = true
|
||||
self.layer.addSublayer(self.hierarchyTrackingLayer)
|
||||
|
||||
self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.updateAnimations()
|
||||
}
|
||||
|
||||
self.layer.addSublayer(self.topStar)
|
||||
self.layer.addSublayer(self.bottomStar)
|
||||
|
||||
let image = UIImage(bundleImageName: "Settings/Storage/ParticleStar")
|
||||
self.topStar.contents = image?.cgImage
|
||||
self.bottomStar.contents = image?.cgImage
|
||||
|
||||
self.topStar.bounds = CGRect(origin: .zero, size: CGSize(width: 10.0, height: 10.0))
|
||||
self.bottomStar.bounds = CGRect(origin: .zero, size: CGSize(width: 10.0, height: 10.0))
|
||||
|
||||
self.topStar.opacity = 0.5
|
||||
self.bottomStar.opacity = 0.5
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func updateAnimations() {
|
||||
let topAnimation = CAKeyframeAnimation(keyPath: "transform.scale")
|
||||
topAnimation.values = [1.0 as NSNumber, 1.0 as NSNumber, 0.55 as NSNumber]
|
||||
topAnimation.keyTimes = [0.0 as NSNumber, 0.1 as NSNumber, 1.0 as NSNumber]
|
||||
topAnimation.duration = 0.9
|
||||
topAnimation.autoreverses = true
|
||||
topAnimation.repeatCount = Float.infinity
|
||||
topAnimation.beginTime = CACurrentMediaTime()
|
||||
self.topStar.add(topAnimation, forKey: "blink")
|
||||
|
||||
let bottomAnimation = CAKeyframeAnimation(keyPath: "transform.scale")
|
||||
bottomAnimation.values = [1.0 as NSNumber, 1.0 as NSNumber, 0.55 as NSNumber]
|
||||
bottomAnimation.keyTimes = [0.0 as NSNumber, 0.1 as NSNumber, 1.0 as NSNumber]
|
||||
bottomAnimation.duration = 0.9
|
||||
bottomAnimation.autoreverses = true
|
||||
bottomAnimation.repeatCount = Float.infinity
|
||||
bottomAnimation.beginTime = CACurrentMediaTime() + 0.9
|
||||
self.bottomStar.add(bottomAnimation, forKey: "blink")
|
||||
}
|
||||
|
||||
func update(size: CGSize, color: UIColor) {
|
||||
self.topStar.layerTintColor = color.cgColor
|
||||
self.bottomStar.layerTintColor = color.cgColor
|
||||
|
||||
self.topStar.position = CGPoint(x: 9.0, y: 9.0)
|
||||
self.bottomStar.position = CGPoint(x: size.width - 9.0, y: size.height - 9.0)
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -777,6 +777,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
quote: replyQuote,
|
||||
todoItemId: replyTodoItemId,
|
||||
story: replyStory,
|
||||
isSummarized: false,
|
||||
parentMessage: item.message,
|
||||
constrainedSize: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude),
|
||||
animationCache: item.controllerInteraction.presentationContext.animationCache,
|
||||
|
||||
+54
-8
@@ -116,6 +116,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
private var appliedExpandedBlockIds: Set<Int>?
|
||||
private var displayContentsUnderSpoilers: (value: Bool, location: CGPoint?) = (false, nil)
|
||||
|
||||
private var isSummaryApplied = false
|
||||
|
||||
private final class TextRevealAnimationState {
|
||||
let fromCount: Int
|
||||
let toCount: Int
|
||||
@@ -404,6 +406,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
var isSummaryApplied = false
|
||||
var isTranslating = false
|
||||
if let invoice {
|
||||
rawText = invoice.description
|
||||
@@ -417,7 +420,14 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
if let updatingMedia = item.attributes.updatingMedia {
|
||||
rawText = updatingMedia.text
|
||||
} else {
|
||||
rawText = item.message.text
|
||||
// MARK: - Ghostgram: Check for local edit first
|
||||
let peerId = item.message.id.peerId.toInt64()
|
||||
let messageId = item.message.id.id
|
||||
if let localEdit = LocalEditManager.shared.getLocalEdit(peerId: peerId, messageId: messageId) {
|
||||
rawText = localEdit
|
||||
} else {
|
||||
rawText = item.message.text
|
||||
}
|
||||
}
|
||||
|
||||
for attribute in item.message.attributes {
|
||||
@@ -441,15 +451,29 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
messageEntities = updatingMedia.entities?.entities ?? []
|
||||
}
|
||||
|
||||
let translateToLanguage = item.associatedData.translateToLanguage
|
||||
var isSummarized = false
|
||||
if item.controllerInteraction.summarizedMessageIds.contains(item.message.id) {
|
||||
isSummarized = true
|
||||
}
|
||||
|
||||
if let subject = item.associatedData.subject, case .messageOptions = subject {
|
||||
} else if let translateToLanguage = item.associatedData.translateToLanguage, !item.message.text.isEmpty && incoming {
|
||||
isTranslating = true
|
||||
for attribute in item.message.attributes {
|
||||
if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage {
|
||||
} else if !item.message.text.isEmpty && incoming {
|
||||
if translateToLanguage != nil || isSummarized {
|
||||
isTranslating = true
|
||||
}
|
||||
if isTranslating {
|
||||
if isSummarized, let attribute = item.message.attributes.first(where: { $0 is SummarizationMessageAttribute }) as? SummarizationMessageAttribute, let summary = attribute.summaryForLang(translateToLanguage) {
|
||||
rawText = summary.text
|
||||
messageEntities = summary.entities
|
||||
isTranslating = false
|
||||
isSummaryApplied = true
|
||||
} else if let attribute = item.message.attributes.first(where: { $0 is TranslationMessageAttribute }) as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage {
|
||||
rawText = attribute.text
|
||||
messageEntities = attribute.entities
|
||||
isTranslating = false
|
||||
break
|
||||
if !isSummarized {
|
||||
isTranslating = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -718,6 +742,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
starsCount: starsCount,
|
||||
isPinned: item.message.tags.contains(.pinned) && (!item.associatedData.isInPinnedListMode || isReplyThread),
|
||||
hasAutoremove: item.message.isSelfExpiring,
|
||||
isDeleted: AntiDeleteManager.shared.isMessageDeleted(peerId: item.message.id.peerId.toInt64(), messageId: item.message.id.id) || AntiDeleteManager.shared.isMessageDeleted(text: item.message.text),
|
||||
canViewReactionList: canViewMessageReactionList(message: item.topMessage),
|
||||
animationCache: item.controllerInteraction.presentationContext.animationCache,
|
||||
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
|
||||
@@ -789,6 +814,20 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
strongSelf.textNode.textNode.displaysAsynchronously = !item.presentationData.isPreview
|
||||
animation.animator.updateFrame(layer: strongSelf.containerNode.layer, frame: CGRect(origin: CGPoint(), size: boundingSize), completion: nil)
|
||||
|
||||
|
||||
if strongSelf.isSummaryApplied != isSummaryApplied {
|
||||
strongSelf.isSummaryApplied = isSummaryApplied
|
||||
itemApply?.setInvertOffsetDirection()
|
||||
|
||||
if let snapshotView = strongSelf.textNode.textNode.view.snapshotContentTree() {
|
||||
strongSelf.view.addSubview(snapshotView)
|
||||
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
snapshotView.removeFromSuperview()
|
||||
})
|
||||
strongSelf.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
}
|
||||
}
|
||||
if strongSelf.appliedExpandedBlockIds != nil && strongSelf.appliedExpandedBlockIds != strongSelf.expandedBlockIds {
|
||||
itemApply?.setInvertOffsetDirection()
|
||||
}
|
||||
@@ -1227,7 +1266,14 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
if let current = self.shimmeringNode {
|
||||
shimmeringNode = current
|
||||
} else {
|
||||
shimmeringNode = ShimmeringLinkNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.secondaryTextColor.withAlphaComponent(0.1) : item.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor.withAlphaComponent(0.1))
|
||||
let color: UIColor
|
||||
let isIncoming = item.message.effectivelyIncoming(item.context.account.peerId)
|
||||
if item.presentationData.theme.theme.overallDarkAppearance {
|
||||
color = isIncoming ? item.presentationData.theme.theme.chat.message.incoming.primaryTextColor.withAlphaComponent(0.1) : item.presentationData.theme.theme.chat.message.outgoing.primaryTextColor.withAlphaComponent(0.1)
|
||||
} else {
|
||||
color = isIncoming ? item.presentationData.theme.theme.chat.message.incoming.accentTextColor.withAlphaComponent(0.1) : item.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor.withAlphaComponent(0.1)
|
||||
}
|
||||
shimmeringNode = ShimmeringLinkNode(color: color)
|
||||
shimmeringNode.updateRects(rects)
|
||||
shimmeringNode.frame = self.textNode.textNode.frame
|
||||
shimmeringNode.updateLayout(self.textNode.textNode.frame.size)
|
||||
|
||||
+8
-1
@@ -1481,8 +1481,15 @@ public class ChatMessageTodoBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
|
||||
if isTranslating, !rects.isEmpty {
|
||||
if self.shimmeringNodes.isEmpty {
|
||||
let color: UIColor
|
||||
let isIncoming = item.message.effectivelyIncoming(item.context.account.peerId)
|
||||
if item.presentationData.theme.theme.overallDarkAppearance {
|
||||
color = isIncoming ? item.presentationData.theme.theme.chat.message.incoming.primaryTextColor.withAlphaComponent(0.1) : item.presentationData.theme.theme.chat.message.outgoing.primaryTextColor.withAlphaComponent(0.1)
|
||||
} else {
|
||||
color = isIncoming ? item.presentationData.theme.theme.chat.message.incoming.accentTextColor.withAlphaComponent(0.1) : item.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor.withAlphaComponent(0.1)
|
||||
}
|
||||
for rects in rects {
|
||||
let shimmeringNode = ShimmeringLinkNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.secondaryTextColor.withAlphaComponent(0.1) : item.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor.withAlphaComponent(0.1))
|
||||
let shimmeringNode = ShimmeringLinkNode(color: color)
|
||||
shimmeringNode.updateRects(rects)
|
||||
shimmeringNode.frame = self.bounds
|
||||
shimmeringNode.updateLayout(self.bounds.size)
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ public final class ChatNewThreadInfoItemNode: ListViewItemNode, ASGestureRecogni
|
||||
|
||||
self.arrowView = UIImageView()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: true, rotated: true)
|
||||
super.init(layerBacked: false, rotated: true)
|
||||
|
||||
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
|
||||
|
||||
|
||||
+3
-3
@@ -333,7 +333,7 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode {
|
||||
|
||||
self.placeholderNode = StickerShimmerEffectNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
|
||||
super.init(layerBacked: false, rotated: false, seeThrough: false)
|
||||
|
||||
self.addSubnode(self.containerNode)
|
||||
self.containerNode.addSubnode(self.imageNode)
|
||||
@@ -721,7 +721,7 @@ public final class ChatQrCodeScreenImpl: ViewController, ChatQrCodeScreen {
|
||||
}
|
||||
|
||||
private func iconColors(theme: PresentationTheme) -> [String: UIColor] {
|
||||
let accentColor = theme.rootController.navigationBar.glassBarButtonForegroundColor
|
||||
let accentColor = theme.chat.inputPanel.panelControlColor
|
||||
var colors: [String: UIColor] = [:]
|
||||
colors["Sunny.Path 14.Path.Stroke 1"] = accentColor
|
||||
colors["Sunny.Path 15.Path.Stroke 1"] = accentColor
|
||||
@@ -1470,7 +1470,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
|
||||
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Navigation/Close",
|
||||
tintColor: self.presentationData.theme.rootController.navigationBar.glassBarButtonForegroundColor
|
||||
tintColor: self.presentationData.theme.chat.inputPanel.panelControlColor
|
||||
)
|
||||
)),
|
||||
action: { [weak self] _ in
|
||||
|
||||
+2
-2
@@ -49,7 +49,7 @@ public final class ChatRecentActionsController: TelegramBaseController {
|
||||
|
||||
self.titleView = CounterControllerTitleView(theme: self.presentationData.theme)
|
||||
|
||||
super.init(context: context, navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData), mediaAccessoryPanelVisibility: .specific(size: .compact), locationBroadcastPanelSource: .none, groupCallPanelSource: .none)
|
||||
super.init(context: context, navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
|
||||
|
||||
self.automaticallyControlPresentationContextLayout = false
|
||||
|
||||
@@ -263,7 +263,7 @@ public final class ChatRecentActionsController: TelegramBaseController {
|
||||
self.navigationItem.setRightBarButton(rightButton.buttonItem, animated: false)
|
||||
|
||||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
|
||||
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData))
|
||||
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData), transition: .immediate)
|
||||
|
||||
self.controllerNode.updatePresentationData(self.presentationData)
|
||||
}
|
||||
|
||||
-1
@@ -130,7 +130,6 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
self.panelInfoButtonNode = HighlightableButtonNode()
|
||||
|
||||
self.listNode = ListView()
|
||||
self.listNode.dynamicBounceEnabled = false
|
||||
self.listNode.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0)
|
||||
self.listNode.accessibilityPageScrolledString = { row, count in
|
||||
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
|
||||
|
||||
+4
-2
@@ -25,7 +25,7 @@ final class ChatRecentActionsSearchNavigationContentNode: NavigationBarContentNo
|
||||
|
||||
self.cancel = cancel
|
||||
|
||||
self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), strings: strings, fieldStyle: .modern, displayBackground: false)
|
||||
self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), presentationTheme: theme, strings: strings, fieldStyle: .modern, displayBackground: false)
|
||||
let placeholderText = strings.Common_Search
|
||||
self.searchBar.placeholderString = NSAttributedString(string: placeholderText, font: searchBarFont, textColor: theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
|
||||
|
||||
@@ -51,10 +51,12 @@ final class ChatRecentActionsSearchNavigationContentNode: NavigationBarContentNo
|
||||
return 54.0
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
let searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - self.nominalHeight), size: CGSize(width: size.width, height: 54.0))
|
||||
self.searchBar.frame = searchBarFrame
|
||||
self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition)
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
func activate() {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ChatSearchNavigationContentNode",
|
||||
module_name = "ChatSearchNavigationContentNode",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/ChatPresentationInterfaceState",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/SearchBarNode",
|
||||
"//submodules/LocalizedPeerData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/ActivityIndicator",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
+299
@@ -0,0 +1,299 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import SearchBarNode
|
||||
import LocalizedPeerData
|
||||
import SwiftSignalKit
|
||||
import AccountContext
|
||||
import ChatPresentationInterfaceState
|
||||
import ComponentFlow
|
||||
import GlassBackgroundComponent
|
||||
import ActivityIndicator
|
||||
|
||||
private let searchBarFont = Font.regular(17.0)
|
||||
|
||||
public final class ChatSearchNavigationContentNode: NavigationBarContentNode {
|
||||
private let context: AccountContext
|
||||
private var theme: PresentationTheme
|
||||
private let strings: PresentationStrings
|
||||
private let chatLocation: ChatLocation
|
||||
|
||||
private let backgroundContainer: GlassBackgroundContainerView
|
||||
private let backgroundView: GlassBackgroundView
|
||||
private let iconView: UIImageView
|
||||
private var activityIndicator: ActivityIndicator?
|
||||
private let searchBar: SearchBarNode
|
||||
private let close: (background: GlassBackgroundView, icon: UIImageView)
|
||||
|
||||
private let interaction: ChatPanelInterfaceInteraction
|
||||
|
||||
private var hasActivity: Bool = false
|
||||
private var searchingActivityDisposable: Disposable?
|
||||
|
||||
private var params: (size: CGSize, leftInset: CGFloat, rightInset: CGFloat)?
|
||||
|
||||
public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, chatLocation: ChatLocation, interaction: ChatPanelInterfaceInteraction, presentationInterfaceState: ChatPresentationInterfaceState) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.chatLocation = chatLocation
|
||||
self.interaction = interaction
|
||||
|
||||
self.backgroundContainer = GlassBackgroundContainerView()
|
||||
self.backgroundView = GlassBackgroundView()
|
||||
self.backgroundContainer.contentView.addSubview(self.backgroundView)
|
||||
self.iconView = UIImageView()
|
||||
self.backgroundView.contentView.addSubview(self.iconView)
|
||||
|
||||
self.close = (GlassBackgroundView(), UIImageView())
|
||||
self.close.background.contentView.addSubview(self.close.icon)
|
||||
|
||||
self.searchBar = SearchBarNode(
|
||||
theme: SearchBarNodeTheme(
|
||||
background: .clear,
|
||||
separator: .clear,
|
||||
inputFill: .clear,
|
||||
primaryText: theme.chat.inputPanel.panelControlColor,
|
||||
placeholder: theme.chat.inputPanel.inputPlaceholderColor,
|
||||
inputIcon: theme.chat.inputPanel.inputControlColor,
|
||||
inputClear: theme.chat.inputPanel.panelControlColor,
|
||||
accent: theme.chat.inputPanel.panelControlAccentColor,
|
||||
keyboard: theme.rootController.keyboardColor
|
||||
),
|
||||
presentationTheme: theme,
|
||||
strings: strings,
|
||||
fieldStyle: .inlineNavigation,
|
||||
forceSeparator: false,
|
||||
displayBackground: false,
|
||||
cancelText: nil
|
||||
)
|
||||
let placeholderText: String
|
||||
switch chatLocation {
|
||||
case .peer, .replyThread, .customChatContents:
|
||||
if chatLocation.peerId == context.account.peerId, presentationInterfaceState.hasSearchTags {
|
||||
if case .standard(.embedded(false)) = presentationInterfaceState.mode {
|
||||
placeholderText = strings.Common_Search
|
||||
} else {
|
||||
placeholderText = strings.Chat_SearchTagsPlaceholder
|
||||
}
|
||||
} else {
|
||||
placeholderText = strings.Conversation_SearchPlaceholder
|
||||
}
|
||||
}
|
||||
self.searchBar.placeholderString = NSAttributedString(string: placeholderText, font: searchBarFont, textColor: theme.chat.inputPanel.inputPlaceholderColor)
|
||||
|
||||
super.init()
|
||||
|
||||
self.view.addSubview(self.backgroundContainer)
|
||||
self.backgroundView.contentView.addSubview(self.searchBar.view)
|
||||
|
||||
self.backgroundContainer.contentView.addSubview(self.close.background)
|
||||
self.close.background.contentView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onCloseTapGesture(_:))))
|
||||
|
||||
self.searchBar.cancel = { [weak self] in
|
||||
self?.searchBar.deactivate(clear: false)
|
||||
self?.interaction.dismissMessageSearch()
|
||||
}
|
||||
|
||||
self.searchBar.textUpdated = { [weak self] query, _ in
|
||||
self?.interaction.updateMessageSearch(query)
|
||||
}
|
||||
|
||||
self.searchBar.clearPrefix = { [weak self] in
|
||||
self?.interaction.toggleMembersSearch(false)
|
||||
}
|
||||
|
||||
self.searchBar.clearTokens = { [weak self] in
|
||||
self?.interaction.toggleMembersSearch(false)
|
||||
}
|
||||
|
||||
self.searchBar.tokensUpdated = { [weak self] tokens in
|
||||
if tokens.isEmpty {
|
||||
self?.interaction.toggleMembersSearch(false)
|
||||
}
|
||||
}
|
||||
|
||||
if let statuses = interaction.statuses {
|
||||
self.searchingActivityDisposable = (statuses.searching
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] value in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.hasActivity != value {
|
||||
self.hasActivity = value
|
||||
if let params = self.params {
|
||||
let _ = self.updateLayout(size: params.size, leftInset: params.leftInset, rightInset: params.rightInset, transition: .immediate)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.searchingActivityDisposable?.dispose()
|
||||
}
|
||||
|
||||
override public var nominalHeight: CGFloat {
|
||||
return 60.0
|
||||
}
|
||||
|
||||
@objc private func onCloseTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.searchBar.cancel?()
|
||||
}
|
||||
}
|
||||
|
||||
override public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
self.params = (size, leftInset, rightInset)
|
||||
|
||||
let transition = ComponentTransition(transition)
|
||||
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: leftInset + 16.0, y: 6.0), size: CGSize(width: size.width - 16.0 * 2.0 - leftInset - rightInset - 44.0 - 8.0, height: 44.0))
|
||||
let closeFrame = CGRect(origin: CGPoint(x: size.width - 16.0 - rightInset - 44.0, y: backgroundFrame.minY), size: CGSize(width: 44.0, height: 44.0))
|
||||
|
||||
transition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: CGPoint(), size: size))
|
||||
self.backgroundContainer.update(size: size, isDark: self.theme.overallDarkAppearance, transition: transition)
|
||||
|
||||
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
|
||||
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: self.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: self.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: transition)
|
||||
|
||||
if self.iconView.image == nil {
|
||||
self.iconView.image = UIImage(bundleImageName: "Navigation/Search")?.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
transition.setTintColor(view: self.iconView, color: self.theme.rootController.navigationSearchBar.inputIconColor)
|
||||
|
||||
if let image = self.iconView.image {
|
||||
let imageSize: CGSize
|
||||
let iconFrame: CGRect
|
||||
let iconFraction: CGFloat = 0.8
|
||||
imageSize = CGSize(width: image.size.width * iconFraction, height: image.size.height * iconFraction)
|
||||
iconFrame = CGRect(origin: CGPoint(x: 12.0, y: floor((backgroundFrame.height - imageSize.height) * 0.5)), size: imageSize)
|
||||
transition.setPosition(view: self.iconView, position: iconFrame.center)
|
||||
transition.setBounds(view: self.iconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
|
||||
}
|
||||
|
||||
if self.hasActivity {
|
||||
let activityIndicator: ActivityIndicator
|
||||
if let current = self.activityIndicator {
|
||||
activityIndicator = current
|
||||
} else {
|
||||
activityIndicator = ActivityIndicator(type: .custom(self.theme.chat.inputPanel.inputControlColor, 14.0, 14.0, false))
|
||||
self.activityIndicator = activityIndicator
|
||||
self.backgroundView.contentView.addSubview(activityIndicator.view)
|
||||
}
|
||||
let indicatorSize = activityIndicator.measure(CGSize(width: 32.0, height: 32.0))
|
||||
let indicatorFrame = CGRect(origin: CGPoint(x: 15.0, y: floorToScreenPixels((backgroundFrame.height - indicatorSize.height) * 0.5)), size: indicatorSize)
|
||||
transition.setPosition(view: activityIndicator.view, position: indicatorFrame.center)
|
||||
transition.setBounds(view: activityIndicator.view, bounds: CGRect(origin: CGPoint(), size: indicatorFrame.size))
|
||||
} else if let activityIndicator = self.activityIndicator {
|
||||
self.activityIndicator = nil
|
||||
activityIndicator.view.removeFromSuperview()
|
||||
}
|
||||
self.iconView.isHidden = self.hasActivity
|
||||
|
||||
let searchBarFrame = CGRect(origin: CGPoint(x: 36.0, y: 0.0), size: CGSize(width: backgroundFrame.width - 36.0 - 4.0, height: 44.0))
|
||||
transition.setFrame(view: self.searchBar.view, frame: searchBarFrame)
|
||||
self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: 0.0, rightInset: 0.0, transition: transition.containedViewLayoutTransition)
|
||||
|
||||
if self.close.icon.image == nil {
|
||||
self.close.icon.image = generateImage(CGSize(width: 40.0, height: 40.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setLineWidth(2.0)
|
||||
context.setLineCap(.round)
|
||||
context.setStrokeColor(UIColor.white.cgColor)
|
||||
|
||||
context.beginPath()
|
||||
context.move(to: CGPoint(x: 12.0, y: 12.0))
|
||||
context.addLine(to: CGPoint(x: size.width - 12.0, y: size.height - 12.0))
|
||||
context.move(to: CGPoint(x: size.width - 12.0, y: 12.0))
|
||||
context.addLine(to: CGPoint(x: 12.0, y: size.height - 12.0))
|
||||
context.strokePath()
|
||||
})?.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
|
||||
if let image = close.icon.image {
|
||||
self.close.icon.frame = image.size.centered(in: CGRect(origin: CGPoint(), size: closeFrame.size))
|
||||
}
|
||||
self.close.icon.tintColor = self.theme.chat.inputPanel.panelControlColor
|
||||
|
||||
transition.setFrame(view: self.close.background, frame: closeFrame)
|
||||
self.close.background.update(size: closeFrame.size, cornerRadius: closeFrame.height * 0.5, isDark: self.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: self.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: transition)
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
public func activate() {
|
||||
self.searchBar.activate()
|
||||
}
|
||||
|
||||
public func deactivate() {
|
||||
self.searchBar.deactivate(clear: false)
|
||||
}
|
||||
|
||||
public func update(presentationInterfaceState: ChatPresentationInterfaceState) {
|
||||
if let search = presentationInterfaceState.search {
|
||||
self.searchBar.updateThemeAndStrings(
|
||||
theme: SearchBarNodeTheme(
|
||||
background: .clear,
|
||||
separator: .clear,
|
||||
inputFill: .clear,
|
||||
primaryText: presentationInterfaceState.theme.chat.inputPanel.panelControlColor,
|
||||
placeholder: presentationInterfaceState.theme.chat.inputPanel.inputPlaceholderColor,
|
||||
inputIcon: presentationInterfaceState.theme.chat.inputPanel.inputControlColor,
|
||||
inputClear: presentationInterfaceState.theme.chat.inputPanel.panelControlColor,
|
||||
accent: presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor,
|
||||
keyboard: presentationInterfaceState.theme.rootController.keyboardColor
|
||||
),
|
||||
presentationTheme: presentationInterfaceState.theme,
|
||||
strings: presentationInterfaceState.strings
|
||||
)
|
||||
|
||||
switch search.domain {
|
||||
case .everything, .tag:
|
||||
self.searchBar.tokens = []
|
||||
self.searchBar.prefixString = nil
|
||||
let placeholderText: String
|
||||
switch self.chatLocation {
|
||||
case .peer, .replyThread, .customChatContents:
|
||||
if presentationInterfaceState.historyFilter != nil {
|
||||
placeholderText = self.strings.Common_Search
|
||||
} else if self.chatLocation.peerId == self.context.account.peerId, presentationInterfaceState.hasSearchTags {
|
||||
if case .standard(.embedded(false)) = presentationInterfaceState.mode {
|
||||
placeholderText = strings.Common_Search
|
||||
} else {
|
||||
placeholderText = self.strings.Chat_SearchTagsPlaceholder
|
||||
}
|
||||
} else {
|
||||
placeholderText = self.strings.Conversation_SearchPlaceholder
|
||||
}
|
||||
}
|
||||
self.searchBar.placeholderString = NSAttributedString(string: placeholderText, font: searchBarFont, textColor: presentationInterfaceState.theme.chat.inputPanel.inputPlaceholderColor)
|
||||
case .members:
|
||||
self.searchBar.tokens = []
|
||||
self.searchBar.prefixString = NSAttributedString(string: strings.Conversation_SearchByName_Prefix, font: searchBarFont, textColor: presentationInterfaceState.theme.chat.inputPanel.inputTextColor)
|
||||
self.searchBar.placeholderString = nil
|
||||
case let .member(peer):
|
||||
self.searchBar.tokens = [SearchBarToken(id: peer.id, icon: UIImage(bundleImageName: "Chat List/Search/User"), title: EnginePeer(peer).compactDisplayTitle, permanent: false)]
|
||||
self.searchBar.prefixString = nil
|
||||
self.searchBar.placeholderString = nil
|
||||
}
|
||||
|
||||
if self.searchBar.text != search.query {
|
||||
self.searchBar.text = search.query
|
||||
self.interaction.updateMessageSearch(search.query)
|
||||
}
|
||||
}
|
||||
|
||||
if presentationInterfaceState.theme != self.theme {
|
||||
self.theme = presentationInterfaceState.theme
|
||||
if let params = self.params {
|
||||
let _ = self.updateLayout(size: params.size, leftInset: params.leftInset, rightInset: params.rightInset, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-9
@@ -151,8 +151,8 @@ public final class ChatFloatingTopicsPanel: Component {
|
||||
},
|
||||
containerSize: CGSize(width: 72.0 + 8.0, height: availableSize.height)
|
||||
)
|
||||
let sidePanelFrame = CGRect(origin: CGPoint(), size: CGSize(width: 8.0 + 80.0, height: availableSize.height - 8.0 - environment.insets.bottom))
|
||||
let sidePanelBackgroundFrame = CGRect(origin: CGPoint(x: 8.0, y: 8.0), size: CGSize(width: 80.0, height: availableSize.height - 8.0 - 8.0 - environment.insets.bottom))
|
||||
let sidePanelFrame = CGRect(origin: CGPoint(x: 8.0, y: 0.0), size: CGSize(width: 16.0 + 80.0, height: availableSize.height - 8.0 - environment.insets.bottom))
|
||||
let sidePanelBackgroundFrame = CGRect(origin: CGPoint(x: 16.0, y: 8.0), size: CGSize(width: 80.0, height: availableSize.height - 8.0 - 8.0 - environment.insets.bottom))
|
||||
currentPanelBackgroundFrame = sidePanelBackgroundFrame
|
||||
if let sidePanelView = sidePanel.view as? ChatSideTopicsPanel.View {
|
||||
if sidePanelView.superview == nil {
|
||||
@@ -160,7 +160,7 @@ public final class ChatFloatingTopicsPanel: Component {
|
||||
sidePanelView.clipsToBounds = true
|
||||
self.addSubview(sidePanelView)
|
||||
|
||||
sidePanelView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: sidePanelSize.height, height: 8.0 + 40.0))
|
||||
sidePanelView.frame = CGRect(origin: CGPoint(x: 8.0, y: 0.0), size: CGSize(width: sidePanelSize.height, height: 8.0 + 40.0))
|
||||
}
|
||||
transition.setFrame(view: sidePanelView, frame: sidePanelFrame)
|
||||
}
|
||||
@@ -168,7 +168,7 @@ public final class ChatFloatingTopicsPanel: Component {
|
||||
self.sidePanel = nil
|
||||
if let sidePanelView = sidePanel.view as? ChatSideTopicsPanel.View {
|
||||
sidePanelView.clipsToBounds = true
|
||||
transition.setFrame(view: sidePanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: sidePanelView.bounds.width, height: 8.0 + 40.0)), completion: { [weak sidePanelView] _ in
|
||||
transition.setFrame(view: sidePanelView, frame: CGRect(origin: CGPoint(x: 8.0, y: 0.0), size: CGSize(width: sidePanelView.bounds.width, height: 8.0 + 40.0)), completion: { [weak sidePanelView] _ in
|
||||
sidePanelView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
@@ -212,17 +212,17 @@ public final class ChatFloatingTopicsPanel: Component {
|
||||
right: 0.0
|
||||
))
|
||||
},
|
||||
containerSize: CGSize(width: availableSize.width, height: 8.0 + 40.0)
|
||||
containerSize: CGSize(width: availableSize.width - 16.0, height: 8.0 + 40.0)
|
||||
)
|
||||
let topPanelFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width - 8.0, height: 8.0 + 40.0))
|
||||
let topPanelBackgroundFrame = CGRect(origin: CGPoint(x: 8.0, y: 8.0), size: CGSize(width: availableSize.width - 8.0 - 8.0, height: 40.0))
|
||||
let topPanelFrame = CGRect(origin: CGPoint(x: 8.0, y: 0.0), size: CGSize(width: availableSize.width - 16.0, height: 8.0 + 40.0))
|
||||
let topPanelBackgroundFrame = CGRect(origin: CGPoint(x: 16.0, y: 8.0), size: CGSize(width: availableSize.width - 16.0 - 16.0, height: 40.0))
|
||||
currentPanelBackgroundFrame = topPanelBackgroundFrame
|
||||
if let topPanelView = topPanel.view as? ChatSideTopicsPanel.View {
|
||||
if topPanelView.superview == nil {
|
||||
topPanelView.clipsToBounds = true
|
||||
topPanelView.layer.cornerRadius = 20.0
|
||||
self.addSubview(topPanelView)
|
||||
topPanelView.frame = CGRect(origin: CGPoint(), size: CGSize(width: 80.0 + 8.0, height: topPanelFrame.height))
|
||||
topPanelView.frame = CGRect(origin: CGPoint(x: 8.0, y: 0.0), size: CGSize(width: 80.0 + 16.0, height: topPanelFrame.height))
|
||||
}
|
||||
transition.setFrame(view: topPanelView, frame: topPanelFrame)
|
||||
}
|
||||
@@ -230,7 +230,7 @@ public final class ChatFloatingTopicsPanel: Component {
|
||||
self.topPanel = nil
|
||||
if let topPanelView = topPanel.view as? ChatSideTopicsPanel.View {
|
||||
topPanelView.clipsToBounds = true
|
||||
transition.setFrame(view: topPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 8.0 + 72.0, height: topPanelView.bounds.height)), completion: { [weak topPanelView] _ in
|
||||
transition.setFrame(view: topPanelView, frame: CGRect(origin: CGPoint(x: 8.0, y: 0.0), size: CGSize(width: 16.0 + 72.0, height: topPanelView.bounds.height)), completion: { [weak topPanelView] _ in
|
||||
topPanelView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
|
||||
+6
-6
@@ -464,7 +464,7 @@ public final class ChatSideTopicsPanel: Component {
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: titleText, font: Font.regular(10.0), textColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.secondaryTextColor)),
|
||||
text: .plain(NSAttributedString(string: titleText, font: Font.regular(10.0), textColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.chat.inputPanel.panelControlColor)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 2
|
||||
)),
|
||||
@@ -897,7 +897,7 @@ public final class ChatSideTopicsPanel: Component {
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: titleText, font: Font.medium(14.0), textColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.secondaryTextColor)),
|
||||
text: .plain(NSAttributedString(string: titleText, font: Font.medium(14.0), textColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.chat.inputPanel.panelControlColor)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 2
|
||||
)),
|
||||
@@ -1109,7 +1109,7 @@ public final class ChatSideTopicsPanel: Component {
|
||||
transition: .immediate,
|
||||
component: AnyComponent(BundleIconComponent(
|
||||
name: isReordering ? "Media Editor/Done" : "Chat/Title Panels/SidebarIcon",
|
||||
tintColor: location == .side ? theme.rootController.navigationBar.accentTextColor : theme.rootController.navigationBar.secondaryTextColor,
|
||||
tintColor: location == .side ? theme.rootController.navigationBar.accentTextColor : theme.chat.inputPanel.panelControlColor,
|
||||
maxSize: CGSize(width: 24.0, height: 24.0),
|
||||
scaleFactor: 1.0
|
||||
)),
|
||||
@@ -1242,7 +1242,7 @@ public final class ChatSideTopicsPanel: Component {
|
||||
transition: .immediate,
|
||||
component: AnyComponent(BundleIconComponent(
|
||||
name: "Chat List/Tabs/IconChats",
|
||||
tintColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.secondaryTextColor
|
||||
tintColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.chat.inputPanel.panelControlColor
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||
@@ -1257,7 +1257,7 @@ public final class ChatSideTopicsPanel: Component {
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: titleText, font: Font.regular(10.0), textColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.secondaryTextColor)),
|
||||
text: .plain(NSAttributedString(string: titleText, font: Font.regular(10.0), textColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.chat.inputPanel.panelControlColor)),
|
||||
maximumNumberOfLines: 2
|
||||
)),
|
||||
environment: {},
|
||||
@@ -1394,7 +1394,7 @@ public final class ChatSideTopicsPanel: Component {
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: titleText, font: Font.medium(14.0), textColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.secondaryTextColor)),
|
||||
text: .plain(NSAttributedString(string: titleText, font: Font.medium(14.0), textColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.chat.inputPanel.panelControlColor)),
|
||||
maximumNumberOfLines: 2
|
||||
)),
|
||||
environment: {},
|
||||
|
||||
+4
-5
@@ -149,7 +149,7 @@ public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessag
|
||||
public let textNode: ImmediateAnimatedCountLabelNode
|
||||
|
||||
public let expandMediaInputButton: HighlightTrackingButton
|
||||
private let expandMediaInputButtonBackgroundView: GlassBackgroundView
|
||||
public let expandMediaInputButtonBackgroundView: GlassBackgroundView
|
||||
private let expandMediaInputButtonIcon: GlassBackgroundView.ContentImageView
|
||||
|
||||
private var effectBadgeView: EffectBadgeView?
|
||||
@@ -201,10 +201,9 @@ public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessag
|
||||
|
||||
self.expandMediaInputButton = HighlightTrackingButton()
|
||||
self.expandMediaInputButtonBackgroundView = GlassBackgroundView()
|
||||
self.expandMediaInputButtonBackgroundView.isUserInteractionEnabled = false
|
||||
self.expandMediaInputButton.addSubview(self.expandMediaInputButtonBackgroundView)
|
||||
self.expandMediaInputButtonIcon = GlassBackgroundView.ContentImageView()
|
||||
self.expandMediaInputButtonBackgroundView.contentView.addSubview(self.expandMediaInputButtonIcon)
|
||||
self.expandMediaInputButtonBackgroundView.contentView.addSubview(self.expandMediaInputButton)
|
||||
self.expandMediaInputButtonIcon.image = PresentationResourcesChat.chatInputPanelExpandButtonImage(presentationInterfaceState.theme)
|
||||
self.expandMediaInputButtonIcon.tintColor = theme.chat.inputPanel.panelControlColor
|
||||
self.expandMediaInputButtonIcon.setMonochromaticEffect(tintColor: theme.chat.inputPanel.panelControlColor)
|
||||
@@ -242,7 +241,7 @@ public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessag
|
||||
self.sendContainerNode.view.addSubview(self.sendButtonBackgroundView)
|
||||
self.sendContainerNode.addSubnode(self.sendButton)
|
||||
self.sendContainerNode.addSubnode(self.textNode)
|
||||
self.view.addSubview(self.expandMediaInputButton)
|
||||
self.view.addSubview(self.expandMediaInputButtonBackgroundView)
|
||||
|
||||
self.expandMediaInputButton.highligthedChanged = { [weak self] highlighted in
|
||||
guard let self else {
|
||||
@@ -402,7 +401,7 @@ public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessag
|
||||
|
||||
transition.updateFrame(view: self.expandMediaInputButton, frame: CGRect(origin: CGPoint(), size: size))
|
||||
transition.updateFrame(view: self.expandMediaInputButtonBackgroundView, frame: CGRect(origin: CGPoint(), size: size))
|
||||
self.expandMediaInputButtonBackgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), transition: ComponentTransition(transition))
|
||||
self.expandMediaInputButtonBackgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), isInteractive: true, transition: ComponentTransition(transition))
|
||||
if let image = self.expandMediaInputButtonIcon.image {
|
||||
let expandIconFrame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) * 0.5), y: floor((size.height - image.size.height) * 0.5)), size: image.size)
|
||||
self.expandMediaInputButtonIcon.center = expandIconFrame.center
|
||||
|
||||
-2
@@ -491,7 +491,6 @@ public final class ChatTextInputPanelComponent: Component {
|
||||
pendingUnpinnedAllMessages: false,
|
||||
activeGroupCallInfo: nil,
|
||||
hasActiveGroupCall: false,
|
||||
importState: nil,
|
||||
threadData: nil,
|
||||
isGeneralThreadClosed: false,
|
||||
replyMessage: nil,
|
||||
@@ -793,7 +792,6 @@ public final class ChatTextInputPanelComponent: Component {
|
||||
pendingUnpinnedAllMessages: false,
|
||||
activeGroupCallInfo: nil,
|
||||
hasActiveGroupCall: false,
|
||||
importState: nil,
|
||||
threadData: nil,
|
||||
isGeneralThreadClosed: false,
|
||||
replyMessage: nil,
|
||||
|
||||
+18
-17
@@ -703,7 +703,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
self.sendActionButtons.micButtonBackgroundView.alpha = 0.0
|
||||
self.sendActionButtons.micButton.alpha = 0.0
|
||||
self.sendActionButtons.micButtonTintMaskView.alpha = 0.0
|
||||
self.sendActionButtons.expandMediaInputButton.alpha = 0.0
|
||||
self.sendActionButtons.expandMediaInputButtonBackgroundView.alpha = 0.0
|
||||
|
||||
self.mediaActionButtons = ChatTextInputActionButtonsNode(context: context, presentationInterfaceState: presentationInterfaceState, presentationContext: presentationContext, presentController: presentController)
|
||||
self.mediaActionButtons.sendContainerNode.alpha = 0.0
|
||||
@@ -811,11 +811,9 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
if highlighted {
|
||||
self.attachmentButtonIcon.layer.removeAnimation(forKey: "opacity")
|
||||
self.attachmentButtonIcon.alpha = 0.4
|
||||
self.attachmentButtonIcon.layer.allowsGroupOpacity = true
|
||||
} else {
|
||||
self.attachmentButtonIcon.alpha = 1.0
|
||||
self.attachmentButtonIcon.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
self.attachmentButtonIcon.layer.allowsGroupOpacity = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -901,7 +899,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
self.mediaActionButtons.updateAccessibility()
|
||||
|
||||
self.mediaActionButtons.expandMediaInputButton.addTarget(self, action: #selector(self.expandButtonPressed), for: .touchUpInside)
|
||||
self.mediaActionButtons.expandMediaInputButton.alpha = 0.0
|
||||
self.mediaActionButtons.expandMediaInputButtonBackgroundView.alpha = 0.0
|
||||
|
||||
self.searchLayoutClearButton.highligthedChanged = { [weak self] highlighted in
|
||||
guard let self else {
|
||||
@@ -1174,6 +1172,9 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
self.touchDownGestureRecognizer = recognizer
|
||||
|
||||
textInputNode.textView.accessibilityHint = self.textPlaceholderNode.attributedText?.string
|
||||
|
||||
self.isAccessibilityContainer = true
|
||||
self.accessibilityElements = [textInputNode.textView]
|
||||
}
|
||||
|
||||
private func textFieldMaxHeight(_ maxHeight: CGFloat, metrics: LayoutMetrics, bottomInset: CGFloat) -> CGFloat {
|
||||
@@ -2437,8 +2438,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
self.attachmentButtonBackground.contentView.addSubview(dotAnimationView)
|
||||
dotAnimationView.frame = dotAnimationSize.centered(in: self.attachmentButtonBackground.contentView.bounds)
|
||||
|
||||
self.attachmentButtonIcon.layer.opacity = 0.0
|
||||
self.attachmentButtonIcon.layer.transform = CATransform3DMakeScale(0.001, 0.001, 1.0)
|
||||
self.attachmentButtonIcon.isHidden = true
|
||||
dotAnimationView.playOnce(completion: { [weak self, weak dotAnimationView] in
|
||||
guard let self else {
|
||||
return
|
||||
@@ -2453,8 +2453,9 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
transition.setScale(view: dotAnimationView, scale: 0.001)
|
||||
}
|
||||
|
||||
transition.setAlpha(view: self.attachmentButtonIcon, alpha: 1.0)
|
||||
transition.setScale(view: self.attachmentButtonIcon, scale: 1.0)
|
||||
self.attachmentButtonIcon.isHidden = false
|
||||
transition.animateAlpha(view: self.attachmentButtonIcon, from: 0.0, to: 1.0)
|
||||
transition.animateScale(view: self.attachmentButtonIcon, from: 0.001, to: 1.0)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2692,8 +2693,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
|
||||
transition.updatePosition(layer: dotAnimationView.layer, position: self.attachmentButtonBackground.contentView.bounds.center)
|
||||
|
||||
self.attachmentButtonIcon.layer.opacity = 0.0
|
||||
self.attachmentButtonIcon.layer.transform = CATransform3DMakeScale(0.001, 0.001, 1.0)
|
||||
self.attachmentButtonIcon.isHidden = true
|
||||
dotAnimationView.playOnce(completion: { [weak self, weak dotAnimationView] in
|
||||
guard let self else {
|
||||
return
|
||||
@@ -2708,8 +2708,9 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
transition.setScale(view: dotAnimationView, scale: 0.001)
|
||||
}
|
||||
|
||||
transition.setAlpha(view: self.attachmentButtonIcon, alpha: 1.0)
|
||||
transition.setScale(view: self.attachmentButtonIcon, scale: 1.0)
|
||||
self.attachmentButtonIcon.isHidden = false
|
||||
transition.animateAlpha(view: self.attachmentButtonIcon, from: 0.0, to: 1.0)
|
||||
transition.animateScale(view: self.attachmentButtonIcon, from: 0.001, to: 1.0)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
@@ -4463,15 +4464,15 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
}
|
||||
|
||||
if mediaInputIsActive && !hideExpandMediaInput {
|
||||
if self.mediaActionButtons.expandMediaInputButton.alpha.isZero {
|
||||
self.mediaActionButtons.expandMediaInputButton.alpha = 1.0
|
||||
if self.mediaActionButtons.expandMediaInputButtonBackgroundView.alpha.isZero {
|
||||
self.mediaActionButtons.expandMediaInputButtonBackgroundView.alpha = 1.0
|
||||
if alphaTransition.isAnimated {
|
||||
self.mediaActionButtons.expandMediaInputButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.mediaActionButtons.expandMediaInputButtonBackgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !self.mediaActionButtons.expandMediaInputButton.alpha.isZero {
|
||||
alphaTransition.updateAlpha(layer: self.mediaActionButtons.expandMediaInputButton.layer, alpha: 0.0)
|
||||
if !self.mediaActionButtons.expandMediaInputButtonBackgroundView.alpha.isZero {
|
||||
alphaTransition.updateAlpha(layer: self.mediaActionButtons.expandMediaInputButtonBackgroundView.layer, alpha: 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -170,7 +170,7 @@ public final class ChatUserInfoItemNode: ListViewItemNode, ASGestureRecognizerDe
|
||||
self.disclaimerTextNode.textNode.isUserInteractionEnabled = false
|
||||
self.disclaimerTextNode.textNode.displaysAsynchronously = false
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: true, rotated: true)
|
||||
super.init(layerBacked: false, rotated: true)
|
||||
|
||||
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@ swift_library(
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
+57
-135
@@ -6,6 +6,9 @@ import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import AvatarNode
|
||||
import AccountContext
|
||||
import GlassBackgroundComponent
|
||||
import ComponentFlow
|
||||
import ComponentDisplayAdapters
|
||||
|
||||
public struct EditableTokenListToken {
|
||||
public enum Subject {
|
||||
@@ -26,23 +29,6 @@ public struct EditableTokenListToken {
|
||||
}
|
||||
}
|
||||
|
||||
private let caretIndicatorImage = generateVerticallyStretchableFilledCircleImage(radius: 1.0, color: UIColor(rgb: 0x3350ee))
|
||||
|
||||
private func caretAnimation() -> CAAnimation {
|
||||
let animation = CAKeyframeAnimation(keyPath: "opacity")
|
||||
animation.values = [1.0 as NSNumber, 0.0 as NSNumber, 1.0 as NSNumber, 1.0 as NSNumber]
|
||||
let firstDuration = 0.3
|
||||
let secondDuration = 0.25
|
||||
let restDuration = 0.35
|
||||
let duration = firstDuration + secondDuration + restDuration
|
||||
let keyTimes: [NSNumber] = [0.0 as NSNumber, (firstDuration / duration) as NSNumber, ((firstDuration + secondDuration) / duration) as NSNumber, ((firstDuration + secondDuration + restDuration) / duration) as NSNumber]
|
||||
|
||||
animation.keyTimes = keyTimes
|
||||
animation.duration = duration
|
||||
animation.repeatCount = Float.greatestFiniteMagnitude
|
||||
return animation
|
||||
}
|
||||
|
||||
private func generateRemoveIcon(_ color: UIColor) -> UIImage? {
|
||||
return generateImage(CGSize(width: 22.0, height: 22.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
@@ -61,35 +47,10 @@ private func generateRemoveIcon(_ color: UIColor) -> UIImage? {
|
||||
})
|
||||
}
|
||||
|
||||
public final class EditableTokenListNodeTheme {
|
||||
public let backgroundColor: UIColor
|
||||
public let separatorColor: UIColor
|
||||
public let placeholderTextColor: UIColor
|
||||
public let primaryTextColor: UIColor
|
||||
public let tokenBackgroundColor: UIColor
|
||||
public let selectedTextColor: UIColor
|
||||
public let selectedBackgroundColor: UIColor
|
||||
public let accentColor: UIColor
|
||||
public let keyboardColor: PresentationThemeKeyboardColor
|
||||
|
||||
public init(backgroundColor: UIColor, separatorColor: UIColor, placeholderTextColor: UIColor, primaryTextColor: UIColor, tokenBackgroundColor: UIColor, selectedTextColor: UIColor, selectedBackgroundColor: UIColor, accentColor: UIColor, keyboardColor: PresentationThemeKeyboardColor) {
|
||||
self.backgroundColor = backgroundColor
|
||||
self.separatorColor = separatorColor
|
||||
self.placeholderTextColor = placeholderTextColor
|
||||
self.primaryTextColor = primaryTextColor
|
||||
self.tokenBackgroundColor = tokenBackgroundColor
|
||||
self.selectedTextColor = selectedTextColor
|
||||
self.selectedBackgroundColor = selectedBackgroundColor
|
||||
self.accentColor = accentColor
|
||||
self.keyboardColor = keyboardColor
|
||||
}
|
||||
}
|
||||
|
||||
private final class TokenNode: ASDisplayNode {
|
||||
private let context: AccountContext
|
||||
private let presentationTheme: PresentationTheme
|
||||
private let theme: PresentationTheme
|
||||
|
||||
let theme: EditableTokenListNodeTheme
|
||||
let token: EditableTokenListToken
|
||||
let avatarNode: AvatarNode
|
||||
let categoryAvatarNode: ASImageNode
|
||||
@@ -98,23 +59,9 @@ private final class TokenNode: ASDisplayNode {
|
||||
let backgroundNode: ASImageNode
|
||||
let selectedBackgroundNode: ASImageNode
|
||||
var isSelected: Bool = false
|
||||
// didSet {
|
||||
// if self.isSelected != oldValue {
|
||||
// self.titleNode.attributedText = NSAttributedString(string: token.title, font: Font.regular(14.0), textColor: self.isSelected ? self.theme.selectedTextColor : self.theme.primaryTextColor)
|
||||
// self.titleNode.redrawIfPossible()
|
||||
// self.backgroundNode.isHidden = self.isSelected
|
||||
// self.selectedBackgroundNode.isHidden = !self.isSelected
|
||||
//
|
||||
// self.avatarNode.isHidden = self.isSelected
|
||||
// self.categoryAvatarNode.isHidden = self.isSelected
|
||||
// self.removeIconNode.isHidden = !self.isSelected
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
init(context: AccountContext, presentationTheme: PresentationTheme, theme: EditableTokenListNodeTheme, token: EditableTokenListToken, isSelected: Bool) {
|
||||
init(context: AccountContext, theme: PresentationTheme, token: EditableTokenListToken, isSelected: Bool) {
|
||||
self.context = context
|
||||
self.presentationTheme = presentationTheme
|
||||
self.theme = theme
|
||||
self.token = token
|
||||
self.titleNode = ASTextNode()
|
||||
@@ -131,39 +78,39 @@ private final class TokenNode: ASDisplayNode {
|
||||
self.removeIconNode.alpha = 0.0
|
||||
self.removeIconNode.displaysAsynchronously = false
|
||||
self.removeIconNode.displayWithoutProcessing = true
|
||||
self.removeIconNode.image = generateRemoveIcon(theme.selectedTextColor)
|
||||
self.removeIconNode.image = generateRemoveIcon(theme.list.itemCheckColors.foregroundColor)
|
||||
|
||||
let cornerRadius: CGFloat
|
||||
let cornerDiameter: CGFloat
|
||||
switch token.subject {
|
||||
case .peer:
|
||||
cornerRadius = 24.0
|
||||
cornerDiameter = 28.0
|
||||
case .category:
|
||||
cornerRadius = 14.0
|
||||
cornerDiameter = 14.0
|
||||
}
|
||||
|
||||
self.backgroundNode = ASImageNode()
|
||||
self.backgroundNode.displaysAsynchronously = false
|
||||
self.backgroundNode.displayWithoutProcessing = true
|
||||
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: cornerRadius, color: theme.tokenBackgroundColor)
|
||||
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: cornerDiameter, color: theme.list.itemCheckColors.strokeColor.withAlphaComponent(0.25))
|
||||
|
||||
self.selectedBackgroundNode = ASImageNode()
|
||||
self.selectedBackgroundNode.alpha = 0.0
|
||||
self.selectedBackgroundNode.displaysAsynchronously = false
|
||||
self.selectedBackgroundNode.displayWithoutProcessing = true
|
||||
self.selectedBackgroundNode.image = generateStretchableFilledCircleImage(diameter: cornerRadius, color: theme.selectedBackgroundColor)
|
||||
self.selectedBackgroundNode.image = generateStretchableFilledCircleImage(diameter: cornerDiameter, color: theme.list.itemCheckColors.fillColor)
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.selectedBackgroundNode)
|
||||
self.titleNode.attributedText = NSAttributedString(string: token.title, font: Font.regular(14.0), textColor: self.isSelected ? self.theme.selectedTextColor : self.theme.primaryTextColor)
|
||||
self.titleNode.attributedText = NSAttributedString(string: token.title, font: Font.regular(15.0), textColor: self.isSelected ? self.theme.list.itemCheckColors.foregroundColor : self.theme.list.itemPrimaryTextColor)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.removeIconNode)
|
||||
|
||||
switch token.subject {
|
||||
case let .peer(peer):
|
||||
self.addSubnode(self.avatarNode)
|
||||
self.avatarNode.setPeer(context: context, theme: presentationTheme, peer: peer)
|
||||
self.avatarNode.setPeer(context: context, theme: theme, peer: peer)
|
||||
case let .category(image):
|
||||
self.addSubnode(self.categoryAvatarNode)
|
||||
self.categoryAvatarNode.image = image
|
||||
@@ -182,9 +129,9 @@ private final class TokenNode: ASDisplayNode {
|
||||
if titleSize.width.isZero {
|
||||
return
|
||||
}
|
||||
self.backgroundNode.frame = self.bounds.insetBy(dx: 2.0, dy: 2.0)
|
||||
self.selectedBackgroundNode.frame = self.bounds.insetBy(dx: 2.0, dy: 2.0)
|
||||
self.avatarNode.frame = CGRect(origin: CGPoint(x: 3.0, y: 3.0), size: CGSize(width: 22.0, height: 22.0))
|
||||
self.backgroundNode.frame = self.bounds
|
||||
self.selectedBackgroundNode.frame = self.bounds
|
||||
self.avatarNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.height, height: self.bounds.height))
|
||||
self.categoryAvatarNode.frame = self.avatarNode.frame
|
||||
self.removeIconNode.frame = self.avatarNode.frame
|
||||
|
||||
@@ -243,91 +190,70 @@ private final class TokenNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
self.titleNode.attributedText = NSAttributedString(string: token.title, font: Font.regular(14.0), textColor: self.isSelected ? self.theme.selectedTextColor : self.theme.primaryTextColor)
|
||||
self.titleNode.attributedText = NSAttributedString(string: token.title, font: Font.regular(15.0), textColor: self.isSelected ? self.theme.list.itemCheckColors.foregroundColor : self.theme.list.itemPrimaryTextColor)
|
||||
self.titleNode.redrawIfPossible()
|
||||
}
|
||||
}
|
||||
|
||||
private final class CaretIndicatorNode: ASImageNode {
|
||||
override func willEnterHierarchy() {
|
||||
super.willEnterHierarchy()
|
||||
|
||||
if self.layer.animation(forKey: "blink") == nil {
|
||||
self.layer.add(caretAnimation(), forKey: "blink")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate {
|
||||
private let context: AccountContext
|
||||
private let presentationTheme: PresentationTheme
|
||||
private let theme: PresentationTheme
|
||||
|
||||
private let placeholder: String
|
||||
private let shortPlaceholder: String?
|
||||
|
||||
private let theme: EditableTokenListNodeTheme
|
||||
private let backgroundNode: NavigationBackgroundNode
|
||||
private let backgroundContainer: GlassBackgroundContainerView
|
||||
private let backgroundView: GlassBackgroundView
|
||||
private let scrollNode: ASScrollNode
|
||||
private let placeholderNode: ASTextNode
|
||||
private var tokenNodes: [TokenNode] = []
|
||||
private let separatorNode: ASDisplayNode
|
||||
private let textFieldScrollNode: ASScrollNode
|
||||
private let textFieldNode: TextFieldNode
|
||||
private let caretIndicatorNode: CaretIndicatorNode
|
||||
private var selectedTokenId: AnyHashable?
|
||||
|
||||
public var textUpdated: ((String) -> Void)?
|
||||
public var deleteToken: ((AnyHashable) -> Void)?
|
||||
public var textReturned: (() -> Void)?
|
||||
|
||||
public init(context: AccountContext, presentationTheme: PresentationTheme, theme: EditableTokenListNodeTheme, placeholder: String, shortPlaceholder: String? = nil) {
|
||||
public init(context: AccountContext, theme: PresentationTheme, placeholder: String, shortPlaceholder: String? = nil) {
|
||||
self.context = context
|
||||
self.presentationTheme = presentationTheme
|
||||
self.theme = theme
|
||||
|
||||
self.placeholder = placeholder
|
||||
self.shortPlaceholder = shortPlaceholder
|
||||
|
||||
self.backgroundNode = NavigationBackgroundNode(color: theme.backgroundColor)
|
||||
self.backgroundContainer = GlassBackgroundContainerView()
|
||||
self.backgroundView = GlassBackgroundView()
|
||||
self.backgroundContainer.contentView.addSubview(self.backgroundView)
|
||||
|
||||
self.scrollNode = ASScrollNode()
|
||||
self.scrollNode.view.alwaysBounceVertical = true
|
||||
self.scrollNode.view.alwaysBounceVertical = false
|
||||
self.scrollNode.clipsToBounds = true
|
||||
|
||||
self.placeholderNode = ASTextNode()
|
||||
self.placeholderNode.isUserInteractionEnabled = false
|
||||
self.placeholderNode.maximumNumberOfLines = 1
|
||||
self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(15.0), textColor: theme.placeholderTextColor)
|
||||
self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(15.0), textColor: theme.list.itemPlaceholderTextColor)
|
||||
|
||||
self.textFieldScrollNode = ASScrollNode()
|
||||
|
||||
self.textFieldNode = TextFieldNode()
|
||||
self.textFieldNode.textField.font = Font.regular(15.0)
|
||||
self.textFieldNode.textField.textColor = theme.primaryTextColor
|
||||
self.textFieldNode.textField.textColor = theme.list.itemPrimaryTextColor
|
||||
self.textFieldNode.textField.autocorrectionType = .no
|
||||
self.textFieldNode.textField.returnKeyType = .done
|
||||
self.textFieldNode.textField.keyboardAppearance = theme.keyboardColor.keyboardAppearance
|
||||
self.textFieldNode.textField.tintColor = theme.accentColor
|
||||
|
||||
self.caretIndicatorNode = CaretIndicatorNode()
|
||||
self.caretIndicatorNode.isLayerBacked = true
|
||||
self.caretIndicatorNode.displayWithoutProcessing = true
|
||||
self.caretIndicatorNode.displaysAsynchronously = false
|
||||
self.caretIndicatorNode.image = caretIndicatorImage
|
||||
|
||||
self.separatorNode = ASDisplayNode()
|
||||
self.separatorNode.isLayerBacked = true
|
||||
self.separatorNode.backgroundColor = theme.separatorColor
|
||||
self.textFieldNode.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
|
||||
self.textFieldNode.textField.tintColor = theme.list.itemAccentColor
|
||||
|
||||
super.init()
|
||||
self.addSubnode(self.backgroundNode)
|
||||
|
||||
self.view.addSubview(self.backgroundContainer)
|
||||
|
||||
self.addSubnode(self.scrollNode)
|
||||
|
||||
self.addSubnode(self.separatorNode)
|
||||
self.scrollNode.addSubnode(self.placeholderNode)
|
||||
self.scrollNode.addSubnode(self.textFieldScrollNode)
|
||||
self.textFieldScrollNode.addSubnode(self.textFieldNode)
|
||||
//self.scrollNode.addSubnode(self.caretIndicatorNode)
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.textFieldNode.textField.delegate = self
|
||||
self.textFieldNode.textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged)
|
||||
@@ -357,7 +283,7 @@ public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate {
|
||||
placeholderSnapshot = self.placeholderNode.layer.snapshotContentTreeAsView()
|
||||
placeholderSnapshot?.frame = self.placeholderNode.frame
|
||||
}
|
||||
self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(15.0), textColor: self.theme.placeholderTextColor)
|
||||
self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(15.0), textColor: self.theme.list.itemPlaceholderTextColor)
|
||||
}
|
||||
|
||||
for i in (0 ..< self.tokenNodes.count).reversed() {
|
||||
@@ -380,8 +306,7 @@ public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate {
|
||||
}
|
||||
|
||||
let sideInset: CGFloat = 12.0 + leftInset
|
||||
let verticalInset: CGFloat = 6.0
|
||||
|
||||
let verticalInset: CGFloat = 8.0
|
||||
|
||||
var animationDelay = 0.0
|
||||
var currentOffset = CGPoint(x: sideInset, y: verticalInset)
|
||||
@@ -398,7 +323,7 @@ public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate {
|
||||
if let currentNode = currentNode {
|
||||
tokenNode = currentNode
|
||||
} else {
|
||||
tokenNode = TokenNode(context: self.context, presentationTheme: self.presentationTheme, theme: self.theme, token: token, isSelected: self.selectedTokenId != nil && token.id == self.selectedTokenId!)
|
||||
tokenNode = TokenNode(context: self.context, theme: self.theme, token: token, isSelected: self.selectedTokenId != nil && token.id == self.selectedTokenId!)
|
||||
self.tokenNodes.append(tokenNode)
|
||||
self.scrollNode.addSubnode(tokenNode)
|
||||
animateIn = true
|
||||
@@ -407,10 +332,10 @@ public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate {
|
||||
let tokenSize = tokenNode.measure(CGSize(width: max(1.0, width - sideInset - sideInset), height: CGFloat.greatestFiniteMagnitude))
|
||||
if tokenSize.width + currentOffset.x >= width - sideInset && !currentOffset.x.isEqual(to: sideInset) {
|
||||
currentOffset.x = sideInset
|
||||
currentOffset.y += tokenSize.height
|
||||
currentOffset.y += tokenSize.height + 6.0
|
||||
}
|
||||
let tokenFrame = CGRect(origin: CGPoint(x: currentOffset.x, y: currentOffset.y), size: tokenSize)
|
||||
currentOffset.x += ceil(tokenSize.width)
|
||||
currentOffset.x += ceil(tokenSize.width) + 6.0
|
||||
|
||||
if animateIn {
|
||||
tokenNode.frame = tokenFrame
|
||||
@@ -451,7 +376,7 @@ public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate {
|
||||
|
||||
let placeholderSize = self.placeholderNode.measure(CGSize(width: max(1.0, width - sideInset - sideInset), height: CGFloat.greatestFiniteMagnitude))
|
||||
if width - currentOffset.x < placeholderSize.width {
|
||||
currentOffset.y += 28.0
|
||||
currentOffset.y += 28.0 + 6.0
|
||||
currentOffset.x = sideInset
|
||||
}
|
||||
|
||||
@@ -472,42 +397,45 @@ public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate {
|
||||
}
|
||||
|
||||
let textNodeFrame = CGRect(origin: CGPoint(x: currentOffset.x + 4.0, y: currentOffset.y + UIScreenPixel), size: CGSize(width: width - currentOffset.x - sideInset - 8.0, height: 28.0))
|
||||
let caretNodeFrame = CGRect(origin: CGPoint(x: textNodeFrame.minX, y: textNodeFrame.minY + 4.0 - UIScreenPixel), size: CGSize(width: 2.0, height: 19.0 + UIScreenPixel))
|
||||
if case .immediate = transition {
|
||||
transition.updateFrame(node: self.textFieldScrollNode, frame: textNodeFrame)
|
||||
transition.updateFrame(node: self.textFieldNode, frame: CGRect(origin: CGPoint(), size: textNodeFrame.size))
|
||||
transition.updateFrame(node: self.caretIndicatorNode, frame: caretNodeFrame)
|
||||
} else {
|
||||
let previousFrame = self.textFieldScrollNode.frame
|
||||
self.textFieldScrollNode.frame = textNodeFrame
|
||||
self.textFieldScrollNode.layer.animateFrame(from: previousFrame, to: textNodeFrame, duration: 0.2 + animationDelay, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
|
||||
transition.updateFrame(node: self.textFieldNode, frame: CGRect(origin: CGPoint(), size: textNodeFrame.size))
|
||||
|
||||
let previousCaretFrame = self.caretIndicatorNode.frame
|
||||
self.caretIndicatorNode.frame = caretNodeFrame
|
||||
self.caretIndicatorNode.layer.animateFrame(from: previousCaretFrame, to: caretNodeFrame, duration: 0.2 + animationDelay, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
|
||||
let previousContentHeight = self.scrollNode.view.contentSize.height
|
||||
let contentHeight = currentOffset.y + 29.0 + verticalInset
|
||||
let contentHeight = currentOffset.y + 28.0 + verticalInset
|
||||
let nodeHeight = min(contentHeight, 110.0)
|
||||
|
||||
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel)))
|
||||
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: nodeHeight)))
|
||||
transition.updateCornerRadius(node: self.scrollNode, cornerRadius: min(44.0, nodeHeight) * 0.5)
|
||||
self.scrollNode.view.scrollIndicatorInsets = UIEdgeInsets(top: 16.0, left: 0.0, bottom: 16.0, right: 0.0)
|
||||
|
||||
if !abs(previousContentHeight - contentHeight).isLess(than: CGFloat.ulpOfOne) {
|
||||
let contentOffset = CGPoint(x: 0.0, y: max(0.0, contentHeight - nodeHeight))
|
||||
if case .immediate = transition {
|
||||
self.scrollNode.view.contentOffset = contentOffset
|
||||
} else {
|
||||
transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: contentOffset.y), size: self.scrollNode.bounds.size))
|
||||
if self.scrollNode.view.contentOffset != contentOffset {
|
||||
if case .immediate = transition {
|
||||
self.scrollNode.view.contentOffset = contentOffset
|
||||
} else {
|
||||
//transition.animateOffsetAdditive(node: self.scrollNode, offset: self.scrollNode.view.contentOffset.y - contentOffset.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.scrollNode.view.contentSize = CGSize(width: width, height: contentHeight)
|
||||
if self.scrollNode.view.contentSize != CGSize(width: width, height: contentHeight) {
|
||||
self.scrollNode.view.contentSize = CGSize(width: width, height: contentHeight)
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: nodeHeight)))
|
||||
self.backgroundNode.update(size: self.backgroundNode.bounds.size, transition: transition)
|
||||
let backgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: nodeHeight))
|
||||
self.backgroundContainer.update(size: backgroundFrame.size, isDark: self.theme.overallDarkAppearance, transition: ComponentTransition(transition))
|
||||
transition.updateFrame(view: self.backgroundContainer, frame: backgroundFrame)
|
||||
|
||||
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: min(44.0, backgroundFrame.height) * 0.5, isDark: self.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: self.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: ComponentTransition(transition))
|
||||
transition.updateFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
|
||||
|
||||
return nodeHeight
|
||||
}
|
||||
@@ -528,15 +456,9 @@ public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate {
|
||||
}
|
||||
|
||||
public func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
/*if self.caretIndicatorNode.supernode == self {
|
||||
self.caretIndicatorNode.removeFromSupernode()
|
||||
}*/
|
||||
}
|
||||
|
||||
public func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
/*if self.caretIndicatorNode.supernode != self.scrollNode {
|
||||
self.scrollNode.addSubnode(self.caretIndicatorNode)
|
||||
}*/
|
||||
}
|
||||
|
||||
public func setText(_ text: String) {
|
||||
|
||||
@@ -21,8 +21,9 @@ swift_library(
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BalancedTextComponent",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/TelegramUI/Components/TextFieldComponent",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
+86
-376
@@ -13,399 +13,109 @@ import BalancedTextComponent
|
||||
import TextFieldComponent
|
||||
import ComponentDisplayAdapters
|
||||
import TextFormat
|
||||
import ComponentFlow
|
||||
import AlertComponent
|
||||
import AlertMultilineInputFieldComponent
|
||||
|
||||
private final class FactCheckAlertContentNode: AlertContentNode {
|
||||
private let context: AccountContext
|
||||
private var theme: AlertControllerTheme
|
||||
private var presentationTheme: PresentationTheme
|
||||
private let strings: PresentationStrings
|
||||
private let text: String
|
||||
private let initialValue: String
|
||||
|
||||
private let titleView = ComponentView<Empty>()
|
||||
public func factCheckAlertController(
|
||||
context: AccountContext,
|
||||
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
|
||||
value: String,
|
||||
entities: [MessageTextEntity],
|
||||
apply: @escaping (String, [MessageTextEntity]) -> Void
|
||||
) -> ViewController {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let strings = presentationData.strings
|
||||
|
||||
let inputState = AlertMultilineInputFieldComponent.ExternalState()
|
||||
|
||||
private let state = ComponentState()
|
||||
|
||||
private let inputBackgroundNode = ASImageNode()
|
||||
private let inputField = ComponentView<Empty>()
|
||||
private let inputFieldExternalState = TextFieldComponent.ExternalState()
|
||||
private let inputPlaceholderView = ComponentView<Empty>()
|
||||
|
||||
private let actionNodesSeparator: ASDisplayNode
|
||||
private let actionNodes: [TextAlertContentActionNode]
|
||||
private let actionVerticalSeparators: [ASDisplayNode]
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
var present: (ViewController) -> () = { _ in }
|
||||
|
||||
var complete: (() -> Void)? {
|
||||
didSet {
|
||||
// self.inputFieldNode.complete = self.complete
|
||||
let doneIsEnabled: Signal<Bool, NoError>
|
||||
if !value.isEmpty {
|
||||
doneIsEnabled = .single(true)
|
||||
} else {
|
||||
doneIsEnabled = inputState.valueSignal
|
||||
|> map { value in
|
||||
return !value.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
override var dismissOnOutsideTap: Bool {
|
||||
return self.isUserInteractionEnabled
|
||||
var characterLimit: Int = 1024
|
||||
if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["factcheck_length_limit"] as? Double {
|
||||
characterLimit = Int(value)
|
||||
}
|
||||
|
||||
init(context: AccountContext, theme: AlertControllerTheme, presentationTheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], text: String, value: String, entities: [MessageTextEntity]) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.presentationTheme = presentationTheme
|
||||
self.strings = strings
|
||||
self.text = text
|
||||
self.initialValue = value
|
||||
|
||||
if !value.isEmpty {
|
||||
self.inputFieldExternalState.initialText = chatInputStateStringWithAppliedEntities(value, entities: entities)
|
||||
}
|
||||
|
||||
self.actionNodesSeparator = ASDisplayNode()
|
||||
self.actionNodesSeparator.isLayerBacked = true
|
||||
|
||||
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
|
||||
return TextAlertContentActionNode(theme: theme, action: action)
|
||||
}
|
||||
|
||||
var actionVerticalSeparators: [ASDisplayNode] = []
|
||||
if actions.count > 1 {
|
||||
for _ in 0 ..< actions.count - 1 {
|
||||
let separatorNode = ASDisplayNode()
|
||||
separatorNode.isLayerBacked = true
|
||||
actionVerticalSeparators.append(separatorNode)
|
||||
}
|
||||
}
|
||||
self.actionVerticalSeparators = actionVerticalSeparators
|
||||
|
||||
super.init()
|
||||
|
||||
self.inputBackgroundNode.displaysAsynchronously = false
|
||||
self.inputBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 16.0, color: presentationTheme.actionSheet.inputHollowBackgroundColor, strokeColor: presentationTheme.actionSheet.inputBorderColor, strokeWidth: UIScreenPixel)
|
||||
|
||||
self.addSubnode(self.actionNodesSeparator)
|
||||
self.addSubnode(self.inputBackgroundNode)
|
||||
|
||||
for actionNode in self.actionNodes {
|
||||
self.addSubnode(actionNode)
|
||||
}
|
||||
self.actionNodes.last?.actionEnabled = true
|
||||
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
|
||||
self.updateTheme(theme)
|
||||
|
||||
self.state._updated = { [weak self] transition, _ in
|
||||
guard let self, let _ = self.validLayout else {
|
||||
return
|
||||
}
|
||||
self.requestLayout?(transition.containedViewLayoutTransition)
|
||||
}
|
||||
}
|
||||
let initialValue = chatInputStateStringWithAppliedEntities(value, entities: entities)
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
}
|
||||
|
||||
var textAndEntities: (String, [MessageTextEntity]) {
|
||||
let text = self.inputFieldExternalState.text.string
|
||||
let entities = generateChatInputTextEntities(self.inputFieldExternalState.text)
|
||||
return (text, entities)
|
||||
}
|
||||
|
||||
override func updateTheme(_ theme: AlertControllerTheme) {
|
||||
self.theme = theme
|
||||
|
||||
self.actionNodesSeparator.backgroundColor = theme.separatorColor
|
||||
for actionNode in self.actionNodes {
|
||||
actionNode.updateTheme(theme)
|
||||
}
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
separatorNode.backgroundColor = theme.separatorColor
|
||||
}
|
||||
|
||||
if let size = self.validLayout {
|
||||
_ = self.updateLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var size = size
|
||||
size.width = min(size.width, 270.0)
|
||||
let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)
|
||||
|
||||
let hadValidLayout = self.validLayout != nil
|
||||
|
||||
self.validLayout = size
|
||||
|
||||
var origin: CGPoint = CGPoint(x: 0.0, y: 16.0)
|
||||
let spacing: CGFloat = 5.0
|
||||
|
||||
|
||||
let titleSize = self.titleView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: self.text, font: Font.semibold(17.0), textColor: self.theme.primaryColor)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: measureSize.width, height: 1000.0)
|
||||
var presentImpl: ((ViewController) -> Void)?
|
||||
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "title",
|
||||
component: AnyComponent(
|
||||
AlertTitleComponent(title: strings.FactCheck_Title)
|
||||
)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: origin.y), size: titleSize)
|
||||
if let titleComponentView = self.titleView.view {
|
||||
if titleComponentView.superview == nil {
|
||||
self.view.addSubview(titleComponentView)
|
||||
}
|
||||
titleComponentView.frame = titleFrame
|
||||
}
|
||||
origin.y += titleSize.height + 17.0
|
||||
|
||||
let actionButtonHeight: CGFloat = 44.0
|
||||
var minActionsWidth: CGFloat = 0.0
|
||||
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
|
||||
let actionTitleInsets: CGFloat = 8.0
|
||||
|
||||
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
|
||||
for actionNode in self.actionNodes {
|
||||
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
|
||||
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
|
||||
effectiveActionLayout = .vertical
|
||||
}
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
minActionsWidth += actionTitleSize.width + actionTitleInsets
|
||||
case .vertical:
|
||||
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
|
||||
}
|
||||
}
|
||||
|
||||
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0)
|
||||
|
||||
var contentWidth = max(titleSize.width, minActionsWidth)
|
||||
contentWidth = max(contentWidth, 234.0)
|
||||
|
||||
var actionsHeight: CGFloat = 0.0
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionsHeight = actionButtonHeight
|
||||
case .vertical:
|
||||
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
|
||||
}
|
||||
|
||||
let resultWidth = contentWidth + insets.left + insets.right
|
||||
|
||||
let inputInset: CGFloat = 16.0
|
||||
let inputWidth = resultWidth - inputInset * 2.0
|
||||
|
||||
var characterLimit: Int = 1024
|
||||
if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["factcheck_length_limit"] as? Double {
|
||||
characterLimit = Int(value)
|
||||
}
|
||||
|
||||
let inputFieldSize = self.inputField.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(TextFieldComponent(
|
||||
context: self.context,
|
||||
theme: self.presentationTheme,
|
||||
strings: self.strings,
|
||||
externalState: self.inputFieldExternalState,
|
||||
fontSize: 14.0,
|
||||
textColor: self.presentationTheme.actionSheet.inputTextColor,
|
||||
accentColor: self.presentationTheme.actionSheet.controlAccentColor,
|
||||
insets: UIEdgeInsets(top: 8.0, left: 2.0, bottom: 8.0, right: 2.0),
|
||||
hideKeyboard: false,
|
||||
customInputView: nil,
|
||||
resetText: nil,
|
||||
isOneLineWhenUnfocused: false,
|
||||
))
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "input",
|
||||
component: AnyComponent(
|
||||
AlertMultilineInputFieldComponent(
|
||||
context: context,
|
||||
initialValue: initialValue,
|
||||
placeholder: strings.FactCheck_Placeholder,
|
||||
characterLimit: characterLimit,
|
||||
formatMenuAvailability: .available([.bold, .italic]),
|
||||
emptyLineHandling: .oneConsecutive,
|
||||
formatMenuAvailability: .available([.bold, .italic, .link]),
|
||||
returnKeyType: .default,
|
||||
lockedFormatAction: {
|
||||
},
|
||||
present: { [weak self] c in
|
||||
self?.present(c)
|
||||
},
|
||||
paste: { _ in
|
||||
},
|
||||
returnKeyAction: nil,
|
||||
backspaceKeyAction: nil
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: inputWidth, height: 270.0)
|
||||
)
|
||||
self.inputField.parentState = self.state
|
||||
let inputFieldFrame = CGRect(origin: CGPoint(x: inputInset, y: origin.y), size: inputFieldSize)
|
||||
if let inputFieldView = self.inputField.view as? TextFieldComponent.View {
|
||||
if inputFieldView.superview == nil {
|
||||
self.view.addSubview(inputFieldView)
|
||||
}
|
||||
transition.updateFrame(view: inputFieldView, frame: inputFieldFrame)
|
||||
transition.updateFrame(node: self.inputBackgroundNode, frame: inputFieldFrame)
|
||||
|
||||
if !hadValidLayout {
|
||||
inputFieldView.activateInput()
|
||||
}
|
||||
}
|
||||
|
||||
let inputPlaceholderSize = self.inputPlaceholderView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(text: .plain(NSAttributedString(
|
||||
string: self.strings.FactCheck_Placeholder,
|
||||
font: Font.regular(14.0),
|
||||
textColor: self.presentationTheme.actionSheet.inputPlaceholderColor
|
||||
)))
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: inputWidth, height: 240.0)
|
||||
)
|
||||
let inputPlaceholderFrame = CGRect(origin: CGPoint(x: inputInset + 10.0, y: floorToScreenPixels(inputFieldFrame.midY - inputPlaceholderSize.height / 2.0)), size: inputPlaceholderSize)
|
||||
if let inputPlaceholderView = self.inputPlaceholderView.view {
|
||||
if inputPlaceholderView.superview == nil {
|
||||
inputPlaceholderView.isUserInteractionEnabled = false
|
||||
self.view.addSubview(inputPlaceholderView)
|
||||
}
|
||||
inputPlaceholderView.frame = inputPlaceholderFrame
|
||||
inputPlaceholderView.isHidden = self.inputFieldExternalState.hasText
|
||||
}
|
||||
|
||||
if let lastActionNode = self.actionNodes.last {
|
||||
if self.initialValue.isEmpty {
|
||||
lastActionNode.actionEnabled = self.inputFieldExternalState.hasText
|
||||
} else {
|
||||
if self.inputFieldExternalState.hasText {
|
||||
lastActionNode.action = TextAlertAction(
|
||||
type: .defaultAction,
|
||||
title: self.strings.Common_Done,
|
||||
action: lastActionNode.action.action
|
||||
)
|
||||
} else {
|
||||
lastActionNode.action = TextAlertAction(
|
||||
type: .defaultDestructiveAction,
|
||||
title: self.strings.FactCheck_Remove,
|
||||
action: lastActionNode.action.action
|
||||
)
|
||||
isInitiallyFocused: true,
|
||||
externalState: inputState,
|
||||
present: { c in
|
||||
presentImpl?(c)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
))
|
||||
|
||||
let doneIsRemove: Signal<Bool, NoError>
|
||||
if !value.isEmpty {
|
||||
doneIsRemove = inputState.valueSignal
|
||||
|> map { value in
|
||||
return value.string.isEmpty
|
||||
}
|
||||
|
||||
let resultSize = CGSize(width: resultWidth, height: titleSize.height + spacing + inputFieldSize.height + 17.0 + actionsHeight + insets.top + insets.bottom)
|
||||
|
||||
transition.updateFrame(node: 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]
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
|
||||
case .vertical:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
}
|
||||
}
|
||||
separatorIndex += 1
|
||||
|
||||
let currentActionWidth: CGFloat
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
if nodeIndex == self.actionNodes.count - 1 {
|
||||
currentActionWidth = resultSize.width - actionOffset
|
||||
} else {
|
||||
currentActionWidth = actionWidth
|
||||
}
|
||||
case .vertical:
|
||||
currentActionWidth = resultSize.width
|
||||
}
|
||||
|
||||
let actionNodeFrame: CGRect
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += currentActionWidth
|
||||
case .vertical:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += actionButtonHeight
|
||||
}
|
||||
|
||||
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
|
||||
|
||||
nodeIndex += 1
|
||||
}
|
||||
|
||||
return resultSize
|
||||
|> distinctUntilChanged
|
||||
} else {
|
||||
doneIsRemove = .single(false)
|
||||
}
|
||||
|
||||
func deactivateInput() {
|
||||
if let inputFieldView = self.inputField.view as? TextFieldComponent.View {
|
||||
inputFieldView.deactivateInput()
|
||||
}
|
||||
let actionsSignal: Signal<[AlertScreen.Action], NoError> = doneIsRemove
|
||||
|> map { doneIsRemove in
|
||||
var actions: [AlertScreen.Action] = []
|
||||
actions.append(.init(title: strings.Common_Cancel))
|
||||
|
||||
let doneTitle: String = doneIsRemove ? strings.FactCheck_Remove : strings.Common_Done
|
||||
let doneType: AlertScreen.Action.ActionType = doneIsRemove ? .defaultDestructive : .default
|
||||
actions.append(
|
||||
.init(id: "done", title: doneTitle, type: doneType, action: {
|
||||
let (text, entities) = inputState.textAndEntities
|
||||
apply(text, entities)
|
||||
}, isEnabled: doneIsEnabled)
|
||||
)
|
||||
|
||||
return actions
|
||||
}
|
||||
|
||||
func animateError() {
|
||||
if let inputFieldView = self.inputField.view as? TextFieldComponent.View {
|
||||
inputFieldView.layer.addShakeAnimation()
|
||||
}
|
||||
var effectiveUpdatedPresentationData: (PresentationData, Signal<PresentationData, NoError>)
|
||||
if let updatedPresentationData {
|
||||
effectiveUpdatedPresentationData = updatedPresentationData
|
||||
} else {
|
||||
effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData)
|
||||
}
|
||||
|
||||
self.hapticFeedback.error()
|
||||
let alertController = AlertScreen(
|
||||
configuration: AlertScreen.Configuration(allowInputInset: true),
|
||||
contentSignal: .single(content),
|
||||
actionsSignal: actionsSignal,
|
||||
updatedPresentationData: effectiveUpdatedPresentationData
|
||||
)
|
||||
presentImpl = { [weak alertController] c in
|
||||
alertController?.present(c, in: .window(.root))
|
||||
}
|
||||
}
|
||||
|
||||
public func factCheckAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, value: String, entities: [MessageTextEntity], apply: @escaping (String, [MessageTextEntity]) -> Void) -> AlertController {
|
||||
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
var dismissImpl: ((Bool) -> Void)?
|
||||
var applyImpl: (() -> Void)?
|
||||
|
||||
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
dismissImpl?(true)
|
||||
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: {
|
||||
dismissImpl?(true)
|
||||
applyImpl?()
|
||||
})]
|
||||
|
||||
let contentNode = FactCheckAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), presentationTheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: presentationData.strings.FactCheck_Title, value: value, entities: entities)
|
||||
contentNode.complete = {
|
||||
applyImpl?()
|
||||
}
|
||||
applyImpl = { [weak contentNode] in
|
||||
guard let contentNode = contentNode else {
|
||||
return
|
||||
}
|
||||
let (text, entities) = contentNode.textAndEntities
|
||||
apply(text, entities)
|
||||
}
|
||||
|
||||
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
|
||||
let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller] presentationData in
|
||||
controller?.theme = AlertControllerTheme(presentationData: presentationData)
|
||||
})
|
||||
controller.dismissed = { _ in
|
||||
presentationDataDisposable.dispose()
|
||||
}
|
||||
dismissImpl = { [weak controller] animated in
|
||||
contentNode.deactivateInput()
|
||||
if animated {
|
||||
controller?.dismissAnimated()
|
||||
} else {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
contentNode.present = { [weak controller] c in
|
||||
controller?.present(c, in: .window(.root))
|
||||
}
|
||||
|
||||
return controller
|
||||
return alertController
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/AnimationCache",
|
||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer",
|
||||
"//submodules/TelegramUI/Components/Chat/AccessoryPanelNode",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
+42
-18
@@ -20,6 +20,8 @@ import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
import AccessoryPanelNode
|
||||
import AppBundle
|
||||
import ComponentFlow
|
||||
import AlertComponent
|
||||
|
||||
func textStringForForwardedMessage(_ message: Message, strings: PresentationStrings) -> (text: String, entities: [MessageTextEntity], isMedia: Bool) {
|
||||
for media in message.media {
|
||||
@@ -378,26 +380,48 @@ public final class ForwardAccessoryPanelNode: AccessoryPanelNode {
|
||||
string = self.strings.Conversation_ForwardOptions_Text(messages, peerDisplayTitle)
|
||||
}
|
||||
|
||||
let font = Font.regular(floor(self.fontSize.baseDisplaySize * 15.0 / 17.0))
|
||||
let boldFont = Font.semibold(floor(self.fontSize.baseDisplaySize * 15.0 / 17.0))
|
||||
let body = MarkdownAttributeSet(font: font, textColor: self.theme.actionSheet.secondaryTextColor)
|
||||
let bold = MarkdownAttributeSet(font: boldFont, textColor: self.theme.actionSheet.secondaryTextColor)
|
||||
let font = Font.regular(15.0)
|
||||
let boldFont = Font.semibold(15.0)
|
||||
let body = MarkdownAttributeSet(font: font, textColor: self.theme.actionSheet.primaryTextColor)
|
||||
let bold = MarkdownAttributeSet(font: boldFont, textColor: self.theme.actionSheet.primaryTextColor)
|
||||
let text = addAttributesToStringWithRanges(string._tuple, body: body, argumentAttributes: [0: bold, 1: bold], textAlignment: .natural)
|
||||
|
||||
let title = NSAttributedString(string: self.strings.Conversation_ForwardOptions_Title(messageCount), font: Font.semibold(floor(self.fontSize.baseDisplaySize)), textColor: self.theme.actionSheet.primaryTextColor, paragraphAlignment: .center)
|
||||
let text = addAttributesToStringWithRanges(string._tuple, body: body, argumentAttributes: [0: bold, 1: bold], textAlignment: .center)
|
||||
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "title",
|
||||
component: AnyComponent(
|
||||
AlertTitleComponent(
|
||||
title: strings.Conversation_ForwardOptions_Title(messageCount)
|
||||
)
|
||||
)
|
||||
))
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "text",
|
||||
component: AnyComponent(
|
||||
AlertTextComponent(content: .attributed(text))
|
||||
)
|
||||
))
|
||||
|
||||
let alertController = richTextAlertController(context: self.context, title: title, text: text, actions: [TextAlertAction(type: .genericAction, title: self.strings.Conversation_ForwardOptions_ShowOptions, action: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.interfaceInteraction?.presentForwardOptions(strongSelf.view)
|
||||
Queue.mainQueue().after(0.5) {
|
||||
strongSelf.updateThemeAndStrings(theme: strongSelf.theme, strings: strongSelf.strings, forwardOptionsState: strongSelf.forwardOptionsState, force: true)
|
||||
}
|
||||
|
||||
let _ = ApplicationSpecificNotice.incrementChatForwardOptionsTip(accountManager: strongSelf.context.sharedContext.accountManager, count: 3).start()
|
||||
}
|
||||
}), TextAlertAction(type: .destructiveAction, title: self.strings.Conversation_ForwardOptions_CancelForwarding, action: { [weak self] in
|
||||
self?.dismiss?()
|
||||
})], actionLayout: .vertical)
|
||||
let alertController = AlertScreen(
|
||||
context: context,
|
||||
configuration: AlertScreen.Configuration(actionAlignment: .vertical),
|
||||
content: content,
|
||||
actions: [
|
||||
.init(title: strings.Conversation_ForwardOptions_ShowOptions, action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.interfaceInteraction?.presentForwardOptions(self.view)
|
||||
Queue.mainQueue().after(0.5) {
|
||||
self.updateThemeAndStrings(theme: self.theme, strings: self.strings, forwardOptionsState: self.forwardOptionsState, force: true)
|
||||
}
|
||||
let _ = ApplicationSpecificNotice.incrementChatForwardOptionsTip(accountManager: self.context.sharedContext.accountManager, count: 3).start()
|
||||
}),
|
||||
.init(title: strings.Conversation_ForwardOptions_CancelForwarding, type: .destructive, action: { [weak self] in
|
||||
self?.dismiss?()
|
||||
})
|
||||
]
|
||||
)
|
||||
self.interfaceInteraction?.presentController(alertController, nil)
|
||||
}
|
||||
|
||||
|
||||
+10
-1
@@ -100,7 +100,7 @@ public struct InteractiveEmojiConfiguration {
|
||||
}
|
||||
|
||||
public static func with(appConfiguration: AppConfiguration) -> InteractiveEmojiConfiguration {
|
||||
if let data = appConfiguration.data, let emojis = data["emojies_send_dice"] as? [String] {
|
||||
if let data = appConfiguration.data, var emojis = data["emojies_send_dice"] as? [String] {
|
||||
var successParameters: [String: InteractiveEmojiSuccessParameters] = [:]
|
||||
if let success = data["emojies_send_dice_success"] as? [String: [String: Double]] {
|
||||
for (key, dict) in success {
|
||||
@@ -109,6 +109,11 @@ public struct InteractiveEmojiConfiguration {
|
||||
}
|
||||
}
|
||||
}
|
||||
#if DEBUG
|
||||
if !emojis.contains("🎲") {
|
||||
emojis.append("🎲")
|
||||
}
|
||||
#endif
|
||||
return InteractiveEmojiConfiguration(emojis: emojis, successParameters: successParameters)
|
||||
} else {
|
||||
return .defaultValue
|
||||
@@ -126,6 +131,10 @@ public final class ManagedDiceAnimationNode: ManagedAnimationNode {
|
||||
private let configuration = Promise<InteractiveEmojiConfiguration?>()
|
||||
private let emojis = Promise<[TelegramMediaFile]>()
|
||||
|
||||
public var isRolling: Bool {
|
||||
return self.diceState == .rolling
|
||||
}
|
||||
|
||||
public var success: (() -> Void)?
|
||||
|
||||
public init(context: AccountContext, emoji: String) {
|
||||
|
||||
+2
-2
@@ -93,7 +93,7 @@ public final class ShimmeringLinkNode: ASDisplayNode {
|
||||
self.shimmerEffectNode.updateAbsoluteRect(CGRect(origin: .zero, size: size), within: size)
|
||||
self.borderShimmerEffectNode.updateAbsoluteRect(CGRect(origin: .zero, size: size), within: size)
|
||||
|
||||
self.shimmerEffectNode.update(backgroundColor: .clear, foregroundColor: self.color.withAlphaComponent(min(1.0, self.color.alpha * 1.2)), horizontal: true, effectSize: nil, globalTimeOffset: false, duration: nil)
|
||||
self.borderShimmerEffectNode.update(backgroundColor: .clear, foregroundColor: self.color.withAlphaComponent(min(1.0, self.color.alpha * 1.5)), horizontal: true, effectSize: nil, globalTimeOffset: false, duration: nil)
|
||||
self.shimmerEffectNode.update(backgroundColor: .clear, foregroundColor: self.color.withMultipliedAlpha(1.75), horizontal: true, effectSize: nil, globalTimeOffset: false, duration: nil)
|
||||
self.borderShimmerEffectNode.update(backgroundColor: .clear, foregroundColor: self.color.withMultipliedAlpha(2.0), horizontal: true, effectSize: nil, globalTimeOffset: false, duration: nil)
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -313,6 +313,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
||||
public var enableFullTranslucency: Bool = true
|
||||
public var chatIsRotated: Bool = true
|
||||
public var canReadHistory: Bool = false
|
||||
public var summarizedMessageIds: Set<MessageId> = Set()
|
||||
|
||||
|
||||
private var isOpeningMediaValue: Bool = false
|
||||
public var isOpeningMedia: Bool {
|
||||
|
||||
+8
-7
@@ -33,6 +33,8 @@ import LegacyMessageInputPanelInputView
|
||||
import AttachmentTextInputPanelNode
|
||||
import GlassBackgroundComponent
|
||||
|
||||
private let keyboardCornerRadius: CGFloat = 30.0
|
||||
|
||||
public final class EmptyInputView: UIView, UIInputViewAudioFeedback {
|
||||
public var enableInputClicksWhenVisible: Bool {
|
||||
return true
|
||||
@@ -490,7 +492,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
|
||||
self.clippingView = UIView()
|
||||
self.clippingView.clipsToBounds = true
|
||||
self.clippingView.layer.cornerRadius = 20.0
|
||||
self.clippingView.layer.cornerRadius = keyboardCornerRadius
|
||||
|
||||
self.entityKeyboardView = ComponentHostView<Empty>()
|
||||
|
||||
@@ -1904,8 +1906,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
theme: interfaceState.theme,
|
||||
strings: interfaceState.strings,
|
||||
isContentInFocus: isVisible,
|
||||
containerInsets: UIEdgeInsets(top: self.isEmojiSearchActive ? -34.0 : 0.0, left: leftInset, bottom: keyboardBottomInset, right: rightInset),
|
||||
topPanelInsets: UIEdgeInsets(),
|
||||
containerInsets: UIEdgeInsets(top: self.isEmojiSearchActive ? -42.0 : 0.0, left: leftInset, bottom: keyboardBottomInset, right: rightInset),
|
||||
topPanelInsets: UIEdgeInsets(top: 0.0, left: 5.0, bottom: 0.0, right: 5.0),
|
||||
emojiContent: emojiContent,
|
||||
stickerContent: stickerContent,
|
||||
maskContent: nil,
|
||||
@@ -2030,16 +2032,16 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
backgroundFrame.size.height += 32.0
|
||||
|
||||
if backgroundChromeView.image == nil {
|
||||
backgroundChromeView.image = GlassBackgroundView.generateForegroundImage(size: CGSize(width: 20.0 * 2.0, height: 20.0 * 2.0), isDark: interfaceState.theme.overallDarkAppearance, fillColor: .clear)
|
||||
backgroundChromeView.image = GlassBackgroundView.generateForegroundImage(size: CGSize(width: keyboardCornerRadius * 2.0, height: keyboardCornerRadius * 2.0), isDark: interfaceState.theme.overallDarkAppearance, fillColor: .clear)
|
||||
}
|
||||
if backgroundTintView.image == nil {
|
||||
backgroundTintView.image = generateStretchableFilledCircleImage(diameter: 20.0 * 2.0, color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||
backgroundTintView.image = generateStretchableFilledCircleImage(diameter: keyboardCornerRadius * 2.0, color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
backgroundTintView.tintColor = interfaceState.theme.chat.inputMediaPanel.backgroundColor
|
||||
|
||||
transition.updateFrame(view: backgroundView, frame: backgroundFrame)
|
||||
backgroundView.updateColor(color: .clear, forceKeepBlur: true, transition: .immediate)
|
||||
backgroundView.update(size: backgroundFrame.size, cornerRadius: 20.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition)
|
||||
backgroundView.update(size: backgroundFrame.size, cornerRadius: keyboardCornerRadius, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition)
|
||||
|
||||
transition.updateFrame(view: backgroundChromeView, frame: backgroundFrame.insetBy(dx: -1.0, dy: 0.0))
|
||||
|
||||
@@ -2666,7 +2668,6 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi
|
||||
pendingUnpinnedAllMessages: false,
|
||||
activeGroupCallInfo: nil,
|
||||
hasActiveGroupCall: false,
|
||||
importState: nil,
|
||||
threadData: nil,
|
||||
isGeneralThreadClosed: nil,
|
||||
replyMessage: nil,
|
||||
|
||||
+5
-2
@@ -1269,7 +1269,10 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
||||
|
||||
if let controller = environment.controller() {
|
||||
let subLayout = ContainerViewLayout(
|
||||
size: availableSize, metrics: environment.metrics, deviceMetrics: environment.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: sideInset - 12.0, bottom: bottomPanelHeight, right: sideInset),
|
||||
size: availableSize,
|
||||
metrics: environment.metrics,
|
||||
deviceMetrics: environment.deviceMetrics,
|
||||
intrinsicInsets: UIEdgeInsets(top: 0.0, left: sideInset - 12.0, bottom: bottomPanelHeight, right: sideInset),
|
||||
safeInsets: UIEdgeInsets(),
|
||||
additionalInsets: UIEdgeInsets(),
|
||||
statusBarHeight: nil,
|
||||
@@ -1513,7 +1516,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
|
||||
case .someUserTooManyChannels:
|
||||
text = presentationData.strings.ChatListFilter_CreateLinkErrorSomeoneHasChannelLimit
|
||||
}
|
||||
controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
controller.present(textAlertController(context: component.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ChatListFilterTabContainerNode",
|
||||
module_name = "ChatListFilterTabContainerNode",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/TelegramUI/Components/TextNodeWithEntities",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
|
||||
"//submodules/TelegramUI/Components/LiquidLens",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
+1127
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ChatListHeaderNoticeComponent",
|
||||
module_name = "ChatListHeaderNoticeComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/ItemListUI",
|
||||
"//submodules/Markdown",
|
||||
"//submodules/TelegramUI/Components/Chat/MergedAvatarsNode",
|
||||
"//submodules/TelegramUI/Components/TextNodeWithEntities",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/TelegramUI/Components/GlobalControlPanelsContext",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import GlobalControlPanelsContext
|
||||
import ComponentFlow
|
||||
import ComponentDisplayAdapters
|
||||
|
||||
public final class ChatListHeaderNoticeComponent: Component {
|
||||
public let context: AccountContext
|
||||
public let theme: PresentationTheme
|
||||
public let strings: PresentationStrings
|
||||
public let data: GlobalControlPanelsContext.ChatListNotice
|
||||
public let activateAction: (GlobalControlPanelsContext.ChatListNotice) -> Void
|
||||
public let dismissAction: (GlobalControlPanelsContext.ChatListNotice) -> Void
|
||||
public let selectAction: (GlobalControlPanelsContext.ChatListNotice, Bool) -> Void
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
data: GlobalControlPanelsContext.ChatListNotice,
|
||||
activateAction: @escaping (GlobalControlPanelsContext.ChatListNotice) -> Void,
|
||||
dismissAction: @escaping (GlobalControlPanelsContext.ChatListNotice) -> Void,
|
||||
selectAction: @escaping (GlobalControlPanelsContext.ChatListNotice, Bool) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.data = data
|
||||
self.activateAction = activateAction
|
||||
self.dismissAction = dismissAction
|
||||
self.selectAction = selectAction
|
||||
}
|
||||
|
||||
public static func ==(lhs: ChatListHeaderNoticeComponent, rhs: ChatListHeaderNoticeComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.data != rhs.data {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private var panel: ChatListNoticeItemNode?
|
||||
|
||||
private var component: ChatListHeaderNoticeComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:))))
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
@objc private func onTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
if case .ended = recognizer.state {
|
||||
component.activateAction(component.data)
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: ChatListHeaderNoticeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let itemNode: ChatListNoticeItemNode
|
||||
if let current = self.panel {
|
||||
itemNode = current
|
||||
} else {
|
||||
itemNode = ChatListNoticeItemNode()
|
||||
self.panel = itemNode
|
||||
self.addSubview(itemNode.view)
|
||||
}
|
||||
|
||||
let item = ChatListNoticeItem(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
notice: component.data,
|
||||
action: { [weak self] action in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
switch action {
|
||||
case .activate:
|
||||
component.activateAction(component.data)
|
||||
case .hide:
|
||||
component.dismissAction(component.data)
|
||||
case let .buttonChoice(isPositive):
|
||||
component.selectAction(component.data, isPositive)
|
||||
}
|
||||
}
|
||||
)
|
||||
let (nodeLayout, apply) = itemNode.asyncLayout()(item, ListViewItemLayoutParams(
|
||||
width: availableSize.width,
|
||||
leftInset: 0.0,
|
||||
rightInset: 0.0,
|
||||
availableHeight: 10000.0,
|
||||
isStandalone: true
|
||||
), false)
|
||||
|
||||
let size = CGSize(width: availableSize.width, height: nodeLayout.contentSize.height)
|
||||
let panelFrame = CGRect(origin: CGPoint(), size: size)
|
||||
transition.setFrame(view: itemNode.view, frame: panelFrame)
|
||||
apply()
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
+548
@@ -0,0 +1,548 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import AppBundle
|
||||
import ItemListUI
|
||||
import Markdown
|
||||
import AccountContext
|
||||
import MergedAvatarsNode
|
||||
import TextNodeWithEntities
|
||||
import TextFormat
|
||||
import AvatarNode
|
||||
import GlobalControlPanelsContext
|
||||
|
||||
class ChatListNoticeItem: ListViewItem {
|
||||
enum Action {
|
||||
case activate
|
||||
case hide
|
||||
case buttonChoice(isPositive: Bool)
|
||||
}
|
||||
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let notice: GlobalControlPanelsContext.ChatListNotice
|
||||
let action: (Action) -> Void
|
||||
|
||||
let selectable: Bool = true
|
||||
|
||||
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, notice: GlobalControlPanelsContext.ChatListNotice, action: @escaping (Action) -> Void) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.notice = notice
|
||||
self.action = action
|
||||
}
|
||||
|
||||
func selected(listView: ListView) {
|
||||
listView.clearHighlightAnimated(true)
|
||||
|
||||
self.action(.activate)
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = ChatListNoticeItemNode()
|
||||
|
||||
let (nodeLayout, apply) = node.asyncLayout()(self, params, false)
|
||||
|
||||
node.insets = nodeLayout.insets
|
||||
node.contentSize = nodeLayout.contentSize
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in
|
||||
apply()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
assert(node() is ChatListNoticeItemNode)
|
||||
if let nodeValue = node() as? ChatListNoticeItemNode {
|
||||
|
||||
let layout = nodeValue.asyncLayout()
|
||||
async {
|
||||
let (nodeLayout, apply) = layout(self, params, nextItem == nil)
|
||||
Queue.mainQueue().async {
|
||||
completion(nodeLayout, { _ in
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let separatorHeight = 1.0 / UIScreen.main.scale
|
||||
|
||||
private let titleFont = Font.semibold(15.0)
|
||||
private let titleBoldFont = Font.bold(15.0)
|
||||
private let titleItalicFont = Font.semiboldItalic(15.0)
|
||||
private let titleBoldItalicFont = Font.semiboldItalic(15.0)
|
||||
|
||||
private let textFont = Font.regular(15.0)
|
||||
private let textBoldFont = Font.semibold(15.0)
|
||||
private let textItalicFont = Font.italic(15.0)
|
||||
private let textBoldItalicFont = Font.semiboldItalic(15.0)
|
||||
|
||||
private let smallTextFont = Font.regular(14.0)
|
||||
|
||||
final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode {
|
||||
private let contentContainer: ASDisplayNode
|
||||
private let titleNode: TextNodeWithEntities
|
||||
private let textNode: TextNodeWithEntities
|
||||
private let arrowNode: ASImageNode
|
||||
private let separatorNode: ASDisplayNode
|
||||
|
||||
private var avatarNode: AvatarNode?
|
||||
private var avatarsNode: MergedAvatarsNode?
|
||||
|
||||
private var closeButton: HighlightableButtonNode?
|
||||
|
||||
private var okButtonText: TextNode?
|
||||
private var cancelButtonText: TextNode?
|
||||
private var okButton: HighlightableButtonNode?
|
||||
private var cancelButton: HighlightableButtonNode?
|
||||
|
||||
private var item: ChatListNoticeItem?
|
||||
|
||||
override var apparentHeight: CGFloat {
|
||||
didSet {
|
||||
self.contentContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.bounds.width, height: self.apparentHeight))
|
||||
self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: self.contentContainer.bounds.height - UIScreenPixel), size: CGSize(width: self.contentContainer.bounds.width, height: UIScreenPixel))
|
||||
}
|
||||
}
|
||||
|
||||
required init() {
|
||||
self.contentContainer = ASDisplayNode()
|
||||
|
||||
self.titleNode = TextNodeWithEntities()
|
||||
self.textNode = TextNodeWithEntities()
|
||||
self.arrowNode = ASImageNode()
|
||||
self.separatorNode = ASDisplayNode()
|
||||
|
||||
super.init(layerBacked: false, rotated: false, seeThrough: false)
|
||||
|
||||
self.contentContainer.clipsToBounds = true
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.contentContainer.addSubnode(self.titleNode.textNode)
|
||||
self.contentContainer.addSubnode(self.textNode.textNode)
|
||||
self.contentContainer.addSubnode(self.arrowNode)
|
||||
|
||||
self.addSubnode(self.contentContainer)
|
||||
}
|
||||
|
||||
@objc private func closePressed() {
|
||||
guard let item = self.item else {
|
||||
return
|
||||
}
|
||||
item.action(.hide)
|
||||
}
|
||||
|
||||
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
|
||||
let layout = self.asyncLayout()
|
||||
let (_, apply) = layout(item as! ChatListNoticeItem, params, nextItem == nil)
|
||||
apply()
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: ChatListNoticeItem, _ params: ListViewItemLayoutParams, _ isLast: Bool) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let previousItem = self.item
|
||||
|
||||
let makeTitleLayout = TextNodeWithEntities.asyncLayout(self.titleNode)
|
||||
let makeTextLayout = TextNodeWithEntities.asyncLayout(self.textNode)
|
||||
|
||||
let makeOkButtonTextLayout = TextNode.asyncLayout(self.okButtonText)
|
||||
let makeCancelButtonTextLayout = TextNode.asyncLayout(self.cancelButtonText)
|
||||
|
||||
return { item, params, last in
|
||||
let baseWidth = params.width - params.leftInset - params.rightInset
|
||||
let _ = baseWidth
|
||||
|
||||
let sideInset: CGFloat = params.leftInset + 16.0
|
||||
let rightInset: CGFloat = sideInset + 24.0
|
||||
var titleRightInset = rightInset - 4.0
|
||||
let verticalInset: CGFloat = 9.0
|
||||
var spacing: CGFloat = 0.0
|
||||
|
||||
let themeUpdated = item.theme !== previousItem?.theme
|
||||
|
||||
let titleString: NSAttributedString
|
||||
let textString: NSAttributedString
|
||||
var avatarPeer: EnginePeer?
|
||||
var avatarPeers: [EnginePeer] = []
|
||||
|
||||
var okButtonLayout: (TextNodeLayout, () -> TextNode)?
|
||||
var cancelButtonLayout: (TextNodeLayout, () -> TextNode)?
|
||||
var alignment: NSTextAlignment = .left
|
||||
|
||||
switch item.notice {
|
||||
case let .clearStorage(sizeFraction):
|
||||
let sizeString = dataSizeString(Int64(sizeFraction), formatting: DataSizeStringFormatting(strings: item.strings, decimalSeparator: "."))
|
||||
let rawTitleString = item.strings.ChatList_StorageHintTitle(sizeString)
|
||||
let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: rawTitleString.string, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor))
|
||||
if let range = rawTitleString.ranges.first {
|
||||
titleStringValue.addAttribute(.foregroundColor, value: item.theme.rootController.navigationBar.accentTextColor, range: range.range)
|
||||
}
|
||||
titleString = titleStringValue
|
||||
|
||||
textString = NSAttributedString(string: item.strings.ChatList_StorageHintText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
|
||||
case .setupPassword:
|
||||
titleString = NSAttributedString(string: item.strings.Settings_SuggestSetupPasswordTitle, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor)
|
||||
textString = NSAttributedString(string: item.strings.Settings_SuggestSetupPasswordText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
|
||||
case let .premiumUpgrade(discount):
|
||||
let discountString = "\(discount)%"
|
||||
let rawTitleString = item.strings.ChatList_PremiumAnnualUpgradeTitle(discountString)
|
||||
let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: rawTitleString.string, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor))
|
||||
if let range = rawTitleString.ranges.first {
|
||||
titleStringValue.addAttribute(.foregroundColor, value: item.theme.rootController.navigationBar.accentTextColor, range: range.range)
|
||||
}
|
||||
titleString = titleStringValue
|
||||
|
||||
textString = NSAttributedString(string: item.strings.ChatList_PremiumAnnualUpgradeText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
|
||||
case let .premiumAnnualDiscount(discount):
|
||||
let discountString = "\(discount)%"
|
||||
let rawTitleString = item.strings.ChatList_PremiumAnnualDiscountTitle(discountString)
|
||||
let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: rawTitleString.string, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor))
|
||||
if let range = rawTitleString.ranges.first {
|
||||
titleStringValue.addAttribute(.foregroundColor, value: item.theme.rootController.navigationBar.accentTextColor, range: range.range)
|
||||
}
|
||||
titleString = titleStringValue
|
||||
|
||||
textString = NSAttributedString(string: item.strings.ChatList_PremiumAnnualDiscountText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
|
||||
titleRightInset = sideInset
|
||||
case let .premiumRestore(discount):
|
||||
let discountString = "\(discount)%"
|
||||
let rawTitleString = item.strings.ChatList_PremiumRestoreDiscountTitle(discountString)
|
||||
let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: rawTitleString.string, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor))
|
||||
if let range = rawTitleString.ranges.first {
|
||||
titleStringValue.addAttribute(.foregroundColor, value: item.theme.rootController.navigationBar.accentTextColor, range: range.range)
|
||||
}
|
||||
titleString = titleStringValue
|
||||
|
||||
textString = NSAttributedString(string: item.strings.ChatList_PremiumRestoreDiscountText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
|
||||
case .xmasPremiumGift:
|
||||
titleString = parseMarkdownIntoAttributedString(item.strings.ChatList_PremiumXmasGiftTitle, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.accentTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor), linkAttribute: { _ in return nil }))
|
||||
textString = NSAttributedString(string: item.strings.ChatList_PremiumXmasGiftText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
|
||||
case .premiumGrace:
|
||||
titleString = parseMarkdownIntoAttributedString(item.strings.ChatList_PremiumGraceTitle, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.accentTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor), linkAttribute: { _ in return nil }))
|
||||
textString = NSAttributedString(string: item.strings.ChatList_PremiumGraceText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
|
||||
case .setupBirthday:
|
||||
titleString = NSAttributedString(string: item.strings.ChatList_AddBirthdayTitle, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor)
|
||||
textString = NSAttributedString(string: item.strings.ChatList_AddBirthdayText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
|
||||
case let .birthdayPremiumGift(peers, _):
|
||||
let title: String
|
||||
let text: String
|
||||
if peers.count == 1, let peer = peers.first {
|
||||
var peerName = peer.compactDisplayTitle
|
||||
if peerName.count > 20 {
|
||||
peerName = peerName.prefix(20).trimmingCharacters(in: .whitespacesAndNewlines) + "\u{2026}"
|
||||
}
|
||||
title = item.strings.ChatList_BirthdaySingleTitle(peerName).string
|
||||
text = item.strings.ChatList_BirthdaySingleText
|
||||
} else {
|
||||
title = item.strings.ChatList_BirthdayMultipleTitle(Int32(peers.count))
|
||||
text = item.strings.ChatList_BirthdayMultipleText
|
||||
}
|
||||
titleString = parseMarkdownIntoAttributedString(title, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.accentTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor), linkAttribute: { _ in return nil }))
|
||||
textString = NSAttributedString(string: text, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
|
||||
avatarPeers = Array(peers.prefix(3))
|
||||
case let .reviewLogin(newSessionReview, totalCount):
|
||||
spacing = 2.0
|
||||
alignment = .center
|
||||
|
||||
var rawTitleString = item.strings.ChatList_SessionReview_PanelTitle
|
||||
if totalCount > 1 {
|
||||
rawTitleString = "1/\(totalCount) \(rawTitleString)"
|
||||
}
|
||||
let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: rawTitleString, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor))
|
||||
titleString = titleStringValue
|
||||
|
||||
textString = NSAttributedString(string: item.strings.ChatList_SessionReview_PanelText(newSessionReview.device, newSessionReview.location).string, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
|
||||
|
||||
okButtonLayout = makeOkButtonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.ChatList_SessionReview_PanelConfirm, font: titleFont, textColor: item.theme.list.itemAccentColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0)))
|
||||
cancelButtonLayout = makeCancelButtonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.ChatList_SessionReview_PanelReject, font: titleFont, textColor: item.theme.list.itemDestructiveColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0)))
|
||||
case let .starsSubscriptionLowBalance(amount, peers):
|
||||
let title: String
|
||||
let text: String
|
||||
let starsValue = item.strings.ChatList_SubscriptionsLowBalance_Stars(Int32(clamping: amount.value))
|
||||
if let peer = peers.first, peers.count == 1 {
|
||||
title = item.strings.ChatList_SubscriptionsLowBalance_Single_Title(starsValue, peer.compactDisplayTitle).string
|
||||
text = item.strings.ChatList_SubscriptionsLowBalance_Single_Text
|
||||
} else {
|
||||
title = item.strings.ChatList_SubscriptionsLowBalance_Multiple_Title(starsValue).string
|
||||
text = item.strings.ChatList_SubscriptionsLowBalance_Multiple_Text
|
||||
}
|
||||
let attributedTitle = NSMutableAttributedString(string: "⭐️\(title)", font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor)
|
||||
if let range = attributedTitle.string.range(of: "⭐️") {
|
||||
attributedTitle.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: attributedTitle.string))
|
||||
attributedTitle.addAttribute(.baselineOffset, value: 2.0, range: NSRange(range, in: attributedTitle.string))
|
||||
}
|
||||
titleString = attributedTitle
|
||||
textString = NSAttributedString(string: text, font: smallTextFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
|
||||
case let .setupPhoto(accountPeer):
|
||||
titleString = NSAttributedString(string: item.strings.ChatList_AddPhoto_Title, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor)
|
||||
textString = NSAttributedString(string: item.strings.ChatList_AddPhoto_Text, font: smallTextFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
|
||||
avatarPeer = accountPeer
|
||||
case .accountFreeze:
|
||||
titleString = NSAttributedString(string: item.strings.ChatList_FrozenAccount_Title, font: titleFont, textColor: item.theme.list.itemDestructiveColor)
|
||||
textString = NSAttributedString(string: item.strings.ChatList_FrozenAccount_Text, font: smallTextFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
|
||||
case let .link(_, _, title, subtitle):
|
||||
titleString = stringWithAppliedEntities(title.string, entities: title.entities, baseColor: item.theme.list.itemPrimaryTextColor, linkColor: item.theme.list.itemAccentColor, baseFont: titleFont, linkFont: titleFont, boldFont: titleBoldFont, italicFont: titleItalicFont, boldItalicFont: titleBoldItalicFont, fixedFont: titleFont, blockQuoteFont: titleFont, message: nil)
|
||||
textString = stringWithAppliedEntities(subtitle.string, entities: subtitle.entities, baseColor: item.theme.list.itemPrimaryTextColor, linkColor: item.theme.list.itemAccentColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFont, blockQuoteFont: textFont, message: nil)
|
||||
}
|
||||
|
||||
var leftInset: CGFloat = sideInset
|
||||
if !avatarPeers.isEmpty {
|
||||
let avatarsWidth = 30.0 + CGFloat(avatarPeers.count - 1) * 16.0
|
||||
leftInset += avatarsWidth + 4.0
|
||||
} else if let _ = avatarPeer {
|
||||
let avatarsWidth: CGFloat = 40.0
|
||||
leftInset += avatarsWidth + 6.0
|
||||
}
|
||||
|
||||
let titleLayout = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - titleRightInset, height: 100.0), alignment: alignment, lineSpacing: 0.18))
|
||||
|
||||
let textLayout = makeTextLayout(TextNodeLayoutArguments(attributedString: textString, maximumNumberOfLines: 10, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: 100.0), alignment: alignment, lineSpacing: 0.18))
|
||||
|
||||
var contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.0.size.height + textLayout.0.size.height)
|
||||
if let okButtonLayout {
|
||||
contentSize.height += okButtonLayout.0.size.height + 20.0
|
||||
}
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets())
|
||||
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
|
||||
if themeUpdated {
|
||||
strongSelf.arrowNode.image = PresentationResourcesItemList.disclosureArrowImage(item.theme)
|
||||
}
|
||||
|
||||
let _ = titleLayout.1(TextNodeWithEntities.Arguments(context: item.context, cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: .white, attemptSynchronous: true))
|
||||
if case .center = alignment {
|
||||
strongSelf.titleNode.textNode.frame = CGRect(origin: CGPoint(x: floor((params.width - titleLayout.0.size.width) * 0.5), y: verticalInset), size: titleLayout.0.size)
|
||||
} else {
|
||||
strongSelf.titleNode.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.0.size)
|
||||
}
|
||||
|
||||
let _ = textLayout.1(TextNodeWithEntities.Arguments(context: item.context, cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: .white, attemptSynchronous: true))
|
||||
|
||||
strongSelf.titleNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 1000000.0, height: 1000000.0))
|
||||
strongSelf.textNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 1000000.0, height: 1000000.0))
|
||||
|
||||
if case .center = alignment {
|
||||
strongSelf.textNode.textNode.frame = CGRect(origin: CGPoint(x: floor((params.width - textLayout.0.size.width) * 0.5), y: strongSelf.titleNode.textNode.frame.maxY + spacing), size: textLayout.0.size)
|
||||
} else {
|
||||
strongSelf.textNode.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.textNode.frame.maxY + spacing), size: textLayout.0.size)
|
||||
}
|
||||
|
||||
if !avatarPeers.isEmpty {
|
||||
let avatarsNode: MergedAvatarsNode
|
||||
if let current = strongSelf.avatarsNode {
|
||||
avatarsNode = current
|
||||
} else {
|
||||
avatarsNode = MergedAvatarsNode()
|
||||
avatarsNode.isUserInteractionEnabled = false
|
||||
strongSelf.addSubnode(avatarsNode)
|
||||
strongSelf.avatarsNode = avatarsNode
|
||||
}
|
||||
let avatarSize = CGSize(width: 30.0, height: 30.0)
|
||||
avatarsNode.update(context: item.context, peers: avatarPeers.map { $0._asPeer() }, synchronousLoad: false, imageSize: avatarSize.width, imageSpacing: 16.0, borderWidth: 2.0 - UIScreenPixel, avatarFontSize: 10.0)
|
||||
let avatarsSize = CGSize(width: avatarSize.width + 16.0 * CGFloat(avatarPeers.count - 1), height: avatarSize.height)
|
||||
avatarsNode.updateLayout(size: avatarsSize)
|
||||
avatarsNode.frame = CGRect(origin: CGPoint(x: sideInset - 6.0, y: floor((layout.size.height - avatarsSize.height) / 2.0)), size: avatarsSize)
|
||||
} else if let avatarsNode = strongSelf.avatarsNode {
|
||||
avatarsNode.removeFromSupernode()
|
||||
strongSelf.avatarsNode = nil
|
||||
}
|
||||
|
||||
if let avatarPeer {
|
||||
let avatarNode: AvatarNode
|
||||
if let current = strongSelf.avatarNode {
|
||||
avatarNode = current
|
||||
} else {
|
||||
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 13.0))
|
||||
avatarNode.isUserInteractionEnabled = false
|
||||
strongSelf.addSubnode(avatarNode)
|
||||
strongSelf.avatarNode = avatarNode
|
||||
|
||||
avatarNode.setPeer(context: item.context, theme: item.theme, peer: avatarPeer, overrideImage: .cameraIcon)
|
||||
}
|
||||
let avatarSize = CGSize(width: 40.0, height: 40.0)
|
||||
avatarNode.frame = CGRect(origin: CGPoint(x: sideInset - 6.0, y: floor((layout.size.height - avatarSize.height) / 2.0)), size: avatarSize)
|
||||
} else if let avatarNode = strongSelf.avatarNode {
|
||||
avatarNode.removeFromSupernode()
|
||||
strongSelf.avatarNode = nil
|
||||
}
|
||||
|
||||
if let image = strongSelf.arrowNode.image {
|
||||
strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: layout.size.width - sideInset - image.size.width + 8.0, y: floor((layout.size.height - image.size.height) / 2.0)), size: image.size)
|
||||
}
|
||||
|
||||
let hasCloseButton: Bool
|
||||
switch item.notice {
|
||||
case .xmasPremiumGift, .setupBirthday, .birthdayPremiumGift, .premiumGrace, .starsSubscriptionLowBalance, .setupPhoto, .link:
|
||||
hasCloseButton = true
|
||||
default:
|
||||
hasCloseButton = false
|
||||
}
|
||||
|
||||
if let okButtonLayout, let cancelButtonLayout {
|
||||
strongSelf.arrowNode.isHidden = true
|
||||
strongSelf.closeButton?.isHidden = true
|
||||
|
||||
let okButton: HighlightableButtonNode
|
||||
if let current = strongSelf.okButton {
|
||||
okButton = current
|
||||
} else {
|
||||
okButton = HighlightableButtonNode()
|
||||
strongSelf.okButton = okButton
|
||||
strongSelf.contentContainer.addSubnode(okButton)
|
||||
okButton.addTarget(strongSelf, action: #selector(strongSelf.okButtonPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
let cancelButton: HighlightableButtonNode
|
||||
if let current = strongSelf.cancelButton {
|
||||
cancelButton = current
|
||||
} else {
|
||||
cancelButton = HighlightableButtonNode()
|
||||
strongSelf.cancelButton = cancelButton
|
||||
strongSelf.contentContainer.addSubnode(cancelButton)
|
||||
cancelButton.addTarget(strongSelf, action: #selector(strongSelf.cancelButtonPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
let okButtonText = okButtonLayout.1()
|
||||
if okButtonText !== strongSelf.okButtonText {
|
||||
strongSelf.okButtonText?.removeFromSupernode()
|
||||
strongSelf.okButtonText = okButtonText
|
||||
okButton.addSubnode(okButtonText)
|
||||
}
|
||||
|
||||
let cancelButtonText = cancelButtonLayout.1()
|
||||
if cancelButtonText !== strongSelf.okButtonText {
|
||||
strongSelf.cancelButtonText?.removeFromSupernode()
|
||||
strongSelf.cancelButtonText = cancelButtonText
|
||||
cancelButton.addSubnode(cancelButtonText)
|
||||
}
|
||||
|
||||
let buttonsWidth: CGFloat = max(min(300.0, params.width), okButtonLayout.0.size.width + cancelButtonLayout.0.size.width + 32.0)
|
||||
let buttonWidth: CGFloat = floor(buttonsWidth * 0.5)
|
||||
let buttonHeight: CGFloat = 32.0
|
||||
|
||||
let okButtonFrame = CGRect(origin: CGPoint(x: floor((params.width - buttonsWidth) * 0.5), y: strongSelf.textNode.textNode.frame.maxY + 6.0), size: CGSize(width: buttonWidth, height: buttonHeight))
|
||||
let cancelButtonFrame = CGRect(origin: CGPoint(x: okButtonFrame.maxX, y: strongSelf.textNode.textNode.frame.maxY + 6.0), size: CGSize(width: buttonWidth, height: buttonHeight))
|
||||
|
||||
okButton.frame = okButtonFrame
|
||||
cancelButton.frame = cancelButtonFrame
|
||||
|
||||
okButtonText.frame = CGRect(origin: CGPoint(x: floor((okButtonFrame.width - okButtonLayout.0.size.width) * 0.5), y: floor((okButtonFrame.height - okButtonLayout.0.size.height) * 0.5)), size: okButtonLayout.0.size)
|
||||
cancelButtonText.frame = CGRect(origin: CGPoint(x: floor((cancelButtonFrame.width - cancelButtonLayout.0.size.width) * 0.5), y: floor((cancelButtonFrame.height - cancelButtonLayout.0.size.height) * 0.5)), size: cancelButtonLayout.0.size)
|
||||
} else {
|
||||
strongSelf.arrowNode.isHidden = hasCloseButton
|
||||
|
||||
if let okButton = strongSelf.okButton {
|
||||
strongSelf.okButton = nil
|
||||
okButton.removeFromSupernode()
|
||||
}
|
||||
if let cancelButton = strongSelf.cancelButton {
|
||||
strongSelf.cancelButton = nil
|
||||
cancelButton.removeFromSupernode()
|
||||
}
|
||||
if let okButtonText = strongSelf.okButtonText {
|
||||
strongSelf.okButtonText = nil
|
||||
okButtonText.removeFromSupernode()
|
||||
}
|
||||
if let cancelButtonText = strongSelf.cancelButtonText {
|
||||
strongSelf.cancelButtonText = nil
|
||||
cancelButtonText.removeFromSupernode()
|
||||
}
|
||||
|
||||
if hasCloseButton {
|
||||
let closeButton: HighlightableButtonNode
|
||||
if let current = strongSelf.closeButton {
|
||||
closeButton = current
|
||||
} else {
|
||||
closeButton = HighlightableButtonNode()
|
||||
closeButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0)
|
||||
closeButton.addTarget(self, action: #selector(strongSelf.closePressed), forControlEvents: [.touchUpInside])
|
||||
strongSelf.contentContainer.addSubnode(closeButton)
|
||||
strongSelf.closeButton = closeButton
|
||||
}
|
||||
|
||||
if themeUpdated || closeButton.image(for: .normal) == nil {
|
||||
closeButton.setImage(PresentationResourcesItemList.itemListCloseIconImage(item.theme), for: .normal)
|
||||
}
|
||||
|
||||
let closeButtonSize = closeButton.measure(CGSize(width: 100.0, height: 100.0))
|
||||
closeButton.frame = CGRect(origin: CGPoint(x: layout.size.width - sideInset - closeButtonSize.width, y: floor((layout.size.height - closeButtonSize.height) / 2.0)), size: closeButtonSize)
|
||||
} else {
|
||||
strongSelf.closeButton?.removeFromSupernode()
|
||||
strongSelf.closeButton = nil
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.contentSize = layout.contentSize
|
||||
strongSelf.insets = layout.insets
|
||||
|
||||
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
|
||||
|
||||
strongSelf.contentContainer.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
|
||||
|
||||
switch item.notice {
|
||||
default:
|
||||
strongSelf.setRevealOptions((left: [], right: []))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override public func selected() {
|
||||
super.selected()
|
||||
|
||||
if case .setupPhoto = self.item?.notice {
|
||||
self.avatarNode?.playCameraAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func okButtonPressed() {
|
||||
self.item?.action(.buttonChoice(isPositive: true))
|
||||
}
|
||||
|
||||
@objc private func cancelButtonPressed() {
|
||||
self.item?.action(.buttonChoice(isPositive: false))
|
||||
}
|
||||
|
||||
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
|
||||
super.animateInsertion(currentTimestamp, duration: duration, options: options)
|
||||
|
||||
//self.transitionOffset = self.bounds.size.height
|
||||
//self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp)
|
||||
}
|
||||
|
||||
override public func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
super.updateRevealOffset(offset: offset, transition: transition)
|
||||
|
||||
transition.updateSublayerTransformOffset(layer: self.contentContainer.layer, offset: CGPoint(x: offset, y: 0.0))
|
||||
}
|
||||
|
||||
override public func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
|
||||
if let item = self.item {
|
||||
item.action(.hide)
|
||||
}
|
||||
|
||||
self.setRevealOptionsOpened(false, animated: true)
|
||||
self.revealOptionsInteractivelyClosed()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ChatListSearchFiltersContainerNode",
|
||||
module_name = "ChatListSearchFiltersContainerNode",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import GlassBackgroundComponent
|
||||
import ComponentFlow
|
||||
import ComponentDisplayAdapters
|
||||
|
||||
public enum ChatListSearchFilterEntryId: Hashable {
|
||||
case filter(Int64)
|
||||
}
|
||||
|
||||
public enum ChatListSearchFilterEntry: Equatable {
|
||||
case filter(ChatListSearchFilter)
|
||||
|
||||
public var id: ChatListSearchFilterEntryId {
|
||||
switch self {
|
||||
case let .filter(filter):
|
||||
return .filter(filter.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class ChatListSearchFiltersContainerNode: ASDisplayNode {
|
||||
private let backgroundContainer: GlassBackgroundContainerView
|
||||
private let backgroundView: GlassBackgroundView
|
||||
|
||||
private let scrollNode: ASScrollNode
|
||||
private let selectionView: UIImageView
|
||||
private var itemNodes: [ChatListSearchFilterEntryId: ItemNode] = [:]
|
||||
|
||||
public var filterPressed: ((ChatListSearchFilter) -> Void)?
|
||||
|
||||
private var currentParams: (size: CGSize, sideInset: CGFloat, filters: [ChatListSearchFilterEntry], selectedFilter: ChatListSearchFilterEntryId?, transitionFraction: CGFloat, presentationData: PresentationData)?
|
||||
|
||||
private var previousSelectedAbsFrame: CGRect?
|
||||
private var previousSelectedFrame: CGRect?
|
||||
|
||||
override public init() {
|
||||
self.backgroundContainer = GlassBackgroundContainerView()
|
||||
self.backgroundView = GlassBackgroundView()
|
||||
self.backgroundContainer.contentView.addSubview(self.backgroundView)
|
||||
|
||||
self.scrollNode = ASScrollNode()
|
||||
|
||||
self.selectionView = UIImageView()
|
||||
|
||||
super.init()
|
||||
|
||||
self.scrollNode.view.showsHorizontalScrollIndicator = false
|
||||
self.scrollNode.view.scrollsToTop = false
|
||||
self.scrollNode.view.delaysContentTouches = false
|
||||
self.scrollNode.view.canCancelContentTouches = true
|
||||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
|
||||
self.view.addSubview(self.backgroundContainer)
|
||||
|
||||
self.backgroundView.contentView.addSubview(self.scrollNode.view)
|
||||
self.scrollNode.view.addSubview(self.selectionView)
|
||||
}
|
||||
|
||||
public func cancelAnimations() {
|
||||
self.scrollNode.layer.removeAllAnimations()
|
||||
}
|
||||
|
||||
public func update(size: CGSize, sideInset: CGFloat, filters: [ChatListSearchFilterEntry], displayGlobalPostsNewBadge: Bool, selectedFilter: ChatListSearchFilterEntryId?, transitionFraction: CGFloat, presentationData: PresentationData, transition proposedTransition: ContainedViewLayoutTransition) {
|
||||
let isFirstTime = self.currentParams == nil
|
||||
let transition: ContainedViewLayoutTransition = isFirstTime ? .immediate : proposedTransition
|
||||
|
||||
let componentTransition = ComponentTransition(transition)
|
||||
|
||||
componentTransition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: CGPoint(), size: size))
|
||||
self.backgroundContainer.update(size: size, isDark: presentationData.theme.overallDarkAppearance, transition: componentTransition)
|
||||
componentTransition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size))
|
||||
self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: presentationData.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: componentTransition)
|
||||
self.scrollNode.view.layer.cornerRadius = size.height * 0.5
|
||||
|
||||
var focusOnSelectedFilter = self.currentParams?.selectedFilter != selectedFilter
|
||||
let previousScrollBounds = self.scrollNode.bounds
|
||||
let previousContentWidth = self.scrollNode.view.contentSize.width
|
||||
|
||||
self.currentParams = (size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, transitionFraction: transitionFraction, presentationData: presentationData)
|
||||
|
||||
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
var hasSelection = false
|
||||
for i in 0 ..< filters.count {
|
||||
let filter = filters[i]
|
||||
if case let .filter(type) = filter {
|
||||
let itemNode: ItemNode
|
||||
var itemNodeTransition = transition
|
||||
if let current = self.itemNodes[filter.id] {
|
||||
itemNode = current
|
||||
} else {
|
||||
itemNodeTransition = .immediate
|
||||
itemNode = ItemNode(pressed: { [weak self] in
|
||||
self?.filterPressed?(type)
|
||||
})
|
||||
self.itemNodes[filter.id] = itemNode
|
||||
}
|
||||
|
||||
let selectionFraction: CGFloat
|
||||
if selectedFilter == filter.id {
|
||||
selectionFraction = 1.0 - abs(transitionFraction)
|
||||
hasSelection = true
|
||||
} else if i != 0 && selectedFilter == filters[i - 1].id {
|
||||
selectionFraction = max(0.0, -transitionFraction)
|
||||
} else if i != filters.count - 1 && selectedFilter == filters[i + 1].id {
|
||||
selectionFraction = max(0.0, transitionFraction)
|
||||
} else {
|
||||
selectionFraction = 0.0
|
||||
}
|
||||
|
||||
var displayNewBadge = false
|
||||
if case .globalPosts = type {
|
||||
displayNewBadge = displayGlobalPostsNewBadge
|
||||
}
|
||||
|
||||
itemNode.update(type: type, displayNewBadge: displayNewBadge, presentationData: presentationData, selectionFraction: selectionFraction, transition: itemNodeTransition)
|
||||
}
|
||||
}
|
||||
|
||||
var updated = false
|
||||
|
||||
var removeKeys: [ChatListSearchFilterEntryId] = []
|
||||
for (id, _) in self.itemNodes {
|
||||
if !filters.contains(where: { $0.id == id }) {
|
||||
removeKeys.append(id)
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
for id in removeKeys {
|
||||
if let itemNode = self.itemNodes.removeValue(forKey: id) {
|
||||
transition.updateAlpha(node: itemNode, alpha: 0.0, completion: { [weak itemNode] _ in
|
||||
itemNode?.removeFromSupernode()
|
||||
})
|
||||
transition.updateTransformScale(node: itemNode, scale: 0.1)
|
||||
}
|
||||
}
|
||||
|
||||
var tabSizes: [(ChatListSearchFilterEntryId, CGSize, ItemNode, Bool)] = []
|
||||
var totalRawTabSize: CGFloat = 0.0
|
||||
var selectionFrames: [CGRect] = []
|
||||
|
||||
for filter in filters {
|
||||
guard let itemNode = self.itemNodes[filter.id] else {
|
||||
continue
|
||||
}
|
||||
let wasAdded = itemNode.supernode == nil
|
||||
var itemNodeTransition = transition
|
||||
if wasAdded {
|
||||
itemNodeTransition = .immediate
|
||||
self.scrollNode.addSubnode(itemNode)
|
||||
}
|
||||
let paneNodeWidth = itemNode.updateLayout(height: size.height, transition: itemNodeTransition)
|
||||
let paneNodeSize = CGSize(width: paneNodeWidth, height: size.height)
|
||||
tabSizes.append((filter.id, paneNodeSize, itemNode, wasAdded))
|
||||
totalRawTabSize += paneNodeSize.width
|
||||
}
|
||||
|
||||
let minSpacing: CGFloat = 24.0
|
||||
var spacing = minSpacing
|
||||
|
||||
let resolvedSideInset: CGFloat = 16.0 + sideInset
|
||||
var leftOffset: CGFloat = resolvedSideInset
|
||||
|
||||
var longTitlesWidth: CGFloat = resolvedSideInset
|
||||
var titlesWidth: CGFloat = 0.0
|
||||
for i in 0 ..< tabSizes.count {
|
||||
let (_, paneNodeSize, _, _) = tabSizes[i]
|
||||
longTitlesWidth += paneNodeSize.width
|
||||
titlesWidth += paneNodeSize.width
|
||||
if i != tabSizes.count - 1 {
|
||||
longTitlesWidth += minSpacing
|
||||
}
|
||||
}
|
||||
longTitlesWidth += resolvedSideInset
|
||||
|
||||
if longTitlesWidth < size.width && hasSelection {
|
||||
spacing = (size.width - titlesWidth - resolvedSideInset * 2.0) / CGFloat(tabSizes.count - 1)
|
||||
}
|
||||
|
||||
let verticalOffset: CGFloat = -4.0
|
||||
for i in 0 ..< tabSizes.count {
|
||||
let (_, paneNodeSize, paneNode, wasAdded) = tabSizes[i]
|
||||
let itemNodeTransition = transition
|
||||
|
||||
let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0) + verticalOffset), size: paneNodeSize)
|
||||
|
||||
var effectiveWasAdded = wasAdded
|
||||
if !effectiveWasAdded && !self.bounds.intersects(self.scrollNode.view.convert(paneNode.frame, to: self.view)) && self.bounds.intersects(self.scrollNode.view.convert(paneFrame, to: self.view)) {
|
||||
effectiveWasAdded = true
|
||||
}
|
||||
|
||||
if effectiveWasAdded {
|
||||
paneNode.frame = paneFrame
|
||||
paneNode.alpha = 0.0
|
||||
paneNode.subnodeTransform = CATransform3DMakeScale(0.1, 0.1, 1.0)
|
||||
itemNodeTransition.updateSublayerTransformScale(node: paneNode, scale: 1.0)
|
||||
itemNodeTransition.updateAlpha(node: paneNode, alpha: 1.0)
|
||||
} else {
|
||||
if self.bounds.intersects(self.scrollNode.view.convert(paneFrame, to: self.view)) {
|
||||
itemNodeTransition.updateFrameAdditive(node: paneNode, frame: paneFrame)
|
||||
} else if paneNode.frame != paneFrame {
|
||||
paneNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4) { [weak paneNode] _ in
|
||||
paneNode?.frame = paneFrame
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
paneNode.updateArea(size: paneFrame.size, sideInset: spacing / 2.0, transition: itemNodeTransition)
|
||||
paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -spacing / 2.0, bottom: 0.0, right: -spacing / 2.0)
|
||||
|
||||
selectionFrames.append(paneFrame)
|
||||
|
||||
leftOffset += paneNodeSize.width + spacing
|
||||
}
|
||||
leftOffset -= spacing
|
||||
leftOffset += resolvedSideInset
|
||||
|
||||
self.scrollNode.view.contentSize = CGSize(width: leftOffset, height: size.height)
|
||||
|
||||
var selectedFrame: CGRect?
|
||||
if let selectedFilter = selectedFilter, let currentIndex = filters.firstIndex(where: { $0.id == selectedFilter }) {
|
||||
func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect {
|
||||
return CGRect(x: floorToScreenPixels(toValue.origin.x * t + fromValue.origin.x * (1.0 - t)), y: floorToScreenPixels(toValue.origin.y * t + fromValue.origin.y * (1.0 - t)), width: floorToScreenPixels(toValue.size.width * t + fromValue.size.width * (1.0 - t)), height: floorToScreenPixels(toValue.size.height * t + fromValue.size.height * (1.0 - t)))
|
||||
}
|
||||
|
||||
if currentIndex != 0 && transitionFraction > 0.0 {
|
||||
let currentFrame = selectionFrames[currentIndex]
|
||||
let previousFrame = selectionFrames[currentIndex - 1]
|
||||
selectedFrame = interpolateFrame(from: currentFrame, to: previousFrame, t: abs(transitionFraction))
|
||||
} else if currentIndex != filters.count - 1 && transitionFraction < 0.0 {
|
||||
let currentFrame = selectionFrames[currentIndex]
|
||||
let previousFrame = selectionFrames[currentIndex + 1]
|
||||
selectedFrame = interpolateFrame(from: currentFrame, to: previousFrame, t: abs(transitionFraction))
|
||||
} else {
|
||||
selectedFrame = selectionFrames[currentIndex]
|
||||
}
|
||||
}
|
||||
|
||||
if let selectedFrame {
|
||||
let wasAdded = self.selectionView.isHidden
|
||||
let selectionFrame = CGRect(origin: CGPoint(x: selectedFrame.minX - 13.0, y: 3.0), size: CGSize(width: selectedFrame.width + 26.0, height: size.height - 3.0 * 2.0))
|
||||
if wasAdded {
|
||||
self.selectionView.frame = selectionFrame
|
||||
ComponentTransition(transition).animateAlpha(view: self.selectionView, from: 0.0, to: 1.0)
|
||||
} else {
|
||||
transition.updateFrame(view: self.selectionView, frame: selectionFrame)
|
||||
}
|
||||
self.selectionView.isHidden = false
|
||||
|
||||
if self.selectionView.image?.size.height != selectionFrame.height {
|
||||
self.selectionView.image = generateStretchableFilledCircleImage(diameter: selectionFrame.height, color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
self.selectionView.tintColor = presentationData.theme.chat.inputPanel.panelControlColor.withAlphaComponent(0.1)
|
||||
|
||||
if let previousSelectedFrame = self.previousSelectedFrame {
|
||||
let previousContentOffsetX = max(0.0, min(previousContentWidth - previousScrollBounds.width, floor(previousSelectedFrame.midX - previousScrollBounds.width / 2.0)))
|
||||
if abs(previousContentOffsetX - previousScrollBounds.minX) < 1.0 {
|
||||
focusOnSelectedFilter = true
|
||||
}
|
||||
}
|
||||
|
||||
if focusOnSelectedFilter {
|
||||
let updatedBounds: CGRect
|
||||
if transitionFraction.isZero && selectedFilter == filters.first?.id {
|
||||
updatedBounds = CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size)
|
||||
} else if transitionFraction.isZero && selectedFilter == filters.last?.id {
|
||||
updatedBounds = CGRect(origin: CGPoint(x: max(0.0, self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width), y: 0.0), size: self.scrollNode.bounds.size)
|
||||
} else {
|
||||
let contentOffsetX = max(0.0, min(self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width, floor(selectedFrame.midX - self.scrollNode.bounds.width / 2.0)))
|
||||
updatedBounds = CGRect(origin: CGPoint(x: contentOffsetX, y: 0.0), size: self.scrollNode.bounds.size)
|
||||
}
|
||||
self.scrollNode.bounds = updatedBounds
|
||||
}
|
||||
transition.animateHorizontalOffsetAdditive(node: self.scrollNode, offset: previousScrollBounds.minX - self.scrollNode.bounds.minX)
|
||||
|
||||
self.previousSelectedAbsFrame = selectedFrame.offsetBy(dx: -self.scrollNode.bounds.minX, dy: 0.0)
|
||||
self.previousSelectedFrame = selectedFrame
|
||||
} else {
|
||||
self.selectionView.isHidden = true
|
||||
self.previousSelectedAbsFrame = nil
|
||||
self.previousSelectedFrame = nil
|
||||
}
|
||||
|
||||
if updated && self.scrollNode.view.contentOffset.x > 0.0 {
|
||||
self.scrollNode.view.contentOffset = CGPoint()
|
||||
}
|
||||
}
|
||||
}
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
|
||||
final class ItemNode: ASDisplayNode {
|
||||
private let pressed: () -> Void
|
||||
|
||||
private let iconNode: ASImageNode
|
||||
private let titleNode: ImmediateTextNode
|
||||
private var titleBadgeView: UIImageView?
|
||||
private let buttonNode: HighlightTrackingButtonNode
|
||||
|
||||
private var selectionFraction: CGFloat = 0.0
|
||||
|
||||
private var theme: PresentationTheme?
|
||||
|
||||
init(pressed: @escaping () -> Void) {
|
||||
self.pressed = pressed
|
||||
|
||||
let titleInset: CGFloat = 4.0
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
self.iconNode.displayWithoutProcessing = true
|
||||
|
||||
self.titleNode = ImmediateTextNode()
|
||||
self.titleNode.displaysAsynchronously = false
|
||||
self.titleNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0)
|
||||
|
||||
self.buttonNode = HighlightTrackingButtonNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.iconNode)
|
||||
self.addSubnode(self.buttonNode)
|
||||
|
||||
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||
self.buttonNode.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.iconNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.iconNode.alpha = 0.4
|
||||
|
||||
strongSelf.titleNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.titleNode.alpha = 0.4
|
||||
} else {
|
||||
strongSelf.iconNode.alpha = 1.0
|
||||
strongSelf.iconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
|
||||
strongSelf.titleNode.alpha = 1.0
|
||||
strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func buttonPressed() {
|
||||
self.pressed()
|
||||
}
|
||||
|
||||
func update(type: ChatListSearchFilter, displayNewBadge: Bool, presentationData: PresentationData, selectionFraction: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.selectionFraction = selectionFraction
|
||||
|
||||
let title: String
|
||||
var titleBadge: String?
|
||||
let icon: UIImage?
|
||||
|
||||
let color = presentationData.theme.chat.inputPanel.panelControlColor
|
||||
switch type {
|
||||
case .chats:
|
||||
title = presentationData.strings.ChatList_Search_FilterChats
|
||||
icon = nil
|
||||
case .topics:
|
||||
title = presentationData.strings.ChatList_Search_FilterChats
|
||||
icon = nil
|
||||
case .channels:
|
||||
title = presentationData.strings.ChatList_Search_FilterChannels
|
||||
icon = nil
|
||||
case .apps:
|
||||
title = presentationData.strings.ChatList_Search_FilterApps
|
||||
icon = nil
|
||||
case .globalPosts:
|
||||
title = presentationData.strings.ChatList_Search_FilterGlobalPosts
|
||||
if displayNewBadge {
|
||||
titleBadge = presentationData.strings.ChatList_ContextMenuBadgeNew
|
||||
}
|
||||
icon = nil
|
||||
case .media:
|
||||
title = presentationData.strings.ChatList_Search_FilterMedia
|
||||
icon = nil
|
||||
case .downloads:
|
||||
title = presentationData.strings.ChatList_Search_FilterDownloads
|
||||
icon = nil
|
||||
case .links:
|
||||
title = presentationData.strings.ChatList_Search_FilterLinks
|
||||
icon = nil
|
||||
case .files:
|
||||
title = presentationData.strings.ChatList_Search_FilterFiles
|
||||
icon = nil
|
||||
case .music:
|
||||
title = presentationData.strings.ChatList_Search_FilterMusic
|
||||
icon = nil
|
||||
case .voice:
|
||||
title = presentationData.strings.ChatList_Search_FilterVoice
|
||||
icon = nil
|
||||
case .instantVideo:
|
||||
title = presentationData.strings.ChatList_Search_FilterVoice
|
||||
icon = nil
|
||||
case .publicPosts:
|
||||
title = presentationData.strings.ChatList_Search_FilterPublicPosts
|
||||
icon = nil
|
||||
case let .peer(peerId, isGroup, displayTitle, _):
|
||||
title = displayTitle
|
||||
let image: UIImage?
|
||||
if isGroup {
|
||||
image = UIImage(bundleImageName: "Chat List/Search/Group")
|
||||
} else if peerId.namespace == Namespaces.Peer.CloudChannel {
|
||||
image = UIImage(bundleImageName: "Chat List/Search/Channel")
|
||||
} else {
|
||||
image = UIImage(bundleImageName: "Chat List/Search/User")
|
||||
}
|
||||
icon = generateTintedImage(image: image, color: color)
|
||||
case let .date(_, _, displayTitle):
|
||||
title = displayTitle
|
||||
icon = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Search/Calendar"), color: color)
|
||||
}
|
||||
|
||||
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(15.0), textColor: color)
|
||||
|
||||
if let titleBadge {
|
||||
let titleBadgeView: UIImageView
|
||||
if let current = self.titleBadgeView {
|
||||
titleBadgeView = current
|
||||
} else {
|
||||
titleBadgeView = UIImageView()
|
||||
self.titleBadgeView = titleBadgeView
|
||||
self.view.addSubview(titleBadgeView)
|
||||
|
||||
let labelText = NSAttributedString(string: titleBadge, font: Font.medium(11.0), textColor: presentationData.theme.list.itemCheckColors.foregroundColor)
|
||||
let labelBounds = labelText.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: [.usesLineFragmentOrigin], context: nil)
|
||||
let labelSize = CGSize(width: ceil(labelBounds.width), height: ceil(labelBounds.height))
|
||||
let badgeSize = CGSize(width: labelSize.width + 8.0, height: labelSize.height + 2.0 + 1.0)
|
||||
titleBadgeView.image = generateImage(badgeSize, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let rect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height - UIScreenPixel * 2.0))
|
||||
|
||||
context.addPath(UIBezierPath(roundedRect: rect, cornerRadius: 5.0).cgPath)
|
||||
context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor)
|
||||
context.fillPath()
|
||||
|
||||
UIGraphicsPushContext(context)
|
||||
labelText.draw(at: CGPoint(x: 4.0, y: 1.0 + UIScreenPixel))
|
||||
UIGraphicsPopContext()
|
||||
})
|
||||
}
|
||||
} else if let titleBadgeView = self.titleBadgeView {
|
||||
self.titleBadgeView = nil
|
||||
titleBadgeView.removeFromSuperview()
|
||||
}
|
||||
|
||||
self.buttonNode.accessibilityLabel = title
|
||||
if selectionFraction == 1.0 {
|
||||
self.buttonNode.accessibilityTraits = [.button, .selected]
|
||||
} else {
|
||||
self.buttonNode.accessibilityTraits = [.button]
|
||||
}
|
||||
|
||||
if self.theme !== presentationData.theme {
|
||||
self.theme = presentationData.theme
|
||||
self.iconNode.image = icon
|
||||
}
|
||||
}
|
||||
|
||||
func updateLayout(height: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
var iconInset: CGFloat = 0.0
|
||||
if let image = self.iconNode.image {
|
||||
iconInset = 22.0
|
||||
self.iconNode.frame = CGRect(x: 0.0, y: 4.0 + floorToScreenPixels((height - image.size.height) / 2.0), width: image.size.width, height: image.size.height)
|
||||
}
|
||||
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: 160.0, height: .greatestFiniteMagnitude))
|
||||
let titleFrame = CGRect(origin: CGPoint(x: -self.titleNode.insets.left + iconInset, y: self.titleNode.insets.top + floorToScreenPixels((height - titleSize.height) / 2.0)), size: titleSize)
|
||||
self.titleNode.frame = titleFrame
|
||||
|
||||
var width = titleSize.width - self.titleNode.insets.left - self.titleNode.insets.right + iconInset
|
||||
|
||||
if let titleBadgeView = self.titleBadgeView, let image = titleBadgeView.image {
|
||||
width += 4.0 + image.size.width
|
||||
titleBadgeView.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: titleFrame.minY + floorToScreenPixels((titleFrame.height - image.size.height) * 0.5) + 1.0), size: image.size)
|
||||
}
|
||||
|
||||
return width
|
||||
}
|
||||
|
||||
func updateArea(size: CGSize, sideInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.buttonNode.frame = CGRect(origin: CGPoint(x: -sideInset, y: 0.0), size: CGSize(width: size.width + sideInset * 2.0, height: size.height))
|
||||
|
||||
self.hitTestSlop = UIEdgeInsets(top: 0.0, left: -sideInset, bottom: 0.0, right: -sideInset)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user