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

View File

@ -5,11 +5,13 @@ public final class Rectangle: Component {
private let color: UIColor private let color: UIColor
private let width: CGFloat? private let width: CGFloat?
private let height: 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.color = color
self.width = width self.width = width
self.height = height self.height = height
self.tag = tag
} }
public static func ==(lhs: Rectangle, rhs: Rectangle) -> Bool { public static func ==(lhs: Rectangle, rhs: Rectangle) -> Bool {
@ -25,7 +27,33 @@ public final class Rectangle: Component {
return true 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 var size = availableSize
if let width = self.width { if let width = self.width {
size.width = min(size.width, width) size.width = min(size.width, width)
@ -35,6 +63,7 @@ public final class Rectangle: Component {
} }
view.backgroundColor = self.color view.backgroundColor = self.color
view.componentTag = self.tag
return size return size
} }

View File

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

View File

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

View File

@ -32,6 +32,7 @@ import ListActionItemComponent
import EmojiStatusSelectionComponent import EmojiStatusSelectionComponent
import EmojiStatusComponent import EmojiStatusComponent
import EntityKeyboard import EntityKeyboard
import EmojiActionIconComponent
public enum PremiumSource: Equatable { public enum PremiumSource: Equatable {
public static func == (lhs: PremiumSource, rhs: PremiumSource) -> Bool { 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)) push(accountContext.sharedContext.makeChatbotSetupScreen(context: accountContext, initialData: initialData))
}) })
case .businessIntro:
push(accountContext.sharedContext.makeBusinessIntroSetupScreen(context: accountContext))
default: default:
fatalError() 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 { private final class BadgeComponent: CombinedComponent {
let color: UIColor let color: UIColor
let text: String let text: String

View File

@ -909,6 +909,34 @@ private func selectivePrivacySettingsControllerEntries(presentationData: Present
return entries 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( func selectivePrivacySettingsController(
context: AccountContext, context: AccountContext,
kind: SelectivePrivacySettingsKind, kind: SelectivePrivacySettingsKind,
@ -1041,7 +1069,41 @@ func selectivePrivacySettingsController(
return state return state
} }
if peerIds.isEmpty { 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 addPeerDisposable.set((controller.result
|> take(1) |> take(1)
|> deliverOnMainQueue).start(next: { [weak controller] result in |> deliverOnMainQueue).start(next: { [weak controller] result in
@ -1128,7 +1190,15 @@ func selectivePrivacySettingsController(
})) }))
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
} else { } 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 updateState { state in
if enable { if enable {
switch target { switch target {

View File

@ -248,7 +248,7 @@ private func selectivePrivacyPeersControllerEntries(presentationData: Presentati
return entries 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 statePromise = ValuePromise(SelectivePrivacyPeersControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: SelectivePrivacyPeersControllerState()) let stateValue = Atomic(value: SelectivePrivacyPeersControllerState())
let updateState: ((SelectivePrivacyPeersControllerState) -> SelectivePrivacyPeersControllerState) -> Void = { f in let updateState: ((SelectivePrivacyPeersControllerState) -> SelectivePrivacyPeersControllerState) -> Void = { f in
@ -307,7 +307,33 @@ public func selectivePrivacyPeersController(context: AccountContext, title: Stri
removePeerDisposable.set(applyPeers.start()) removePeerDisposable.set(applyPeers.start())
}, addPeer: { }, 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 addPeerDisposable.set((controller.result
|> take(1) |> take(1)
|> deliverOnMainQueue).start(next: { [weak controller] result in |> deliverOnMainQueue).start(next: { [weak controller] result in

View File

@ -436,9 +436,12 @@ swift_library(
"//submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen", "//submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen",
"//submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen", "//submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen",
"//submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen", "//submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen",
"//submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen",
"//submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController", "//submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController",
"//submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen", "//submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen",
"//submodules/TelegramUI/Components/StickerPickerScreen", "//submodules/TelegramUI/Components/StickerPickerScreen",
"//submodules/TelegramUI/Components/Chat/ChatEmptyNode",
"//submodules/TelegramUI/Components/Chat/ChatMediaInputStickerGridItem",
] + select({ ] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [], "//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 BalancedTextComponent
import Markdown import Markdown
import ReactionSelectionNode import ReactionSelectionNode
import ChatMediaInputStickerGridItem
private protocol ChatEmptyNodeContent { private protocol ChatEmptyNodeContent {
func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize 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 } 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 context: AccountContext
private let interaction: ChatPanelInterfaceInteraction? private let interaction: ChatPanelInterfaceInteraction?
@ -91,15 +92,16 @@ final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeSticke
private let textNode: ImmediateTextNode private let textNode: ImmediateTextNode
private var stickerItem: ChatMediaInputStickerGridItem? private var stickerItem: ChatMediaInputStickerGridItem?
let stickerNode: ChatMediaInputStickerGridItemNode public var stickerNode: ChatMediaInputStickerGridItemNode
private var currentTheme: PresentationTheme? private var currentTheme: PresentationTheme?
private var currentStrings: PresentationStrings? private var currentStrings: PresentationStrings?
private var didSetupSticker = false private var didSetupSticker = false
private let disposable = MetaDisposable() private let disposable = MetaDisposable()
private var currentCustomStickerFile: TelegramMediaFile?
init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) { public init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) {
self.context = context self.context = context
self.interaction = interaction self.interaction = interaction
@ -126,7 +128,7 @@ final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeSticke
self.addSubnode(self.stickerNode) self.addSubnode(self.stickerNode)
} }
override func didLoad() { override public func didLoad() {
super.didLoad() super.didLoad()
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.stickerTapGesture(_:))) let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.stickerTapGesture(_:)))
@ -138,7 +140,7 @@ final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeSticke
self.disposable.dispose() self.disposable.dispose()
} }
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true 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, []) 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 { if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings {
self.currentTheme = interfaceState.theme self.currentTheme = interfaceState.theme
self.currentStrings = interfaceState.strings self.currentStrings = interfaceState.strings
}
var customStickerFile: TelegramMediaFile?
let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper) 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.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) 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 stickerSize: CGSize
let inset: CGFloat let inset: CGFloat
if size.width == 320.0 { if size.width == 320.0 {
@ -170,11 +183,13 @@ final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeSticke
stickerSize = CGSize(width: 160.0, height: 160.0) stickerSize = CGSize(width: 160.0, height: 160.0)
inset = 15.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) 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> 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 sticker = preloadedSticker
} else { } else {
sticker = self.context.engine.stickers.randomGreetingSticker() 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.didSetupSticker = true
self.disposable.set((sticker self.disposable.set((sticker
|> deliverOnMainQueue).startStrict(next: { [weak self] sticker in |> 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 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: {}) 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 strongSelf.stickerItem = item
if isFirstTime {
}
strongSelf.stickerNode.updateLayout(item: item, size: stickerSize, isVisible: true, synchronousLoads: true) strongSelf.stickerNode.updateLayout(item: item, size: stickerSize, isVisible: true, synchronousLoads: true)
strongSelf.stickerNode.isVisibleInGrid = true strongSelf.stickerNode.isVisibleInGrid = true
strongSelf.stickerNode.updateIsPanelVisible(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 context: AccountContext
private let interaction: ChatPanelInterfaceInteraction? private let interaction: ChatPanelInterfaceInteraction?
@ -260,7 +292,7 @@ final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNodeStickerC
private let textNode: ImmediateTextNode private let textNode: ImmediateTextNode
private var stickerItem: ChatMediaInputStickerGridItem? private var stickerItem: ChatMediaInputStickerGridItem?
let stickerNode: ChatMediaInputStickerGridItemNode public let stickerNode: ChatMediaInputStickerGridItemNode
private var currentTheme: PresentationTheme? private var currentTheme: PresentationTheme?
private var currentStrings: PresentationStrings? private var currentStrings: PresentationStrings?
@ -268,7 +300,7 @@ final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNodeStickerC
private var didSetupSticker = false private var didSetupSticker = false
private let disposable = MetaDisposable() private let disposable = MetaDisposable()
init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) { public init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) {
self.context = context self.context = context
self.interaction = interaction self.interaction = interaction
@ -295,7 +327,7 @@ final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNodeStickerC
self.addSubnode(self.stickerNode) self.addSubnode(self.stickerNode)
} }
override func didLoad() { override public func didLoad() {
super.didLoad() super.didLoad()
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.stickerTapGesture(_:))) let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.stickerTapGesture(_:)))
@ -307,7 +339,7 @@ final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNodeStickerC
self.disposable.dispose() self.disposable.dispose()
} }
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true 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, []) 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 { if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings {
self.currentTheme = interfaceState.theme self.currentTheme = interfaceState.theme
self.currentStrings = interfaceState.strings 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 context: AccountContext
private let titleNode: ImmediateTextNode private let titleNode: ImmediateTextNode
@ -855,7 +887,7 @@ final class ChatEmptyNodeTopicChatContent: ASDisplayNode, ChatEmptyNodeContent,
private let iconView: ComponentView<Empty> private let iconView: ComponentView<Empty>
init(context: AccountContext) { public init(context: AccountContext) {
self.context = context self.context = context
self.titleNode = ImmediateTextNode() self.titleNode = ImmediateTextNode()
@ -880,7 +912,7 @@ final class ChatEmptyNodeTopicChatContent: ASDisplayNode, ChatEmptyNodeContent,
self.addSubnode(self.textNode) 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) let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper)
if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings { if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings {
self.currentTheme = interfaceState.theme 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 isPremiumDisabled: Bool
private let interaction: ChatPanelInterfaceInteraction? private let interaction: ChatPanelInterfaceInteraction?
@ -969,7 +1001,7 @@ final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatEmptyNod
private var currentTheme: PresentationTheme? private var currentTheme: PresentationTheme?
private var currentStrings: PresentationStrings? private var currentStrings: PresentationStrings?
init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) { public init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
self.isPremiumDisabled = premiumConfiguration.isPremiumDisabled 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 serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper)
let maxWidth = min(200.0, size.width) let maxWidth = min(200.0, size.width)
@ -1139,9 +1171,18 @@ private enum ChatEmptyNodeContentType: Equatable {
case premiumRequired case premiumRequired
} }
final class ChatEmptyNode: ASDisplayNode { public final class ChatEmptyNode: ASDisplayNode {
enum Subject { public enum Subject {
case emptyChat(ChatHistoryNodeLoadState.EmptyType) 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 case detailsPlaceholder
} }
private let context: AccountContext private let context: AccountContext
@ -1159,7 +1200,7 @@ final class ChatEmptyNode: ASDisplayNode {
private var content: (ChatEmptyNodeContentType, ASDisplayNode & ChatEmptyNodeContent)? private var content: (ChatEmptyNodeContentType, ASDisplayNode & ChatEmptyNodeContent)?
init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) { public init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) {
self.context = context self.context = context
self.interaction = interaction self.interaction = interaction
@ -1172,14 +1213,14 @@ final class ChatEmptyNode: ASDisplayNode {
self.addSubnode(self.backgroundNode) 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 { guard let result = super.hitTest(point, with: event) else {
return nil return nil
} }
return result return result
} }
func animateFromLoadingNode(_ loadingNode: ChatLoadingNode) { public func animateFromLoadingNode(_ loadingNode: ChatLoadingNode) {
guard let (_, node) = self.content else { guard let (_, node) = self.content else {
return 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 self.wallpaperBackgroundNode = backgroundNode
if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings { if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings {
@ -1224,7 +1265,9 @@ final class ChatEmptyNode: ASDisplayNode {
case .detailsPlaceholder: case .detailsPlaceholder:
contentType = .regular contentType = .regular
case let .emptyChat(emptyType): case let .emptyChat(emptyType):
if case .customChatContents = interfaceState.subject { if case .customGreeting = emptyType {
contentType = .greeting
} else if case .customChatContents = interfaceState.subject {
contentType = .cloud contentType = .cloud
} else if case .replyThread = interfaceState.chatLocation { } else if case .replyThread = interfaceState.chatLocation {
if case .topic = emptyType { 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 ChatControllerInteraction
import ChatPresentationInterfaceState import ChatPresentationInterfaceState
enum ChatMediaInputStickerGridSectionAccessory { public enum ChatMediaInputStickerGridSectionAccessory {
case none case none
case setup case setup
case clear case clear
} }
final class ChatMediaInputStickerGridSection: GridSection { public final class ChatMediaInputStickerGridSection: GridSection {
let collectionId: ItemCollectionId public let collectionId: ItemCollectionId
let collectionInfo: StickerPackCollectionInfo? public let collectionInfo: StickerPackCollectionInfo?
let accessory: ChatMediaInputStickerGridSectionAccessory public let accessory: ChatMediaInputStickerGridSectionAccessory
let interaction: ChatMediaInputNodeInteraction public let interaction: ChatMediaInputNodeInteraction
let theme: PresentationTheme public let theme: PresentationTheme
let height: CGFloat = 26.0 public let height: CGFloat = 26.0
var hashValue: Int { public var hashValue: Int {
return self.collectionId.hashValue 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.collectionId = collectionId
self.collectionInfo = collectionInfo self.collectionInfo = collectionInfo
self.accessory = accessory self.accessory = accessory
@ -40,7 +40,7 @@ final class ChatMediaInputStickerGridSection: GridSection {
self.interaction = interaction self.interaction = interaction
} }
func isEqual(to: GridSection) -> Bool { public func isEqual(to: GridSection) -> Bool {
if let to = to as? ChatMediaInputStickerGridSection { if let to = to as? ChatMediaInputStickerGridSection {
return self.collectionId == to.collectionId && self.theme === to.theme return self.collectionId == to.collectionId && self.theme === to.theme
} else { } 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) return ChatMediaInputStickerGridSectionNode(collectionInfo: self.collectionInfo, accessory: self.accessory, theme: self.theme, interaction: self.interaction)
} }
} }
private let sectionTitleFont = Font.medium(12.0) private let sectionTitleFont = Font.medium(12.0)
final class ChatMediaInputStickerGridSectionNode: ASDisplayNode { public final class ChatMediaInputStickerGridSectionNode: ASDisplayNode {
let titleNode: ASTextNode public let titleNode: ASTextNode
let setupNode: HighlightableButtonNode? public let setupNode: HighlightableButtonNode?
let interaction: ChatMediaInputNodeInteraction public let interaction: ChatMediaInputNodeInteraction
let accessory: ChatMediaInputStickerGridSectionAccessory 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.interaction = interaction
self.titleNode = ASTextNode() self.titleNode = ASTextNode()
self.titleNode.isUserInteractionEnabled = false self.titleNode.isUserInteractionEnabled = false
@ -91,7 +91,7 @@ final class ChatMediaInputStickerGridSectionNode: ASDisplayNode {
self.setupNode?.addTarget(self, action: #selector(self.setupPressed), forControlEvents: .touchUpInside) self.setupNode?.addTarget(self, action: #selector(self.setupPressed), forControlEvents: .touchUpInside)
} }
override func layout() { override public func layout() {
super.layout() super.layout()
let bounds = self.bounds let bounds = self.bounds
@ -116,20 +116,20 @@ final class ChatMediaInputStickerGridSectionNode: ASDisplayNode {
} }
} }
final class ChatMediaInputStickerGridItem: GridItem { public final class ChatMediaInputStickerGridItem: GridItem {
let context: AccountContext public let context: AccountContext
let index: ItemCollectionViewEntryIndex public let index: ItemCollectionViewEntryIndex
let stickerItem: StickerPackItem public let stickerItem: StickerPackItem
let selected: () -> Void public let selected: () -> Void
let interfaceInteraction: ChatControllerInteraction? public let interfaceInteraction: ChatControllerInteraction?
let inputNodeInteraction: ChatMediaInputNodeInteraction public let inputNodeInteraction: ChatMediaInputNodeInteraction
let theme: PresentationTheme public let theme: PresentationTheme
let large: Bool public let large: Bool
let isLocked: 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.context = context
self.index = index self.index = index
self.stickerItem = stickerItem self.stickerItem = stickerItem
@ -145,7 +145,7 @@ final class ChatMediaInputStickerGridItem: GridItem {
self.section = ChatMediaInputStickerGridSection(collectionId: collectionId, collectionInfo: stickerPackInfo, accessory: accessory, theme: theme, interaction: inputNodeInteraction) 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() let node = ChatMediaInputStickerGridItemNode()
node.interfaceInteraction = self.interfaceInteraction node.interfaceInteraction = self.interfaceInteraction
node.inputNodeInteraction = self.inputNodeInteraction node.inputNodeInteraction = self.inputNodeInteraction
@ -153,7 +153,7 @@ final class ChatMediaInputStickerGridItem: GridItem {
return node return node
} }
func update(node: GridItemNode) { public func update(node: GridItemNode) {
guard let node = node as? ChatMediaInputStickerGridItemNode else { guard let node = node as? ChatMediaInputStickerGridItemNode else {
assertionFailure() assertionFailure()
return 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 currentState: (AccountContext, StickerPackItem, CGSize)?
private var currentSize: CGSize? private var currentSize: CGSize?
let imageNode: TransformImageNode public let imageNode: TransformImageNode
private(set) var animationNode: AnimatedStickerNode? public private(set) var animationNode: AnimatedStickerNode?
private(set) var placeholderNode: StickerShimmerEffectNode? public private(set) var placeholderNode: StickerShimmerEffectNode?
private var lockBackground: UIVisualEffectView? private var lockBackground: UIVisualEffectView?
private var lockTintView: UIView? private var lockTintView: UIView?
private var lockIconNode: ASImageNode? private var lockIconNode: ASImageNode?
var isLocked: Bool? public var isLocked: Bool?
private var didSetUpAnimationNode = false private var didSetUpAnimationNode = false
private var item: ChatMediaInputStickerGridItem? private var item: ChatMediaInputStickerGridItem?
private let stickerFetchedDisposable = MetaDisposable() private let stickerFetchedDisposable = MetaDisposable()
var currentIsPreviewing = false public var currentIsPreviewing = false
override var isVisibleInGrid: Bool { override public var isVisibleInGrid: Bool {
didSet { didSet {
self.updateVisibility() self.updateVisibility()
} }
@ -192,15 +192,15 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
private var isPanelVisible = false private var isPanelVisible = false
private var isPlaying = false private var isPlaying = false
var interfaceInteraction: ChatControllerInteraction? public var interfaceInteraction: ChatControllerInteraction?
var inputNodeInteraction: ChatMediaInputNodeInteraction? public var inputNodeInteraction: ChatMediaInputNodeInteraction?
var selected: (() -> Void)? public var selected: (() -> Void)?
var stickerPackItem: StickerPackItem? { public var stickerPackItem: StickerPackItem? {
return self.currentState?.1 return self.currentState?.1
} }
override init() { override public init() {
self.imageNode = TransformImageNode() self.imageNode = TransformImageNode()
self.placeholderNode = StickerShimmerEffectNode() self.placeholderNode = StickerShimmerEffectNode()
self.placeholderNode?.isUserInteractionEnabled = false self.placeholderNode?.isUserInteractionEnabled = false
@ -244,13 +244,13 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
} }
} }
override func didLoad() { override public func didLoad() {
super.didLoad() super.didLoad()
self.imageNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:)))) 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 { guard let item = item as? ChatMediaInputStickerGridItem else {
return 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 { 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) 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 { if self.imageNode.layer.animation(forKey: "opacity") != nil {
return return
} }
@ -411,18 +411,18 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
} }
} }
func transitionNode() -> ASDisplayNode? { public func transitionNode() -> ASDisplayNode? {
return self.imageNode return self.imageNode
} }
func updateIsPanelVisible(_ isPanelVisible: Bool) { public func updateIsPanelVisible(_ isPanelVisible: Bool) {
if self.isPanelVisible != isPanelVisible { if self.isPanelVisible != isPanelVisible {
self.isPanelVisible = isPanelVisible self.isPanelVisible = isPanelVisible
self.updateVisibility() self.updateVisibility()
} }
} }
func updateVisibility() { public func updateVisibility() {
guard let item = self.item else { guard let item = self.item else {
return return
} }
@ -444,7 +444,7 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
} }
} }
func updatePreviewing(animated: Bool) { public func updatePreviewing(animated: Bool) {
var isPreviewing = false var isPreviewing = false
if let (_, item, _) = self.currentState, let interaction = self.inputNodeInteraction { if let (_, item, _) = self.currentState, let interaction = self.inputNodeInteraction {
isPreviewing = interaction.previewedStickerPackItemFile?.id == item.file.id 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 ListMultilineTextFieldItemComponent: Component {
public final class ExternalState { public final class ExternalState {
public fileprivate(set) var hasText: Bool = false public fileprivate(set) var hasText: Bool = false
public fileprivate(set) var text: NSAttributedString = NSAttributedString()
public init() { public init() {
} }
@ -206,6 +207,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
transition: transition, transition: transition,
component: AnyComponent(TextFieldComponent( component: AnyComponent(TextFieldComponent(
context: component.context, context: component.context,
theme: component.theme,
strings: component.strings, strings: component.strings,
externalState: self.textFieldExternalState, externalState: self.textFieldExternalState,
fontSize: 17.0, fontSize: 17.0,
@ -266,6 +268,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
self.separatorInset = 16.0 self.separatorInset = 16.0
component.externalState?.hasText = self.textFieldExternalState.hasText component.externalState?.hasText = self.textFieldExternalState.hasText
component.externalState?.text = self.textFieldExternalState.text
return size return size
} }

View File

@ -24,6 +24,7 @@ public final class ListSectionComponent: Component {
public let header: AnyComponent<Empty>? public let header: AnyComponent<Empty>?
public let footer: AnyComponent<Empty>? public let footer: AnyComponent<Empty>?
public let items: [AnyComponentWithIdentity<Empty>] public let items: [AnyComponentWithIdentity<Empty>]
public let itemUpdateOrder: [AnyHashable]?
public let displaySeparators: Bool public let displaySeparators: Bool
public let extendsItemHighlightToSection: Bool public let extendsItemHighlightToSection: Bool
@ -33,6 +34,7 @@ public final class ListSectionComponent: Component {
header: AnyComponent<Empty>?, header: AnyComponent<Empty>?,
footer: AnyComponent<Empty>?, footer: AnyComponent<Empty>?,
items: [AnyComponentWithIdentity<Empty>], items: [AnyComponentWithIdentity<Empty>],
itemUpdateOrder: [AnyHashable]? = nil,
displaySeparators: Bool = true, displaySeparators: Bool = true,
extendsItemHighlightToSection: Bool = false extendsItemHighlightToSection: Bool = false
) { ) {
@ -41,6 +43,7 @@ public final class ListSectionComponent: Component {
self.header = header self.header = header
self.footer = footer self.footer = footer
self.items = items self.items = items
self.itemUpdateOrder = itemUpdateOrder
self.displaySeparators = displaySeparators self.displaySeparators = displaySeparators
self.extendsItemHighlightToSection = extendsItemHighlightToSection self.extendsItemHighlightToSection = extendsItemHighlightToSection
} }
@ -61,6 +64,9 @@ public final class ListSectionComponent: Component {
if lhs.items != rhs.items { if lhs.items != rhs.items {
return false return false
} }
if lhs.itemUpdateOrder != rhs.itemUpdateOrder {
return false
}
if lhs.displaySeparators != rhs.displaySeparators { if lhs.displaySeparators != rhs.displaySeparators {
return false return false
} }
@ -204,7 +210,41 @@ public final class ListSectionComponent: Component {
var innerContentHeight: CGFloat = 0.0 var innerContentHeight: CGFloat = 0.0
var validItemIds: [AnyHashable] = [] 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 { for i in 0 ..< component.items.count {
if !itemUpdateOrder.contains(i) {
itemUpdateOrder.append(i)
}
}
for i in itemUpdateOrder {
let item = component.items[i] let item = component.items[i]
let itemId = item.id let itemId = item.id
validItemIds.append(itemId) validItemIds.append(itemId)
@ -226,17 +266,29 @@ public final class ListSectionComponent: Component {
environment: {}, environment: {},
containerSize: CGSize(width: availableSize.width, height: availableSize.height) 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 { if let itemComponentView = itemComponentView as? ChildView {
itemComponentView.customUpdateIsHighlighted = { [weak self] isHighlighted in itemComponentView.customUpdateIsHighlighted = { [weak self] isHighlighted in
guard let self else { guard let self else {
@ -250,20 +302,20 @@ public final class ListSectionComponent: Component {
if let itemComponentView = itemComponentView as? ChildView { if let itemComponentView = itemComponentView as? ChildView {
separatorInset = itemComponentView.separatorInset 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)) 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)) 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 let separatorAlpha: CGFloat
if component.displaySeparators { if component.displaySeparators {
if i != component.items.count - 1 { if readyItem.index != component.items.count - 1 {
separatorAlpha = 1.0 separatorAlpha = 1.0
} else { } else {
separatorAlpha = 0.0 separatorAlpha = 0.0
@ -271,11 +323,12 @@ public final class ListSectionComponent: Component {
} else { } else {
separatorAlpha = 0.0 separatorAlpha = 0.0
} }
itemTransition.setAlpha(layer: itemView.separatorLayer, alpha: separatorAlpha) readyItem.itemTransition.setAlpha(layer: readyItem.itemView.separatorLayer, alpha: separatorAlpha)
itemView.separatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor readyItem.itemView.separatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor
} }
innerContentHeight += itemSize.height innerContentHeight += readyItem.itemSize.height
} }
var removedItemIds: [AnyHashable] = [] var removedItemIds: [AnyHashable] = []
for (id, itemView) in self.itemViews { for (id, itemView) in self.itemViews {
if !validItemIds.contains(id) { if !validItemIds.contains(id) {

View File

@ -776,6 +776,7 @@ public final class MessageInputPanelComponent: Component {
transition: .immediate, transition: .immediate,
component: AnyComponent(TextFieldComponent( component: AnyComponent(TextFieldComponent(
context: component.context, context: component.context,
theme: component.theme,
strings: component.strings, strings: component.strings,
externalState: self.textFieldExternalState, externalState: self.textFieldExternalState,
fontSize: 17.0, 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 contentHeight += mapSectionSize.height
var deleteSectionHeight: CGFloat = 0.0 var deleteSectionHeight: CGFloat = 0.0
deleteSectionHeight += sectionSpacing deleteSectionHeight += sectionSpacing
let deleteSectionSize = self.deleteSection.update( let deleteSectionSize = self.deleteSection.update(
transition: transition, transition: transition,

View File

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

View File

@ -38,100 +38,7 @@ import BundleIconComponent
import Markdown import Markdown
import GroupStickerPackSetupController import GroupStickerPackSetupController
import PeerNameColorItem import PeerNameColorItem
import EmojiActionIconComponent
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)
}
}
final class ChannelAppearanceScreenComponent: Component { final class ChannelAppearanceScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment typealias EnvironmentType = ViewControllerComponentContainer.Environment

View File

@ -27,6 +27,7 @@ public final class TextFieldComponent: Component {
public final class ExternalState { public final class ExternalState {
public fileprivate(set) var isEditing: Bool = false public fileprivate(set) var isEditing: Bool = false
public fileprivate(set) var hasText: Bool = false public fileprivate(set) var hasText: Bool = false
public fileprivate(set) var text: NSAttributedString = NSAttributedString()
public fileprivate(set) var textLength: Int = 0 public fileprivate(set) var textLength: Int = 0
public var initialText: NSAttributedString? public var initialText: NSAttributedString?
@ -87,6 +88,7 @@ public final class TextFieldComponent: Component {
} }
public let context: AccountContext public let context: AccountContext
public let theme: PresentationTheme
public let strings: PresentationStrings public let strings: PresentationStrings
public let externalState: ExternalState public let externalState: ExternalState
public let fontSize: CGFloat public let fontSize: CGFloat
@ -105,6 +107,7 @@ public final class TextFieldComponent: Component {
public init( public init(
context: AccountContext, context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings, strings: PresentationStrings,
externalState: ExternalState, externalState: ExternalState,
fontSize: CGFloat, fontSize: CGFloat,
@ -122,6 +125,7 @@ public final class TextFieldComponent: Component {
paste: @escaping (PasteData) -> Void paste: @escaping (PasteData) -> Void
) { ) {
self.context = context self.context = context
self.theme = theme
self.strings = strings self.strings = strings
self.externalState = externalState self.externalState = externalState
self.fontSize = fontSize self.fontSize = fontSize
@ -140,6 +144,12 @@ public final class TextFieldComponent: Component {
} }
public static func ==(lhs: TextFieldComponent, rhs: TextFieldComponent) -> Bool { 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 { if lhs.strings !== rhs.strings {
return false return false
} }
@ -219,7 +229,6 @@ public final class TextFieldComponent: Component {
self.textView.translatesAutoresizingMaskIntoConstraints = false self.textView.translatesAutoresizingMaskIntoConstraints = false
self.textView.backgroundColor = nil self.textView.backgroundColor = nil
self.textView.layer.isOpaque = false self.textView.layer.isOpaque = false
self.textView.keyboardAppearance = .dark
self.textView.indicatorStyle = .white self.textView.indicatorStyle = .white
self.textView.scrollIndicatorInsets = UIEdgeInsets(top: 9.0, left: 0.0, bottom: 9.0, right: 0.0) 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.textView.customDelegate = self
self.addSubview(self.textView) self.addSubview(self.textView)
if #available(iOS 13.0, *) {
self.textView.overrideUserInterfaceStyle = .dark
}
self.textView.typingAttributes = [ self.textView.typingAttributes = [
NSAttributedString.Key.font: Font.regular(17.0), NSAttributedString.Key.font: Font.regular(17.0),
NSAttributedString.Key.foregroundColor: UIColor.white 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 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 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 { if let self {
@ -1048,9 +1053,17 @@ public final class TextFieldComponent: Component {
self.isUpdating = false self.isUpdating = false
} }
let previousComponent = self.component
self.component = component self.component = component
self.state = state 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 { if let initialText = component.externalState.initialText {
component.externalState.initialText = nil component.externalState.initialText = nil
self.updateInputState { _ in self.updateInputState { _ in
@ -1128,6 +1141,7 @@ public final class TextFieldComponent: Component {
component.externalState.hasText = self.textView.textStorage.length != 0 component.externalState.hasText = self.textView.textStorage.length != 0
component.externalState.isEditing = isEditing component.externalState.isEditing = isEditing
component.externalState.textLength = self.textView.textStorage.string.count component.externalState.textLength = self.textView.textStorage.string.count
component.externalState.text = NSAttributedString(attributedString: self.textView.textStorage)
if let inputView = component.customInputView { if let inputView = component.customInputView {
if self.textView.inputView == nil { if self.textView.inputView == nil {

View File

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

View File

@ -42,6 +42,7 @@ import UIKitRuntimeUtils
import ChatInlineSearchResultsListComponent import ChatInlineSearchResultsListComponent
import ComponentDisplayAdapters import ComponentDisplayAdapters
import ComponentFlow import ComponentFlow
import ChatEmptyNode
final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem { final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem {
let itemNode: OverlayMediaItemNode let itemNode: OverlayMediaItemNode
@ -990,7 +991,20 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
self.emptyNode = emptyNode self.emptyNode = emptyNode
self.historyNodeContainer.supernode?.insertSubnode(emptyNode, aboveSubnode: self.historyNodeContainer) self.historyNodeContainer.supernode?.insertSubnode(emptyNode, aboveSubnode: self.historyNodeContainer)
if let (size, insets) = self.validEmptyNodeLayout { 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 { if animated {
emptyNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) emptyNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
@ -1842,7 +1856,20 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
emptyNodeInsets.bottom += inputPanelsHeight emptyNodeInsets.bottom += inputPanelsHeight
self.validEmptyNodeLayout = (contentBounds.size, emptyNodeInsets) self.validEmptyNodeLayout = (contentBounds.size, emptyNodeInsets)
if let emptyNode = self.emptyNode, let emptyType = self.emptyType { 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) transition.updateFrame(node: emptyNode, frame: contentBounds)
emptyNode.update(rect: contentBounds, within: contentBounds.size, transition: transition) emptyNode.update(rect: contentBounds, within: contentBounds.size, transition: transition)
} }

View File

@ -19,6 +19,8 @@ import ChatMessageInstantVideoItemNode
import ChatMessageAnimatedStickerItemNode import ChatMessageAnimatedStickerItemNode
import ChatMessageTransitionNode import ChatMessageTransitionNode
import ChatMessageBubbleItemNode import ChatMessageBubbleItemNode
import ChatEmptyNode
import ChatMediaInputStickerGridItem
private func convertAnimatingSourceRect(_ rect: CGRect, fromView: UIView, toView: UIView?) -> CGRect { private func convertAnimatingSourceRect(_ rect: CGRect, fromView: UIView, toView: UIView?) -> CGRect {
if let presentationLayer = fromView.layer.presentation() { 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 { if let updatedCount = updatedCount {
switch strongSelf.mode { switch strongSelf.mode {
case .groupCreation, .peerSelection, .chatSelection: 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: case .channelCreation, .premiumGifting, .requestedUsersSelection:
break 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)) strongSelf.requestLayout(transition: ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring))
if displayCountAlert { if displayCountAlert {

View File

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

View File

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