Business features

This commit is contained in:
Isaac 2024-03-25 19:34:03 +04:00
parent 6b227b8aa4
commit 5fc369ae4a
26 changed files with 861 additions and 109 deletions

View File

@ -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

View File

@ -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))
}
}

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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))
}
}

View File

@ -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<Empty>
@ -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<Empty>?
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
}

View File

@ -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,

View File

@ -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<Empty>?
private var avatarIconComponent: EmojiStatusComponent?
private let titleNode: TextNode
public let titleNode: TextNode
private var credibilityIconView: ComponentHostView<Empty>?
private var credibilityIconComponent: EmojiStatusComponent?
private var verifiedIconView: ComponentHostView<Empty>?
private var verifiedIconComponent: EmojiStatusComponent?
private let statusNode: TextNode
public let statusNode: TextNode
private var statusIconNode: ASImageNode?
private var badgeBackgroundNode: ASImageNode?
private var badgeTextNode: TextNode?

View File

@ -393,6 +393,7 @@ final class MutableMessageHistoryView: MutablePostboxView {
break
}
}
self.sampledState = self.state.sample(postbox: postbox, clipHoles: self.clipHoles)
self.render(postbox: postbox)

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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)
}
}
}

View File

@ -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<Void, NoError> = Signal<Void, NoError>.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
)
}

View File

@ -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<Bool>?) -> 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)
}))
})
}

View File

@ -52,6 +52,7 @@ swift_library(
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/TelegramUI/Components/ListItemSwipeOptionContainer",
"//submodules/UndoUI",
"//submodules/ShareController",
],
visibility = [
"//visibility:public",

View File

@ -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))

View File

@ -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<EnvironmentType>, 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)
}
))))
}

View File

@ -28,6 +28,8 @@ swift_library(
"//submodules/Components/BalancedTextComponent",
"//submodules/ItemListPeerActionItem",
"//submodules/AccountContext",
"//submodules/Components/HierarchyTrackingLayer",
"//submodules/Components/ComponentDisplayAdapters",
],
visibility = [
"//visibility:public",

View File

@ -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))
}
}

View File

@ -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<Empty>?
private var contentListNode: ContentListNode?
private var emptySearchState: ComponentView<Empty>?
private var loadingView: PeerSelectionLoadingView?
private let navigationBarView = ComponentView<Empty>()
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<EnvironmentType>, 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

View File

@ -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)?)?

View File

@ -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:

View File

@ -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))
}

View File

@ -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
))
}

View File

@ -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)
}
}
}