[WIP] Premium setup

This commit is contained in:
Isaac 2024-03-15 15:10:58 +04:00
parent ae3ee3d063
commit d8026b4009
30 changed files with 1495 additions and 309 deletions

View File

@ -856,6 +856,9 @@ public protocol AutomaticBusinessMessageSetupScreenInitialData: AnyObject {
public protocol ChatbotSetupScreenInitialData: AnyObject {
}
public protocol BusinessIntroSetupScreenInitialData: AnyObject {
}
public protocol CollectibleItemInfoScreenInitialData: AnyObject {
var collectibleItemInfo: TelegramCollectibleItemInfo { get }
}
@ -960,6 +963,7 @@ public protocol SharedAccountContext: AnyObject {
func makeAutomaticBusinessMessageSetupScreenInitialData(context: AccountContext) -> Signal<AutomaticBusinessMessageSetupScreenInitialData, NoError>
func makeQuickReplySetupScreen(context: AccountContext, initialData: QuickReplySetupScreenInitialData) -> ViewController
func makeQuickReplySetupScreenInitialData(context: AccountContext) -> Signal<QuickReplySetupScreenInitialData, NoError>
func makeBusinessIntroSetupScreen(context: AccountContext) -> ViewController
func makeCollectibleItemInfoScreen(context: AccountContext, initialData: CollectibleItemInfoScreenInitialData) -> ViewController
func makeCollectibleItemInfoScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, subject: CollectibleItemInfoScreenSubject) -> Signal<CollectibleItemInfoScreenInitialData?, NoError>
func navigateToChatController(_ params: NavigateToChatControllerParams)

View File

@ -5,11 +5,13 @@ public final class Rectangle: Component {
private let color: UIColor
private let width: CGFloat?
private let height: CGFloat?
private let tag: NSObject?
public init(color: UIColor, width: CGFloat? = nil, height: CGFloat? = nil) {
public init(color: UIColor, width: CGFloat? = nil, height: CGFloat? = nil, tag: NSObject? = nil) {
self.color = color
self.width = width
self.height = height
self.tag = tag
}
public static func ==(lhs: Rectangle, rhs: Rectangle) -> Bool {
@ -25,7 +27,33 @@ public final class Rectangle: Component {
return true
}
public func update(view: UIView, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
public final class View: UIView, ComponentTaggedView {
fileprivate var componentTag: NSObject?
override public init(frame: CGRect) {
super.init(frame: frame)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func matches(tag: Any) -> Bool {
if let componentTag = self.componentTag {
let tag = tag as AnyObject
if componentTag === tag {
return true
}
}
return false
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
var size = availableSize
if let width = self.width {
size.width = min(size.width, width)
@ -35,6 +63,7 @@ public final class Rectangle: Component {
}
view.backgroundColor = self.color
view.componentTag = self.tag
return size
}

View File

@ -13,11 +13,13 @@ public final class VStack<ChildEnvironment: Equatable>: CombinedComponent {
private let items: [AnyComponentWithIdentity<ChildEnvironment>]
private let alignment: VStackAlignment
private let spacing: CGFloat
private let fillWidth: Bool
public init(_ items: [AnyComponentWithIdentity<ChildEnvironment>], alignment: VStackAlignment = .center, spacing: CGFloat) {
public init(_ items: [AnyComponentWithIdentity<ChildEnvironment>], alignment: VStackAlignment = .center, spacing: CGFloat, fillWidth: Bool = false) {
self.items = items
self.alignment = alignment
self.spacing = spacing
self.fillWidth = fillWidth
}
public static func ==(lhs: VStack<ChildEnvironment>, rhs: VStack<ChildEnvironment>) -> Bool {
@ -30,6 +32,9 @@ public final class VStack<ChildEnvironment: Equatable>: CombinedComponent {
if lhs.spacing != rhs.spacing {
return false
}
if lhs.fillWidth != rhs.fillWidth {
return false
}
return true
}
@ -48,6 +53,9 @@ public final class VStack<ChildEnvironment: Equatable>: CombinedComponent {
}
var size = CGSize(width: 0.0, height: 0.0)
if context.component.fillWidth {
size.width = context.availableSize.width
}
for child in updatedChildren {
size.height += child.size.height
size.width = max(size.width, child.size.width)

View File

@ -117,6 +117,7 @@ swift_library(
"//submodules/TelegramUI/Components/EmojiStatusSelectionComponent",
"//submodules/TelegramUI/Components/EntityKeyboard",
"//submodules/TelegramUI/Components/PremiumPeerShortcutComponent",
"//submodules/TelegramUI/Components/EmojiActionIconComponent",
],
visibility = [
"//visibility:public",

View File

@ -32,6 +32,7 @@ import ListActionItemComponent
import EmojiStatusSelectionComponent
import EmojiStatusComponent
import EntityKeyboard
import EmojiActionIconComponent
public enum PremiumSource: Equatable {
public static func == (lhs: PremiumSource, rhs: PremiumSource) -> Bool {
@ -2241,6 +2242,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
}
push(accountContext.sharedContext.makeChatbotSetupScreen(context: accountContext, initialData: initialData))
})
case .businessIntro:
push(accountContext.sharedContext.makeBusinessIntroSetupScreen(context: accountContext))
default:
fatalError()
}
@ -3715,89 +3718,6 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
}
}
private final class EmojiActionIconComponent: Component {
let context: AccountContext
let color: UIColor
let fileId: Int64?
let file: TelegramMediaFile?
init(
context: AccountContext,
color: UIColor,
fileId: Int64?,
file: TelegramMediaFile?
) {
self.context = context
self.color = color
self.fileId = fileId
self.file = file
}
static func ==(lhs: EmojiActionIconComponent, rhs: EmojiActionIconComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.color != rhs.color {
return false
}
if lhs.fileId != rhs.fileId {
return false
}
if lhs.file != rhs.file {
return false
}
return true
}
final class View: UIView {
private let icon = ComponentView<Empty>()
func update(component: EmojiActionIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let size = CGSize(width: 24.0, height: 24.0)
let _ = self.icon.update(
transition: .immediate,
component: AnyComponent(EmojiStatusComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
content: component.fileId.flatMap { .animation(
content: .customEmoji(fileId: $0),
size: CGSize(width: size.width * 2.0, height: size.height * 2.0),
placeholderColor: .lightGray,
themeColor: component.color,
loopMode: .forever
) } ?? .premium(color: component.color),
isVisibleForAnimations: false,
action: nil
)),
environment: {},
containerSize: size
)
let iconFrame = CGRect(origin: CGPoint(), size: size)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.addSubview(iconView)
}
iconView.frame = iconFrame
}
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class BadgeComponent: CombinedComponent {
let color: UIColor
let text: String

View File

@ -909,6 +909,34 @@ private func selectivePrivacySettingsControllerEntries(presentationData: Present
return entries
}
func generatePremiumCategoryIcon(size: CGSize, cornerRadius: CGFloat) -> UIImage {
return generateImage(size, contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
context.addPath(path.cgPath)
context.clip()
let colorsArray: [CGColor] = [
UIColor(rgb: 0xF161DD).cgColor,
UIColor(rgb: 0xF161DD).cgColor,
UIColor(rgb: 0x8d77ff).cgColor,
UIColor(rgb: 0xb56eec).cgColor,
UIColor(rgb: 0xb56eec).cgColor
]
var locations: [CGFloat] = [0.0, 0.15, 0.5, 0.85, 1.0]
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions())
if let image = generateTintedImage(image: UIImage(bundleImageName: "Premium/ButtonIcon"), color: UIColor(rgb: 0xffffff)), let cgImage = image.cgImage {
let imageSize = image.size.aspectFitted(CGSize(width: floor(size.width * 0.6), height: floor(size.height * 0.6)))
context.draw(cgImage, in: CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - imageSize.width) / 2.0), y: floorToScreenPixels((bounds.height - imageSize.height) / 2.0)), size: imageSize))
}
})!
}
func selectivePrivacySettingsController(
context: AccountContext,
kind: SelectivePrivacySettingsKind,
@ -1041,7 +1069,41 @@ func selectivePrivacySettingsController(
return state
}
if peerIds.isEmpty {
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .peerSelection(searchChatList: true, searchGroups: true, searchChannels: false), options: []))
enum AdditionalCategoryId: Int {
case premiumUsers
}
var displayPremiumCategory = false
switch kind {
case .groupInvitations:
displayPremiumCategory = true
default:
break
}
//TODO:localize
var additionalCategories: [ChatListNodeAdditionalCategory] = []
if displayPremiumCategory {
additionalCategories = [
ChatListNodeAdditionalCategory(
id: AdditionalCategoryId.premiumUsers.rawValue,
icon: generatePremiumCategoryIcon(size: CGSize(width: 40.0, height: 40.0), cornerRadius: 12.0),
smallIcon: generatePremiumCategoryIcon(size: CGSize(width: 22.0, height: 22.0), cornerRadius: 6.0),
title: "Premium Users"
)
]
}
let selectedCategories = Set<Int>()
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection(
title: "Add Users",
searchPlaceholder: "Search users and groups",
selectedChats: Set(),
additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories),
chatListFilters: nil,
onlyUsers: false
)), options: []))
addPeerDisposable.set((controller.result
|> take(1)
|> deliverOnMainQueue).start(next: { [weak controller] result in
@ -1128,7 +1190,15 @@ func selectivePrivacySettingsController(
}))
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
} else {
let controller = selectivePrivacyPeersController(context: context, title: title, initialPeers: peerIds, updated: { updatedPeerIds in
var displayPremiumCategory = false
switch kind {
case .groupInvitations:
displayPremiumCategory = true
default:
break
}
let controller = selectivePrivacyPeersController(context: context, title: title, initialPeers: peerIds, displayPremiumCategory: displayPremiumCategory, updated: { updatedPeerIds in
updateState { state in
if enable {
switch target {

View File

@ -248,7 +248,7 @@ private func selectivePrivacyPeersControllerEntries(presentationData: Presentati
return entries
}
public func selectivePrivacyPeersController(context: AccountContext, title: String, initialPeers: [EnginePeer.Id: SelectivePrivacyPeer], updated: @escaping ([EnginePeer.Id: SelectivePrivacyPeer]) -> Void) -> ViewController {
public func selectivePrivacyPeersController(context: AccountContext, title: String, initialPeers: [EnginePeer.Id: SelectivePrivacyPeer], displayPremiumCategory: Bool, updated: @escaping ([EnginePeer.Id: SelectivePrivacyPeer]) -> Void) -> ViewController {
let statePromise = ValuePromise(SelectivePrivacyPeersControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: SelectivePrivacyPeersControllerState())
let updateState: ((SelectivePrivacyPeersControllerState) -> SelectivePrivacyPeersControllerState) -> Void = { f in
@ -307,7 +307,33 @@ public func selectivePrivacyPeersController(context: AccountContext, title: Stri
removePeerDisposable.set(applyPeers.start())
}, addPeer: {
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .peerSelection(searchChatList: true, searchGroups: true, searchChannels: false), options: []))
enum AdditionalCategoryId: Int {
case premiumUsers
}
//TODO:localize
var additionalCategories: [ChatListNodeAdditionalCategory] = []
if displayPremiumCategory {
additionalCategories = [
ChatListNodeAdditionalCategory(
id: AdditionalCategoryId.premiumUsers.rawValue,
icon: generatePremiumCategoryIcon(size: CGSize(width: 40.0, height: 40.0), cornerRadius: 12.0),
smallIcon: generatePremiumCategoryIcon(size: CGSize(width: 22.0, height: 22.0), cornerRadius: 6.0),
title: "Premium Users"
)
]
}
let selectedCategories = Set<Int>()
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection(
title: "Add Users",
searchPlaceholder: "Search users and groups",
selectedChats: Set(),
additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories),
chatListFilters: nil,
onlyUsers: false
)), options: []))
addPeerDisposable.set((controller.result
|> take(1)
|> deliverOnMainQueue).start(next: { [weak controller] result in

View File

@ -436,9 +436,12 @@ swift_library(
"//submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen",
"//submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen",
"//submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen",
"//submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen",
"//submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController",
"//submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen",
"//submodules/TelegramUI/Components/StickerPickerScreen",
"//submodules/TelegramUI/Components/Chat/ChatEmptyNode",
"//submodules/TelegramUI/Components/Chat/ChatMediaInputStickerGridItem",
] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [],

View File

@ -0,0 +1,37 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatEmptyNode",
module_name = "ChatEmptyNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/AppBundle",
"//submodules/LocalizedPeerData",
"//submodules/TelegramStringFormatting",
"//submodules/AccountContext",
"//submodules/ChatPresentationInterfaceState",
"//submodules/WallpaperBackgroundNode",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/TelegramUI/Components/Chat/ChatLoadingNode",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/Markdown",
"//submodules/ReactionSelectionNode",
"//submodules/TelegramUI/Components/Chat/ChatMediaInputStickerGridItem",
],
visibility = [
"//visibility:public",
],
)

View File

@ -19,6 +19,7 @@ import MultilineTextComponent
import BalancedTextComponent
import Markdown
import ReactionSelectionNode
import ChatMediaInputStickerGridItem
private protocol ChatEmptyNodeContent {
func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize
@ -79,11 +80,11 @@ private final class ChatEmptyNodeRegularChatContent: ASDisplayNode, ChatEmptyNod
}
}
protocol ChatEmptyNodeStickerContentNode: ASDisplayNode {
public protocol ChatEmptyNodeStickerContentNode: ASDisplayNode {
var stickerNode: ChatMediaInputStickerGridItemNode { get }
}
final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeStickerContentNode, ChatEmptyNodeContent, UIGestureRecognizerDelegate {
public final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeStickerContentNode, ChatEmptyNodeContent, UIGestureRecognizerDelegate {
private let context: AccountContext
private let interaction: ChatPanelInterfaceInteraction?
@ -91,15 +92,16 @@ final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeSticke
private let textNode: ImmediateTextNode
private var stickerItem: ChatMediaInputStickerGridItem?
let stickerNode: ChatMediaInputStickerGridItemNode
public var stickerNode: ChatMediaInputStickerGridItemNode
private var currentTheme: PresentationTheme?
private var currentStrings: PresentationStrings?
private var didSetupSticker = false
private let disposable = MetaDisposable()
private var currentCustomStickerFile: TelegramMediaFile?
init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) {
public init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) {
self.context = context
self.interaction = interaction
@ -126,7 +128,7 @@ final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeSticke
self.addSubnode(self.stickerNode)
}
override func didLoad() {
override public func didLoad() {
super.didLoad()
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.stickerTapGesture(_:)))
@ -138,7 +140,7 @@ final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeSticke
self.disposable.dispose()
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
@ -149,18 +151,29 @@ final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeSticke
let _ = self.interaction?.sendSticker(.standalone(media: stickerItem.stickerItem.file), false, self.view, self.stickerNode.bounds, nil, [])
}
func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let isFirstTime = self.currentTheme == nil
if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings {
self.currentTheme = interfaceState.theme
self.currentStrings = interfaceState.strings
}
var customStickerFile: TelegramMediaFile?
let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper)
if case let .emptyChat(emptyChat) = subject, case let .customGreeting(stickerFile, title, text) = emptyChat {
customStickerFile = stickerFile
self.titleNode.attributedText = NSAttributedString(string: title, font: titleFont, textColor: serviceColor.primaryText)
self.textNode.attributedText = NSAttributedString(string: text, font: messageFont, textColor: serviceColor.primaryText)
} else {
self.titleNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_EmptyPlaceholder, font: titleFont, textColor: serviceColor.primaryText)
self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_GreetingText, font: messageFont, textColor: serviceColor.primaryText)
}
let previousCustomStickerFile = self.currentCustomStickerFile
self.currentCustomStickerFile = customStickerFile
let stickerSize: CGSize
let inset: CGFloat
if size.width == 320.0 {
@ -170,11 +183,13 @@ final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeSticke
stickerSize = CGSize(width: 160.0, height: 160.0)
inset = 15.0
}
if let item = self.stickerItem {
if let item = self.stickerItem, previousCustomStickerFile == customStickerFile {
self.stickerNode.updateLayout(item: item, size: stickerSize, isVisible: true, synchronousLoads: true)
} else if !self.didSetupSticker {
} else if !self.didSetupSticker || previousCustomStickerFile != customStickerFile {
let sticker: Signal<TelegramMediaFile?, NoError>
if let preloadedSticker = interfaceState.greetingData?.sticker {
if let customStickerFile {
sticker = .single(customStickerFile)
} else if let preloadedSticker = interfaceState.greetingData?.sticker {
sticker = preloadedSticker
} else {
sticker = self.context.engine.stickers.randomGreetingSticker()
@ -183,6 +198,19 @@ final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeSticke
}
}
if !isFirstTime, case let .emptyChat(emptyChat) = subject, case .customGreeting = emptyChat {
let previousStickerNode = self.stickerNode
previousStickerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousStickerNode] _ in
previousStickerNode?.removeFromSupernode()
})
previousStickerNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false)
self.stickerNode = ChatMediaInputStickerGridItemNode()
self.addSubnode(self.stickerNode)
self.stickerNode.layer.animateSpring(from: 0.001 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5)
self.stickerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
self.didSetupSticker = true
self.disposable.set((sticker
|> deliverOnMainQueue).startStrict(next: { [weak self] sticker in
@ -216,6 +244,10 @@ final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeSticke
let stickerPackItem = StickerPackItem(index: index, file: sticker, indexKeys: [])
let item = ChatMediaInputStickerGridItem(context: strongSelf.context, collectionId: collectionId, stickerPackInfo: nil, index: ItemCollectionViewEntryIndex(collectionIndex: 0, collectionId: collectionId, itemIndex: index), stickerItem: stickerPackItem, canManagePeerSpecificPack: nil, interfaceInteraction: nil, inputNodeInteraction: inputNodeInteraction, hasAccessory: false, theme: interfaceState.theme, large: true, selected: {})
strongSelf.stickerItem = item
if isFirstTime {
}
strongSelf.stickerNode.updateLayout(item: item, size: stickerSize, isVisible: true, synchronousLoads: true)
strongSelf.stickerNode.isVisibleInGrid = true
strongSelf.stickerNode.updateIsPanelVisible(true)
@ -252,7 +284,7 @@ final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeSticke
}
}
final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNodeStickerContentNode, ChatEmptyNodeContent, UIGestureRecognizerDelegate {
public final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNodeStickerContentNode, ChatEmptyNodeContent, UIGestureRecognizerDelegate {
private let context: AccountContext
private let interaction: ChatPanelInterfaceInteraction?
@ -260,7 +292,7 @@ final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNodeStickerC
private let textNode: ImmediateTextNode
private var stickerItem: ChatMediaInputStickerGridItem?
let stickerNode: ChatMediaInputStickerGridItemNode
public let stickerNode: ChatMediaInputStickerGridItemNode
private var currentTheme: PresentationTheme?
private var currentStrings: PresentationStrings?
@ -268,7 +300,7 @@ final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNodeStickerC
private var didSetupSticker = false
private let disposable = MetaDisposable()
init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) {
public init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) {
self.context = context
self.interaction = interaction
@ -295,7 +327,7 @@ final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNodeStickerC
self.addSubnode(self.stickerNode)
}
override func didLoad() {
override public func didLoad() {
super.didLoad()
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.stickerTapGesture(_:)))
@ -307,7 +339,7 @@ final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNodeStickerC
self.disposable.dispose()
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
@ -318,7 +350,7 @@ final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNodeStickerC
let _ = self.interaction?.sendSticker(.standalone(media: stickerItem.stickerItem.file), false, self.view, self.stickerNode.bounds, nil, [])
}
func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings {
self.currentTheme = interfaceState.theme
self.currentStrings = interfaceState.strings
@ -844,7 +876,7 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC
}
}
final class ChatEmptyNodeTopicChatContent: ASDisplayNode, ChatEmptyNodeContent, UIGestureRecognizerDelegate {
public final class ChatEmptyNodeTopicChatContent: ASDisplayNode, ChatEmptyNodeContent, UIGestureRecognizerDelegate {
private let context: AccountContext
private let titleNode: ImmediateTextNode
@ -855,7 +887,7 @@ final class ChatEmptyNodeTopicChatContent: ASDisplayNode, ChatEmptyNodeContent,
private let iconView: ComponentView<Empty>
init(context: AccountContext) {
public init(context: AccountContext) {
self.context = context
self.titleNode = ImmediateTextNode()
@ -880,7 +912,7 @@ final class ChatEmptyNodeTopicChatContent: ASDisplayNode, ChatEmptyNodeContent,
self.addSubnode(self.textNode)
}
func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper)
if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings {
self.currentTheme = interfaceState.theme
@ -955,7 +987,7 @@ final class ChatEmptyNodeTopicChatContent: ASDisplayNode, ChatEmptyNodeContent,
}
}
final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatEmptyNodeContent {
public final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatEmptyNodeContent {
private let isPremiumDisabled: Bool
private let interaction: ChatPanelInterfaceInteraction?
@ -969,7 +1001,7 @@ final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatEmptyNod
private var currentTheme: PresentationTheme?
private var currentStrings: PresentationStrings?
init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) {
public init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
self.isPremiumDisabled = premiumConfiguration.isPremiumDisabled
@ -1016,7 +1048,7 @@ final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatEmptyNod
}
}
func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper)
let maxWidth = min(200.0, size.width)
@ -1139,9 +1171,18 @@ private enum ChatEmptyNodeContentType: Equatable {
case premiumRequired
}
final class ChatEmptyNode: ASDisplayNode {
enum Subject {
case emptyChat(ChatHistoryNodeLoadState.EmptyType)
public final class ChatEmptyNode: ASDisplayNode {
public enum Subject {
public enum EmptyType: Equatable {
case generic
case joined
case clearedHistory
case topic
case botInfo
case customGreeting(sticker: TelegramMediaFile?, title: String, text: String)
}
case emptyChat(EmptyType)
case detailsPlaceholder
}
private let context: AccountContext
@ -1159,7 +1200,7 @@ final class ChatEmptyNode: ASDisplayNode {
private var content: (ChatEmptyNodeContentType, ASDisplayNode & ChatEmptyNodeContent)?
init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) {
public init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) {
self.context = context
self.interaction = interaction
@ -1172,14 +1213,14 @@ final class ChatEmptyNode: ASDisplayNode {
self.addSubnode(self.backgroundNode)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let result = super.hitTest(point, with: event) else {
return nil
}
return result
}
func animateFromLoadingNode(_ loadingNode: ChatLoadingNode) {
public func animateFromLoadingNode(_ loadingNode: ChatLoadingNode) {
guard let (_, node) = self.content else {
return
}
@ -1204,7 +1245,7 @@ final class ChatEmptyNode: ASDisplayNode {
}
}
func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: Subject, loadingNode: ChatLoadingNode?, backgroundNode: WallpaperBackgroundNode?, size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) {
public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: Subject, loadingNode: ChatLoadingNode?, backgroundNode: WallpaperBackgroundNode?, size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) {
self.wallpaperBackgroundNode = backgroundNode
if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings {
@ -1224,7 +1265,9 @@ final class ChatEmptyNode: ASDisplayNode {
case .detailsPlaceholder:
contentType = .regular
case let .emptyChat(emptyType):
if case .customChatContents = interfaceState.subject {
if case .customGreeting = emptyType {
contentType = .greeting
} else if case .customChatContents = interfaceState.subject {
contentType = .cloud
} else if case .replyThread = interfaceState.chatLocation {
if case .topic = emptyType {

View File

@ -0,0 +1,30 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatMediaInputStickerGridItem",
module_name = "ChatMediaInputStickerGridItem",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AsyncDisplayKit",
"//submodules/Postbox",
"//submodules/TelegramPresentationData",
"//submodules/StickerResources",
"//submodules/AccountContext",
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
"//submodules/ShimmerEffect",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
"//submodules/ChatPresentationInterfaceState",
],
visibility = [
"//visibility:public",
],
)

View File

@ -14,25 +14,25 @@ import ShimmerEffect
import ChatControllerInteraction
import ChatPresentationInterfaceState
enum ChatMediaInputStickerGridSectionAccessory {
public enum ChatMediaInputStickerGridSectionAccessory {
case none
case setup
case clear
}
final class ChatMediaInputStickerGridSection: GridSection {
let collectionId: ItemCollectionId
let collectionInfo: StickerPackCollectionInfo?
let accessory: ChatMediaInputStickerGridSectionAccessory
let interaction: ChatMediaInputNodeInteraction
let theme: PresentationTheme
let height: CGFloat = 26.0
public final class ChatMediaInputStickerGridSection: GridSection {
public let collectionId: ItemCollectionId
public let collectionInfo: StickerPackCollectionInfo?
public let accessory: ChatMediaInputStickerGridSectionAccessory
public let interaction: ChatMediaInputNodeInteraction
public let theme: PresentationTheme
public let height: CGFloat = 26.0
var hashValue: Int {
public var hashValue: Int {
return self.collectionId.hashValue
}
init(collectionId: ItemCollectionId, collectionInfo: StickerPackCollectionInfo?, accessory: ChatMediaInputStickerGridSectionAccessory, theme: PresentationTheme, interaction: ChatMediaInputNodeInteraction) {
public init(collectionId: ItemCollectionId, collectionInfo: StickerPackCollectionInfo?, accessory: ChatMediaInputStickerGridSectionAccessory, theme: PresentationTheme, interaction: ChatMediaInputNodeInteraction) {
self.collectionId = collectionId
self.collectionInfo = collectionInfo
self.accessory = accessory
@ -40,7 +40,7 @@ final class ChatMediaInputStickerGridSection: GridSection {
self.interaction = interaction
}
func isEqual(to: GridSection) -> Bool {
public func isEqual(to: GridSection) -> Bool {
if let to = to as? ChatMediaInputStickerGridSection {
return self.collectionId == to.collectionId && self.theme === to.theme
} else {
@ -48,20 +48,20 @@ final class ChatMediaInputStickerGridSection: GridSection {
}
}
func node() -> ASDisplayNode {
public func node() -> ASDisplayNode {
return ChatMediaInputStickerGridSectionNode(collectionInfo: self.collectionInfo, accessory: self.accessory, theme: self.theme, interaction: self.interaction)
}
}
private let sectionTitleFont = Font.medium(12.0)
final class ChatMediaInputStickerGridSectionNode: ASDisplayNode {
let titleNode: ASTextNode
let setupNode: HighlightableButtonNode?
let interaction: ChatMediaInputNodeInteraction
let accessory: ChatMediaInputStickerGridSectionAccessory
public final class ChatMediaInputStickerGridSectionNode: ASDisplayNode {
public let titleNode: ASTextNode
public let setupNode: HighlightableButtonNode?
public let interaction: ChatMediaInputNodeInteraction
public let accessory: ChatMediaInputStickerGridSectionAccessory
init(collectionInfo: StickerPackCollectionInfo?, accessory: ChatMediaInputStickerGridSectionAccessory, theme: PresentationTheme, interaction: ChatMediaInputNodeInteraction) {
public init(collectionInfo: StickerPackCollectionInfo?, accessory: ChatMediaInputStickerGridSectionAccessory, theme: PresentationTheme, interaction: ChatMediaInputNodeInteraction) {
self.interaction = interaction
self.titleNode = ASTextNode()
self.titleNode.isUserInteractionEnabled = false
@ -91,7 +91,7 @@ final class ChatMediaInputStickerGridSectionNode: ASDisplayNode {
self.setupNode?.addTarget(self, action: #selector(self.setupPressed), forControlEvents: .touchUpInside)
}
override func layout() {
override public func layout() {
super.layout()
let bounds = self.bounds
@ -116,20 +116,20 @@ final class ChatMediaInputStickerGridSectionNode: ASDisplayNode {
}
}
final class ChatMediaInputStickerGridItem: GridItem {
let context: AccountContext
let index: ItemCollectionViewEntryIndex
let stickerItem: StickerPackItem
let selected: () -> Void
let interfaceInteraction: ChatControllerInteraction?
let inputNodeInteraction: ChatMediaInputNodeInteraction
let theme: PresentationTheme
let large: Bool
let isLocked: Bool
public final class ChatMediaInputStickerGridItem: GridItem {
public let context: AccountContext
public let index: ItemCollectionViewEntryIndex
public let stickerItem: StickerPackItem
public let selected: () -> Void
public let interfaceInteraction: ChatControllerInteraction?
public let inputNodeInteraction: ChatMediaInputNodeInteraction
public let theme: PresentationTheme
public let large: Bool
public let isLocked: Bool
let section: GridSection?
public let section: GridSection?
init(context: AccountContext, collectionId: ItemCollectionId, stickerPackInfo: StickerPackCollectionInfo?, index: ItemCollectionViewEntryIndex, stickerItem: StickerPackItem, canManagePeerSpecificPack: Bool?, interfaceInteraction: ChatControllerInteraction?, inputNodeInteraction: ChatMediaInputNodeInteraction, hasAccessory: Bool, theme: PresentationTheme, large: Bool = false, isLocked: Bool = false, selected: @escaping () -> Void) {
public init(context: AccountContext, collectionId: ItemCollectionId, stickerPackInfo: StickerPackCollectionInfo?, index: ItemCollectionViewEntryIndex, stickerItem: StickerPackItem, canManagePeerSpecificPack: Bool?, interfaceInteraction: ChatControllerInteraction?, inputNodeInteraction: ChatMediaInputNodeInteraction, hasAccessory: Bool, theme: PresentationTheme, large: Bool = false, isLocked: Bool = false, selected: @escaping () -> Void) {
self.context = context
self.index = index
self.stickerItem = stickerItem
@ -145,7 +145,7 @@ final class ChatMediaInputStickerGridItem: GridItem {
self.section = ChatMediaInputStickerGridSection(collectionId: collectionId, collectionInfo: stickerPackInfo, accessory: accessory, theme: theme, interaction: inputNodeInteraction)
}
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
public func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
let node = ChatMediaInputStickerGridItemNode()
node.interfaceInteraction = self.interfaceInteraction
node.inputNodeInteraction = self.inputNodeInteraction
@ -153,7 +153,7 @@ final class ChatMediaInputStickerGridItem: GridItem {
return node
}
func update(node: GridItemNode) {
public func update(node: GridItemNode) {
guard let node = node as? ChatMediaInputStickerGridItemNode else {
assertionFailure()
return
@ -164,26 +164,26 @@ final class ChatMediaInputStickerGridItem: GridItem {
}
}
final class ChatMediaInputStickerGridItemNode: GridItemNode {
public final class ChatMediaInputStickerGridItemNode: GridItemNode {
private var currentState: (AccountContext, StickerPackItem, CGSize)?
private var currentSize: CGSize?
let imageNode: TransformImageNode
private(set) var animationNode: AnimatedStickerNode?
private(set) var placeholderNode: StickerShimmerEffectNode?
public let imageNode: TransformImageNode
public private(set) var animationNode: AnimatedStickerNode?
public private(set) var placeholderNode: StickerShimmerEffectNode?
private var lockBackground: UIVisualEffectView?
private var lockTintView: UIView?
private var lockIconNode: ASImageNode?
var isLocked: Bool?
public var isLocked: Bool?
private var didSetUpAnimationNode = false
private var item: ChatMediaInputStickerGridItem?
private let stickerFetchedDisposable = MetaDisposable()
var currentIsPreviewing = false
public var currentIsPreviewing = false
override var isVisibleInGrid: Bool {
override public var isVisibleInGrid: Bool {
didSet {
self.updateVisibility()
}
@ -192,15 +192,15 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
private var isPanelVisible = false
private var isPlaying = false
var interfaceInteraction: ChatControllerInteraction?
var inputNodeInteraction: ChatMediaInputNodeInteraction?
var selected: (() -> Void)?
public var interfaceInteraction: ChatControllerInteraction?
public var inputNodeInteraction: ChatMediaInputNodeInteraction?
public var selected: (() -> Void)?
var stickerPackItem: StickerPackItem? {
public var stickerPackItem: StickerPackItem? {
return self.currentState?.1
}
override init() {
override public init() {
self.imageNode = TransformImageNode()
self.placeholderNode = StickerShimmerEffectNode()
self.placeholderNode?.isUserInteractionEnabled = false
@ -244,13 +244,13 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
}
}
override func didLoad() {
override public func didLoad() {
super.didLoad()
self.imageNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:))))
}
override func updateLayout(item: GridItem, size: CGSize, isVisible: Bool, synchronousLoads: Bool) {
override public func updateLayout(item: GridItem, size: CGSize, isVisible: Bool, synchronousLoads: Bool) {
guard let item = item as? ChatMediaInputStickerGridItem else {
return
}
@ -392,13 +392,13 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
}
}
override func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) {
override public func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) {
if let placeholderNode = self.placeholderNode {
placeholderNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: absoluteRect.minX + placeholderNode.frame.minX, y: absoluteRect.minY + placeholderNode.frame.minY), size: placeholderNode.frame.size), within: containerSize)
}
}
@objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) {
@objc private func imageNodeTap(_ recognizer: UITapGestureRecognizer) {
if self.imageNode.layer.animation(forKey: "opacity") != nil {
return
}
@ -411,18 +411,18 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
}
}
func transitionNode() -> ASDisplayNode? {
public func transitionNode() -> ASDisplayNode? {
return self.imageNode
}
func updateIsPanelVisible(_ isPanelVisible: Bool) {
public func updateIsPanelVisible(_ isPanelVisible: Bool) {
if self.isPanelVisible != isPanelVisible {
self.isPanelVisible = isPanelVisible
self.updateVisibility()
}
}
func updateVisibility() {
public func updateVisibility() {
guard let item = self.item else {
return
}
@ -444,7 +444,7 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
}
}
func updatePreviewing(animated: Bool) {
public func updatePreviewing(animated: Bool) {
var isPreviewing = false
if let (_, item, _) = self.currentState, let interaction = self.inputNodeInteraction {
isPreviewing = interaction.previewedStickerPackItemFile?.id == item.file.id

View File

@ -0,0 +1,22 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "EmojiActionIconComponent",
module_name = "EmojiActionIconComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/TelegramCore",
"//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/AccountContext",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,107 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramCore
import EmojiStatusComponent
import AccountContext
public final class EmojiActionIconComponent: Component {
public let context: AccountContext
public let color: UIColor
public let fileId: Int64?
public let file: TelegramMediaFile?
public init(
context: AccountContext,
color: UIColor,
fileId: Int64?,
file: TelegramMediaFile?
) {
self.context = context
self.color = color
self.fileId = fileId
self.file = file
}
public static func ==(lhs: EmojiActionIconComponent, rhs: EmojiActionIconComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.color != rhs.color {
return false
}
if lhs.fileId != rhs.fileId {
return false
}
if lhs.file != rhs.file {
return false
}
return true
}
public final class View: UIView {
private var icon: ComponentView<Empty>?
func update(component: EmojiActionIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let size = CGSize(width: 24.0, height: 24.0)
if let fileId = component.fileId {
let icon: ComponentView<Empty>
if let current = self.icon {
icon = current
} else {
icon = ComponentView()
self.icon = icon
}
let content: EmojiStatusComponent.AnimationContent
if let file = component.file {
content = .file(file: file)
} else {
content = .customEmoji(fileId: fileId)
}
let _ = icon.update(
transition: .immediate,
component: AnyComponent(EmojiStatusComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
content: .animation(
content: content,
size: size,
placeholderColor: .lightGray,
themeColor: component.color,
loopMode: .forever
),
isVisibleForAnimations: false,
action: nil
)),
environment: {},
containerSize: size
)
let iconFrame = CGRect(origin: CGPoint(), size: size)
if let iconView = icon.view {
if iconView.superview == nil {
self.addSubview(iconView)
}
iconView.frame = iconFrame
}
} else {
if let icon = self.icon {
self.icon = nil
icon.view?.removeFromSuperview()
}
}
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -11,6 +11,7 @@ import AccountContext
public final class ListMultilineTextFieldItemComponent: Component {
public final class ExternalState {
public fileprivate(set) var hasText: Bool = false
public fileprivate(set) var text: NSAttributedString = NSAttributedString()
public init() {
}
@ -206,6 +207,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
transition: transition,
component: AnyComponent(TextFieldComponent(
context: component.context,
theme: component.theme,
strings: component.strings,
externalState: self.textFieldExternalState,
fontSize: 17.0,
@ -266,6 +268,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
self.separatorInset = 16.0
component.externalState?.hasText = self.textFieldExternalState.hasText
component.externalState?.text = self.textFieldExternalState.text
return size
}

View File

@ -24,6 +24,7 @@ public final class ListSectionComponent: Component {
public let header: AnyComponent<Empty>?
public let footer: AnyComponent<Empty>?
public let items: [AnyComponentWithIdentity<Empty>]
public let itemUpdateOrder: [AnyHashable]?
public let displaySeparators: Bool
public let extendsItemHighlightToSection: Bool
@ -33,6 +34,7 @@ public final class ListSectionComponent: Component {
header: AnyComponent<Empty>?,
footer: AnyComponent<Empty>?,
items: [AnyComponentWithIdentity<Empty>],
itemUpdateOrder: [AnyHashable]? = nil,
displaySeparators: Bool = true,
extendsItemHighlightToSection: Bool = false
) {
@ -41,6 +43,7 @@ public final class ListSectionComponent: Component {
self.header = header
self.footer = footer
self.items = items
self.itemUpdateOrder = itemUpdateOrder
self.displaySeparators = displaySeparators
self.extendsItemHighlightToSection = extendsItemHighlightToSection
}
@ -61,6 +64,9 @@ public final class ListSectionComponent: Component {
if lhs.items != rhs.items {
return false
}
if lhs.itemUpdateOrder != rhs.itemUpdateOrder {
return false
}
if lhs.displaySeparators != rhs.displaySeparators {
return false
}
@ -204,7 +210,41 @@ public final class ListSectionComponent: Component {
var innerContentHeight: CGFloat = 0.0
var validItemIds: [AnyHashable] = []
struct ReadyItem {
var index: Int
var itemId: AnyHashable
var itemView: ItemView
var itemTransition: Transition
var itemSize: CGSize
init(index: Int, itemId: AnyHashable, itemView: ItemView, itemTransition: Transition, itemSize: CGSize) {
self.index = index
self.itemId = itemId
self.itemView = itemView
self.itemTransition = itemTransition
self.itemSize = itemSize
}
}
var readyItems: [ReadyItem] = []
var itemUpdateOrder: [Int] = []
if let itemUpdateOrderValue = component.itemUpdateOrder {
for id in itemUpdateOrderValue {
if let index = component.items.firstIndex(where: { $0.id == id }) {
if !itemUpdateOrder.contains(index) {
itemUpdateOrder.append(index)
}
}
}
}
for i in 0 ..< component.items.count {
if !itemUpdateOrder.contains(i) {
itemUpdateOrder.append(i)
}
}
for i in itemUpdateOrder {
let item = component.items[i]
let itemId = item.id
validItemIds.append(itemId)
@ -226,17 +266,29 @@ public final class ListSectionComponent: Component {
environment: {},
containerSize: CGSize(width: availableSize.width, height: availableSize.height)
)
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: innerContentHeight), size: itemSize)
if let itemComponentView = itemView.contents.view {
if itemComponentView.superview == nil {
itemView.addSubview(itemComponentView)
self.contentItemContainerView.addSubview(itemView)
self.contentSeparatorContainerLayer.addSublayer(itemView.separatorLayer)
self.contentHighlightContainerLayer.addSublayer(itemView.highlightLayer)
transition.animateAlpha(view: itemView, from: 0.0, to: 1.0)
transition.animateAlpha(layer: itemView.separatorLayer, from: 0.0, to: 1.0)
transition.animateAlpha(layer: itemView.highlightLayer, from: 0.0, to: 1.0)
readyItems.append(ReadyItem(
index: i,
itemId: itemId,
itemView: itemView,
itemTransition: itemTransition,
itemSize: itemSize
))
}
for readyItem in readyItems.sorted(by: { $0.index < $1.index }) {
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: innerContentHeight), size: readyItem.itemSize)
if let itemComponentView = readyItem.itemView.contents.view {
if itemComponentView.superview == nil {
readyItem.itemView.addSubview(itemComponentView)
self.contentItemContainerView.addSubview(readyItem.itemView)
self.contentSeparatorContainerLayer.addSublayer(readyItem.itemView.separatorLayer)
self.contentHighlightContainerLayer.addSublayer(readyItem.itemView.highlightLayer)
transition.animateAlpha(view: readyItem.itemView, from: 0.0, to: 1.0)
transition.animateAlpha(layer: readyItem.itemView.separatorLayer, from: 0.0, to: 1.0)
transition.animateAlpha(layer: readyItem.itemView.highlightLayer, from: 0.0, to: 1.0)
let itemId = readyItem.itemId
if let itemComponentView = itemComponentView as? ChildView {
itemComponentView.customUpdateIsHighlighted = { [weak self] isHighlighted in
guard let self else {
@ -250,20 +302,20 @@ public final class ListSectionComponent: Component {
if let itemComponentView = itemComponentView as? ChildView {
separatorInset = itemComponentView.separatorInset
}
itemTransition.setFrame(view: itemView, frame: itemFrame)
readyItem.itemTransition.setFrame(view: readyItem.itemView, frame: itemFrame)
let itemSeparatorTopOffset: CGFloat = i == 0 ? 0.0 : -UIScreenPixel
let itemSeparatorTopOffset: CGFloat = readyItem.index == 0 ? 0.0 : -UIScreenPixel
let itemHighlightFrame = CGRect(origin: CGPoint(x: itemFrame.minX, y: itemFrame.minY + itemSeparatorTopOffset), size: CGSize(width: itemFrame.width, height: itemFrame.height - itemSeparatorTopOffset))
itemTransition.setFrame(layer: itemView.highlightLayer, frame: itemHighlightFrame)
readyItem.itemTransition.setFrame(layer: readyItem.itemView.highlightLayer, frame: itemHighlightFrame)
itemTransition.setFrame(view: itemComponentView, frame: CGRect(origin: CGPoint(), size: itemFrame.size))
readyItem.itemTransition.setFrame(view: itemComponentView, frame: CGRect(origin: CGPoint(), size: itemFrame.size))
let itemSeparatorFrame = CGRect(origin: CGPoint(x: separatorInset, y: itemFrame.maxY - UIScreenPixel), size: CGSize(width: availableSize.width - separatorInset, height: UIScreenPixel))
itemTransition.setFrame(layer: itemView.separatorLayer, frame: itemSeparatorFrame)
readyItem.itemTransition.setFrame(layer: readyItem.itemView.separatorLayer, frame: itemSeparatorFrame)
let separatorAlpha: CGFloat
if component.displaySeparators {
if i != component.items.count - 1 {
if readyItem.index != component.items.count - 1 {
separatorAlpha = 1.0
} else {
separatorAlpha = 0.0
@ -271,11 +323,12 @@ public final class ListSectionComponent: Component {
} else {
separatorAlpha = 0.0
}
itemTransition.setAlpha(layer: itemView.separatorLayer, alpha: separatorAlpha)
itemView.separatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor
readyItem.itemTransition.setAlpha(layer: readyItem.itemView.separatorLayer, alpha: separatorAlpha)
readyItem.itemView.separatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor
}
innerContentHeight += itemSize.height
innerContentHeight += readyItem.itemSize.height
}
var removedItemIds: [AnyHashable] = []
for (id, itemView) in self.itemViews {
if !validItemIds.contains(id) {

View File

@ -776,6 +776,7 @@ public final class MessageInputPanelComponent: Component {
transition: .immediate,
component: AnyComponent(TextFieldComponent(
context: component.context,
theme: component.theme,
strings: component.strings,
externalState: self.textFieldExternalState,
fontSize: 17.0,

View File

@ -0,0 +1,47 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "BusinessIntroSetupScreen",
module_name = "BusinessIntroSetupScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/AccountContext",
"//submodules/PresentationDataUtils",
"//submodules/Markdown",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/LocationUI",
"//submodules/AppBundle",
"//submodules/Geocoding",
"//submodules/TelegramUI/Components/Chat/ChatEmptyNode",
"//submodules/WallpaperBackgroundNode",
"//submodules/ChatPresentationInterfaceState",
"//submodules/TelegramUI/Components/EntityKeyboard",
"//submodules/TelegramUI/Components/PeerAllowedReactionsScreen",
"//submodules/TelegramUI/Components/EmojiActionIconComponent",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,662 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import PresentationDataUtils
import AccountContext
import ComponentFlow
import ViewControllerComponent
import MultilineTextComponent
import BalancedTextComponent
import ListSectionComponent
import ListActionItemComponent
import ListMultilineTextFieldItemComponent
import BundleIconComponent
import LottieComponent
import EntityKeyboard
import PeerAllowedReactionsScreen
import EmojiActionIconComponent
final class BusinessIntroSetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
init(
context: AccountContext
) {
self.context = context
}
static func ==(lhs: BusinessIntroSetupScreenComponent, rhs: BusinessIntroSetupScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
private final class ScrollView: UIScrollView {
override func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
}
final class View: UIView, UIScrollViewDelegate {
private let topOverscrollLayer = SimpleLayer()
private let scrollView: ScrollView
private let navigationTitle = ComponentView<Empty>()
private let introContent = ComponentView<Empty>()
private let introSection = ComponentView<Empty>()
private let deleteSection = ComponentView<Empty>()
private var isUpdating: Bool = false
private var component: BusinessIntroSetupScreenComponent?
private(set) weak var state: EmptyComponentState?
private var environment: EnvironmentType?
private let introPlaceholderTag = NSObject()
private let titleInputState = ListMultilineTextFieldItemComponent.ExternalState()
private let titleInputTag = NSObject()
private var resetTitle: String?
private let textInputState = ListMultilineTextFieldItemComponent.ExternalState()
private let textInputTag = NSObject()
private var resetText: String?
private var stickerFile: TelegramMediaFile?
private var stickerContent: EmojiPagerContentComponent?
private var stickerContentDisposable: Disposable?
private var displayStickerInput: Bool = false
private var stickerSelectionControl: ComponentView<Empty>?
override init(frame: CGRect) {
self.scrollView = ScrollView()
self.scrollView.showsVerticalScrollIndicator = true
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.scrollsToTop = false
self.scrollView.delaysContentTouches = false
self.scrollView.canCancelContentTouches = true
self.scrollView.contentInsetAdjustmentBehavior = .never
if #available(iOS 13.0, *) {
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
}
self.scrollView.alwaysBounceVertical = true
super.init(frame: frame)
self.scrollView.delegate = self
self.addSubview(self.scrollView)
self.scrollView.layer.addSublayer(self.topOverscrollLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.stickerContentDisposable?.dispose()
}
func scrollToTop() {
self.scrollView.setContentOffset(CGPoint(), animated: true)
}
func attemptNavigation(complete: @escaping () -> Void) -> Bool {
guard let component = self.component, let environment = self.environment else {
return true
}
let _ = component
let _ = environment
return true
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.updateScrolling(transition: .immediate)
}
private var scrolledUp = true
private func updateScrolling(transition: Transition) {
let navigationRevealOffsetY: CGFloat = 0.0
let navigationAlphaDistance: CGFloat = 16.0
let navigationAlpha: CGFloat = max(0.0, min(1.0, (self.scrollView.contentOffset.y - navigationRevealOffsetY) / navigationAlphaDistance))
if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar {
transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha)
transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha)
}
var scrolledUp = false
if navigationAlpha < 0.5 {
scrolledUp = true
} else if navigationAlpha > 0.5 {
scrolledUp = false
}
if self.scrolledUp != scrolledUp {
self.scrolledUp = scrolledUp
if !self.isUpdating {
self.state?.updated()
}
}
if let navigationTitleView = self.navigationTitle.view {
transition.setAlpha(view: navigationTitleView, alpha: 1.0)
}
}
func update(component: BusinessIntroSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
if self.component == nil {
}
if self.stickerContentDisposable == nil {
let stickerContent = EmojiPagerContentComponent.stickerInputData(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks],
stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers],
chatPeerId: nil,
hasSearch: true,
hasTrending: true,
forceHasPremium: true
)
self.stickerContentDisposable = (stickerContent
|> deliverOnMainQueue).start(next: { [weak self] stickerContent in
guard let self else {
return
}
self.stickerContent = stickerContent
stickerContent.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { [weak self] _, item, _, _, _, _ in
guard let self else {
return
}
guard let itemFile = item.itemFile else {
return
}
self.stickerFile = itemFile
self.displayStickerInput = false
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.25))
}
},
deleteBackwards: {
},
openStickerSettings: {
},
openFeatured: {
},
openSearch: {
},
addGroupAction: { _, _, _ in
},
clearGroup: { _ in
},
editAction: { _ in
},
pushController: { c in
},
presentController: { c in
},
presentGlobalOverlayController: { c in
},
navigationController: {
return nil
},
requestUpdate: { _ in
},
updateSearchQuery: { _ in
},
updateScrollingToItemGroup: {
},
onScroll: {},
chatPeerId: nil,
peekBehavior: nil,
customLayout: nil,
externalBackground: nil,
externalExpansionView: nil,
customContentView: nil,
useOpaqueTheme: true,
hideBackground: false,
stateContext: nil,
addImage: nil
)
if !self.isUpdating {
self.state?.updated(transition: .immediate)
}
})
}
let environment = environment[EnvironmentType.self].value
let themeUpdated = self.environment?.theme !== environment.theme
self.environment = environment
self.component = component
self.state = state
let alphaTransition: Transition
if !transition.animation.isImmediate {
alphaTransition = .easeInOut(duration: 0.25)
} else {
alphaTransition = .immediate
}
if themeUpdated {
self.backgroundColor = environment.theme.list.blocksBackgroundColor
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let _ = alphaTransition
let _ = presentationData
//TODO:localize
let navigationTitleSize = self.navigationTitle.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "Intro", font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
horizontalAlignment: .center
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0)
)
let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) / 2.0), y: environment.statusBarHeight + floor((environment.navigationHeight - environment.statusBarHeight - navigationTitleSize.height) / 2.0)), size: navigationTitleSize)
if let navigationTitleView = self.navigationTitle.view {
if navigationTitleView.superview == nil {
if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar {
navigationBar.view.addSubview(navigationTitleView)
}
}
transition.setFrame(view: navigationTitleView, frame: navigationTitleFrame)
}
let bottomContentInset: CGFloat = 24.0
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let sectionSpacing: CGFloat = 24.0
var contentHeight: CGFloat = 0.0
contentHeight += environment.navigationHeight
contentHeight += 26.0
var introSectionItems: [AnyComponentWithIdentity<Empty>] = []
introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(Rectangle(color: .clear, height: 346.0, tag: self.introPlaceholderTag))))
introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(ListMultilineTextFieldItemComponent(
externalState: self.titleInputState,
context: component.context,
theme: environment.theme,
strings: environment.strings,
initialText: "",
resetText: self.resetTitle.flatMap {
return ListMultilineTextFieldItemComponent.ResetText(value: $0)
},
placeholder: "Enter Title",
autocapitalizationType: .none,
autocorrectionType: .no,
characterLimit: 256,
allowEmptyLines: false,
updated: { _ in
},
textUpdateTransition: .spring(duration: 0.4),
tag: self.titleInputTag
))))
self.resetTitle = nil
introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(ListMultilineTextFieldItemComponent(
externalState: self.textInputState,
context: component.context,
theme: environment.theme,
strings: environment.strings,
initialText: "",
resetText: self.resetText.flatMap {
return ListMultilineTextFieldItemComponent.ResetText(value: $0)
},
placeholder: "Enter Message",
autocapitalizationType: .none,
autocorrectionType: .no,
characterLimit: 256,
allowEmptyLines: false,
updated: { _ in
},
textUpdateTransition: .spring(duration: 0.4),
tag: self.textInputTag
))))
self.resetText = nil
let stickerIcon: ListActionItemComponent.Icon
if let stickerFile = self.stickerFile {
stickerIcon = ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent(
context: component.context,
color: environment.theme.list.itemPrimaryTextColor,
fileId: stickerFile.fileId.id,
file: stickerFile
))))
} else {
stickerIcon = ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Random",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemSecondaryTextColor
)),
maximumNumberOfLines: 1
))))
}
introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Choose Sticker",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
icon: stickerIcon,
accessory: .none,
action: { [weak self] _ in
guard let self else {
return
}
self.displayStickerInput = true
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.5))
}
}
))))
let introSectionSize = self.introSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "CUSTOMIZE YOUR INTRO",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "You can customize the message people see before they start a chat with you.",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
items: introSectionItems,
itemUpdateOrder: introSectionItems.map(\.id).reversed()
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let introSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: introSectionSize)
if let introSectionView = self.introSection.view {
if introSectionView.superview == nil {
self.scrollView.addSubview(introSectionView)
self.introSection.parentState = state
}
transition.setFrame(view: introSectionView, frame: introSectionFrame)
}
contentHeight += introSectionSize.height
contentHeight += sectionSpacing
let titleText: String
if self.titleInputState.text.string.isEmpty {
titleText = "No messages here yet..."
} else {
titleText = self.titleInputState.text.string
}
let textText: String
if self.textInputState.text.string.isEmpty {
textText = "Send a message or tap on the greeting below"
} else {
textText = self.textInputState.text.string
}
let introContentSize = self.introContent.update(
transition: transition,
component: AnyComponent(ChatIntroItemComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
stickerFile: stickerFile,
title: titleText,
text: textText
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
if let introContentView = self.introContent.view {
if introContentView.superview == nil {
if let placeholderView = self.introSection.findTaggedView(tag: self.introPlaceholderTag) {
placeholderView.addSubview(introContentView)
}
}
transition.setFrame(view: introContentView, frame: CGRect(origin: CGPoint(), size: introContentSize))
}
let displayDelete = !self.titleInputState.text.string.isEmpty || !self.textInputState.text.string.isEmpty || self.stickerFile != nil
var deleteSectionHeight: CGFloat = 0.0
deleteSectionHeight += sectionSpacing
let deleteSectionSize = self.deleteSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: nil,
footer: nil,
items: [
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Reset to Default",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemDestructiveColor
)),
maximumNumberOfLines: 1
))),
], alignment: .center, spacing: 2.0, fillWidth: true)),
accessory: nil,
action: { [weak self] _ in
guard let self else {
return
}
self.resetTitle = ""
self.resetText = ""
self.stickerFile = nil
self.state?.updated(transition: .spring(duration: 0.4))
}
)))
],
displaySeparators: false
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let deleteSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + deleteSectionHeight), size: deleteSectionSize)
if let deleteSectionView = self.deleteSection.view {
if deleteSectionView.superview == nil {
self.scrollView.addSubview(deleteSectionView)
}
transition.setFrame(view: deleteSectionView, frame: deleteSectionFrame)
if displayDelete {
alphaTransition.setAlpha(view: deleteSectionView, alpha: 1.0)
} else {
alphaTransition.setAlpha(view: deleteSectionView, alpha: 0.0)
}
}
deleteSectionHeight += deleteSectionSize.height
if displayDelete {
contentHeight += deleteSectionHeight
}
contentHeight += bottomContentInset
var inputHeight: CGFloat = environment.inputHeight
if self.displayStickerInput, let stickerContent = self.stickerContent {
let stickerSelectionControl: ComponentView<Empty>
var animateIn = false
if let current = self.stickerSelectionControl {
stickerSelectionControl = current
} else {
animateIn = true
stickerSelectionControl = ComponentView()
self.stickerSelectionControl = stickerSelectionControl
}
var selectedItems = Set<MediaId>()
if let stickerFile = self.stickerFile {
selectedItems.insert(stickerFile.fileId)
}
let stickerSelectionControlSize = stickerSelectionControl.update(
transition: animateIn ? .immediate : transition,
component: AnyComponent(EmojiSelectionComponent(
theme: environment.theme,
strings: environment.strings,
sideInset: environment.safeInsets.left,
bottomInset: environment.safeInsets.bottom,
deviceMetrics: environment.deviceMetrics,
emojiContent: stickerContent.withSelectedItems(selectedItems),
backgroundIconColor: nil,
backgroundColor: environment.theme.list.itemBlocksBackgroundColor,
separatorColor: environment.theme.list.itemBlocksSeparatorColor,
backspace: nil
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: min(340.0, max(50.0, availableSize.height - 200.0)))
)
let stickerSelectionControlFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - stickerSelectionControlSize.height), size: stickerSelectionControlSize)
if let stickerSelectionControlView = stickerSelectionControl.view {
if stickerSelectionControlView.superview == nil {
self.addSubview(stickerSelectionControlView)
}
if animateIn {
stickerSelectionControlView.frame = stickerSelectionControlFrame
transition.animatePosition(view: stickerSelectionControlView, from: CGPoint(x: 0.0, y: stickerSelectionControlFrame.height), to: CGPoint(), additive: true)
} else {
transition.setFrame(view: stickerSelectionControlView, frame: stickerSelectionControlFrame)
}
}
inputHeight = stickerSelectionControlSize.height
} else if let stickerSelectionControl = self.stickerSelectionControl {
self.stickerSelectionControl = nil
if let stickerSelectionControlView = stickerSelectionControl.view {
transition.setPosition(view: stickerSelectionControlView, position: CGPoint(x: stickerSelectionControlView.center.x, y: availableSize.height + stickerSelectionControlView.bounds.height * 0.5), completion: { [weak stickerSelectionControlView] _ in
stickerSelectionControlView?.removeFromSuperview()
})
}
}
contentHeight += max(inputHeight, environment.safeInsets.bottom)
let previousBounds = self.scrollView.bounds
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) {
self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize)
}
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0)
if self.scrollView.scrollIndicatorInsets != scrollInsets {
self.scrollView.scrollIndicatorInsets = scrollInsets
}
if !previousBounds.isEmpty, !transition.animation.isImmediate {
let bounds = self.scrollView.bounds
if bounds.maxY != previousBounds.maxY {
let offsetY = previousBounds.maxY - bounds.maxY
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true)
}
}
self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0))
self.updateScrolling(transition: transition)
return availableSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class BusinessIntroSetupScreen: ViewControllerComponentContainer {
private let context: AccountContext
public init(
context: AccountContext
) {
self.context = context
super.init(context: context, component: BusinessIntroSetupScreenComponent(
context: context
), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.title = ""
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
self.scrollToTop = { [weak self] in
guard let self, let componentView = self.node.hostView.componentView as? BusinessIntroSetupScreenComponent.View else {
return
}
componentView.scrollToTop()
}
self.attemptNavigation = { [weak self] complete in
guard let self, let componentView = self.node.hostView.componentView as? BusinessIntroSetupScreenComponent.View else {
return true
}
return componentView.attemptNavigation(complete: complete)
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
@objc private func cancelPressed() {
self.dismiss()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
}
}

View File

@ -0,0 +1,161 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import ListSectionComponent
import TelegramPresentationData
import AppBundle
import AccountContext
import ChatEmptyNode
import AsyncDisplayKit
import WallpaperBackgroundNode
import ComponentDisplayAdapters
import TelegramCore
import ChatPresentationInterfaceState
final class ChatIntroItemComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let stickerFile: TelegramMediaFile?
let title: String
let text: String
init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
stickerFile: TelegramMediaFile?,
title: String,
text: String
) {
self.context = context
self.theme = theme
self.strings = strings
self.stickerFile = stickerFile
self.title = title
self.text = text
}
static func ==(lhs: ChatIntroItemComponent, rhs: ChatIntroItemComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.stickerFile != rhs.stickerFile {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.text != rhs.text {
return false
}
return true
}
final class View: UIView, ListSectionComponent.ChildView {
private var component: ChatIntroItemComponent?
private weak var componentState: EmptyComponentState?
private var backgroundNode: WallpaperBackgroundNode?
private var emptyNode: ChatEmptyNode?
var customUpdateIsHighlighted: ((Bool) -> Void)?
private(set) var separatorInset: CGFloat = 0.0
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: ChatIntroItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
self.componentState = state
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
let size = CGSize(width: availableSize.width, height: 346.0)
let backgroundNode: WallpaperBackgroundNode
if let current = self.backgroundNode {
backgroundNode = current
} else {
backgroundNode = createWallpaperBackgroundNode(context: component.context, forChatDisplay: false)
self.backgroundNode = backgroundNode
self.addSubview(backgroundNode.view)
}
transition.setFrame(view: backgroundNode.view, frame: CGRect(origin: CGPoint(), size: size))
backgroundNode.update(wallpaper: presentationData.chatWallpaper, animated: false)
backgroundNode.updateLayout(size: size, displayMode: .aspectFill, transition: transition.containedViewLayoutTransition)
let emptyNode: ChatEmptyNode
if let current = self.emptyNode {
emptyNode = current
} else {
emptyNode = ChatEmptyNode(context: component.context, interaction: nil)
self.emptyNode = emptyNode
self.addSubview(emptyNode.view)
}
let interfaceState = ChatPresentationInterfaceState(
chatWallpaper: presentationData.chatWallpaper,
theme: component.theme,
strings: component.strings,
dateTimeFormat: presentationData.dateTimeFormat,
nameDisplayOrder: presentationData.nameDisplayOrder,
limitsConfiguration: component.context.currentLimitsConfiguration.with { $0 },
fontSize: presentationData.chatFontSize,
bubbleCorners: presentationData.chatBubbleCorners,
accountPeerId: component.context.account.peerId,
mode: .standard(.default),
chatLocation: .peer(id: component.context.account.peerId),
subject: nil,
peerNearbyData: nil,
greetingData: nil,
pendingUnpinnedAllMessages: false,
activeGroupCallInfo: nil,
hasActiveGroupCall: false,
importState: nil,
threadData: nil,
isGeneralThreadClosed: nil,
replyMessage: nil,
accountPeerColor: nil
)
transition.setFrame(view: emptyNode.view, frame: CGRect(origin: CGPoint(), size: size))
emptyNode.updateLayout(
interfaceState: interfaceState,
subject: .emptyChat(.customGreeting(
sticker: component.stickerFile,
title: component.title,
text: component.text
)),
loadingNode: nil,
backgroundNode: backgroundNode,
size: size,
insets: UIEdgeInsets(),
transition: .immediate
)
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -509,7 +509,6 @@ final class BusinessLocationSetupScreenComponent: Component {
contentHeight += mapSectionSize.height
var deleteSectionHeight: CGFloat = 0.0
deleteSectionHeight += sectionSpacing
let deleteSectionSize = self.deleteSection.update(
transition: transition,

View File

@ -51,6 +51,7 @@ swift_library(
"//submodules/TelegramUI/Components/GroupStickerPackSetupController",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemImpl",
"//submodules/TelegramUI/Components/Settings/PeerNameColorItem",
"//submodules/TelegramUI/Components/EmojiActionIconComponent",
],
visibility = [
"//visibility:public",

View File

@ -38,100 +38,7 @@ import BundleIconComponent
import Markdown
import GroupStickerPackSetupController
import PeerNameColorItem
private final class EmojiActionIconComponent: Component {
let context: AccountContext
let color: UIColor
let fileId: Int64?
let file: TelegramMediaFile?
init(
context: AccountContext,
color: UIColor,
fileId: Int64?,
file: TelegramMediaFile?
) {
self.context = context
self.color = color
self.fileId = fileId
self.file = file
}
static func ==(lhs: EmojiActionIconComponent, rhs: EmojiActionIconComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.color != rhs.color {
return false
}
if lhs.fileId != rhs.fileId {
return false
}
if lhs.file != rhs.file {
return false
}
return true
}
final class View: UIView {
private var icon: ComponentView<Empty>?
func update(component: EmojiActionIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let size = CGSize(width: 24.0, height: 24.0)
if let fileId = component.fileId {
let icon: ComponentView<Empty>
if let current = self.icon {
icon = current
} else {
icon = ComponentView()
self.icon = icon
}
let _ = icon.update(
transition: .immediate,
component: AnyComponent(EmojiStatusComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
content: .animation(
content: .customEmoji(fileId: fileId),
size: size,
placeholderColor: .lightGray,
themeColor: component.color,
loopMode: .forever
),
isVisibleForAnimations: false,
action: nil
)),
environment: {},
containerSize: size
)
let iconFrame = CGRect(origin: CGPoint(), size: size)
if let iconView = icon.view {
if iconView.superview == nil {
self.addSubview(iconView)
}
iconView.frame = iconFrame
}
} else {
if let icon = self.icon {
self.icon = nil
icon.view?.removeFromSuperview()
}
}
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
import EmojiActionIconComponent
final class ChannelAppearanceScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment

View File

@ -27,6 +27,7 @@ public final class TextFieldComponent: Component {
public final class ExternalState {
public fileprivate(set) var isEditing: Bool = false
public fileprivate(set) var hasText: Bool = false
public fileprivate(set) var text: NSAttributedString = NSAttributedString()
public fileprivate(set) var textLength: Int = 0
public var initialText: NSAttributedString?
@ -87,6 +88,7 @@ public final class TextFieldComponent: Component {
}
public let context: AccountContext
public let theme: PresentationTheme
public let strings: PresentationStrings
public let externalState: ExternalState
public let fontSize: CGFloat
@ -105,6 +107,7 @@ public final class TextFieldComponent: Component {
public init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
externalState: ExternalState,
fontSize: CGFloat,
@ -122,6 +125,7 @@ public final class TextFieldComponent: Component {
paste: @escaping (PasteData) -> Void
) {
self.context = context
self.theme = theme
self.strings = strings
self.externalState = externalState
self.fontSize = fontSize
@ -140,6 +144,12 @@ public final class TextFieldComponent: Component {
}
public static func ==(lhs: TextFieldComponent, rhs: TextFieldComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
@ -219,7 +229,6 @@ public final class TextFieldComponent: Component {
self.textView.translatesAutoresizingMaskIntoConstraints = false
self.textView.backgroundColor = nil
self.textView.layer.isOpaque = false
self.textView.keyboardAppearance = .dark
self.textView.indicatorStyle = .white
self.textView.scrollIndicatorInsets = UIEdgeInsets(top: 9.0, left: 0.0, bottom: 9.0, right: 0.0)
@ -232,10 +241,6 @@ public final class TextFieldComponent: Component {
self.textView.customDelegate = self
self.addSubview(self.textView)
if #available(iOS 13.0, *) {
self.textView.overrideUserInterfaceStyle = .dark
}
self.textView.typingAttributes = [
NSAttributedString.Key.font: Font.regular(17.0),
NSAttributedString.Key.foregroundColor: UIColor.white
@ -724,7 +729,7 @@ public final class TextFieldComponent: Component {
}
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme)
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: component.theme)
let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>) = (presentationData, .single(presentationData))
let controller = chatTextLinkEditController(sharedContext: component.context.sharedContext, updatedPresentationData: updatedPresentationData, account: component.context.account, text: text.string, link: link, apply: { [weak self] link in
if let self {
@ -1048,9 +1053,17 @@ public final class TextFieldComponent: Component {
self.isUpdating = false
}
let previousComponent = self.component
self.component = component
self.state = state
if previousComponent?.theme !== component.theme {
self.textView.keyboardAppearance = component.theme.overallDarkAppearance ? .dark : .light
if #available(iOS 13.0, *) {
self.textView.overrideUserInterfaceStyle = component.theme.overallDarkAppearance ? .dark : .light
}
}
if let initialText = component.externalState.initialText {
component.externalState.initialText = nil
self.updateInputState { _ in
@ -1128,6 +1141,7 @@ public final class TextFieldComponent: Component {
component.externalState.hasText = self.textView.textStorage.length != 0
component.externalState.isEditing = isEditing
component.externalState.textLength = self.textView.textStorage.string.count
component.externalState.text = NSAttributedString(attributedString: self.textView.textStorage)
if let inputView = component.customInputView {
if self.textView.inputView == nil {

View File

@ -121,6 +121,8 @@ import TopMessageReactions
import PeerInfoScreen
import AudioWaveform
import PeerNameColorScreen
import ChatEmptyNode
import ChatMediaInputStickerGridItem
public enum ChatControllerPeekActions {
case standard

View File

@ -42,6 +42,7 @@ import UIKitRuntimeUtils
import ChatInlineSearchResultsListComponent
import ComponentDisplayAdapters
import ComponentFlow
import ChatEmptyNode
final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem {
let itemNode: OverlayMediaItemNode
@ -990,7 +991,20 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
self.emptyNode = emptyNode
self.historyNodeContainer.supernode?.insertSubnode(emptyNode, aboveSubnode: self.historyNodeContainer)
if let (size, insets) = self.validEmptyNodeLayout {
emptyNode.updateLayout(interfaceState: self.chatPresentationInterfaceState, subject: .emptyChat(emptyType), loadingNode: wasLoading && self.loadingNode.supernode != nil ? self.loadingNode : nil, backgroundNode: self.backgroundNode, size: size, insets: insets, transition: .immediate)
let mappedType: ChatEmptyNode.Subject.EmptyType
switch emptyType {
case .generic:
mappedType = .generic
case .joined:
mappedType = .joined
case .clearedHistory:
mappedType = .clearedHistory
case .topic:
mappedType = .topic
case .botInfo:
mappedType = .botInfo
}
emptyNode.updateLayout(interfaceState: self.chatPresentationInterfaceState, subject: .emptyChat(mappedType), loadingNode: wasLoading && self.loadingNode.supernode != nil ? self.loadingNode : nil, backgroundNode: self.backgroundNode, size: size, insets: insets, transition: .immediate)
}
if animated {
emptyNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
@ -1842,7 +1856,20 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
emptyNodeInsets.bottom += inputPanelsHeight
self.validEmptyNodeLayout = (contentBounds.size, emptyNodeInsets)
if let emptyNode = self.emptyNode, let emptyType = self.emptyType {
emptyNode.updateLayout(interfaceState: self.chatPresentationInterfaceState, subject: .emptyChat(emptyType), loadingNode: nil, backgroundNode: self.backgroundNode, size: contentBounds.size, insets: emptyNodeInsets, transition: transition)
let mappedType: ChatEmptyNode.Subject.EmptyType
switch emptyType {
case .generic:
mappedType = .generic
case .joined:
mappedType = .joined
case .clearedHistory:
mappedType = .clearedHistory
case .topic:
mappedType = .topic
case .botInfo:
mappedType = .botInfo
}
emptyNode.updateLayout(interfaceState: self.chatPresentationInterfaceState, subject: .emptyChat(mappedType), loadingNode: nil, backgroundNode: self.backgroundNode, size: contentBounds.size, insets: emptyNodeInsets, transition: transition)
transition.updateFrame(node: emptyNode, frame: contentBounds)
emptyNode.update(rect: contentBounds, within: contentBounds.size, transition: transition)
}

View File

@ -19,6 +19,8 @@ import ChatMessageInstantVideoItemNode
import ChatMessageAnimatedStickerItemNode
import ChatMessageTransitionNode
import ChatMessageBubbleItemNode
import ChatEmptyNode
import ChatMediaInputStickerGridItem
private func convertAnimatingSourceRect(_ rect: CGRect, fromView: UIView, toView: UIView?) -> CGRect {
if let presentationLayer = fromView.layer.presentation() {

View File

@ -332,10 +332,18 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
}
}
if let addedToken = addedToken {
strongSelf.contactsNode.editableTokens.append(addedToken)
} else if let removedTokenId = removedTokenId {
strongSelf.contactsNode.editableTokens = strongSelf.contactsNode.editableTokens.filter { token in
return token.id != removedTokenId
}
}
if let updatedCount = updatedCount {
switch strongSelf.mode {
case .groupCreation, .peerSelection, .chatSelection:
strongSelf.rightNavigationButton?.isEnabled = updatedCount != 0 || strongSelf.params.alwaysEnabled
strongSelf.rightNavigationButton?.isEnabled = updatedCount != 0 || !strongSelf.contactsNode.editableTokens.isEmpty || strongSelf.params.alwaysEnabled
case .channelCreation, .premiumGifting, .requestedUsersSelection:
break
}
@ -355,13 +363,6 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
}
}
if let addedToken = addedToken {
strongSelf.contactsNode.editableTokens.append(addedToken)
} else if let removedTokenId = removedTokenId {
strongSelf.contactsNode.editableTokens = strongSelf.contactsNode.editableTokens.filter { token in
return token.id != removedTokenId
}
}
strongSelf.requestLayout(transition: ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring))
if displayCountAlert {

View File

@ -59,6 +59,7 @@ import CollectibleItemInfoScreen
import StickerPickerScreen
import MediaEditor
import MediaEditorScreen
import BusinessIntroSetupScreen
private final class AccountUserInterfaceInUseContext {
let subscribers = Bag<(Bool) -> Void>()
@ -1927,6 +1928,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return QuickReplySetupScreen.initialData(context: context)
}
public func makeBusinessIntroSetupScreen(context: AccountContext) -> ViewController {
return BusinessIntroSetupScreen(context: context)
}
public func makeCollectibleItemInfoScreen(context: AccountContext, initialData: CollectibleItemInfoScreenInitialData) -> ViewController {
return CollectibleItemInfoScreen(context: context, initialData: initialData as! CollectibleItemInfoScreen.InitialData)
}

View File

@ -30,6 +30,7 @@ import MediaEditor
import PeerInfoScreen
import PeerInfoStoryGridScreen
import ShareWithPeersScreen
import ChatEmptyNode
private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode {
private var presentationData: PresentationData