diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index bde186f915..5fd7f05025 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -995,6 +995,7 @@ public protocol ChatController: ViewController { var isSelectingMessagesUpdated: ((Bool) -> Void)? { get set } func cancelSelectingMessages() func activateSearch(domain: ChatSearchDomain, query: String) + func activateInput(type: ChatControllerActivateInput) func beginClearHistory(type: InteractiveHistoryClearingType) func performScrollToTop() -> Bool diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift index 0b323ae9fc..96fb118dab 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift @@ -812,7 +812,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz } } - let openNewCard: (String?) -> Void = { [weak self] customUrl in + let openNewCard: (String?, String?) -> Void = { [weak self] customUrl, customTitle in if let strongSelf = self, let paymentForm = strongSelf.paymentFormValue { if customUrl == nil, let nativeProvider = paymentForm.nativeProvider, nativeProvider.name == "stripe" { guard let paramsData = nativeProvider.params.data(using: .utf8) else { @@ -970,7 +970,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz strongSelf.present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } else { var dismissImpl: (() -> Void)? - let controller = BotCheckoutWebInteractionController(context: context, url: customUrl ?? paymentForm.url, intent: .addPaymentMethod({ [weak self] token in + let controller = BotCheckoutWebInteractionController(context: context, url: customUrl ?? paymentForm.url, intent: .addPaymentMethod(customTitle: customTitle, completion: { [weak self] token in dismissImpl?() guard let strongSelf = self else { @@ -1069,14 +1069,14 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz strongSelf.controller?.view.endEditing(true) let methods = availablePaymentMethods(form: paymentForm, current: strongSelf.currentPaymentMethod) if methods.isEmpty { - openNewCard(nil) + openNewCard(nil, nil) } else { strongSelf.present(BotCheckoutPaymentMethodSheetController(context: strongSelf.context, currentMethod: strongSelf.currentPaymentMethod, methods: methods, applyValue: { method in applyPaymentMethod(method) }, newCard: { - openNewCard(nil) - }, otherMethod: { url in - openNewCard(url) + openNewCard(nil, nil) + }, otherMethod: { url, title in + openNewCard(url, title) }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentMethodSheet.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentMethodSheet.swift index 3ebc536d6d..a728d78155 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentMethodSheet.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentMethodSheet.swift @@ -39,7 +39,7 @@ enum BotCheckoutPaymentMethod: Equatable { final class BotCheckoutPaymentMethodSheetController: ActionSheetController { private var presentationDisposable: Disposable? - init(context: AccountContext, currentMethod: BotCheckoutPaymentMethod?, methods: [BotCheckoutPaymentMethod], applyValue: @escaping (BotCheckoutPaymentMethod) -> Void, newCard: @escaping () -> Void, otherMethod: @escaping (String) -> Void) { + init(context: AccountContext, currentMethod: BotCheckoutPaymentMethod?, methods: [BotCheckoutPaymentMethod], applyValue: @escaping (BotCheckoutPaymentMethod) -> Void, newCard: @escaping () -> Void, otherMethod: @escaping (String, String) -> Void) { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let strings = presentationData.strings @@ -83,7 +83,7 @@ final class BotCheckoutPaymentMethodSheetController: ActionSheetController { } items.append(BotCheckoutPaymentMethodItem(title: title, icon: icon, value: value, action: { [weak self] _ in if case let .other(method) = method { - otherMethod(method.url) + otherMethod(method.url, method.title) } else { applyValue(method) } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionController.swift index 2bf329b15d..bc03da266a 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionController.swift @@ -8,7 +8,7 @@ import TelegramPresentationData import AccountContext enum BotCheckoutWebInteractionControllerIntent { - case addPaymentMethod((BotCheckoutPaymentWebToken) -> Void) + case addPaymentMethod(customTitle: String?, completion: (BotCheckoutPaymentWebToken) -> Void) case externalVerification((Bool) -> Void) } @@ -39,10 +39,10 @@ final class BotCheckoutWebInteractionController: ViewController { self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) switch intent { - case .addPaymentMethod: - self.title = self.presentationData.strings.Checkout_NewCard_Title - case .externalVerification: - self.title = self.presentationData.strings.Checkout_WebConfirmation_Title + case let .addPaymentMethod(customTitle, _): + self.title = customTitle ?? self.presentationData.strings.Checkout_NewCard_Title + case .externalVerification: + self.title = self.presentationData.strings.Checkout_WebConfirmation_Title } } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionControllerNode.swift index 4352cb60a0..cf6d9647c7 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionControllerNode.swift @@ -130,7 +130,7 @@ final class BotCheckoutWebInteractionControllerNode: ViewControllerTracingNode, return } - if case let .addPaymentMethod(completion) = self.intent { + if case let .addPaymentMethod(_, completion) = self.intent { completion(BotCheckoutPaymentWebToken(title: title, data: credentialsString, saveOnServer: false)) } } diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 025d315385..a499049300 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -910,7 +910,7 @@ private final class ChatListMediaPreviewNode: ASDisplayNode { private let loginCodeRegex = try? NSRegularExpression(pattern: "[\\d\\-]{5,7}", options: []) -class ChatListItemNode: ItemListRevealOptionsItemNode { +public class ChatListItemNode: ItemListRevealOptionsItemNode { final class TopicItemNode: ASDisplayNode { let topicTitleNode: TextNode let titleTopicIconView: ComponentHostView @@ -998,13 +998,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } - final class AuthorNode: ASDisplayNode { - let authorNode: TextNode + public final class AuthorNode: ASDisplayNode { + public let authorNode: TextNode var titleTopicArrowNode: ASImageNode? var topicNodes: [Int64: TopicItemNode] = [:] var topicNodeOrder: [Int64] = [] - var visibilityStatus: Bool = false { + public var visibilityStatus: Bool = false { didSet { if self.visibilityStatus != oldValue { for (_, topicNode) in self.topicNodes { @@ -1014,7 +1014,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } - override init() { + override public init() { self.authorNode = TextNode() self.authorNode.displaysAsynchronously = true @@ -1186,8 +1186,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let contextContainer: ContextControllerSourceNode let mainContentContainerNode: ASDisplayNode - let avatarContainerNode: ASDisplayNode - let avatarNode: AvatarNode + public let avatarContainerNode: ASDisplayNode + public let avatarNode: AvatarNode var avatarIconView: ComponentHostView? var avatarIconComponent: EmojiStatusComponent? var avatarVideoNode: AvatarVideoNode? @@ -1196,23 +1196,23 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { private var inlineNavigationMarkLayer: SimpleLayer? - let titleNode: TextNode - let authorNode: AuthorNode + public let titleNode: TextNode + public let authorNode: AuthorNode private var compoundHighlightingNode: LinkHighlightingNode? private var textArrowNode: ASImageNode? private var compoundTextButtonNode: HighlightTrackingButtonNode? let measureNode: TextNode private var currentItemHeight: CGFloat? let forwardedIconNode: ASImageNode - let textNode: TextNodeWithEntities + public let textNode: TextNodeWithEntities var trailingTextBadgeNode: TextNode? var trailingTextBadgeBackground: UIImageView? var dustNode: InvisibleInkDustNode? let inputActivitiesNode: ChatListInputActivitiesNode - let dateNode: TextNode + public let dateNode: TextNode var dateStatusIconNode: ASImageNode? var dateDisclosureIconView: UIImageView? - let separatorNode: ASDisplayNode + public let separatorNode: ASDisplayNode let statusNode: ChatListStatusNode let badgeNode: ChatListBadgeNode let mentionBadgeNode: ChatListBadgeNode @@ -1255,7 +1255,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { private var onlineIsVoiceChat: Bool = false private var currentOnline: Bool? - override var canBeSelected: Bool { + override public var canBeSelected: Bool { if self.selectableControlNode != nil || self.item?.editing == true { return false } else { @@ -1263,26 +1263,26 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } - override var defaultAccessibilityLabel: String? { + override public var defaultAccessibilityLabel: String? { get { return self.accessibilityLabel } set(value) { } } - override var accessibilityAttributedLabel: NSAttributedString? { + override public var accessibilityAttributedLabel: NSAttributedString? { get { return self.accessibilityLabel.flatMap(NSAttributedString.init(string:)) } set(value) { } } - override var accessibilityAttributedValue: NSAttributedString? { + override public var accessibilityAttributedValue: NSAttributedString? { get { return self.accessibilityValue.flatMap(NSAttributedString.init(string:)) } set(value) { } } - override var accessibilityLabel: String? { + override public var accessibilityLabel: String? { get { guard let item = self.item else { return nil @@ -1314,7 +1314,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } - override var accessibilityValue: String? { + override public var accessibilityValue: String? { get { guard let item = self.item else { return nil @@ -1382,7 +1382,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } - override var visibility: ListViewItemNodeVisibility { + override public var visibility: ListViewItemNodeVisibility { didSet { let wasVisible = self.visibilityStatus let isVisible: Bool @@ -1575,7 +1575,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { threadId = topicItem.id } } - item.interaction.activateChatPreview(item, threadId, strongSelf.contextContainer, gesture, nil) + item.interaction.activateChatPreview?(item, threadId, strongSelf.contextContainer, gesture, nil) } self.onDidLoad { [weak self] _ in @@ -1592,11 +1592,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { self.cachedDataDisposable.dispose() } - override func secondaryAction(at point: CGPoint) { + override public func secondaryAction(at point: CGPoint) { guard let item = self.item else { return } - item.interaction.activateChatPreview(item, nil, self.contextContainer, nil, point) + item.interaction.activateChatPreview?(item, nil, self.contextContainer, nil, point) } func setupItem(item: ChatListItem, synchronousLoads: Bool) { @@ -1632,6 +1632,10 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { self.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: .archivedChatsIcon(hiddenByDefault: groupReferenceData.hiddenByDefault), emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads) } + if item.interaction.activateChatPreview == nil { + enablePreview = false + } + self.avatarNode.setStoryStats(storyStats: storyState.flatMap { storyState in return AvatarNode.StoryStats( totalCount: storyState.stats.totalCount, @@ -1768,7 +1772,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { self.contextContainer.isGestureEnabled = enablePreview && !item.editing } - override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { let layout = self.asyncLayout() let (first, last, firstWithHeader, nextIsPinned) = ChatListItem.mergeType(item: item as! ChatListItem, previousItem: previousItem, nextItem: nextItem) let (nodeLayout, apply) = layout(item as! ChatListItem, params, first, last, firstWithHeader, nextIsPinned) @@ -1781,7 +1785,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { return UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0) } - override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { super.setHighlighted(highlighted, at: point, animated: animated) self.isHighlighted = highlighted @@ -1846,7 +1850,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } - override func tapped() { + override public func tapped() { guard let item = self.item, item.editing else { return } @@ -4414,11 +4418,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { item.interaction.openForumThread(index.messageIndex.id.peerId, topicItem.id) } - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.clipsToBounds = true if self.skipFadeout { self.skipFadeout = false @@ -4445,20 +4449,20 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } - override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { + override public func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { super.updateRevealOffset(offset: offset, transition: transition) transition.updateBounds(node: self.contextContainer, bounds: self.contextContainer.frame.offsetBy(dx: -offset, dy: 0.0)) } - override func touchesToOtherItemsPrevented() { + override public func touchesToOtherItemsPrevented() { super.touchesToOtherItemsPrevented() if let item = self.item { item.interaction.setPeerIdWithRevealedOptions(nil, nil) } } - override func revealOptionsInteractivelyOpened() { + override public func revealOptionsInteractivelyOpened() { if let item = self.item { switch item.index { case let .chatList(index): @@ -4469,7 +4473,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } - override func revealOptionsInteractivelyClosed() { + override public func revealOptionsInteractivelyClosed() { if let item = self.item { switch item.index { case let .chatList(index): @@ -4480,7 +4484,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } - override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { + override public func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { guard let item = self.item else { return } @@ -4607,7 +4611,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } - override func isReorderable(at point: CGPoint) -> Bool { + override public func isReorderable(at point: CGPoint) -> Bool { if let reorderControlNode = self.reorderControlNode, reorderControlNode.frame.contains(point) { return true } @@ -4632,7 +4636,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { self.avatarNode.playArchiveAnimation() } - override func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) { + override public func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) { super.animateFrameTransition(progress, currentValue) if let item = self.item { @@ -4652,14 +4656,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } - override func snapshotForReordering() -> UIView? { + override public func snapshotForReordering() -> UIView? { self.backgroundNode.alpha = 0.9 let result = self.view.snapshotContentTree() self.backgroundNode.alpha = 1.0 return result } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { guard let item = self.item else { return nil } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 8bc2745f76..eaa684efb9 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -95,7 +95,7 @@ public final class ChatListNodeInteraction { let toggleArchivedFolderHiddenByDefault: () -> Void let toggleThreadsSelection: ([Int64], Bool) -> Void let hidePsa: (EnginePeer.Id) -> Void - let activateChatPreview: (ChatListItem, Int64?, ASDisplayNode, ContextGesture?, CGPoint?) -> Void + let activateChatPreview: ((ChatListItem, Int64?, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? let present: (ViewController) -> Void let openForumThread: (EnginePeer.Id, Int64) -> Void let openStorageManagement: () -> Void @@ -149,7 +149,7 @@ public final class ChatListNodeInteraction { toggleArchivedFolderHiddenByDefault: @escaping () -> Void, toggleThreadsSelection: @escaping ([Int64], Bool) -> Void, hidePsa: @escaping (EnginePeer.Id) -> Void, - activateChatPreview: @escaping (ChatListItem, Int64?, ASDisplayNode, ContextGesture?, CGPoint?) -> Void, + activateChatPreview: ((ChatListItem, Int64?, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController) -> Void, openForumThread: @escaping (EnginePeer.Id, Int64) -> Void, openStorageManagement: @escaping () -> Void, diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index ff4e1753ef..aee623f148 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -387,7 +387,7 @@ private let avatarFont = avatarPlaceholderFont(size: 16.0) public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { private let backgroundNode: ASDisplayNode private let topSeparatorNode: ASDisplayNode - private let separatorNode: ASDisplayNode + public let separatorNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode private let maskNode: ASImageNode @@ -406,12 +406,12 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { private var avatarBadge: UIImageView? private var avatarIconView: ComponentHostView? private var avatarIconComponent: EmojiStatusComponent? - private let titleNode: TextNode + public let titleNode: TextNode private var credibilityIconView: ComponentHostView? private var credibilityIconComponent: EmojiStatusComponent? private var verifiedIconView: ComponentHostView? private var verifiedIconComponent: EmojiStatusComponent? - private let statusNode: TextNode + public let statusNode: TextNode private var statusIconNode: ASImageNode? private var badgeBackgroundNode: ASImageNode? private var badgeTextNode: TextNode? diff --git a/submodules/Postbox/Sources/MessageHistoryView.swift b/submodules/Postbox/Sources/MessageHistoryView.swift index cca6c8d97d..9d09f85842 100644 --- a/submodules/Postbox/Sources/MessageHistoryView.swift +++ b/submodules/Postbox/Sources/MessageHistoryView.swift @@ -393,6 +393,7 @@ final class MutableMessageHistoryView: MutablePostboxView { break } } + self.sampledState = self.state.sample(postbox: postbox, clipHoles: self.clipHoles) self.render(postbox: postbox) diff --git a/submodules/TabBarUI/Sources/TabBarController.swift b/submodules/TabBarUI/Sources/TabBarController.swift index f834824761..1579700413 100644 --- a/submodules/TabBarUI/Sources/TabBarController.swift +++ b/submodules/TabBarUI/Sources/TabBarController.swift @@ -417,7 +417,9 @@ open class TabBarControllerImpl: ViewController, TabBarController { } else { tabBarHeight = 49.0 + bottomInset } - updatedLayout.intrinsicInsets.bottom = tabBarHeight + if !self.tabBarControllerNode.tabBarHidden { + updatedLayout.intrinsicInsets.bottom = tabBarHeight + } currentController.containerLayoutUpdated(updatedLayout, transition: transition) } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift index dde199f34a..e9c38bdd93 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift @@ -96,6 +96,9 @@ extension TelegramUser { if (flags2 & (1 << 1)) != 0 { botFlags.insert(.canEdit) } + if (flags2 & (1 << 11)) != 0 { + botFlags.insert(.isBusiness) + } botInfo = BotUserInfo(flags: botFlags, inlinePlaceholder: botInlinePlaceholder) } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift index 8abaa7b54d..4183975e7a 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift @@ -37,6 +37,7 @@ public struct BotUserInfoFlags: OptionSet { public static let requiresGeolocationForInlineRequests = BotUserInfoFlags(rawValue: (1 << 3)) public static let canBeAddedToAttachMenu = BotUserInfoFlags(rawValue: (1 << 4)) public static let canEdit = BotUserInfoFlags(rawValue: (1 << 5)) + public static let isBusiness = BotUserInfoFlags(rawValue: (1 << 5)) } public struct BotUserInfo: PostboxCoding, Equatable { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift index 4dd76478de..46ad4f7be8 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift @@ -10,25 +10,27 @@ import ContextUI import TelegramCore import ChatListUI import Postbox +import StoryContainerScreen +import AvatarNode final class PeerInfoScreenPersonalChannelItem: PeerInfoScreenItem { let id: AnyHashable let context: AccountContext let data: PeerInfoPersonalChannelData - let requestLayout: (Bool) -> Void + let controller: () -> ViewController? let action: () -> Void init( id: AnyHashable, context: AccountContext, data: PeerInfoPersonalChannelData, - requestLayout: @escaping (Bool) -> Void, + controller: @escaping () -> ViewController?, action: @escaping () -> Void ) { self.id = id self.context = context self.data = data - self.requestLayout = requestLayout + self.controller = controller self.action = action } @@ -37,6 +39,261 @@ final class PeerInfoScreenPersonalChannelItem: PeerInfoScreenItem { } } +private final class LoadingOverlayShimmerNode: ASDisplayNode { + private var currentBackgroundColor: UIColor? + private var currentForegroundColor: UIColor? + private let imageNodeContainer: ASDisplayNode + private let imageNode: ASImageNode + + private var absoluteLocation: (CGRect, CGSize)? + private var isCurrentlyInHierarchy = false + private var shouldBeAnimating = false + + override init() { + self.imageNodeContainer = ASDisplayNode() + self.imageNodeContainer.isLayerBacked = true + + self.imageNode = ASImageNode() + self.imageNode.isLayerBacked = true + self.imageNode.displaysAsynchronously = false + self.imageNode.displayWithoutProcessing = true + self.imageNode.contentMode = .scaleToFill + + super.init() + + self.imageNodeContainer.addSubnode(self.imageNode) + self.addSubnode(self.imageNodeContainer) + } + + override func didEnterHierarchy() { + super.didEnterHierarchy() + + self.isCurrentlyInHierarchy = true + self.updateAnimation() + } + + override func didExitHierarchy() { + super.didExitHierarchy() + + self.isCurrentlyInHierarchy = false + self.updateAnimation() + } + + func update(backgroundColor: UIColor, foregroundColor: UIColor) { + if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor) { + return + } + self.currentBackgroundColor = backgroundColor + self.currentForegroundColor = foregroundColor + self.backgroundColor = backgroundColor + + self.imageNode.image = generateImage(CGSize(width: 4.0, height: 320.0), opaque: true, scale: 1.0, rotatedContext: { size, context in + context.setFillColor(backgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + + context.clip(to: CGRect(origin: CGPoint(), size: size)) + + let transparentColor = backgroundColor.cgColor + let peakColor = foregroundColor.cgColor + + var locations: [CGFloat] = [0.0, 0.5, 1.0] + let colors: [CGColor] = [transparentColor, peakColor, transparentColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + }) + } + + func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + if let absoluteLocation = self.absoluteLocation, absoluteLocation.0 == rect && absoluteLocation.1 == containerSize { + return + } + let sizeUpdated = self.absoluteLocation?.1 != containerSize + let frameUpdated = self.absoluteLocation?.0 != rect + self.absoluteLocation = (rect, containerSize) + + if sizeUpdated { + if self.shouldBeAnimating { + self.imageNode.layer.removeAnimation(forKey: "shimmer") + self.addImageAnimation() + } + } + + if frameUpdated { + self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize) + } + + self.updateAnimation() + } + + private func updateAnimation() { + let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil + if shouldBeAnimating != self.shouldBeAnimating { + self.shouldBeAnimating = shouldBeAnimating + if shouldBeAnimating { + self.addImageAnimation() + } else { + self.imageNode.layer.removeAnimation(forKey: "shimmer") + } + } + } + + private func addImageAnimation() { + guard let containerSize = self.absoluteLocation?.1 else { + return + } + let gradientHeight: CGFloat = 250.0 + self.imageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -gradientHeight), size: CGSize(width: containerSize.width, height: gradientHeight)) + let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.height + gradientHeight) as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 1.3 * 1.0, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true) + animation.repeatCount = Float.infinity + animation.beginTime = 1.0 + self.imageNode.layer.add(animation, forKey: "shimmer") + } +} + +public final class LoadingOverlayNode: ASDisplayNode { + private let effectNode: LoadingOverlayShimmerNode + private let maskNode: ASImageNode + private var currentParams: (size: CGSize, presentationData: PresentationData)? + + override public init() { + self.effectNode = LoadingOverlayShimmerNode() + self.maskNode = ASImageNode() + + super.init() + + self.isUserInteractionEnabled = false + + self.addSubnode(self.effectNode) + self.effectNode.view.mask = self.maskNode.view + } + + public func update(context: AccountContext, size: CGSize, isInlineMode: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { + if self.currentParams?.size != size || self.currentParams?.presentationData !== presentationData { + self.currentParams = (size, presentationData) + + let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) + + let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) + let timestamp1: Int32 = 100000 + let peers: [EnginePeer.Id: EnginePeer] = [:] + let interaction = ChatListNodeInteraction(context: context, animationCache: context.animationCache, animationRenderer: context.animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in + }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in + gesture?.cancel() + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, dismissNotice: { _ in + }, editPeer: { _ in + }) + + let items = (0 ..< 1).map { _ -> ChatListItem in + let message = EngineMessage( + stableId: 0, + stableVersion: 0, + id: EngineMessage.Id(peerId: peer1.id, namespace: 0, id: 0), + globallyUniqueId: nil, + groupingKey: nil, + groupInfo: nil, + threadId: nil, + timestamp: timestamp1, + flags: [], + tags: [], + globalTags: [], + localTags: [], + customTags: [], + forwardInfo: nil, + author: peer1, + text: "Text", + attributes: [], + media: [], + peers: peers, + associatedMessages: [:], + associatedMessageIds: [], + associatedMedia: [:], + associatedThreadInfo: nil, + associatedStories: [:] + ) + let readState = EnginePeerReadCounters() + + return ChatListItem(presentationData: chatListPresentationData, context: context, chatListLocation: .chatList(groupId: .root), filterData: nil, index: .chatList(EngineChatList.Item.Index.ChatList(pinningIndex: 0, messageIndex: EngineMessage.Index(id: EngineMessage.Id(peerId: peer1.id, namespace: 0, id: 0), timestamp: timestamp1))), content: .peer(ChatListItemContent.PeerData( + messages: [message], + peer: EngineRenderedPeer(peer: peer1), + threadInfo: nil, + combinedReadState: readState, + isRemovedFromTotalUnreadCount: false, + presence: nil, + hasUnseenMentions: false, + hasUnseenReactions: false, + draftState: nil, + mediaDraftContentType: nil, + inputActivities: nil, + promoInfo: nil, + ignoreUnreadBadge: false, + displayAsMessage: false, + hasFailedMessages: false, + forumTopicData: nil, + topForumTopicItems: [], + autoremoveTimeout: nil, + storyState: nil, + requiresPremiumForMessaging: false, + displayAsTopicList: false, + tags: [] + )), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction) + } + + var itemNodes: [ChatListItemNode] = [] + for i in 0 ..< items.count { + items[i].nodeConfiguredForParams(async: { f in f() }, params: ListViewItemLayoutParams(width: size.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 100.0), synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: (i == items.count - 1) ? nil : items[i + 1], completion: { node, apply in + if let itemNode = node as? ChatListItemNode { + itemNodes.append(itemNode) + } + apply().1(ListViewItemApply(isOnScreen: true)) + }) + } + + self.maskNode.image = generateImage(size, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + var currentY: CGFloat = 0.0 + let fakeLabelPlaceholderHeight: CGFloat = 8.0 + + func fillLabelPlaceholderRect(origin: CGPoint, width: CGFloat) { + let startPoint = origin + let diameter = fakeLabelPlaceholderHeight + context.fillEllipse(in: CGRect(origin: startPoint, size: CGSize(width: diameter, height: diameter))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: startPoint.x + width - diameter, y: startPoint.y), size: CGSize(width: diameter, height: diameter))) + context.fill(CGRect(origin: CGPoint(x: startPoint.x + diameter / 2.0, y: startPoint.y), size: CGSize(width: width - diameter, height: diameter))) + } + + while currentY < size.height { + let sampleIndex = 0 + let itemHeight: CGFloat = itemNodes[sampleIndex].contentSize.height + + context.setFillColor(UIColor.black.cgColor) + + let textFrame = itemNodes[sampleIndex].textNode.textNode.frame.offsetBy(dx: 0.0, dy: currentY) + + fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX, y: currentY + itemHeight - floor(itemNodes[sampleIndex].titleNode.frame.midY - fakeLabelPlaceholderHeight / 2.0) - fakeLabelPlaceholderHeight), width: 60.0) + + fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX, y: currentY + floor((itemHeight - fakeLabelPlaceholderHeight) / 2.0)), width: 120.0) + fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX + 120.0 + 10.0, y: currentY + floor((itemHeight - fakeLabelPlaceholderHeight) / 2.0)), width: 60.0) + + let dateFrame = itemNodes[sampleIndex].dateNode.frame.offsetBy(dx: 0.0, dy: currentY) + fillLabelPlaceholderRect(origin: CGPoint(x: dateFrame.maxX - 30.0, y: dateFrame.minY + 4.0), width: 30.0) + + currentY += itemHeight + } + }) + + self.effectNode.update(backgroundColor: presentationData.theme.list.mediaPlaceholderColor, foregroundColor: presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4)) + self.effectNode.updateAbsoluteRect(CGRect(origin: CGPoint(), size: size), within: size) + } + transition.updateFrame(node: self.maskNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) + transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) + } +} + + private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNode { private let containerNode: ContextControllerSourceNode private let contextSourceNode: ContextExtractedContentContainingNode @@ -46,6 +303,7 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod private var extractedRect: CGRect? private var nonExtractedRect: CGRect? + private let selectionNode: PeerInfoScreenSelectableBackgroundNode private let maskNode: ASImageNode private let bottomSeparatorNode: ASDisplayNode @@ -57,6 +315,7 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod private var theme: PresentationTheme? private var itemNode: ListViewItemNode? + private var loadingOverlayNode: LoadingOverlayNode? override init() { self.contextSourceNode = ContextExtractedContentContainingNode() @@ -66,6 +325,10 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod self.extractedBackgroundImageNode.displaysAsynchronously = false self.extractedBackgroundImageNode.alpha = 0.0 + var bringToFrontForHighlightImpl: (() -> Void)? + self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() }) + self.selectionNode.isUserInteractionEnabled = false + self.maskNode = ASImageNode() self.maskNode.isUserInteractionEnabled = false @@ -76,7 +339,12 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod super.init() + bringToFrontForHighlightImpl = { [weak self] in + self?.bringToFrontForHighlight?() + } + self.addSubnode(self.bottomSeparatorNode) + self.addSubnode(self.selectionNode) self.containerNode.addSubnode(self.contextSourceNode) self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode @@ -156,6 +424,12 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod self.item = item self.presentationData = presentationData self.theme = presentationData.theme + + self.selectionNode.pressed = { [weak self] in + if let strongSelf = self { + strongSelf.item?.action() + } + } let sideInset: CGFloat = 16.0 + safeInsets.left @@ -221,8 +495,7 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod }, hidePsa: { _ in }, - activateChatPreview: { _, _, _, _, _ in - }, + activateChatPreview: nil, present: { _ in }, openForumThread: { _, _ in @@ -245,7 +518,18 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod }, hideChatFolderUpdates: { }, - openStories: { _, _ in + openStories: { [weak self] _, sourceNode in + guard let self, let item = self.item else { + return + } + guard let itemNode = self.itemNode as? ChatListItemNode else { + return + } + guard let controller = item.controller() else { + return + } + + StoryContainerScreen.openPeerStories(context: item.context, peerId: item.data.peer.id, parentController: controller, avatarNode: itemNode.avatarNode) }, dismissNotice: { _ in }, @@ -255,9 +539,11 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod let index: EngineChatList.Item.Index let messages: [EngineMessage] - if let message = item.data.topMessage { - index = EngineChatList.Item.Index.chatList(ChatListIndex(pinningIndex: nil, messageIndex: message.index)) - messages = [message] + let isLoading = item.data.isLoading + + if !isLoading, !item.data.topMessages.isEmpty { + index = EngineChatList.Item.Index.chatList(ChatListIndex(pinningIndex: nil, messageIndex: item.data.topMessages[0].index)) + messages = item.data.topMessages } else { index = EngineChatList.Item.Index.chatList(ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: item.data.peer.id, namespace: Namespaces.Message.Cloud, id: 1), timestamp: 0))) messages = [] @@ -288,7 +574,12 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod forumTopicData: nil, topForumTopicItems: [], autoremoveTimeout: nil, - storyState: nil, + storyState: item.data.storyStats.flatMap { storyStats in + return ChatListItemContent.StoryState( + stats: storyStats, + hasUnseenCloseFriends: false + ) + }, requiresPremiumForMessaging: false, displayAsTopicList: false, tags: [], @@ -297,7 +588,7 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod searchQuery: nil, messageCount: nil, hideSeparator: true, - hideDate: false + hideDate: isLoading ) )), editing: false, @@ -356,10 +647,39 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod self.contextSourceNode.contentNode.addSubnode(itemNode) } } + let itemFrame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height)) if let itemNode = self.itemNode { - itemNode.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height)) + itemNode.frame = itemFrame } + if let itemNode = self.itemNode, item.data.isLoading { + let loadingOverlayNode: LoadingOverlayNode + if let current = self.loadingOverlayNode { + loadingOverlayNode = current + } else { + loadingOverlayNode = LoadingOverlayNode() + self.loadingOverlayNode = loadingOverlayNode + itemNode.supernode?.insertSubnode(loadingOverlayNode, aboveSubnode: itemNode) + } + loadingOverlayNode.frame = itemFrame + loadingOverlayNode.update( + context: item.context, + size: itemFrame.size, + isInlineMode: false, + presentationData: presentationData, + transition: .immediate + ) + } else { + if let loadingOverlayNode = self.loadingOverlayNode { + self.loadingOverlayNode = nil + loadingOverlayNode.removeFromSupernode() + } + } + + let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel + self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition) + transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset))) + transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel))) transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0) @@ -394,5 +714,19 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod } private func updateTouchesAtPoint(_ point: CGPoint?) { + var isHighlighted = false + if let point, let itemNode = self.itemNode as? ChatListItemNode { + if !itemNode.avatarNode.view.convert(itemNode.avatarNode.view.bounds, to: self.view).contains(point) { + isHighlighted = true + } else if let item = self.item, item.data.storyStats == nil { + isHighlighted = true + } + } + + if isHighlighted { + self.selectionNode.updateIsHighlighted(true) + } else { + self.selectionNode.updateIsHighlighted(false) + } } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index f8f7e09d11..46f87af3bb 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -231,13 +231,15 @@ final class TelegramGlobalSettings { final class PeerInfoPersonalChannelData: Equatable { let peer: EnginePeer let subscriberCount: Int? - let topMessage: EngineMessage? + let topMessages: [EngineMessage] + let storyStats: PeerStoryStats? let isLoading: Bool - init(peer: EnginePeer, subscriberCount: Int?, topMessage: EngineMessage?, isLoading: Bool) { + init(peer: EnginePeer, subscriberCount: Int?, topMessages: [EngineMessage], storyStats: PeerStoryStats?, isLoading: Bool) { self.peer = peer self.subscriberCount = subscriberCount - self.topMessage = topMessage + self.topMessages = topMessages + self.storyStats = storyStats self.isLoading = isLoading } @@ -251,7 +253,10 @@ final class PeerInfoPersonalChannelData: Equatable { if lhs.subscriberCount != rhs.subscriberCount { return false } - if lhs.topMessage != rhs.topMessage { + if lhs.topMessages != rhs.topMessages { + return false + } + if lhs.storyStats != rhs.storyStats { return false } if lhs.isLoading != rhs.isLoading { @@ -555,13 +560,36 @@ private func peerInfoPersonalChannel(context: AccountContext, peerId: EnginePeer return .single(nil) } - //TODO:localize and keep updated - return context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId: channelPeer.id, threadId: nil), index: .upperBound, anchorIndex: .upperBound, count: 5, fixedCombinedReadStates: nil) - |> map { view, _, _ -> PeerInfoPersonalChannelData? in - let entry = view.entries.last + + let polledChannel: Signal = Signal.single(Void()) + |> then( + context.account.viewTracker.polledChannel(peerId: channelPeer.id) + |> ignoreValues + |> map { _ -> Void in + } + ) + + return combineLatest( + context.account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: channelPeer.id, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 10, clipHoles: false, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: []), + context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.StoryStats(id: channelPeer.id) + ), + polledChannel + ) + |> map { viewData, storyStats, _ -> PeerInfoPersonalChannelData? in + let (view, _, _) = viewData + var messages: [EngineMessage] = [] + for i in (0 ..< view.entries.count).reversed() { + if messages.isEmpty { + messages.append(EngineMessage(view.entries[i].message)) + } else if messages[0].groupingKey == view.entries[i].message.groupingKey { + messages.append(EngineMessage(view.entries[i].message)) + } + } + messages = messages.reversed() var isLoading = false - if entry == nil && view.isLoading { + if messages.isEmpty && view.isLoading { isLoading = true } @@ -575,7 +603,8 @@ private func peerInfoPersonalChannel(context: AccountContext, peerId: EnginePeer return PeerInfoPersonalChannelData( peer: channelPeer, subscriberCount: mappedParticipantCount, - topMessage: (entry?.message).flatMap(EngineMessage.init), + topMessages: messages, + storyStats: storyStats, isLoading: isLoading ) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 6d1441dd42..b628a8d753 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -591,6 +591,7 @@ private final class PeerInfoInteraction { let openBirthdatePrivacy: () -> Void let openPremiumGift: () -> Void let editingOpenPersonalChannel: () -> Void + let getController: () -> ViewController? init( openUsername: @escaping (String, Bool, Promise?) -> Void, @@ -651,7 +652,8 @@ private final class PeerInfoInteraction { openBioPrivacy: @escaping () -> Void, openBirthdatePrivacy: @escaping () -> Void, openPremiumGift: @escaping () -> Void, - editingOpenPersonalChannel: @escaping () -> Void + editingOpenPersonalChannel: @escaping () -> Void, + getController: @escaping () -> ViewController? ) { self.openUsername = openUsername self.openPhone = openPhone @@ -712,6 +714,7 @@ private final class PeerInfoInteraction { self.openBirthdatePrivacy = openBirthdatePrivacy self.openPremiumGift = openPremiumGift self.editingOpenPersonalChannel = editingOpenPersonalChannel + self.getController = getController } } @@ -1107,7 +1110,7 @@ private func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoStat if let personalChannel = data.personalChannel { personalChannelTitle = personalChannel.peer.compactDisplayTitle } - items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerPersonalChannel, label: .text(personalChannelTitle ?? "Add"), text: "Personal Channel", icon: nil, action: { + items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerPersonalChannel, label: .text(personalChannelTitle ?? "Add"), text: "Channel", icon: nil, action: { interaction.editingOpenPersonalChannel() })) } @@ -1183,8 +1186,12 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese label = presentationData.strings.Conversation_StatusSubscribers(Int32(subscriberCount)) } //TODO:localize - items[.personalChannel]?.append(PeerInfoScreenHeaderItem(id: 0, text: "PERSONAL CHANNEL", label: label)) - items[.personalChannel]?.append(PeerInfoScreenPersonalChannelItem(id: 1, context: context, data: personalChannel, requestLayout: { _ in + items[.personalChannel]?.append(PeerInfoScreenHeaderItem(id: 0, text: "CHANNEL", label: label)) + items[.personalChannel]?.append(PeerInfoScreenPersonalChannelItem(id: 1, context: context, data: personalChannel, controller: { [weak interaction] in + guard let interaction else { + return nil + } + return interaction.getController() }, action: { [weak interaction] in guard let interaction else { return @@ -2727,6 +2734,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return } self.editingOpenPersonalChannel() + }, + getController: { [weak self] in + return self?.controller } ) @@ -7664,12 +7674,26 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let self else { return } + if initialData.channelId == channel?.peer.id { + return + } + //TODO:localize + let toastText: String var mappedChannel: TelegramPersonalChannel? if let channel { mappedChannel = TelegramPersonalChannel(peerId: channel.peer.id, subscriberCount: channel.subscriberCount.flatMap(Int32.init(clamping:)), topMessageId: nil) + if initialData.channelId != nil { + toastText = "Personal channel updated." + } else { + toastText = "Personal channel added." + } + } else { + toastText = "Personal channel removed." } let _ = self.context.engine.accountData.updatePersonalChannel(personalChannel: mappedChannel).startStandalone() + + self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: .actionSucceeded(title: nil, text: toastText, cancel: nil, destructive: false), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) })) }) } diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/BUILD b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/BUILD index 7329e7c0bc..82b3cd5127 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/BUILD @@ -52,6 +52,7 @@ swift_library( "//submodules/TelegramUI/Components/TextNodeWithEntities", "//submodules/TelegramUI/Components/ListItemSwipeOptionContainer", "//submodules/UndoUI", + "//submodules/ShareController", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkListItemComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkListItemComponent.swift index 7656dcd092..faf03178a8 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkListItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkListItemComponent.swift @@ -20,6 +20,7 @@ final class BusinessLinkListItemComponent: Component { let link: TelegramBusinessChatLinks.Link let action: () -> Void let deleteAction: () -> Void + let shareAction: () -> Void init( context: AccountContext, @@ -27,7 +28,8 @@ final class BusinessLinkListItemComponent: Component { strings: PresentationStrings, link: TelegramBusinessChatLinks.Link, action: @escaping () -> Void, - deleteAction: @escaping () -> Void + deleteAction: @escaping () -> Void, + shareAction: @escaping () -> Void ) { self.context = context self.theme = theme @@ -35,6 +37,7 @@ final class BusinessLinkListItemComponent: Component { self.link = link self.action = action self.deleteAction = deleteAction + self.shareAction = shareAction } static func ==(lhs: BusinessLinkListItemComponent, rhs: BusinessLinkListItemComponent) -> Bool { @@ -98,7 +101,11 @@ final class BusinessLinkListItemComponent: Component { return } self.swipeOptionContainer.setRevealOptionsOpened(false, animated: true) - component.deleteAction() + if option.key == AnyHashable(0 as Int) { + component.shareAction() + } else { + component.deleteAction() + } } self.addSubview(self.swipeOptionContainer) @@ -237,15 +244,21 @@ final class BusinessLinkListItemComponent: Component { self.swipeOptionContainer.updateLayout(size: swipeOptionContainerFrame.size, leftInset: 0.0, rightInset: 0.0) var rightOptions: [ListItemSwipeOptionContainer.Option] = [] - let color: UIColor = component.theme.list.itemDisclosureActions.destructive.fillColor - let textColor: UIColor = component.theme.list.itemDisclosureActions.destructive.foregroundColor + //TODO:localize rightOptions = [ ListItemSwipeOptionContainer.Option( key: 0, + title: "Share", + icon: .none, + color: component.theme.list.itemDisclosureActions.accent.fillColor, + textColor: component.theme.list.itemDisclosureActions.accent.foregroundColor + ), + ListItemSwipeOptionContainer.Option( + key: 1, title: component.strings.Common_Delete, icon: .none, - color: color, - textColor: textColor + color: component.theme.list.itemDisclosureActions.destructive.fillColor, + textColor: component.theme.list.itemDisclosureActions.destructive.foregroundColor ) ] self.swipeOptionContainer.setRevealOptions(([], rightOptions)) diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift index a70cba1794..b72141151f 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift @@ -17,6 +17,7 @@ import ListActionItemComponent import BundleIconComponent import TextFormat import UndoUI +import ShareController final class BusinessLinksSetupScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -164,7 +165,7 @@ final class BusinessLinksSetupScreenComponent: Component { self.isCreatingLink = false self.state?.updated(transition: .immediate) - self.openLink(link: link) + self.openLink(link: link, openKeyboard: true) }, error: { [weak self] error in guard let self, let component = self.component, let environment = self.environment else { return @@ -192,11 +193,11 @@ final class BusinessLinksSetupScreenComponent: Component { private func openLink(url: String) { if let link = self.links.first(where: { $0.url == url }) { - self.openLink(link: link) + self.openLink(link: link, openKeyboard: false) } } - private func openLink(link: TelegramBusinessChatLinks.Link) { + private func openLink(link: TelegramBusinessChatLinks.Link, openKeyboard: Bool) { guard let component = self.component else { return } @@ -212,6 +213,9 @@ final class BusinessLinksSetupScreenComponent: Component { botStart: nil, mode: .standard(.default) ) + if openKeyboard { + chatController.activateInput(type: .text) + } chatController.navigationPresentation = .modal self.environment?.controller()?.push(chatController) } @@ -242,6 +246,18 @@ final class BusinessLinksSetupScreenComponent: Component { self.environment?.controller()?.present(actionSheet, in: .window(.root)) } + private func openShareLink(url: String) { + guard let component = self.component, let environment = self.environment else { + return + } + + guard let link = self.links.first(where: { $0.url == url }) else { + return + } + + environment.controller()?.present(ShareController(context: component.context, subject: .url(link.url), showInChat: nil, externalShare: false, immediateExternalShare: false), in: .window(.root)) + } + func update(component: BusinessLinksSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.isUpdating = true defer { @@ -515,6 +531,12 @@ final class BusinessLinksSetupScreenComponent: Component { return } self.openDeleteLink(url: link.url) + }, + shareAction: { [weak self] in + guard let self else { + return + } + self.openShareLink(url: link.url) } )))) } diff --git a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/BUILD b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/BUILD index c70e2fd32a..a9240c4e47 100644 --- a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/BUILD @@ -28,6 +28,8 @@ swift_library( "//submodules/Components/BalancedTextComponent", "//submodules/ItemListPeerActionItem", "//submodules/AccountContext", + "//submodules/Components/HierarchyTrackingLayer", + "//submodules/Components/ComponentDisplayAdapters", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionLoadingView.swift b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionLoadingView.swift new file mode 100644 index 0000000000..7b2d0511a5 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionLoadingView.swift @@ -0,0 +1,263 @@ +import Foundation +import UIKit +import Display +import HierarchyTrackingLayer +import TelegramPresentationData +import AccountContext +import ContactsPeerItem +import ItemListUI +import TelegramCore + +final class ShimmerEffectView: UIView { + private var currentBackgroundColor: UIColor? + private var currentForegroundColor: UIColor? + private let imageViewContainer: UIView + private let imageView: UIImageView + private let hierarchyTrackingLayer: HierarchyTrackingLayer + + private var absoluteLocation: (CGRect, CGSize)? + private var isCurrentlyInHierarchy = false + private var shouldBeAnimating = false + + override init(frame: CGRect) { + self.hierarchyTrackingLayer = HierarchyTrackingLayer() + + self.imageViewContainer = UIView() + + self.imageView = UIImageView() + self.imageView.contentMode = .scaleToFill + + super.init(frame: frame) + + self.layer.addSublayer(self.hierarchyTrackingLayer) + + self.clipsToBounds = true + + self.imageViewContainer.addSubview(self.imageView) + self.addSubview(self.imageViewContainer) + + self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in + self?.didEnterHierarchy() + } + self.hierarchyTrackingLayer.didExitHierarchy = { [weak self] in + self?.didExitHierarchy() + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func didEnterHierarchy() { + self.isCurrentlyInHierarchy = true + self.updateAnimation() + } + + private func didExitHierarchy() { + self.isCurrentlyInHierarchy = false + self.updateAnimation() + } + + func update(backgroundColor: UIColor, foregroundColor: UIColor) { + if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor) { + return + } + self.currentBackgroundColor = backgroundColor + self.currentForegroundColor = foregroundColor + + self.imageView.image = generateImage(CGSize(width: 4.0, height: 320.0), opaque: true, scale: 1.0, rotatedContext: { size, context in + context.setFillColor(backgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + + context.clip(to: CGRect(origin: CGPoint(), size: size)) + + let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor + let peakColor = foregroundColor.cgColor + + var locations: [CGFloat] = [0.0, 0.5, 1.0] + let colors: [CGColor] = [transparentColor, peakColor, transparentColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + }) + } + + func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + if let absoluteLocation = self.absoluteLocation, absoluteLocation.0 == rect && absoluteLocation.1 == containerSize { + return + } + let sizeUpdated = self.absoluteLocation?.1 != containerSize + let frameUpdated = self.absoluteLocation?.0 != rect + self.absoluteLocation = (rect, containerSize) + + if sizeUpdated { + if self.shouldBeAnimating { + self.imageView.layer.removeAnimation(forKey: "shimmer") + self.addImageAnimation() + } + } + + if frameUpdated { + self.imageViewContainer.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize) + } + + self.updateAnimation() + } + + private func updateAnimation() { + let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil + if shouldBeAnimating != self.shouldBeAnimating { + self.shouldBeAnimating = shouldBeAnimating + if shouldBeAnimating { + self.addImageAnimation() + } else { + self.imageView.layer.removeAnimation(forKey: "shimmer") + } + } + } + + private func addImageAnimation() { + guard let containerSize = self.absoluteLocation?.1 else { + return + } + let gradientHeight: CGFloat = 250.0 + self.imageView.frame = CGRect(origin: CGPoint(x: 0.0, y: -gradientHeight), size: CGSize(width: containerSize.width, height: gradientHeight)) + let animation = self.imageView.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.height + gradientHeight) as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 1.3 * 1.0, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true) + animation.repeatCount = Float.infinity + animation.beginTime = 1.0 + self.imageView.layer.add(animation, forKey: "shimmer") + } +} + +final class PeerSelectionLoadingView: UIView { + private let backgroundColorView: UIView + private let effectView: ShimmerEffectView + private let maskImageView: UIImageView + private var currentParams: (size: CGSize, presentationData: PresentationData)? + + override init(frame: CGRect) { + self.backgroundColorView = UIView() + self.effectView = ShimmerEffectView() + self.maskImageView = UIImageView() + + super.init(frame: frame) + + self.isUserInteractionEnabled = false + + self.addSubview(self.backgroundColorView) + self.addSubview(self.effectView) + self.addSubview(self.maskImageView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func update(context: AccountContext, size: CGSize, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { + if self.currentParams?.size != size || self.currentParams?.presentationData !== presentationData { + self.currentParams = (size, presentationData) + + let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)) + + let items = (0 ..< 1).map { _ -> ContactsPeerItem in + return ContactsPeerItem( + presentationData: ItemListPresentationData(presentationData), + style: .plain, + sectionId: 0, + sortOrder: .firstLast, + displayOrder: .firstLast, + context: context, + peerMode: .peer, + peer: .peer(peer: peer1, chatPeer: peer1), + status: .custom(string: "status", multiline: false, isActive: false, icon: nil), + badge: nil, + requiresPremiumForMessaging: false, + enabled: true, + selection: .none, + selectionPosition: .left, + editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), + options: [], + additionalActions: [], + actionIcon: .none, + index: nil, + header: nil, + action: { _ in }, + disabledAction: nil, + setPeerIdWithRevealedOptions: nil, + deletePeer: nil, + itemHighlighting: nil, + contextAction: nil, + arrowAction: nil, + animationCache: nil, + animationRenderer: nil, + storyStats: nil, + openStories: nil + ) + } + + var itemNodes: [ContactsPeerItemNode] = [] + for i in 0 ..< items.count { + items[i].nodeConfiguredForParams(async: { f in f() }, params: ListViewItemLayoutParams(width: size.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 100.0), synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: (i == items.count - 1) ? nil : items[i + 1], completion: { node, apply in + if let itemNode = node as? ContactsPeerItemNode { + itemNodes.append(itemNode) + } + apply().1(ListViewItemApply(isOnScreen: true)) + }) + } + + self.backgroundColorView.backgroundColor = presentationData.theme.list.mediaPlaceholderColor + + let maskSize = CGSize(width: size.width, height: round(size.height / itemNodes[0].contentSize.height) * itemNodes[0].contentSize.height) + + self.maskImageView.image = generateImage(size, rotatedContext: { size, context in + context.setFillColor(presentationData.theme.chatList.backgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + + let size = maskSize + + var currentY: CGFloat = 0.0 + let fakeLabelPlaceholderHeight: CGFloat = 8.0 + + func fillLabelPlaceholderRect(origin: CGPoint, width: CGFloat) { + let startPoint = origin + let diameter = fakeLabelPlaceholderHeight + context.fillEllipse(in: CGRect(origin: startPoint, size: CGSize(width: diameter, height: diameter))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: startPoint.x + width - diameter, y: startPoint.y), size: CGSize(width: diameter, height: diameter))) + context.fill(CGRect(origin: CGPoint(x: startPoint.x + diameter / 2.0, y: startPoint.y), size: CGSize(width: width - diameter, height: diameter))) + } + + while currentY < size.height { + let sampleIndex = 0 + let itemHeight: CGFloat = itemNodes[sampleIndex].contentSize.height + + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + + if !itemNodes[sampleIndex].avatarNode.isHidden { + context.fillEllipse(in: itemNodes[sampleIndex].avatarNode.view.convert(itemNodes[sampleIndex].avatarNode.bounds, to: itemNodes[sampleIndex].view).offsetBy(dx: 0.0, dy: currentY)) + } + + let titleFrame = itemNodes[sampleIndex].titleNode.frame.offsetBy(dx: 0.0, dy: currentY) + fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX, y: floor(titleFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 100.0) + + let textFrame = itemNodes[sampleIndex].statusNode.frame.offsetBy(dx: 0.0, dy: currentY) + fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX, y: currentY + itemHeight - floor(itemNodes[sampleIndex].titleNode.frame.midY - fakeLabelPlaceholderHeight / 2.0) - fakeLabelPlaceholderHeight), width: 40.0) + + context.setBlendMode(.normal) + context.setFillColor(presentationData.theme.chatList.itemSeparatorColor.cgColor) + context.fill(itemNodes[sampleIndex].separatorNode.frame.offsetBy(dx: 0.0, dy: currentY)) + + currentY += itemHeight + } + }) + + self.effectView.update(backgroundColor: presentationData.theme.list.mediaPlaceholderColor, foregroundColor: presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4)) + self.effectView.updateAbsoluteRect(CGRect(origin: CGPoint(), size: size), within: size) + } + transition.updateFrame(view: self.backgroundColorView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) + transition.updateFrame(view: self.maskImageView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) + transition.updateFrame(view: self.effectView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) + } +} diff --git a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift index 18b1cfb37e..8c1900de34 100644 --- a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift @@ -18,6 +18,7 @@ import ComponentFlow import BalancedTextComponent import MultilineTextComponent import ItemListPeerActionItem +import ComponentDisplayAdapters final class PeerSelectionScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -202,6 +203,7 @@ final class PeerSelectionScreenComponent: Component { private var emptyState: ComponentView? private var contentListNode: ContentListNode? private var emptySearchState: ComponentView? + private var loadingView: PeerSelectionLoadingView? private let navigationBarView = ComponentView() private var navigationHeight: CGFloat? @@ -214,7 +216,7 @@ final class PeerSelectionScreenComponent: Component { private(set) weak var state: EmptyComponentState? private var environment: EnvironmentType? - private var channels: [PeerSelectionScreen.ChannelInfo] = [] + private var channels: [PeerSelectionScreen.ChannelInfo]? private var channelsDisposable: Disposable? private var isSearchDisplayControllerActive: Bool = false @@ -248,7 +250,7 @@ final class PeerSelectionScreenComponent: Component { } if let peer { - guard let channel = self.channels.first(where: { $0.peer.id == peer.id }) else { + guard let channel = self.channels?.first(where: { $0.peer.id == peer.id }) else { return } component.completion(channel) @@ -279,7 +281,7 @@ final class PeerSelectionScreenComponent: Component { navigationBackTitle: nil, titleComponent: AnyComponent(VStack([ AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: "Personal Channel", font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)) + text: .plain(NSAttributedString(string: "Channel", font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)) ))), AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: "select your channel", font: Font.regular(12.0), textColor: theme.rootController.navigationBar.secondaryTextColor)) @@ -395,6 +397,10 @@ final class PeerSelectionScreenComponent: Component { crossfadeStoryPeers: false ))) } + + if let contentListNode = self.contentListNode, let loadingView = self.loadingView { + transition.setFrame(view: loadingView, frame: contentListNode.frame.offsetBy(dx: 0.0, dy: -offset + contentListNode.insets.top)) + } } func update(component: PeerSelectionScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { @@ -563,20 +569,22 @@ final class PeerSelectionScreenComponent: Component { if component.initialData.channelId != nil && self.searchQuery.isEmpty { entries.append(.hide) } - for channel in self.channels { - if !self.searchQuery.isEmpty { - var matches = false + if let channels = self.channels { + for channel in channels { + if !self.searchQuery.isEmpty { + var matches = false inner: for nameComponent in channel.peer.compactDisplayTitle.lowercased().components(separatedBy: self.searchQueryComponentSeparationCharacterSet) { if nameComponent.lowercased().hasPrefix(self.searchQuery) { matches = true break inner } } - if !matches { - continue + if !matches { + continue + } } + entries.append(.item(peer: channel.peer, subscriberCount: channel.subscriberCount, sortIndex: entries.count)) } - entries.append(.item(peer: channel.peer, subscriberCount: channel.subscriberCount, sortIndex: entries.count)) } contentListNode.setEntries(entries: entries, animated: !transition.animation.isImmediate) @@ -619,6 +627,32 @@ final class PeerSelectionScreenComponent: Component { emptySearchState.view?.removeFromSuperview() } + if self.channels == nil, let contentListNode = self.contentListNode { + let loadingView: PeerSelectionLoadingView + if let current = self.loadingView { + loadingView = current + } else { + loadingView = PeerSelectionLoadingView() + self.loadingView = loadingView + contentListNode.view.superview?.insertSubview(loadingView, aboveSubview: contentListNode.view) + } + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) + loadingView.update( + context: component.context, + size: CGSize(width: contentListNode.bounds.size.width, height: floor(contentListNode.bounds.size.height * 1.2)), + presentationData: presentationData, + transition: transition.containedViewLayoutTransition + ) + } else { + if let loadingView = self.loadingView { + self.loadingView = nil + let removeTransition: Transition = .easeInOut(duration: 0.2) + removeTransition.setAlpha(view: loadingView, alpha: 0.0, completion: { [weak loadingView] _ in + loadingView?.removeFromSuperview() + }) + } + } + self.updateNavigationScrolling(navigationHeight: navigationHeight, transition: transition) if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { @@ -652,7 +686,7 @@ final class PeerSelectionScreenComponent: Component { public final class PeerSelectionScreen: ViewControllerComponentContainer { public final class InitialData { - fileprivate let channelId: EnginePeer.Id? + public let channelId: EnginePeer.Id? init(channelId: EnginePeer.Id?) { self.channelId = channelId diff --git a/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift index 7b3c3d951d..09780c583b 100644 --- a/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift +++ b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift @@ -220,7 +220,17 @@ func updateChatPresentationInterfaceStateImpl( } } - if let (updatedUrlPreviewState, updatedUrlPreviewSignal) = urlPreviewStateForInputText(updatedChatPresentationInterfaceState.interfaceState.composeInputState.inputText, context: selfController.context, currentQuery: selfController.urlPreviewQueryState?.0, forPeerId: selfController.chatLocation.peerId) { + var canHaveUrlPreview = true + if case let .customChatContents(customChatContents) = updatedChatPresentationInterfaceState.subject { + switch customChatContents.kind { + case .quickReplyMessageInput: + break + case .businessLinkSetup: + canHaveUrlPreview = false + } + } + + if canHaveUrlPreview, let (updatedUrlPreviewState, updatedUrlPreviewSignal) = urlPreviewStateForInputText(updatedChatPresentationInterfaceState.interfaceState.composeInputState.inputText, context: selfController.context, currentQuery: selfController.urlPreviewQueryState?.0, forPeerId: selfController.chatLocation.peerId) { selfController.urlPreviewQueryState?.1.dispose() var inScope = true var inScopeResult: ((TelegramMediaWebpage?) -> (TelegramMediaWebpage, String)?)? diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 29c5fc2bf9..c7b37f22ec 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -763,7 +763,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } case let .businessLinkSetup(link): let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText - let entities = generateTextEntities(inputText.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(inputText, maxAnimatedEmojisInText: 0)) + let entities = generateChatInputTextEntities(inputText, generateLinks: false) let message = inputText.string @@ -12127,7 +12127,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = self.context.engine.peers.checkPeerChatServiceActions(peerId: peerId).startStandalone() } - if self.chatDisplayNode.frameForInputActionButton() != nil { + if self.chatLocation.peerId != nil && self.chatDisplayNode.frameForInputActionButton() != nil { let inputText = self.presentationInterfaceState.interfaceState.effectiveInputState.inputText.string if !inputText.isEmpty { if inputText.count > 4 { @@ -16319,7 +16319,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - func activateInput(type: ChatControllerActivateInput) { + public func activateInput(type: ChatControllerActivateInput) { if self.didAppear { switch type { case .text: diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 3786654f24..1e88539f5e 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -3968,7 +3968,12 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { for text in breakChatInputText(trimChatInputText(inputText)) { if text.length != 0 { var attributes: [MessageAttribute] = [] - let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text, maxAnimatedEmojisInText: 0)) + let entities: [MessageTextEntity] + if case let .customChatContents(customChatContents) = self.chatPresentationInterfaceState.subject, case .businessLinkSetup = customChatContents.kind { + entities = generateChatInputTextEntities(text, generateLinks: false) + } else { + entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text, maxAnimatedEmojisInText: 0)) + } if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 9ec02dd8e9..3253a92b0c 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -1065,6 +1065,7 @@ func openResolvedUrlImpl( context: context, chatLocation: .peer(link.peer), updateTextInputState: ChatTextInputState(inputText: chatInputStateStringWithAppliedEntities(link.message, entities: link.entities)), + activateInput: .text, keepStack: .always )) } diff --git a/submodules/TextFormat/Sources/GenerateTextEntities.swift b/submodules/TextFormat/Sources/GenerateTextEntities.swift index f04532aa5c..a97df3aef1 100644 --- a/submodules/TextFormat/Sources/GenerateTextEntities.swift +++ b/submodules/TextFormat/Sources/GenerateTextEntities.swift @@ -178,9 +178,11 @@ public func generateChatInputTextEntities(_ text: NSAttributedString, maxAnimate } }) - for entity in generateTextEntities(text.string, enabledTypes: .allUrl) { - if case .Url = entity.type { - entities.append(entity) + if generateLinks { + for entity in generateTextEntities(text.string, enabledTypes: .allUrl) { + if case .Url = entity.type { + entities.append(entity) + } } }