diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index d76ab65342..b85f4ef104 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -856,6 +856,9 @@ public protocol AutomaticBusinessMessageSetupScreenInitialData: AnyObject { public protocol ChatbotSetupScreenInitialData: AnyObject { } +public protocol BusinessIntroSetupScreenInitialData: AnyObject { +} + public protocol CollectibleItemInfoScreenInitialData: AnyObject { var collectibleItemInfo: TelegramCollectibleItemInfo { get } } @@ -960,6 +963,7 @@ public protocol SharedAccountContext: AnyObject { func makeAutomaticBusinessMessageSetupScreenInitialData(context: AccountContext) -> Signal func makeQuickReplySetupScreen(context: AccountContext, initialData: QuickReplySetupScreenInitialData) -> ViewController func makeQuickReplySetupScreenInitialData(context: AccountContext) -> Signal + func makeBusinessIntroSetupScreen(context: AccountContext) -> ViewController func makeCollectibleItemInfoScreen(context: AccountContext, initialData: CollectibleItemInfoScreenInitialData) -> ViewController func makeCollectibleItemInfoScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, subject: CollectibleItemInfoScreenSubject) -> Signal func navigateToChatController(_ params: NavigateToChatControllerParams) diff --git a/submodules/ComponentFlow/Source/Components/Rectangle.swift b/submodules/ComponentFlow/Source/Components/Rectangle.swift index e1254ec1de..8eaf0971e4 100644 --- a/submodules/ComponentFlow/Source/Components/Rectangle.swift +++ b/submodules/ComponentFlow/Source/Components/Rectangle.swift @@ -5,11 +5,13 @@ public final class Rectangle: Component { private let color: UIColor private let width: CGFloat? private let height: CGFloat? + private let tag: NSObject? - public init(color: UIColor, width: CGFloat? = nil, height: CGFloat? = nil) { + public init(color: UIColor, width: CGFloat? = nil, height: CGFloat? = nil, tag: NSObject? = nil) { self.color = color self.width = width self.height = height + self.tag = tag } public static func ==(lhs: Rectangle, rhs: Rectangle) -> Bool { @@ -25,7 +27,33 @@ public final class Rectangle: Component { return true } - public func update(view: UIView, availableSize: CGSize, state: EmptyComponentState, environment: Environment, 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, transition: Transition) -> CGSize { var size = availableSize if let width = self.width { size.width = min(size.width, width) @@ -35,6 +63,7 @@ public final class Rectangle: Component { } view.backgroundColor = self.color + view.componentTag = self.tag return size } diff --git a/submodules/ComponentFlow/Source/Components/VStack.swift b/submodules/ComponentFlow/Source/Components/VStack.swift index 1dd0e3d31d..b114765b0a 100644 --- a/submodules/ComponentFlow/Source/Components/VStack.swift +++ b/submodules/ComponentFlow/Source/Components/VStack.swift @@ -13,11 +13,13 @@ public final class VStack: CombinedComponent { private let items: [AnyComponentWithIdentity] private let alignment: VStackAlignment private let spacing: CGFloat + private let fillWidth: Bool - public init(_ items: [AnyComponentWithIdentity], alignment: VStackAlignment = .center, spacing: CGFloat) { + public init(_ items: [AnyComponentWithIdentity], alignment: VStackAlignment = .center, spacing: CGFloat, fillWidth: Bool = false) { self.items = items self.alignment = alignment self.spacing = spacing + self.fillWidth = fillWidth } public static func ==(lhs: VStack, rhs: VStack) -> Bool { @@ -30,6 +32,9 @@ public final class VStack: CombinedComponent { if lhs.spacing != rhs.spacing { return false } + if lhs.fillWidth != rhs.fillWidth { + return false + } return true } @@ -48,6 +53,9 @@ public final class VStack: CombinedComponent { } var size = CGSize(width: 0.0, height: 0.0) + if context.component.fillWidth { + size.width = context.availableSize.width + } for child in updatedChildren { size.height += child.size.height size.width = max(size.width, child.size.width) diff --git a/submodules/PremiumUI/BUILD b/submodules/PremiumUI/BUILD index 5872e7f481..87252b60f1 100644 --- a/submodules/PremiumUI/BUILD +++ b/submodules/PremiumUI/BUILD @@ -117,6 +117,7 @@ swift_library( "//submodules/TelegramUI/Components/EmojiStatusSelectionComponent", "//submodules/TelegramUI/Components/EntityKeyboard", "//submodules/TelegramUI/Components/PremiumPeerShortcutComponent", + "//submodules/TelegramUI/Components/EmojiActionIconComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 729c907ca1..a4132d0775 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -32,6 +32,7 @@ import ListActionItemComponent import EmojiStatusSelectionComponent import EmojiStatusComponent import EntityKeyboard +import EmojiActionIconComponent public enum PremiumSource: Equatable { public static func == (lhs: PremiumSource, rhs: PremiumSource) -> Bool { @@ -2241,6 +2242,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } push(accountContext.sharedContext.makeChatbotSetupScreen(context: accountContext, initialData: initialData)) }) + case .businessIntro: + push(accountContext.sharedContext.makeBusinessIntroSetupScreen(context: accountContext)) default: fatalError() } @@ -3715,89 +3718,6 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { } } - - - -private final class EmojiActionIconComponent: Component { - let context: AccountContext - let color: UIColor - let fileId: Int64? - let file: TelegramMediaFile? - - init( - context: AccountContext, - color: UIColor, - fileId: Int64?, - file: TelegramMediaFile? - ) { - self.context = context - self.color = color - self.fileId = fileId - self.file = file - } - - static func ==(lhs: EmojiActionIconComponent, rhs: EmojiActionIconComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } - if lhs.color != rhs.color { - return false - } - if lhs.fileId != rhs.fileId { - return false - } - if lhs.file != rhs.file { - return false - } - return true - } - - final class View: UIView { - private let icon = ComponentView() - - func update(component: EmojiActionIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, 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, transition: Transition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } -} - private final class BadgeComponent: CombinedComponent { let color: UIColor let text: String diff --git a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift index f0dba91eb5..e83455cce8 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift @@ -909,6 +909,34 @@ private func selectivePrivacySettingsControllerEntries(presentationData: Present return entries } +func generatePremiumCategoryIcon(size: CGSize, cornerRadius: CGFloat) -> UIImage { + return generateImage(size, contextGenerator: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius) + context.addPath(path.cgPath) + context.clip() + + let colorsArray: [CGColor] = [ + UIColor(rgb: 0xF161DD).cgColor, + UIColor(rgb: 0xF161DD).cgColor, + UIColor(rgb: 0x8d77ff).cgColor, + UIColor(rgb: 0xb56eec).cgColor, + UIColor(rgb: 0xb56eec).cgColor + ] + var locations: [CGFloat] = [0.0, 0.15, 0.5, 0.85, 1.0] + let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions()) + + if let image = generateTintedImage(image: UIImage(bundleImageName: "Premium/ButtonIcon"), color: UIColor(rgb: 0xffffff)), let cgImage = image.cgImage { + let imageSize = image.size.aspectFitted(CGSize(width: floor(size.width * 0.6), height: floor(size.height * 0.6))) + context.draw(cgImage, in: CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - imageSize.width) / 2.0), y: floorToScreenPixels((bounds.height - imageSize.height) / 2.0)), size: imageSize)) + } + })! +} + func selectivePrivacySettingsController( context: AccountContext, kind: SelectivePrivacySettingsKind, @@ -1041,7 +1069,41 @@ func selectivePrivacySettingsController( return state } if peerIds.isEmpty { - let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .peerSelection(searchChatList: true, searchGroups: true, searchChannels: false), options: [])) + enum AdditionalCategoryId: Int { + case premiumUsers + } + + var displayPremiumCategory = false + switch kind { + case .groupInvitations: + displayPremiumCategory = true + default: + break + } + + //TODO:localize + var additionalCategories: [ChatListNodeAdditionalCategory] = [] + + if displayPremiumCategory { + additionalCategories = [ + ChatListNodeAdditionalCategory( + id: AdditionalCategoryId.premiumUsers.rawValue, + icon: generatePremiumCategoryIcon(size: CGSize(width: 40.0, height: 40.0), cornerRadius: 12.0), + smallIcon: generatePremiumCategoryIcon(size: CGSize(width: 22.0, height: 22.0), cornerRadius: 6.0), + title: "Premium Users" + ) + ] + } + let selectedCategories = Set() + + let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection( + title: "Add Users", + searchPlaceholder: "Search users and groups", + selectedChats: Set(), + additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories), + chatListFilters: nil, + onlyUsers: false + )), options: [])) addPeerDisposable.set((controller.result |> take(1) |> deliverOnMainQueue).start(next: { [weak controller] result in @@ -1128,7 +1190,15 @@ func selectivePrivacySettingsController( })) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } else { - let controller = selectivePrivacyPeersController(context: context, title: title, initialPeers: peerIds, updated: { updatedPeerIds in + var displayPremiumCategory = false + switch kind { + case .groupInvitations: + displayPremiumCategory = true + default: + break + } + + let controller = selectivePrivacyPeersController(context: context, title: title, initialPeers: peerIds, displayPremiumCategory: displayPremiumCategory, updated: { updatedPeerIds in updateState { state in if enable { switch target { diff --git a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift index 4d1d743535..b2aad40413 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift @@ -248,7 +248,7 @@ private func selectivePrivacyPeersControllerEntries(presentationData: Presentati return entries } -public func selectivePrivacyPeersController(context: AccountContext, title: String, initialPeers: [EnginePeer.Id: SelectivePrivacyPeer], updated: @escaping ([EnginePeer.Id: SelectivePrivacyPeer]) -> Void) -> ViewController { +public func selectivePrivacyPeersController(context: AccountContext, title: String, initialPeers: [EnginePeer.Id: SelectivePrivacyPeer], displayPremiumCategory: Bool, updated: @escaping ([EnginePeer.Id: SelectivePrivacyPeer]) -> Void) -> ViewController { let statePromise = ValuePromise(SelectivePrivacyPeersControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: SelectivePrivacyPeersControllerState()) let updateState: ((SelectivePrivacyPeersControllerState) -> SelectivePrivacyPeersControllerState) -> Void = { f in @@ -307,7 +307,33 @@ public func selectivePrivacyPeersController(context: AccountContext, title: Stri removePeerDisposable.set(applyPeers.start()) }, addPeer: { - let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .peerSelection(searchChatList: true, searchGroups: true, searchChannels: false), options: [])) + enum AdditionalCategoryId: Int { + case premiumUsers + } + + //TODO:localize + var additionalCategories: [ChatListNodeAdditionalCategory] = [] + + if displayPremiumCategory { + additionalCategories = [ + ChatListNodeAdditionalCategory( + id: AdditionalCategoryId.premiumUsers.rawValue, + icon: generatePremiumCategoryIcon(size: CGSize(width: 40.0, height: 40.0), cornerRadius: 12.0), + smallIcon: generatePremiumCategoryIcon(size: CGSize(width: 22.0, height: 22.0), cornerRadius: 6.0), + title: "Premium Users" + ) + ] + } + let selectedCategories = Set() + + let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection( + title: "Add Users", + searchPlaceholder: "Search users and groups", + selectedChats: Set(), + additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories), + chatListFilters: nil, + onlyUsers: false + )), options: [])) addPeerDisposable.set((controller.result |> take(1) |> deliverOnMainQueue).start(next: { [weak controller] result in diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 5892c6150a..a32a83ba44 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -436,9 +436,12 @@ swift_library( "//submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen", "//submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen", "//submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen", + "//submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen", "//submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController", "//submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen", "//submodules/TelegramUI/Components/StickerPickerScreen", + "//submodules/TelegramUI/Components/Chat/ChatEmptyNode", + "//submodules/TelegramUI/Components/Chat/ChatMediaInputStickerGridItem", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/BUILD new file mode 100644 index 0000000000..c9ad95f646 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/BUILD @@ -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", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift similarity index 92% rename from submodules/TelegramUI/Sources/ChatEmptyNode.swift rename to submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift index b81dd01bc0..0de95fdd6c 100644 --- a/submodules/TelegramUI/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift @@ -19,6 +19,7 @@ import MultilineTextComponent import BalancedTextComponent import Markdown import ReactionSelectionNode +import ChatMediaInputStickerGridItem private protocol ChatEmptyNodeContent { func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize @@ -79,11 +80,11 @@ private final class ChatEmptyNodeRegularChatContent: ASDisplayNode, ChatEmptyNod } } -protocol ChatEmptyNodeStickerContentNode: ASDisplayNode { +public protocol ChatEmptyNodeStickerContentNode: ASDisplayNode { var stickerNode: ChatMediaInputStickerGridItemNode { get } } -final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeStickerContentNode, ChatEmptyNodeContent, UIGestureRecognizerDelegate { +public final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeStickerContentNode, ChatEmptyNodeContent, UIGestureRecognizerDelegate { private let context: AccountContext private let interaction: ChatPanelInterfaceInteraction? @@ -91,15 +92,16 @@ final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeSticke private let textNode: ImmediateTextNode private var stickerItem: ChatMediaInputStickerGridItem? - let stickerNode: ChatMediaInputStickerGridItemNode + public var stickerNode: ChatMediaInputStickerGridItemNode private var currentTheme: PresentationTheme? private var currentStrings: PresentationStrings? private var didSetupSticker = false private let disposable = MetaDisposable() + private var currentCustomStickerFile: TelegramMediaFile? - init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) { + public init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) { self.context = context self.interaction = interaction @@ -126,7 +128,7 @@ final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeSticke self.addSubnode(self.stickerNode) } - override func didLoad() { + override public func didLoad() { super.didLoad() let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.stickerTapGesture(_:))) @@ -138,7 +140,7 @@ final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeSticke self.disposable.dispose() } - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } @@ -149,18 +151,29 @@ final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeSticke let _ = self.interaction?.sendSticker(.standalone(media: stickerItem.stickerItem.file), false, self.view, self.stickerNode.bounds, nil, []) } - func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + let isFirstTime = self.currentTheme == nil + if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings { self.currentTheme = interfaceState.theme self.currentStrings = interfaceState.strings - - let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper) - + } + + var customStickerFile: TelegramMediaFile? + + let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper) + if case let .emptyChat(emptyChat) = subject, case let .customGreeting(stickerFile, title, text) = emptyChat { + customStickerFile = stickerFile + self.titleNode.attributedText = NSAttributedString(string: title, font: titleFont, textColor: serviceColor.primaryText) + self.textNode.attributedText = NSAttributedString(string: text, font: messageFont, textColor: serviceColor.primaryText) + } else { self.titleNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_EmptyPlaceholder, font: titleFont, textColor: serviceColor.primaryText) - self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_GreetingText, font: messageFont, textColor: serviceColor.primaryText) } + let previousCustomStickerFile = self.currentCustomStickerFile + self.currentCustomStickerFile = customStickerFile + let stickerSize: CGSize let inset: CGFloat if size.width == 320.0 { @@ -170,11 +183,13 @@ final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeSticke stickerSize = CGSize(width: 160.0, height: 160.0) inset = 15.0 } - if let item = self.stickerItem { + if let item = self.stickerItem, previousCustomStickerFile == customStickerFile { self.stickerNode.updateLayout(item: item, size: stickerSize, isVisible: true, synchronousLoads: true) - } else if !self.didSetupSticker { + } else if !self.didSetupSticker || previousCustomStickerFile != customStickerFile { let sticker: Signal - if let preloadedSticker = interfaceState.greetingData?.sticker { + if let customStickerFile { + sticker = .single(customStickerFile) + } else if let preloadedSticker = interfaceState.greetingData?.sticker { sticker = preloadedSticker } else { sticker = self.context.engine.stickers.randomGreetingSticker() @@ -183,6 +198,19 @@ final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeSticke } } + if !isFirstTime, case let .emptyChat(emptyChat) = subject, case .customGreeting = emptyChat { + let previousStickerNode = self.stickerNode + previousStickerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousStickerNode] _ in + previousStickerNode?.removeFromSupernode() + }) + previousStickerNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) + + self.stickerNode = ChatMediaInputStickerGridItemNode() + self.addSubnode(self.stickerNode) + self.stickerNode.layer.animateSpring(from: 0.001 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) + self.stickerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + self.didSetupSticker = true self.disposable.set((sticker |> deliverOnMainQueue).startStrict(next: { [weak self] sticker in @@ -216,6 +244,10 @@ final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeSticke let stickerPackItem = StickerPackItem(index: index, file: sticker, indexKeys: []) let item = ChatMediaInputStickerGridItem(context: strongSelf.context, collectionId: collectionId, stickerPackInfo: nil, index: ItemCollectionViewEntryIndex(collectionIndex: 0, collectionId: collectionId, itemIndex: index), stickerItem: stickerPackItem, canManagePeerSpecificPack: nil, interfaceInteraction: nil, inputNodeInteraction: inputNodeInteraction, hasAccessory: false, theme: interfaceState.theme, large: true, selected: {}) strongSelf.stickerItem = item + + if isFirstTime { + + } strongSelf.stickerNode.updateLayout(item: item, size: stickerSize, isVisible: true, synchronousLoads: true) strongSelf.stickerNode.isVisibleInGrid = true strongSelf.stickerNode.updateIsPanelVisible(true) @@ -252,7 +284,7 @@ final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeSticke } } -final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNodeStickerContentNode, ChatEmptyNodeContent, UIGestureRecognizerDelegate { +public final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNodeStickerContentNode, ChatEmptyNodeContent, UIGestureRecognizerDelegate { private let context: AccountContext private let interaction: ChatPanelInterfaceInteraction? @@ -260,7 +292,7 @@ final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNodeStickerC private let textNode: ImmediateTextNode private var stickerItem: ChatMediaInputStickerGridItem? - let stickerNode: ChatMediaInputStickerGridItemNode + public let stickerNode: ChatMediaInputStickerGridItemNode private var currentTheme: PresentationTheme? private var currentStrings: PresentationStrings? @@ -268,7 +300,7 @@ final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNodeStickerC private var didSetupSticker = false private let disposable = MetaDisposable() - init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) { + public init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) { self.context = context self.interaction = interaction @@ -295,7 +327,7 @@ final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNodeStickerC self.addSubnode(self.stickerNode) } - override func didLoad() { + override public func didLoad() { super.didLoad() let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.stickerTapGesture(_:))) @@ -307,7 +339,7 @@ final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNodeStickerC self.disposable.dispose() } - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } @@ -318,7 +350,7 @@ final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNodeStickerC let _ = self.interaction?.sendSticker(.standalone(media: stickerItem.stickerItem.file), false, self.view, self.stickerNode.bounds, nil, []) } - func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings { self.currentTheme = interfaceState.theme self.currentStrings = interfaceState.strings @@ -844,7 +876,7 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC } } -final class ChatEmptyNodeTopicChatContent: ASDisplayNode, ChatEmptyNodeContent, UIGestureRecognizerDelegate { +public final class ChatEmptyNodeTopicChatContent: ASDisplayNode, ChatEmptyNodeContent, UIGestureRecognizerDelegate { private let context: AccountContext private let titleNode: ImmediateTextNode @@ -855,7 +887,7 @@ final class ChatEmptyNodeTopicChatContent: ASDisplayNode, ChatEmptyNodeContent, private let iconView: ComponentView - init(context: AccountContext) { + public init(context: AccountContext) { self.context = context self.titleNode = ImmediateTextNode() @@ -880,7 +912,7 @@ final class ChatEmptyNodeTopicChatContent: ASDisplayNode, ChatEmptyNodeContent, self.addSubnode(self.textNode) } - func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper) if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings { self.currentTheme = interfaceState.theme @@ -955,7 +987,7 @@ final class ChatEmptyNodeTopicChatContent: ASDisplayNode, ChatEmptyNodeContent, } } -final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatEmptyNodeContent { +public final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatEmptyNodeContent { private let isPremiumDisabled: Bool private let interaction: ChatPanelInterfaceInteraction? @@ -969,7 +1001,7 @@ final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatEmptyNod private var currentTheme: PresentationTheme? private var currentStrings: PresentationStrings? - init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) { + public init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) self.isPremiumDisabled = premiumConfiguration.isPremiumDisabled @@ -1016,7 +1048,7 @@ final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatEmptyNod } } - func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper) let maxWidth = min(200.0, size.width) @@ -1139,9 +1171,18 @@ private enum ChatEmptyNodeContentType: Equatable { case premiumRequired } -final class ChatEmptyNode: ASDisplayNode { - enum Subject { - case emptyChat(ChatHistoryNodeLoadState.EmptyType) +public final class ChatEmptyNode: ASDisplayNode { + public enum Subject { + public enum EmptyType: Equatable { + case generic + case joined + case clearedHistory + case topic + case botInfo + case customGreeting(sticker: TelegramMediaFile?, title: String, text: String) + } + + case emptyChat(EmptyType) case detailsPlaceholder } private let context: AccountContext @@ -1159,7 +1200,7 @@ final class ChatEmptyNode: ASDisplayNode { private var content: (ChatEmptyNodeContentType, ASDisplayNode & ChatEmptyNodeContent)? - init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) { + public init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) { self.context = context self.interaction = interaction @@ -1172,14 +1213,14 @@ final class ChatEmptyNode: ASDisplayNode { self.addSubnode(self.backgroundNode) } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { guard let result = super.hitTest(point, with: event) else { return nil } return result } - func animateFromLoadingNode(_ loadingNode: ChatLoadingNode) { + public func animateFromLoadingNode(_ loadingNode: ChatLoadingNode) { guard let (_, node) = self.content else { return } @@ -1204,7 +1245,7 @@ final class ChatEmptyNode: ASDisplayNode { } } - func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: Subject, loadingNode: ChatLoadingNode?, backgroundNode: WallpaperBackgroundNode?, size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) { + public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: Subject, loadingNode: ChatLoadingNode?, backgroundNode: WallpaperBackgroundNode?, size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) { self.wallpaperBackgroundNode = backgroundNode if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings { @@ -1224,7 +1265,9 @@ final class ChatEmptyNode: ASDisplayNode { case .detailsPlaceholder: contentType = .regular case let .emptyChat(emptyType): - if case .customChatContents = interfaceState.subject { + if case .customGreeting = emptyType { + contentType = .greeting + } else if case .customChatContents = interfaceState.subject { contentType = .cloud } else if case .replyThread = interfaceState.chatLocation { if case .topic = emptyType { diff --git a/submodules/TelegramUI/Components/Chat/ChatMediaInputStickerGridItem/BUILD b/submodules/TelegramUI/Components/Chat/ChatMediaInputStickerGridItem/BUILD new file mode 100644 index 0000000000..9c809d2612 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMediaInputStickerGridItem/BUILD @@ -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", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift b/submodules/TelegramUI/Components/Chat/ChatMediaInputStickerGridItem/Sources/ChatMediaInputStickerGridItem.swift similarity index 84% rename from submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift rename to submodules/TelegramUI/Components/Chat/ChatMediaInputStickerGridItem/Sources/ChatMediaInputStickerGridItem.swift index b1d3e896aa..3d2a80eea9 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMediaInputStickerGridItem/Sources/ChatMediaInputStickerGridItem.swift @@ -14,25 +14,25 @@ import ShimmerEffect import ChatControllerInteraction import ChatPresentationInterfaceState -enum ChatMediaInputStickerGridSectionAccessory { +public enum ChatMediaInputStickerGridSectionAccessory { case none case setup case clear } -final class ChatMediaInputStickerGridSection: GridSection { - let collectionId: ItemCollectionId - let collectionInfo: StickerPackCollectionInfo? - let accessory: ChatMediaInputStickerGridSectionAccessory - let interaction: ChatMediaInputNodeInteraction - let theme: PresentationTheme - let height: CGFloat = 26.0 +public final class ChatMediaInputStickerGridSection: GridSection { + public let collectionId: ItemCollectionId + public let collectionInfo: StickerPackCollectionInfo? + public let accessory: ChatMediaInputStickerGridSectionAccessory + public let interaction: ChatMediaInputNodeInteraction + public let theme: PresentationTheme + public let height: CGFloat = 26.0 - var hashValue: Int { + public var hashValue: Int { return self.collectionId.hashValue } - init(collectionId: ItemCollectionId, collectionInfo: StickerPackCollectionInfo?, accessory: ChatMediaInputStickerGridSectionAccessory, theme: PresentationTheme, interaction: ChatMediaInputNodeInteraction) { + public init(collectionId: ItemCollectionId, collectionInfo: StickerPackCollectionInfo?, accessory: ChatMediaInputStickerGridSectionAccessory, theme: PresentationTheme, interaction: ChatMediaInputNodeInteraction) { self.collectionId = collectionId self.collectionInfo = collectionInfo self.accessory = accessory @@ -40,7 +40,7 @@ final class ChatMediaInputStickerGridSection: GridSection { self.interaction = interaction } - func isEqual(to: GridSection) -> Bool { + public func isEqual(to: GridSection) -> Bool { if let to = to as? ChatMediaInputStickerGridSection { return self.collectionId == to.collectionId && self.theme === to.theme } else { @@ -48,20 +48,20 @@ final class ChatMediaInputStickerGridSection: GridSection { } } - func node() -> ASDisplayNode { + public func node() -> ASDisplayNode { return ChatMediaInputStickerGridSectionNode(collectionInfo: self.collectionInfo, accessory: self.accessory, theme: self.theme, interaction: self.interaction) } } private let sectionTitleFont = Font.medium(12.0) -final class ChatMediaInputStickerGridSectionNode: ASDisplayNode { - let titleNode: ASTextNode - let setupNode: HighlightableButtonNode? - let interaction: ChatMediaInputNodeInteraction - let accessory: ChatMediaInputStickerGridSectionAccessory +public final class ChatMediaInputStickerGridSectionNode: ASDisplayNode { + public let titleNode: ASTextNode + public let setupNode: HighlightableButtonNode? + public let interaction: ChatMediaInputNodeInteraction + public let accessory: ChatMediaInputStickerGridSectionAccessory - init(collectionInfo: StickerPackCollectionInfo?, accessory: ChatMediaInputStickerGridSectionAccessory, theme: PresentationTheme, interaction: ChatMediaInputNodeInteraction) { + public init(collectionInfo: StickerPackCollectionInfo?, accessory: ChatMediaInputStickerGridSectionAccessory, theme: PresentationTheme, interaction: ChatMediaInputNodeInteraction) { self.interaction = interaction self.titleNode = ASTextNode() self.titleNode.isUserInteractionEnabled = false @@ -91,7 +91,7 @@ final class ChatMediaInputStickerGridSectionNode: ASDisplayNode { self.setupNode?.addTarget(self, action: #selector(self.setupPressed), forControlEvents: .touchUpInside) } - override func layout() { + override public func layout() { super.layout() let bounds = self.bounds @@ -116,20 +116,20 @@ final class ChatMediaInputStickerGridSectionNode: ASDisplayNode { } } -final class ChatMediaInputStickerGridItem: GridItem { - let context: AccountContext - let index: ItemCollectionViewEntryIndex - let stickerItem: StickerPackItem - let selected: () -> Void - let interfaceInteraction: ChatControllerInteraction? - let inputNodeInteraction: ChatMediaInputNodeInteraction - let theme: PresentationTheme - let large: Bool - let isLocked: Bool +public final class ChatMediaInputStickerGridItem: GridItem { + public let context: AccountContext + public let index: ItemCollectionViewEntryIndex + public let stickerItem: StickerPackItem + public let selected: () -> Void + public let interfaceInteraction: ChatControllerInteraction? + public let inputNodeInteraction: ChatMediaInputNodeInteraction + public let theme: PresentationTheme + public let large: Bool + public let isLocked: Bool - let section: GridSection? + public let section: GridSection? - init(context: AccountContext, collectionId: ItemCollectionId, stickerPackInfo: StickerPackCollectionInfo?, index: ItemCollectionViewEntryIndex, stickerItem: StickerPackItem, canManagePeerSpecificPack: Bool?, interfaceInteraction: ChatControllerInteraction?, inputNodeInteraction: ChatMediaInputNodeInteraction, hasAccessory: Bool, theme: PresentationTheme, large: Bool = false, isLocked: Bool = false, selected: @escaping () -> Void) { + public init(context: AccountContext, collectionId: ItemCollectionId, stickerPackInfo: StickerPackCollectionInfo?, index: ItemCollectionViewEntryIndex, stickerItem: StickerPackItem, canManagePeerSpecificPack: Bool?, interfaceInteraction: ChatControllerInteraction?, inputNodeInteraction: ChatMediaInputNodeInteraction, hasAccessory: Bool, theme: PresentationTheme, large: Bool = false, isLocked: Bool = false, selected: @escaping () -> Void) { self.context = context self.index = index self.stickerItem = stickerItem @@ -145,7 +145,7 @@ final class ChatMediaInputStickerGridItem: GridItem { self.section = ChatMediaInputStickerGridSection(collectionId: collectionId, collectionInfo: stickerPackInfo, accessory: accessory, theme: theme, interaction: inputNodeInteraction) } - func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { + public func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { let node = ChatMediaInputStickerGridItemNode() node.interfaceInteraction = self.interfaceInteraction node.inputNodeInteraction = self.inputNodeInteraction @@ -153,7 +153,7 @@ final class ChatMediaInputStickerGridItem: GridItem { return node } - func update(node: GridItemNode) { + public func update(node: GridItemNode) { guard let node = node as? ChatMediaInputStickerGridItemNode else { assertionFailure() return @@ -164,26 +164,26 @@ final class ChatMediaInputStickerGridItem: GridItem { } } -final class ChatMediaInputStickerGridItemNode: GridItemNode { +public final class ChatMediaInputStickerGridItemNode: GridItemNode { private var currentState: (AccountContext, StickerPackItem, CGSize)? private var currentSize: CGSize? - let imageNode: TransformImageNode - private(set) var animationNode: AnimatedStickerNode? - private(set) var placeholderNode: StickerShimmerEffectNode? + public let imageNode: TransformImageNode + public private(set) var animationNode: AnimatedStickerNode? + public private(set) var placeholderNode: StickerShimmerEffectNode? private var lockBackground: UIVisualEffectView? private var lockTintView: UIView? private var lockIconNode: ASImageNode? - var isLocked: Bool? + public var isLocked: Bool? private var didSetUpAnimationNode = false private var item: ChatMediaInputStickerGridItem? private let stickerFetchedDisposable = MetaDisposable() - var currentIsPreviewing = false + public var currentIsPreviewing = false - override var isVisibleInGrid: Bool { + override public var isVisibleInGrid: Bool { didSet { self.updateVisibility() } @@ -192,15 +192,15 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { private var isPanelVisible = false private var isPlaying = false - var interfaceInteraction: ChatControllerInteraction? - var inputNodeInteraction: ChatMediaInputNodeInteraction? - var selected: (() -> Void)? + public var interfaceInteraction: ChatControllerInteraction? + public var inputNodeInteraction: ChatMediaInputNodeInteraction? + public var selected: (() -> Void)? - var stickerPackItem: StickerPackItem? { + public var stickerPackItem: StickerPackItem? { return self.currentState?.1 } - override init() { + override public init() { self.imageNode = TransformImageNode() self.placeholderNode = StickerShimmerEffectNode() self.placeholderNode?.isUserInteractionEnabled = false @@ -244,13 +244,13 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { } } - override func didLoad() { + override public func didLoad() { super.didLoad() self.imageNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:)))) } - override func updateLayout(item: GridItem, size: CGSize, isVisible: Bool, synchronousLoads: Bool) { + override public func updateLayout(item: GridItem, size: CGSize, isVisible: Bool, synchronousLoads: Bool) { guard let item = item as? ChatMediaInputStickerGridItem else { return } @@ -392,13 +392,13 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { } } - override func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) { + override public func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) { if let placeholderNode = self.placeholderNode { placeholderNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: absoluteRect.minX + placeholderNode.frame.minX, y: absoluteRect.minY + placeholderNode.frame.minY), size: placeholderNode.frame.size), within: containerSize) } } - @objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) { + @objc private func imageNodeTap(_ recognizer: UITapGestureRecognizer) { if self.imageNode.layer.animation(forKey: "opacity") != nil { return } @@ -411,18 +411,18 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { } } - func transitionNode() -> ASDisplayNode? { + public func transitionNode() -> ASDisplayNode? { return self.imageNode } - func updateIsPanelVisible(_ isPanelVisible: Bool) { + public func updateIsPanelVisible(_ isPanelVisible: Bool) { if self.isPanelVisible != isPanelVisible { self.isPanelVisible = isPanelVisible self.updateVisibility() } } - func updateVisibility() { + public func updateVisibility() { guard let item = self.item else { return } @@ -444,7 +444,7 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { } } - func updatePreviewing(animated: Bool) { + public func updatePreviewing(animated: Bool) { var isPreviewing = false if let (_, item, _) = self.currentState, let interaction = self.inputNodeInteraction { isPreviewing = interaction.previewedStickerPackItemFile?.id == item.file.id diff --git a/submodules/TelegramUI/Components/EmojiActionIconComponent/BUILD b/submodules/TelegramUI/Components/EmojiActionIconComponent/BUILD new file mode 100644 index 0000000000..abd044bb77 --- /dev/null +++ b/submodules/TelegramUI/Components/EmojiActionIconComponent/BUILD @@ -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", + ], +) diff --git a/submodules/TelegramUI/Components/EmojiActionIconComponent/Sources/EmojiActionIconComponent.swift b/submodules/TelegramUI/Components/EmojiActionIconComponent/Sources/EmojiActionIconComponent.swift new file mode 100644 index 0000000000..76d434634c --- /dev/null +++ b/submodules/TelegramUI/Components/EmojiActionIconComponent/Sources/EmojiActionIconComponent.swift @@ -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? + + func update(component: EmojiActionIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let size = CGSize(width: 24.0, height: 24.0) + + if let fileId = component.fileId { + let icon: ComponentView + 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, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift index d2c964545d..11342df326 100644 --- a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift +++ b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift @@ -11,6 +11,7 @@ import AccountContext public final class ListMultilineTextFieldItemComponent: Component { public final class ExternalState { public fileprivate(set) var hasText: Bool = false + public fileprivate(set) var text: NSAttributedString = NSAttributedString() public init() { } @@ -206,6 +207,7 @@ public final class ListMultilineTextFieldItemComponent: Component { transition: transition, component: AnyComponent(TextFieldComponent( context: component.context, + theme: component.theme, strings: component.strings, externalState: self.textFieldExternalState, fontSize: 17.0, @@ -266,6 +268,7 @@ public final class ListMultilineTextFieldItemComponent: Component { self.separatorInset = 16.0 component.externalState?.hasText = self.textFieldExternalState.hasText + component.externalState?.text = self.textFieldExternalState.text return size } diff --git a/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift index 714250a551..9071b79f60 100644 --- a/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift +++ b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift @@ -24,6 +24,7 @@ public final class ListSectionComponent: Component { public let header: AnyComponent? public let footer: AnyComponent? public let items: [AnyComponentWithIdentity] + public let itemUpdateOrder: [AnyHashable]? public let displaySeparators: Bool public let extendsItemHighlightToSection: Bool @@ -33,6 +34,7 @@ public final class ListSectionComponent: Component { header: AnyComponent?, footer: AnyComponent?, items: [AnyComponentWithIdentity], + itemUpdateOrder: [AnyHashable]? = nil, displaySeparators: Bool = true, extendsItemHighlightToSection: Bool = false ) { @@ -41,6 +43,7 @@ public final class ListSectionComponent: Component { self.header = header self.footer = footer self.items = items + self.itemUpdateOrder = itemUpdateOrder self.displaySeparators = displaySeparators self.extendsItemHighlightToSection = extendsItemHighlightToSection } @@ -61,6 +64,9 @@ public final class ListSectionComponent: Component { if lhs.items != rhs.items { return false } + if lhs.itemUpdateOrder != rhs.itemUpdateOrder { + return false + } if lhs.displaySeparators != rhs.displaySeparators { return false } @@ -204,7 +210,41 @@ public final class ListSectionComponent: Component { var innerContentHeight: CGFloat = 0.0 var validItemIds: [AnyHashable] = [] + + struct ReadyItem { + var index: Int + var itemId: AnyHashable + var itemView: ItemView + var itemTransition: Transition + var itemSize: CGSize + + init(index: Int, itemId: AnyHashable, itemView: ItemView, itemTransition: Transition, itemSize: CGSize) { + self.index = index + self.itemId = itemId + self.itemView = itemView + self.itemTransition = itemTransition + self.itemSize = itemSize + } + } + + var readyItems: [ReadyItem] = [] + var itemUpdateOrder: [Int] = [] + if let itemUpdateOrderValue = component.itemUpdateOrder { + for id in itemUpdateOrderValue { + if let index = component.items.firstIndex(where: { $0.id == id }) { + if !itemUpdateOrder.contains(index) { + itemUpdateOrder.append(index) + } + } + } + } for i in 0 ..< component.items.count { + if !itemUpdateOrder.contains(i) { + itemUpdateOrder.append(i) + } + } + + for i in itemUpdateOrder { let item = component.items[i] let itemId = item.id validItemIds.append(itemId) @@ -226,17 +266,29 @@ public final class ListSectionComponent: Component { environment: {}, containerSize: CGSize(width: availableSize.width, height: availableSize.height) ) - let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: innerContentHeight), size: itemSize) - if let itemComponentView = itemView.contents.view { + + 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 { - 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) + readyItem.itemView.addSubview(itemComponentView) + self.contentItemContainerView.addSubview(readyItem.itemView) + self.contentSeparatorContainerLayer.addSublayer(readyItem.itemView.separatorLayer) + self.contentHighlightContainerLayer.addSublayer(readyItem.itemView.highlightLayer) + transition.animateAlpha(view: readyItem.itemView, from: 0.0, to: 1.0) + transition.animateAlpha(layer: readyItem.itemView.separatorLayer, from: 0.0, to: 1.0) + transition.animateAlpha(layer: readyItem.itemView.highlightLayer, from: 0.0, to: 1.0) + let itemId = readyItem.itemId if let itemComponentView = itemComponentView as? ChildView { itemComponentView.customUpdateIsHighlighted = { [weak self] isHighlighted in guard let self else { @@ -250,20 +302,20 @@ public final class ListSectionComponent: Component { if let itemComponentView = itemComponentView as? ChildView { separatorInset = itemComponentView.separatorInset } - itemTransition.setFrame(view: itemView, frame: itemFrame) + readyItem.itemTransition.setFrame(view: readyItem.itemView, frame: itemFrame) - let itemSeparatorTopOffset: CGFloat = i == 0 ? 0.0 : -UIScreenPixel + let itemSeparatorTopOffset: CGFloat = readyItem.index == 0 ? 0.0 : -UIScreenPixel let itemHighlightFrame = CGRect(origin: CGPoint(x: itemFrame.minX, y: itemFrame.minY + itemSeparatorTopOffset), size: CGSize(width: itemFrame.width, height: itemFrame.height - itemSeparatorTopOffset)) - itemTransition.setFrame(layer: itemView.highlightLayer, frame: itemHighlightFrame) + readyItem.itemTransition.setFrame(layer: readyItem.itemView.highlightLayer, frame: itemHighlightFrame) - itemTransition.setFrame(view: itemComponentView, frame: CGRect(origin: CGPoint(), size: itemFrame.size)) + readyItem.itemTransition.setFrame(view: itemComponentView, frame: CGRect(origin: CGPoint(), size: itemFrame.size)) let itemSeparatorFrame = CGRect(origin: CGPoint(x: separatorInset, y: itemFrame.maxY - UIScreenPixel), size: CGSize(width: availableSize.width - separatorInset, height: UIScreenPixel)) - itemTransition.setFrame(layer: itemView.separatorLayer, frame: itemSeparatorFrame) + readyItem.itemTransition.setFrame(layer: readyItem.itemView.separatorLayer, frame: itemSeparatorFrame) let separatorAlpha: CGFloat if component.displaySeparators { - if i != component.items.count - 1 { + if readyItem.index != component.items.count - 1 { separatorAlpha = 1.0 } else { separatorAlpha = 0.0 @@ -271,11 +323,12 @@ public final class ListSectionComponent: Component { } else { separatorAlpha = 0.0 } - itemTransition.setAlpha(layer: itemView.separatorLayer, alpha: separatorAlpha) - itemView.separatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor + readyItem.itemTransition.setAlpha(layer: readyItem.itemView.separatorLayer, alpha: separatorAlpha) + readyItem.itemView.separatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor } - innerContentHeight += itemSize.height + innerContentHeight += readyItem.itemSize.height } + var removedItemIds: [AnyHashable] = [] for (id, itemView) in self.itemViews { if !validItemIds.contains(id) { diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index c4b9b0d6c1..e8fcbc188c 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -776,6 +776,7 @@ public final class MessageInputPanelComponent: Component { transition: .immediate, component: AnyComponent(TextFieldComponent( context: component.context, + theme: component.theme, strings: component.strings, externalState: self.textFieldExternalState, fontSize: 17.0, diff --git a/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/BUILD b/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/BUILD new file mode 100644 index 0000000000..293e05c70e --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/BUILD @@ -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", + ], +) diff --git a/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/BusinessIntroSetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/BusinessIntroSetupScreen.swift new file mode 100644 index 0000000000..2294553396 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/BusinessIntroSetupScreen.swift @@ -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() + private let introContent = ComponentView() + private let introSection = ComponentView() + private let deleteSection = ComponentView() + + 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? + + 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, 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] = [] + 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 + var animateIn = false + if let current = self.stickerSelectionControl { + stickerSelectionControl = current + } else { + animateIn = true + stickerSelectionControl = ComponentView() + self.stickerSelectionControl = stickerSelectionControl + } + var selectedItems = Set() + 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, 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) + } +} diff --git a/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/ChatIntroItemComponent.swift b/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/ChatIntroItemComponent.swift new file mode 100644 index 0000000000..ada5480c79 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/ChatIntroItemComponent.swift @@ -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, 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, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift index 95f51e3f2c..5e200654a7 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift @@ -509,7 +509,6 @@ final class BusinessLocationSetupScreenComponent: Component { contentHeight += mapSectionSize.height var deleteSectionHeight: CGFloat = 0.0 - deleteSectionHeight += sectionSpacing let deleteSectionSize = self.deleteSection.update( transition: transition, diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD index 56a50b2be2..59f5675707 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/BUILD @@ -51,6 +51,7 @@ swift_library( "//submodules/TelegramUI/Components/GroupStickerPackSetupController", "//submodules/TelegramUI/Components/Chat/ChatMessageItemImpl", "//submodules/TelegramUI/Components/Settings/PeerNameColorItem", + "//submodules/TelegramUI/Components/EmojiActionIconComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift index e805cbd4b7..6080858a9b 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift @@ -38,100 +38,7 @@ import BundleIconComponent import Markdown import GroupStickerPackSetupController import PeerNameColorItem - -private final class EmojiActionIconComponent: Component { - let context: AccountContext - let color: UIColor - let fileId: Int64? - let file: TelegramMediaFile? - - init( - context: AccountContext, - color: UIColor, - fileId: Int64?, - file: TelegramMediaFile? - ) { - self.context = context - self.color = color - self.fileId = fileId - self.file = file - } - - static func ==(lhs: EmojiActionIconComponent, rhs: EmojiActionIconComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } - if lhs.color != rhs.color { - return false - } - if lhs.fileId != rhs.fileId { - return false - } - if lhs.file != rhs.file { - return false - } - return true - } - - final class View: UIView { - private var icon: ComponentView? - - func update(component: EmojiActionIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - let size = CGSize(width: 24.0, height: 24.0) - - if let fileId = component.fileId { - let icon: ComponentView - 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, transition: Transition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } -} +import EmojiActionIconComponent final class ChannelAppearanceScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index 7fa657812c..6935a7cd60 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -27,6 +27,7 @@ public final class TextFieldComponent: Component { public final class ExternalState { public fileprivate(set) var isEditing: Bool = false public fileprivate(set) var hasText: Bool = false + public fileprivate(set) var text: NSAttributedString = NSAttributedString() public fileprivate(set) var textLength: Int = 0 public var initialText: NSAttributedString? @@ -87,6 +88,7 @@ public final class TextFieldComponent: Component { } public let context: AccountContext + public let theme: PresentationTheme public let strings: PresentationStrings public let externalState: ExternalState public let fontSize: CGFloat @@ -105,6 +107,7 @@ public final class TextFieldComponent: Component { public init( context: AccountContext, + theme: PresentationTheme, strings: PresentationStrings, externalState: ExternalState, fontSize: CGFloat, @@ -122,6 +125,7 @@ public final class TextFieldComponent: Component { paste: @escaping (PasteData) -> Void ) { self.context = context + self.theme = theme self.strings = strings self.externalState = externalState self.fontSize = fontSize @@ -140,6 +144,12 @@ public final class TextFieldComponent: Component { } public static func ==(lhs: TextFieldComponent, rhs: TextFieldComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } if lhs.strings !== rhs.strings { return false } @@ -219,7 +229,6 @@ public final class TextFieldComponent: Component { self.textView.translatesAutoresizingMaskIntoConstraints = false self.textView.backgroundColor = nil self.textView.layer.isOpaque = false - self.textView.keyboardAppearance = .dark self.textView.indicatorStyle = .white self.textView.scrollIndicatorInsets = UIEdgeInsets(top: 9.0, left: 0.0, bottom: 9.0, right: 0.0) @@ -232,10 +241,6 @@ public final class TextFieldComponent: Component { self.textView.customDelegate = self self.addSubview(self.textView) - if #available(iOS 13.0, *) { - self.textView.overrideUserInterfaceStyle = .dark - } - self.textView.typingAttributes = [ NSAttributedString.Key.font: Font.regular(17.0), NSAttributedString.Key.foregroundColor: UIColor.white @@ -724,7 +729,7 @@ public final class TextFieldComponent: Component { } } - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: component.theme) let updatedPresentationData: (initial: PresentationData, signal: Signal) = (presentationData, .single(presentationData)) let controller = chatTextLinkEditController(sharedContext: component.context.sharedContext, updatedPresentationData: updatedPresentationData, account: component.context.account, text: text.string, link: link, apply: { [weak self] link in if let self { @@ -1048,9 +1053,17 @@ public final class TextFieldComponent: Component { self.isUpdating = false } + let previousComponent = self.component self.component = component self.state = state + if previousComponent?.theme !== component.theme { + self.textView.keyboardAppearance = component.theme.overallDarkAppearance ? .dark : .light + if #available(iOS 13.0, *) { + self.textView.overrideUserInterfaceStyle = component.theme.overallDarkAppearance ? .dark : .light + } + } + if let initialText = component.externalState.initialText { component.externalState.initialText = nil self.updateInputState { _ in @@ -1128,6 +1141,7 @@ public final class TextFieldComponent: Component { component.externalState.hasText = self.textView.textStorage.length != 0 component.externalState.isEditing = isEditing component.externalState.textLength = self.textView.textStorage.string.count + component.externalState.text = NSAttributedString(attributedString: self.textView.textStorage) if let inputView = component.customInputView { if self.textView.inputView == nil { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index aaf9c723e9..bf657946b9 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -121,6 +121,8 @@ import TopMessageReactions import PeerInfoScreen import AudioWaveform import PeerNameColorScreen +import ChatEmptyNode +import ChatMediaInputStickerGridItem public enum ChatControllerPeekActions { case standard diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 7de7df129b..9eee014f4c 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -42,6 +42,7 @@ import UIKitRuntimeUtils import ChatInlineSearchResultsListComponent import ComponentDisplayAdapters import ComponentFlow +import ChatEmptyNode final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem { let itemNode: OverlayMediaItemNode @@ -990,7 +991,20 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.emptyNode = emptyNode self.historyNodeContainer.supernode?.insertSubnode(emptyNode, aboveSubnode: self.historyNodeContainer) if let (size, insets) = self.validEmptyNodeLayout { - emptyNode.updateLayout(interfaceState: self.chatPresentationInterfaceState, subject: .emptyChat(emptyType), loadingNode: wasLoading && self.loadingNode.supernode != nil ? self.loadingNode : nil, backgroundNode: self.backgroundNode, size: size, insets: insets, transition: .immediate) + let mappedType: ChatEmptyNode.Subject.EmptyType + switch emptyType { + case .generic: + mappedType = .generic + case .joined: + mappedType = .joined + case .clearedHistory: + mappedType = .clearedHistory + case .topic: + mappedType = .topic + case .botInfo: + mappedType = .botInfo + } + emptyNode.updateLayout(interfaceState: self.chatPresentationInterfaceState, subject: .emptyChat(mappedType), loadingNode: wasLoading && self.loadingNode.supernode != nil ? self.loadingNode : nil, backgroundNode: self.backgroundNode, size: size, insets: insets, transition: .immediate) } if animated { emptyNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) @@ -1842,7 +1856,20 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { emptyNodeInsets.bottom += inputPanelsHeight self.validEmptyNodeLayout = (contentBounds.size, emptyNodeInsets) if let emptyNode = self.emptyNode, let emptyType = self.emptyType { - emptyNode.updateLayout(interfaceState: self.chatPresentationInterfaceState, subject: .emptyChat(emptyType), loadingNode: nil, backgroundNode: self.backgroundNode, size: contentBounds.size, insets: emptyNodeInsets, transition: transition) + let mappedType: ChatEmptyNode.Subject.EmptyType + switch emptyType { + case .generic: + mappedType = .generic + case .joined: + mappedType = .joined + case .clearedHistory: + mappedType = .clearedHistory + case .topic: + mappedType = .topic + case .botInfo: + mappedType = .botInfo + } + emptyNode.updateLayout(interfaceState: self.chatPresentationInterfaceState, subject: .emptyChat(mappedType), loadingNode: nil, backgroundNode: self.backgroundNode, size: contentBounds.size, insets: emptyNodeInsets, transition: transition) transition.updateFrame(node: emptyNode, frame: contentBounds) emptyNode.update(rect: contentBounds, within: contentBounds.size, transition: transition) } diff --git a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift index 9b4547a611..6033787e87 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift @@ -19,6 +19,8 @@ import ChatMessageInstantVideoItemNode import ChatMessageAnimatedStickerItemNode import ChatMessageTransitionNode import ChatMessageBubbleItemNode +import ChatEmptyNode +import ChatMediaInputStickerGridItem private func convertAnimatingSourceRect(_ rect: CGRect, fromView: UIView, toView: UIView?) -> CGRect { if let presentationLayer = fromView.layer.presentation() { diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift index e7e3ee8dce..0a086cedf0 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift @@ -332,12 +332,20 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection } } + if let addedToken = addedToken { + strongSelf.contactsNode.editableTokens.append(addedToken) + } else if let removedTokenId = removedTokenId { + strongSelf.contactsNode.editableTokens = strongSelf.contactsNode.editableTokens.filter { token in + return token.id != removedTokenId + } + } + if let updatedCount = updatedCount { switch strongSelf.mode { - case .groupCreation, .peerSelection, .chatSelection: - strongSelf.rightNavigationButton?.isEnabled = updatedCount != 0 || strongSelf.params.alwaysEnabled - case .channelCreation, .premiumGifting, .requestedUsersSelection: - break + case .groupCreation, .peerSelection, .chatSelection: + strongSelf.rightNavigationButton?.isEnabled = updatedCount != 0 || !strongSelf.contactsNode.editableTokens.isEmpty || strongSelf.params.alwaysEnabled + case .channelCreation, .premiumGifting, .requestedUsersSelection: + break } switch strongSelf.mode { @@ -355,13 +363,6 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection } } - if let addedToken = addedToken { - strongSelf.contactsNode.editableTokens.append(addedToken) - } else if let removedTokenId = removedTokenId { - strongSelf.contactsNode.editableTokens = strongSelf.contactsNode.editableTokens.filter { token in - return token.id != removedTokenId - } - } strongSelf.requestLayout(transition: ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)) if displayCountAlert { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 48b0935967..7eaa2431f5 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -59,6 +59,7 @@ import CollectibleItemInfoScreen import StickerPickerScreen import MediaEditor import MediaEditorScreen +import BusinessIntroSetupScreen private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() @@ -1927,6 +1928,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { return QuickReplySetupScreen.initialData(context: context) } + public func makeBusinessIntroSetupScreen(context: AccountContext) -> ViewController { + return BusinessIntroSetupScreen(context: context) + } + public func makeCollectibleItemInfoScreen(context: AccountContext, initialData: CollectibleItemInfoScreenInitialData) -> ViewController { return CollectibleItemInfoScreen(context: context, initialData: initialData as! CollectibleItemInfoScreen.InitialData) } diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index aefb623683..75668dbb9c 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -30,6 +30,7 @@ import MediaEditor import PeerInfoScreen import PeerInfoStoryGridScreen import ShareWithPeersScreen +import ChatEmptyNode private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode { private var presentationData: PresentationData