diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 6eeb296390..39290bfcbb 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7547,3 +7547,20 @@ Sorry for the inconvenience."; "Chat.MultipleTypingPair" = "%@ and %@"; "Chat.MultipleTypingMore" = "%@ and %@ others"; + +"DialogList.ExtendedPinLimitError" = "Sorry, you can pin more than **%1$@** chats to the top. Unpin some of the currently pinned ones or subscribe to **Telegram Premium** to double the limit to **%2$@** chats."; +"DialogList.ExtendedPinLimitIncrease" = "Increase Limit"; + +"Group.Username.RemoveExistingUsernamesTitle" = "Too Many Public Links"; +"Group.Username.RemoveExistingUsernamesOrExtendInfo" = "You have reserved too many public links. Try revoking a link from an older group or channel, or upgrade to **Telegram Premium** to double the limit to **%@** public links."; +"Group.Username.IncreaseLimit" = "Increase Limit"; + +"OldChannels.TooManyCommunitiesTitle" = "Too Many Communities"; +"OldChannels.TooManyCommunitiesText" = "You are a member of **%@** groups and channels. Please leave some before joining a new one or upgrade to **Telegram Premium** to double the limit to **%@** groups and channels."; +"OldChannels.IncreaseLimit" = "Increase Limit"; +"OldChannels.LeaveCommunities_1" = "Leave %@ Community"; +"OldChannels.LeaveCommunities_any" = "Leave %@ Communities"; + +"Stickers.FaveLimitReachedInfo" = "Sorry, you can't add more than **%@** stickers to favorites. Replace an older saved sticker or subscribe to **Telegram Premium** to double the limit to **%@** favorites stickers."; +"Stickers.FaveLimitReplaceOlder" = "Replace Older Sticker"; +"Stickers.FaveLimitIncrease" = "Increase Limit"; diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index fae5b54a5f..7c56109f2e 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -67,6 +67,7 @@ swift_library( "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent", "//submodules/Components/ProgressIndicatorComponent:ProgressIndicatorComponent", "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", + "//submodules/PremiumUI:PremiumUI", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index 7752f8e0c5..1dfa364c8b 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -311,16 +311,36 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch } if isPinned || chatListFilter == nil || peerId.namespace != Namespaces.Peer.SecretChat { - items.append(.action(ContextMenuActionItem(text: isPinned ? strings.ChatList_Context_Unpin : strings.ChatList_Context_Pin, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin" : "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { _, f in + items.append(.action(ContextMenuActionItem(text: isPinned ? strings.ChatList_Context_Unpin : strings.ChatList_Context_Pin, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin" : "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { c, f in let _ = (context.engine.peers.toggleItemPinned(location: location, itemId: .peer(peerId)) |> deliverOnMainQueue).start(next: { result in switch result { case .done: - break + f(.default) case .limitExceeded: - break + var subItems: [ContextMenuItem] = [] + + subItems.append(.action(ContextMenuActionItem(text: strings.Common_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) + }, action: { c, _ in + c.popItems() + }))) + subItems.append(.separator) + + subItems.append(.action(ContextMenuActionItem(text: strings.DialogList_ExtendedPinLimitError("5", "10").string, textLayout: .multiline, textFont: .small, parseMarkdown: true, icon: { _ in + return nil + }, action: nil as ((ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void)?))) + + subItems.append(.action(ContextMenuActionItem(text: strings.DialogList_ExtendedPinLimitIncrease, icon: { _ in + return nil + }, action: { _, f in + f(.default) + + }))) + + c.pushItems(items: .single(ContextController.Items(content: .list(subItems)))) } - f(.default) + }) }))) } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index c88e6b7822..6132643570 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -13,6 +13,7 @@ import ContextUI import ItemListUI import SearchUI import ChatListSearchItemHeader +import PremiumUI public enum ChatListNodeMode { case chatList @@ -840,14 +841,16 @@ public final class ChatListNode: ListView { switch result { case .done: break - case let .limitExceeded(maxCount): - let text: String - if chatListFilter != nil { - text = strongSelf.currentState.presentationData.strings.DialogList_UnknownPinLimitError - } else { - text = strongSelf.currentState.presentationData.strings.DialogList_PinLimitError("\(maxCount)").string - } - strongSelf.presentAlert?(text) + case .limitExceeded: + let controller = LimitScreen(context: strongSelf.context, subject: .pins) + strongSelf.present?(controller) +// let text: String +// if chatListFilter != nil { +// text = strongSelf.currentState.presentationData.strings.DialogList_UnknownPinLimitError +// } else { +// text = strongSelf.currentState.presentationData.strings.DialogList_PinLimitError("\(maxCount)").string +// } +// strongSelf.presentAlert?(text) } } }) diff --git a/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift b/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift index 7fe9f3bb59..333fb5eae8 100644 --- a/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift +++ b/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift @@ -3,6 +3,7 @@ import UIKit import ComponentFlow import Display import SolidRoundedButtonNode +import AppBundle public final class SolidRoundedButtonComponent: Component { public typealias Theme = SolidRoundedButtonTheme @@ -15,6 +16,8 @@ public final class SolidRoundedButtonComponent: Component { public let height: CGFloat public let cornerRadius: CGFloat public let gloss: Bool + public let iconName: String? + public let iconPosition: SolidRoundedButtonIconPosition public let action: () -> Void public init( @@ -26,6 +29,8 @@ public final class SolidRoundedButtonComponent: Component { height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0, gloss: Bool = false, + iconName: String? = nil, + iconPosition: SolidRoundedButtonIconPosition = .left, action: @escaping () -> Void ) { self.title = title @@ -36,6 +41,8 @@ public final class SolidRoundedButtonComponent: Component { self.height = height self.cornerRadius = cornerRadius self.gloss = gloss + self.iconName = iconName + self.iconPosition = iconPosition self.action = action } @@ -64,6 +71,12 @@ public final class SolidRoundedButtonComponent: Component { if lhs.gloss != rhs.gloss { return false } + if lhs.iconName != rhs.iconName { + return false + } + if lhs.iconPosition != rhs.iconPosition { + return false + } return true } @@ -84,6 +97,8 @@ public final class SolidRoundedButtonComponent: Component { cornerRadius: component.cornerRadius, gloss: component.gloss ) + button.iconPosition = component.iconPosition + button.icon = component.iconName.flatMap { UIImage(bundleImageName: $0) } self.button = button self.addSubview(button) diff --git a/submodules/ContextUI/BUILD b/submodules/ContextUI/BUILD index 54f8c87581..ec5a372e7e 100644 --- a/submodules/ContextUI/BUILD +++ b/submodules/ContextUI/BUILD @@ -18,6 +18,7 @@ swift_library( "//submodules/AppBundle:AppBundle", "//submodules/AccountContext:AccountContext", "//submodules/ReactionSelectionNode:ReactionSelectionNode", + "//submodules/Markdown:Markdown", ], visibility = [ "//visibility:public", diff --git a/submodules/ContextUI/Sources/ContextActionNode.swift b/submodules/ContextUI/Sources/ContextActionNode.swift index 9fcacd707a..6d4881cafd 100644 --- a/submodules/ContextUI/Sources/ContextActionNode.swift +++ b/submodules/ContextUI/Sources/ContextActionNode.swift @@ -3,6 +3,7 @@ import AsyncDisplayKit import Display import TelegramPresentationData import SwiftSignalKit +import Markdown public enum ContextActionSibling { case none @@ -53,6 +54,9 @@ public final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol { let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize) let smallTextFont = Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0)) + let boldTextFont = Font.semibold(presentationData.listsFontSize.baseDisplaySize) + let smallBoldTextFont = Font.semibold(floor(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0)) + self.backgroundNode = ASDisplayNode() self.backgroundNode.isAccessibilityElement = false self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor @@ -76,18 +80,29 @@ public final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol { } let titleFont: UIFont + let titleBoldFont: UIFont switch action.textFont { case .regular: titleFont = textFont + titleBoldFont = boldTextFont case .small: titleFont = smallTextFont + titleBoldFont = smallBoldTextFont case let .custom(customFont): titleFont = customFont + titleBoldFont = customFont } let subtitleFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0) - self.textNode.attributedText = NSAttributedString(string: action.text, font: titleFont, textColor: textColor) + if action.parseMarkdown { + let attributedText = parseMarkdownIntoAttributedString(action.text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: textColor), bold: MarkdownAttributeSet(font: titleBoldFont, textColor: textColor), link: MarkdownAttributeSet(font: titleFont, textColor: textColor), linkAttribute: { _ in + return nil + })) + self.textNode.attributedText = attributedText + } else { + self.textNode.attributedText = NSAttributedString(string: action.text, font: titleFont, textColor: textColor) + } switch action.textLayout { case .singleLine: diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index bd72e883a4..ffb2f11e73 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -92,6 +92,7 @@ public final class ContextMenuActionItem { public let textColor: ContextMenuActionItemTextColor public let textFont: ContextMenuActionItemFont public let textLayout: ContextMenuActionItemTextLayout + public let parseMarkdown: Bool public let badge: ContextMenuActionBadge? public let icon: (PresentationTheme) -> UIImage? public let iconSource: ContextMenuActionItemIconSource? @@ -103,6 +104,7 @@ public final class ContextMenuActionItem { textColor: ContextMenuActionItemTextColor = .primary, textLayout: ContextMenuActionItemTextLayout = .twoLinesMax, textFont: ContextMenuActionItemFont = .regular, + parseMarkdown: Bool = false, badge: ContextMenuActionBadge? = nil, icon: @escaping (PresentationTheme) -> UIImage?, iconSource: ContextMenuActionItemIconSource? = nil, @@ -114,6 +116,7 @@ public final class ContextMenuActionItem { textColor: textColor, textLayout: textLayout, textFont: textFont, + parseMarkdown: parseMarkdown, badge: badge, icon: icon, iconSource: iconSource, @@ -131,6 +134,7 @@ public final class ContextMenuActionItem { textColor: ContextMenuActionItemTextColor = .primary, textLayout: ContextMenuActionItemTextLayout = .twoLinesMax, textFont: ContextMenuActionItemFont = .regular, + parseMarkdown: Bool = false, badge: ContextMenuActionBadge? = nil, icon: @escaping (PresentationTheme) -> UIImage?, iconSource: ContextMenuActionItemIconSource? = nil, @@ -141,6 +145,7 @@ public final class ContextMenuActionItem { self.textColor = textColor self.textFont = textFont self.textLayout = textLayout + self.parseMarkdown = parseMarkdown self.badge = badge self.icon = icon self.iconSource = iconSource diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index 49a5be128b..c69127b8c1 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -8,6 +8,7 @@ import TelegramCore import SwiftSignalKit import AccountContext import ReactionSelectionNode +import Markdown public protocol ContextControllerActionsStackItemNode: ASDisplayNode { func update( @@ -170,17 +171,22 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin subtitle = subtitleValue case .multiline: self.titleLabelNode.maximumNumberOfLines = 0 + self.titleLabelNode.lineSpacing = 0.1 } let titleFont: UIFont + let titleBoldFont: UIFont switch self.item.textFont { case let .custom(font): titleFont = font + titleBoldFont = font case .small: let smallTextFont = Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0)) titleFont = smallTextFont + titleBoldFont = Font.semibold(floor(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0)) case .regular: titleFont = Font.regular(presentationData.listsFontSize.baseDisplaySize) + titleBoldFont = Font.semibold(presentationData.listsFontSize.baseDisplaySize) } let subtitleFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0) @@ -196,11 +202,23 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin titleColor = presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.4) } - self.titleLabelNode.attributedText = NSAttributedString( - string: self.item.text, - font: titleFont, - textColor: titleColor - ) + if self.item.parseMarkdown { + let attributedText = parseMarkdownIntoAttributedString( + self.item.text, + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: titleFont, textColor: titleColor), + bold: MarkdownAttributeSet(font: titleBoldFont, textColor: titleColor), + link: MarkdownAttributeSet(font: titleFont, textColor: titleColor), + linkAttribute: { _ in return nil } + ) + ) + self.titleLabelNode.attributedText = attributedText + } else { + self.titleLabelNode.attributedText = NSAttributedString( + string: self.item.text, + font: titleFont, + textColor: titleColor) + } self.subtitleNode.attributedText = subtitle.flatMap { subtitle in return NSAttributedString( diff --git a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift index ff2fe98453..b05755a7a1 100644 --- a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift @@ -59,6 +59,7 @@ private final class ChannelVisibilityControllerArguments { private enum ChannelVisibilitySection: Int32 { case type + case limitInfo case link case linkActions case joinToSend @@ -87,6 +88,7 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { case publicLinkHeader(PresentationTheme, String) case publicLinkAvailability(PresentationTheme, String, Bool) + case linksLimitInfo(PresentationTheme, String, String, Int) case editablePublicLink(PresentationTheme, PresentationStrings, String, String) case privateLinkHeader(PresentationTheme, String) case privateLink(PresentationTheme, ExportedInvitation?, [EnginePeer], Int32, Bool) @@ -115,6 +117,8 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { switch self { case .typeHeader, .typePublic, .typePrivate, .typeInfo: return ChannelVisibilitySection.type.rawValue + case .linksLimitInfo: + return ChannelVisibilitySection.limitInfo.rawValue case .publicLinkHeader, .publicLinkAvailability, .privateLinkHeader, .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkInfo, .publicLinkStatus: return ChannelVisibilitySection.link.rawValue case .privateLinkManage, .privateLinkManageInfo: @@ -144,22 +148,24 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { return 4 case .publicLinkAvailability: return 5 - case .privateLinkHeader: + case .linksLimitInfo: return 6 - case .privateLink: + case .privateLinkHeader: return 7 - case .editablePublicLink: + case .privateLink: return 8 - case .privateLinkInfo: + case .editablePublicLink: return 9 - case .publicLinkStatus: + case .privateLinkInfo: return 10 - case .publicLinkInfo: + case .publicLinkStatus: return 11 - case .existingLinksInfo: + case .publicLinkInfo: return 12 + case .existingLinksInfo: + return 13 case let .existingLinkPeerItem(index, _, _, _, _, _, _, _): - return 13 + index + return 14 + index case .privateLinkManage: return 1000 case .privateLinkManageInfo: @@ -221,6 +227,12 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { } else { return false } + case let .linksLimitInfo(lhsTheme, lhsTitle, lhsText, lhsLimit): + if case let .linksLimitInfo(rhsTheme, rhsTitle, rhsText, rhsLimit) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText, lhsLimit == rhsLimit { + return true + } else { + return false + } case let .privateLinkHeader(lhsTheme, lhsTitle): if case let .privateLinkHeader(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle { return true @@ -381,6 +393,8 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { let attr = NSMutableAttributedString(string: text, textColor: value ? theme.list.freeTextColor : theme.list.freeTextErrorColor) attr.addAttribute(.font, value: Font.regular(13), range: NSMakeRange(0, attr.length)) return ItemListActivityTextItem(displayActivity: value, presentationData: presentationData, text: attr, sectionId: self.section) + case let .linksLimitInfo(theme, title, text, limit): + return IncreaseLimitHeaderItem(theme: theme, icon: .link, count: limit, title: title, text: text, sectionId: self.section) case let .privateLinkHeader(_, title): return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .privateLink(_, invite, peers, importersCount, displayImporters): @@ -715,7 +729,8 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa if displayAvailability { if let publicChannelsToRevoke = publicChannelsToRevoke { - entries.append(.publicLinkAvailability(presentationData.theme, presentationData.strings.Group_Username_RemoveExistingUsernamesInfo, false)) + entries.append(.linksLimitInfo(presentationData.theme, presentationData.strings.Group_Username_RemoveExistingUsernamesTitle, presentationData.strings.Group_Username_RemoveExistingUsernamesOrExtendInfo("\(20)").string, 10)) + var index: Int32 = 0 for peer in publicChannelsToRevoke.sorted(by: { lhs, rhs in var lhsDate: Int32 = 0 @@ -803,7 +818,7 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa case .privateChannel: let invite = (view.cachedData as? CachedChannelData)?.exportedInvitation entries.append(.privateLinkHeader(presentationData.theme, presentationData.strings.InviteLink_InviteLink.uppercased())) - entries.append(.privateLink(presentationData.theme, invite, importers?.importers.prefix(3).compactMap { $0.peer.peer.flatMap(EnginePeer.init) } ?? [], importers?.count ?? 0, mode != .initialSetup)) + entries.append(.privateLink(presentationData.theme, invite, importers?.importers.prefix(3).compactMap { $0.peer.peer.flatMap(EnginePeer.init) } ?? [], importers?.count ?? 0, mode != .initialSetup)) if isGroup { entries.append(.privateLinkInfo(presentationData.theme, presentationData.strings.Group_Username_CreatePrivateLinkHelp)) } else { @@ -885,6 +900,8 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa if displayAvailability { if let publicChannelsToRevoke = publicChannelsToRevoke { + entries.append(.linksLimitInfo(presentationData.theme, presentationData.strings.Group_Username_RemoveExistingUsernamesTitle, presentationData.strings.Group_Username_RemoveExistingUsernamesOrExtendInfo("\(1000)").string, 500)) + entries.append(.publicLinkAvailability(presentationData.theme, presentationData.strings.Group_Username_RemoveExistingUsernamesInfo, false)) var index: Int32 = 0 for peer in publicChannelsToRevoke.sorted(by: { lhs, rhs in @@ -1338,6 +1355,8 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta |> map { presentationData, state, view, publicChannelsToRevoke, importersContext, importers -> (ItemListControllerState, (ItemListNodeState, Any)) in let peer = peerViewMainPeer(view) + var footerItem: ItemListControllerFooterItem? + var rightNavigationButton: ItemListNavigationButton? if let peer = peer as? TelegramChannel { var doneEnabled = true @@ -1548,6 +1567,7 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta } var crossfade: Bool = false + var animateChanges: Bool = false if let cachedData = view.cachedData as? CachedChannelData { let invitation = cachedData.exportedInvitation let previousInvitation = previousInvitation.swap(invitation) @@ -1580,6 +1600,14 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta if selectedType == .publicChannel, let hadNamesToRevoke = hadNamesToRevoke, !crossfade { crossfade = hadNamesToRevoke != hasNamesToRevoke } + + if hasNamesToRevoke && selectedType == .publicChannel { + footerItem = IncreaseLimitFooterItem(theme: presentationData.theme, title: presentationData.strings.Group_Username_IncreaseLimit, colorful: true, action: {}) + } + + if let hadNamesToRevoke = hadNamesToRevoke { + animateChanges = hadNamesToRevoke != hasNamesToRevoke + } } let title: String @@ -1601,7 +1629,7 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, focusItemTag: focusItemTag, crossfadeState: crossfade, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, focusItemTag: focusItemTag, footerItem: footerItem, crossfadeState: crossfade, animateChanges: animateChanges) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/submodules/PeerInfoUI/Sources/IncreaseLimitFooterItem.swift b/submodules/PeerInfoUI/Sources/IncreaseLimitFooterItem.swift new file mode 100644 index 0000000000..940c50071d --- /dev/null +++ b/submodules/PeerInfoUI/Sources/IncreaseLimitFooterItem.swift @@ -0,0 +1,141 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramPresentationData +import ItemListUI +import PresentationDataUtils +import SolidRoundedButtonNode +import AppBundle + +final class IncreaseLimitFooterItem: ItemListControllerFooterItem { + let theme: PresentationTheme + let title: String + let colorful: Bool + let action: () -> Void + + init(theme: PresentationTheme, title: String, colorful: Bool, action: @escaping () -> Void) { + self.theme = theme + self.title = title + self.colorful = colorful + self.action = action + } + + func isEqual(to: ItemListControllerFooterItem) -> Bool { + if let item = to as? IncreaseLimitFooterItem { + return self.theme === item.theme && self.title == item.title + } else { + return false + } + } + + func node(current: ItemListControllerFooterItemNode?) -> ItemListControllerFooterItemNode { + if let current = current as? IncreaseLimitFooterItemNode { + current.item = self + return current + } else { + return IncreaseLimitFooterItemNode(item: self) + } + } +} + +final class IncreaseLimitFooterItemNode: ItemListControllerFooterItemNode { + private let backgroundNode: NavigationBackgroundNode + private let separatorNode: ASDisplayNode + private let buttonNode: SolidRoundedButtonNode + + private var validLayout: ContainerViewLayout? + + var item: IncreaseLimitFooterItem { + didSet { + self.updateItem() + if let layout = self.validLayout { + let _ = self.updateLayout(layout: layout, transition: .immediate) + } + } + } + + init(item: IncreaseLimitFooterItem) { + self.item = item + + self.backgroundNode = NavigationBackgroundNode(color: item.theme.rootController.tabBar.backgroundColor) + self.separatorNode = ASDisplayNode() + + self.buttonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: .black, foregroundColor: .white), height: 50.0, cornerRadius: 11.0) + self.buttonNode.iconPosition = .right + self.buttonNode.icon = UIImage(bundleImageName: "Premium/X2") + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.separatorNode) + self.addSubnode(self.buttonNode) + + self.updateItem() + } + + private func updateItem() { + self.backgroundNode.updateColor(color: self.item.theme.rootController.tabBar.backgroundColor, transition: .immediate) + self.separatorNode.backgroundColor = self.item.theme.rootController.tabBar.separatorColor + + let backgroundColor = self.item.theme.list.itemCheckColors.fillColor + let backgroundColors: [UIColor] + if self.item.colorful { + backgroundColors = [UIColor(rgb: 0x407af0), UIColor(rgb: 0x9551e8), UIColor(rgb: 0xbf499a), UIColor(rgb: 0xf17b30)] + self.buttonNode.icon = UIImage(bundleImageName: "Premium/X2") + } else { + backgroundColors = [] + self.buttonNode.icon = nil + } + + self.buttonNode.updateTheme(SolidRoundedButtonTheme(backgroundColor: backgroundColor, backgroundColors: backgroundColors, foregroundColor: self.item.theme.list.itemCheckColors.foregroundColor), animated: true) + self.buttonNode.title = self.item.title + + self.buttonNode.pressed = { [weak self] in + self?.item.action() + } + } + + override func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) { + transition.updateAlpha(node: self.backgroundNode, alpha: alpha) + transition.updateAlpha(node: self.separatorNode, alpha: alpha) + } + + override func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat { + self.validLayout = layout + + let buttonInset: CGFloat = 16.0 + let buttonWidth = layout.size.width - layout.safeInsets.left - layout.safeInsets.right - buttonInset * 2.0 + let buttonHeight = self.buttonNode.updateLayout(width: buttonWidth, transition: transition) + let inset: CGFloat = 9.0 + + let insets = layout.insets(options: [.input]) + + var panelHeight: CGFloat = buttonHeight + inset * 2.0 + let totalPanelHeight: CGFloat + if let inputHeight = layout.inputHeight, inputHeight > 0.0 { + totalPanelHeight = panelHeight + insets.bottom + } else { + panelHeight += insets.bottom + totalPanelHeight = panelHeight + } + + let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - totalPanelHeight), size: CGSize(width: layout.size.width, height: panelHeight)) + transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + buttonInset, y: panelFrame.minY + inset), size: CGSize(width: buttonWidth, height: buttonHeight))) + + transition.updateFrame(node: self.backgroundNode, frame: panelFrame) + self.backgroundNode.update(size: panelFrame.size, transition: transition) + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: panelFrame.origin, size: CGSize(width: panelFrame.width, height: UIScreenPixel))) + + return panelHeight + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + if self.backgroundNode.frame.contains(point) { + return true + } else { + return false + } + } +} diff --git a/submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift b/submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift new file mode 100644 index 0000000000..ad8c3b62d1 --- /dev/null +++ b/submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift @@ -0,0 +1,201 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramPresentationData +import ItemListUI +import PresentationDataUtils +import Markdown + +class IncreaseLimitHeaderItem: ListViewItem, ItemListItem { + enum Icon { + case group + case link + } + + let theme: PresentationTheme + let icon: Icon + let count: Int + let title: String + let text: String + let sectionId: ItemListSectionId + + init(theme: PresentationTheme, icon: Icon, count: Int, title: String, text: String, sectionId: ItemListSectionId) { + self.theme = theme + self.icon = icon + self.count = count + self.title = title + self.text = text + self.sectionId = sectionId + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = IncreaseLimitHeaderItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + guard let nodeValue = node() as? IncreaseLimitHeaderItemNode else { + assertionFailure() + return + } + + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } +} + +private let titleFont = Font.semibold(17.0) +private let textFont = Font.regular(14.0) +private let boldTextFont = Font.semibold(13.0) + +class IncreaseLimitHeaderItemNode: ListViewItemNode { + private var backgroundNode: ASImageNode + private var iconNode: ASImageNode + private var countNode: TextNode + private let titleNode: TextNode + private let textNode: TextNode + + private var item: IncreaseLimitHeaderItem? + + init() { + self.titleNode = TextNode() + self.titleNode.isUserInteractionEnabled = false + self.titleNode.contentMode = .left + self.titleNode.contentsScale = UIScreen.main.scale + + self.textNode = TextNode() + self.textNode.isUserInteractionEnabled = false + self.textNode.contentMode = .left + self.textNode.contentsScale = UIScreen.main.scale + + self.backgroundNode = ASImageNode() + self.backgroundNode.clipsToBounds = true + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.image = generateGradientImage(size: CGSize(width: 100.0, height: 47.0), colors: [UIColor(rgb: 0xa44ece), UIColor(rgb: 0xff7924)], locations: [0.0, 1.0], direction: .horizontal) + self.backgroundNode.cornerRadius = 23.5 + + self.iconNode = ASImageNode() + self.iconNode.displaysAsynchronously = false + + self.countNode = TextNode() + self.countNode.isUserInteractionEnabled = false + self.countNode.contentMode = .left + self.countNode.contentsScale = UIScreen.main.scale + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.iconNode) + self.addSubnode(self.countNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + } + + override func didLoad() { + super.didLoad() + + if #available(iOS 13.0, *) { + self.backgroundNode.layer.cornerCurve = .continuous + } + } + + func asyncLayout() -> (_ item: IncreaseLimitHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let makeCountLayout = TextNode.asyncLayout(self.countNode) + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeTextLayout = TextNode.asyncLayout(self.textNode) + + return { item, params, neighbors in + let leftInset: CGFloat = 32.0 + params.leftInset + let topInset: CGFloat = 2.0 + + let badgeHeight: CGFloat = 47.0 + let titleSpacing: CGFloat = 19.0 + let textSpacing: CGFloat = 15.0 + let bottomInset: CGFloat = 2.0 + + let countAttributedText = NSAttributedString(string: "\(item.count)", font: Font.with(size: 24.0, design: .round, weight: .semibold, traits: []), textColor: .white) + let (countLayout, countApply) = makeCountLayout(TextNodeLayoutArguments(attributedString: countAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + let titleAttributedText = NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + let textColor = item.theme.list.freeTextColor + let attributedText = parseMarkdownIntoAttributedString(item.text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: titleFont, textColor: textColor), linkAttribute: { _ in + return nil + })) + + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + let contentSize = CGSize(width: params.width, height: topInset + badgeHeight + titleSpacing + titleLayout.size.height + textSpacing + textLayout.size.height + bottomInset) + let insets = itemListNeighborsGroupedInsets(neighbors, params) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + strongSelf.accessibilityLabel = attributedText.string + + if strongSelf.iconNode.image == nil { + let image: UIImage? + switch item.icon { + case .group: + image = UIImage(bundleImageName: "Premium/Group") + case .link: + image = UIImage(bundleImageName: "Premium/Link") + } + strongSelf.iconNode.image = generateTintedImage(image: image, color: .white) + } + + let countBackgroundWidth: CGFloat = countLayout.size.width + 67.0 + let countBackgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - countBackgroundWidth) / 2.0), y: topInset), size: CGSize(width: countBackgroundWidth, height: badgeHeight)) + strongSelf.backgroundNode.frame = countBackgroundFrame + + let _ = countApply() + strongSelf.countNode.frame = CGRect(origin: CGPoint(x: countBackgroundFrame.maxX - countLayout.size.width - 15.0, y: countBackgroundFrame.minY + floorToScreenPixels((countBackgroundFrame.height - countLayout.size.height) / 2.0)), size: countLayout.size) + + if let image = strongSelf.iconNode.image { + strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: countBackgroundFrame.minX + 18.0, y: countBackgroundFrame.minY + floorToScreenPixels((countBackgroundFrame.height - image.size.height) / 2.0)), size: image.size) + } + + let _ = titleApply() + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - titleLayout.size.width) / 2.0), y: countBackgroundFrame.maxY + titleSpacing), size: titleLayout.size) + + let _ = textApply() + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - textLayout.size.width) / 2.0), y: countBackgroundFrame.maxY + titleSpacing + titleLayout.size.height + textSpacing), size: textLayout.size) + } + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} diff --git a/submodules/PeerInfoUI/Sources/OldChannelsController.swift b/submodules/PeerInfoUI/Sources/OldChannelsController.swift index f349c25671..1a0e33d27f 100644 --- a/submodules/PeerInfoUI/Sources/OldChannelsController.swift +++ b/submodules/PeerInfoUI/Sources/OldChannelsController.swift @@ -47,9 +47,9 @@ func localizedOldChannelDate(peer: InactiveChannel, strings: PresentationStrings if let channel = peer.peer as? TelegramChannel, case .group = channel.info { if let participantsCount = peer.participantsCount, participantsCount != 0 { - string = strings.OldChannels_GroupFormat(participantsCount) + string + string = strings.OldChannels_GroupFormat(participantsCount) + ", " + string } else { - string = strings.OldChannels_GroupEmptyFormat + string + string = strings.OldChannels_GroupEmptyFormat + ", " + string } } else { string = strings.OldChannels_ChannelFormat + string @@ -83,7 +83,7 @@ private enum OldChannelsEntryId: Hashable { } private enum OldChannelsEntry: ItemListNodeEntry { - case info(String, String) + case info(Int, String, String) case peersHeader(String) case peer(Int, InactiveChannel, Bool) @@ -109,8 +109,8 @@ private enum OldChannelsEntry: ItemListNodeEntry { static func ==(lhs: OldChannelsEntry, rhs: OldChannelsEntry) -> Bool { switch lhs { - case let .info(title, text): - if case .info(title, text) = rhs { + case let .info(count, title, text): + if case .info(count, title, text) = rhs { return true } else { return false @@ -167,8 +167,8 @@ private enum OldChannelsEntry: ItemListNodeEntry { func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! OldChannelsItemArguments switch self { - case let .info(title, text): - return ItemListInfoItem(presentationData: presentationData, title: title, text: .plain(text), style: .blocks, sectionId: self.section, closeAction: nil) + case let .info(count, title, text): + return IncreaseLimitHeaderItem(theme: presentationData.theme, icon: .group, count: count, title: title, text: text, sectionId: self.section) case let .peersHeader(title): return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .peer(_, peer, selected): @@ -184,19 +184,10 @@ private struct OldChannelsState: Equatable { var isSearching: Bool = false } -private func oldChannelsEntries(presentationData: PresentationData, state: OldChannelsState, peers: [InactiveChannel]?, intent: OldChannelsControllerIntent) -> [OldChannelsEntry] { +private func oldChannelsEntries(presentationData: PresentationData, state: OldChannelsState, limit: Int, peers: [InactiveChannel]?, intent: OldChannelsControllerIntent) -> [OldChannelsEntry] { var entries: [OldChannelsEntry] = [] - - let noticeText: String - switch intent { - case .join: - noticeText = presentationData.strings.OldChannels_NoticeText - case .create: - noticeText = presentationData.strings.OldChannels_NoticeCreateText - case .upgrade: - noticeText = presentationData.strings.OldChannels_NoticeUpgradeText - } - entries.append(.info(presentationData.strings.OldChannels_NoticeTitle, noticeText)) + + entries.append(.info(limit, presentationData.strings.OldChannels_TooManyCommunitiesTitle, presentationData.strings.OldChannels_TooManyCommunitiesText("\(limit)", "\(limit * 2)").string)) if let peers = peers, !peers.isEmpty { entries.append(.peersHeader(presentationData.strings.OldChannels_ChannelsHeader)) @@ -209,127 +200,6 @@ private func oldChannelsEntries(presentationData: PresentationData, state: OldCh return entries } -private final class OldChannelsActionPanelNode: ASDisplayNode { - private let separatorNode: ASDisplayNode - let buttonNode: SolidRoundedButtonNode - - init(presentationData: ItemListPresentationData, leaveAction: @escaping () -> Void) { - self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor - self.buttonNode = SolidRoundedButtonNode(title: "", icon: nil, theme: SolidRoundedButtonTheme(theme: presentationData.theme), height: 50.0, cornerRadius: 11.0, gloss: false) - - super.init() - - self.backgroundColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor - - self.addSubnode(self.separatorNode) - self.addSubnode(self.buttonNode) - - self.buttonNode.pressed = { - leaveAction() - } - } - - func updatePresentationData(_ presentationData: ItemListPresentationData) { - self.separatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor - self.backgroundColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor - } - - func updateLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat { - let sideInset: CGFloat = 16.0 - let verticalInset: CGFloat = 16.0 - let buttonHeight: CGFloat = 50.0 - - let insets = layout.insets(options: [.input]) - - transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: UIScreenPixel))) - - let _ = self.buttonNode.updateLayout(width: layout.size.width - sideInset * 2.0, transition: transition) - transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: CGSize(width: layout.size.width, height: buttonHeight))) - - return buttonHeight + verticalInset * 2.0 + insets.bottom - } -} - -private final class OldChannelsControllerImpl: ItemListController { - private let panelNode: OldChannelsActionPanelNode - - private var displayPanel: Bool = false - private var validLayout: ContainerViewLayout? - - private var presentationData: ItemListPresentationData - private var presentationDataDisposable: Disposable? - - var leaveAction: (() -> Void)? - - override init(presentationData: ItemListPresentationData, updatedPresentationData: Signal, state: Signal<(ItemListControllerState, (ItemListNodeState, ItemGenerationArguments)), NoError>, tabBarItem: Signal?) { - self.presentationData = presentationData - - var leaveActionImpl: (() -> Void)? - self.panelNode = OldChannelsActionPanelNode(presentationData: presentationData, leaveAction: { - leaveActionImpl?() - }) - - super.init(presentationData: presentationData, updatedPresentationData: updatedPresentationData, state: state, tabBarItem: tabBarItem) - - self.presentationDataDisposable = (updatedPresentationData - |> deliverOnMainQueue).start(next: { [weak self] presentationData in - guard let strongSelf = self else { - return - } - strongSelf.presentationData = presentationData - strongSelf.panelNode.updatePresentationData(presentationData) - }) - - leaveActionImpl = { [weak self] in - self?.leaveAction?() - } - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.presentationDataDisposable?.dispose() - } - - override var navigationBarRequiresEntireLayoutUpdate: Bool { - return false - } - - override func loadDisplayNode() { - super.loadDisplayNode() - - self.displayNode.addSubnode(self.panelNode) - } - - override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - self.validLayout = layout - - let panelHeight = self.panelNode.updateLayout(layout, transition: transition) - - var additionalInsets = UIEdgeInsets() - additionalInsets.bottom = max(layout.intrinsicInsets.bottom, panelHeight) - - self.additionalInsets = additionalInsets - - super.containerLayoutUpdated(layout, transition: transition) - - transition.updateFrame(node: self.panelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.displayPanel ? (layout.size.height - panelHeight) : layout.size.height), size: CGSize(width: layout.size.width, height: panelHeight)), beginWithCurrentState: true) - } - - func updatePanelPeerCount(_ value: Int) { - self.panelNode.buttonNode.title = self.presentationData.strings.OldChannels_Leave(Int32(value)) - - if self.displayPanel != (value != 0) { - self.displayPanel = (value != 0) - if let layout = self.validLayout { - self.containerLayoutUpdated(layout, transition: .animated(duration: 0.3, curve: .spring)) - } - } - } -} public enum OldChannelsControllerIntent { case join @@ -344,20 +214,19 @@ public func oldChannelsController(context: AccountContext, updatedPresentationDa let updateState: ((OldChannelsState) -> OldChannelsState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } - - var updateSelectedPeersImpl: ((Int) -> Void)? - + var dismissImpl: (() -> Void)? var setDisplayNavigationBarImpl: ((Bool) -> Void)? var ensurePeerVisibleImpl: ((PeerId) -> Void)? + var leaveActionImpl: (() -> Void)? + let actionsDisposable = DisposableSet() let arguments = OldChannelsItemArguments( context: context, togglePeer: { peerId, ensureVisible in - var selectedPeerCount = 0 var didSelect = false updateState { state in var state = state @@ -367,10 +236,8 @@ public func oldChannelsController(context: AccountContext, updatedPresentationDa state.selectedPeers.insert(peerId) didSelect = true } - selectedPeerCount = state.selectedPeers.count return state } - updateSelectedPeersImpl?(selectedPeerCount) if didSelect && ensureVisible { ensurePeerVisibleImpl?(peerId) } @@ -436,7 +303,20 @@ public func oldChannelsController(context: AccountContext, updatedPresentationDa emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: oldChannelsEntries(presentationData: presentationData, state: state, peers: peers, intent: intent), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, initialScrollToItem: ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: 0.0), directionHint: .Up), crossfadeState: peersAreEmptyUpdated, animateChanges: false) + let buttonText: String + let colorful: Bool + if state.selectedPeers.count > 0 { + buttonText = presentationData.strings.OldChannels_LeaveCommunities(Int32(state.selectedPeers.count)) + colorful = false + } else { + buttonText = presentationData.strings.OldChannels_IncreaseLimit + colorful = true + } + let footerItem = IncreaseLimitFooterItem(theme: presentationData.theme, title: buttonText, colorful: colorful, action: { + leaveActionImpl?() + }) + + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: oldChannelsEntries(presentationData: presentationData, state: state, limit: 500, peers: peers, intent: intent), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, footerItem: footerItem, initialScrollToItem: ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: 0.0), directionHint: .Up), crossfadeState: peersAreEmptyUpdated, animateChanges: false) return (controllerState, (listState, arguments)) } @@ -444,14 +324,10 @@ public func oldChannelsController(context: AccountContext, updatedPresentationDa actionsDisposable.dispose() } - let controller = OldChannelsControllerImpl(context: context, state: signal) + let controller = ItemListController(context: context, state: signal) controller.navigationPresentation = .modal - updateSelectedPeersImpl = { [weak controller] value in - controller?.updatePanelPeerCount(value) - } - - controller.leaveAction = { + leaveActionImpl = { let state = stateValue.with { $0 } let _ = (peersPromise.get() |> take(1) diff --git a/submodules/PremiumUI/BUILD b/submodules/PremiumUI/BUILD index 6748e54348..dafcf76cd0 100644 --- a/submodules/PremiumUI/BUILD +++ b/submodules/PremiumUI/BUILD @@ -25,6 +25,11 @@ swift_library( "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", "//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/ReactionSelectionNode:ReactionSelectionNode", + "//submodules/ComponentFlow:ComponentFlow", + "//submodules/Components/ViewControllerComponent:ViewControllerComponent", + "//submodules/Components/MultilineTextComponent:MultilineTextComponent", + "//submodules/Components/BundleIconComponent:BundleIconComponent", + "//submodules/Components/SolidRoundedButtonComponent:SolidRoundedButtonComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/PremiumUI/Sources/LimitScreen.swift b/submodules/PremiumUI/Sources/LimitScreen.swift new file mode 100644 index 0000000000..c21329c43e --- /dev/null +++ b/submodules/PremiumUI/Sources/LimitScreen.swift @@ -0,0 +1,754 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import PresentationDataUtils +import ComponentFlow +import ViewControllerComponent +import MultilineTextComponent +import BundleIconComponent +import SolidRoundedButtonComponent +import Markdown + +private final class LimitScreenComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let subject: LimitScreen.Subject + let proceed: () -> Void + + init(context: AccountContext, subject: LimitScreen.Subject, proceed: @escaping () -> Void) { + self.context = context + self.subject = subject + self.proceed = proceed + } + + static func ==(lhs: LimitScreenComponent, rhs: LimitScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.subject != rhs.subject { + return false + } + return true + } + + final class State: ComponentState { + private let context: AccountContext + + init(context: AccountContext) { + self.context = context + + super.init() + } + } + + func makeState() -> State { + return State(context: self.context) + } + + static var body: Body { + let icon = Child(BundleIconComponent.self) + let title = Child(MultilineTextComponent.self) + let text = Child(MultilineTextComponent.self) + + let button = Child(SolidRoundedButtonComponent.self) + let cancel = Child(Button.self) + + return { context in + let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + let component = context.component + let theme = environment.theme + let strings = environment.strings + + let topInset: CGFloat = 34.0 + 38.0 + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let textSideInset: CGFloat = 24.0 + environment.safeInsets.left + + let icon = icon.update( + component: BundleIconComponent( + name: "Premium/Tmp", + tintColor: nil + ), + availableSize: CGSize(width: context.availableSize.width, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + let title = title.update( + component: MultilineTextComponent( + text: NSAttributedString(string: "Limit Reached", font: Font.semibold(17.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + + let textColor = theme.actionSheet.secondaryTextColor + let string: String + switch component.subject { + case .chatsInFolder: + string = "" + case .folders: + string = "" + case .pins: + string = strings.DialogList_ExtendedPinLimitError("\(5)", "\(10)").string + } + let attributedText = parseMarkdownIntoAttributedString(string, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: textColor), linkAttribute: { _ in + return nil + })) + + let text = text.update( + component: MultilineTextComponent( + text: attributedText, + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.1 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + + let button = button.update( + component: SolidRoundedButtonComponent( + title: "Increase Limit", + theme: SolidRoundedButtonComponent.Theme( + backgroundColor: .black, + backgroundColors: [UIColor(rgb: 0x407af0), UIColor(rgb: 0x9551e8), UIColor(rgb: 0xbf499a), UIColor(rgb: 0xf17b30)], + foregroundColor: .white + ), + font: .bold, + fontSize: 17.0, + height: 50.0, + cornerRadius: 10.0, + gloss: false, + iconName: "Premium/X2", + iconPosition: .right, + action: { [weak component] in + guard let component = component else { + return + } + component.proceed() + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + transition: context.transition + ) + + let cancel = cancel.update(component: Button( + content: AnyComponent(Text(text: strings.Common_Cancel, font: Font.regular(17.0), color: theme.actionSheet.controlAccentColor)), + action: { + + } + ), + availableSize: context.availableSize, + transition: context.transition) + + let width = context.availableSize.width + + context.add(icon + .position(CGPoint(x: width / 2.0, y: 57.0)) + ) + + context.add(title + .position(CGPoint(x: width / 2.0, y: topInset + 39.0)) + ) + context.add(text + .position(CGPoint(x: width / 2.0, y: topInset + 101.0)) + ) + + let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset + 76.0 + text.size.height + 27.0), size: button.size) + context.add(button + .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) + ) + + context.add(cancel + .position(CGPoint(x: width / 2.0, y: topInset + 76.0 + text.size.height + 20.0 + button.size.height + 40.0)) + ) + + let contentSize = CGSize(width: context.availableSize.width, height: topInset + title.size.height + text.size.height) + + return contentSize + } + } +} + +public class LimitScreen: ViewController { + final class Node: ViewControllerTracingNode, UIScrollViewDelegate, UIGestureRecognizerDelegate { + private var presentationData: PresentationData + private weak var controller: LimitScreen? + + private let component: AnyComponent + private let theme: PresentationTheme? + + let dim: ASDisplayNode + let wrappingView: UIView + let containerView: UIView + let scrollView: UIScrollView + let hostView: ComponentHostView + + private(set) var isExpanded = false + private var panGestureRecognizer: UIPanGestureRecognizer? + private var panGestureArguments: (topInset: CGFloat, offset: CGFloat, scrollView: UIScrollView?, listNode: ListView?)? + + private var currentIsVisible: Bool = false + private var currentLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? + + fileprivate var temporaryDismiss = false + + init(context: AccountContext, controller: LimitScreen, component: AnyComponent, theme: PresentationTheme?) { + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + self.controller = controller + + self.component = component + self.theme = theme + + self.dim = ASDisplayNode() + self.dim.alpha = 0.0 + self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25) + + self.wrappingView = UIView() + self.containerView = UIView() + self.scrollView = UIScrollView() + self.hostView = ComponentHostView() + + super.init() + + self.scrollView.delegate = self + self.scrollView.showsVerticalScrollIndicator = false + + self.containerView.clipsToBounds = true + self.containerView.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemBackgroundColor + + self.addSubnode(self.dim) + + self.view.addSubview(self.wrappingView) + self.wrappingView.addSubview(self.containerView) + self.containerView.addSubview(self.scrollView) + self.scrollView.addSubview(self.hostView) + } + + override func didLoad() { + super.didLoad() + + let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + panRecognizer.delegate = self + panRecognizer.delaysTouchesBegan = false + panRecognizer.cancelsTouchesInView = true + self.panGestureRecognizer = panRecognizer + self.wrappingView.addGestureRecognizer(panRecognizer) + + self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + + self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.controller?.dismiss(animated: true) + } + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if let (layout, _) = self.currentLayout { + if case .regular = layout.metrics.widthClass { + return false + } + } + return true + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + let contentOffset = self.scrollView.contentOffset.y + self.controller?.navigationBar?.updateBackgroundAlpha(min(30.0, contentOffset) / 30.0, transition: .immediate) + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer { + return true + } + return false + } + + private var isDismissing = false + func animateIn() { + ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 1.0) + + let targetPosition = self.containerView.center + let startPosition = targetPosition.offsetBy(dx: 0.0, dy: self.bounds.height) + + self.containerView.center = startPosition + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + transition.animateView(allowUserInteraction: true, { + self.containerView.center = targetPosition + }, completion: { _ in + }) + } + + func animateOut(completion: @escaping () -> Void = {}) { + self.isDismissing = true + + let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) + positionTransition.updatePosition(layer: self.containerView.layer, position: CGPoint(x: self.containerView.center.x, y: self.bounds.height + self.containerView.bounds.height / 2.0), completion: { [weak self] _ in + self?.controller?.dismiss(animated: false, completion: completion) + }) + let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) + alphaTransition.updateAlpha(node: self.dim, alpha: 0.0) + + if !self.temporaryDismiss { + self.controller?.updateModalStyleOverlayTransitionFactor(0.0, transition: positionTransition) + } + } + + func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: Transition) { + self.currentLayout = (layout, navigationHeight) + + if let controller = self.controller, let navigationBar = controller.navigationBar, navigationBar.view.superview !== self.wrappingView { + self.containerView.addSubview(navigationBar.view) + } + + self.dim.frame = CGRect(origin: CGPoint(x: 0.0, y: -layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height * 3.0)) + + var effectiveExpanded = self.isExpanded + if case .regular = layout.metrics.widthClass { + effectiveExpanded = true + } + + let isLandscape = layout.orientation == .landscape + let edgeTopInset = isLandscape ? 0.0 : self.defaultTopInset + let topInset: CGFloat + if let (panInitialTopInset, panOffset, _, _) = self.panGestureArguments { + if effectiveExpanded { + topInset = min(edgeTopInset, panInitialTopInset + max(0.0, panOffset)) + } else { + topInset = max(0.0, panInitialTopInset + min(0.0, panOffset)) + } + } else { + topInset = effectiveExpanded ? 0.0 : edgeTopInset + } + transition.setFrame(view: self.wrappingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: layout.size), completion: nil) + + let modalProgress = isLandscape ? 0.0 : (1.0 - topInset / self.defaultTopInset) + self.controller?.updateModalStyleOverlayTransitionFactor(modalProgress, transition: transition.containedViewLayoutTransition) + + let clipFrame: CGRect + if layout.metrics.widthClass == .compact { + self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.25) + if isLandscape { + self.containerView.layer.cornerRadius = 0.0 + } else { + self.containerView.layer.cornerRadius = 10.0 + } + + if #available(iOS 11.0, *) { + if layout.safeInsets.bottom.isZero { + self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + } else { + self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner] + } + } + + if isLandscape { + clipFrame = CGRect(origin: CGPoint(), size: layout.size) + } else { + let coveredByModalTransition: CGFloat = 0.0 + var containerTopInset: CGFloat = 10.0 + if let statusBarHeight = layout.statusBarHeight { + containerTopInset += statusBarHeight + } + + let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: containerTopInset - coveredByModalTransition * 10.0), size: CGSize(width: layout.size.width, height: layout.size.height - containerTopInset)) + let maxScale: CGFloat = (layout.size.width - 16.0 * 2.0) / layout.size.width + let containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition + let maxScaledTopInset: CGFloat = containerTopInset - 10.0 + let scaledTopInset: CGFloat = containerTopInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition + let containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0)) + + clipFrame = CGRect(x: containerFrame.minX, y: containerFrame.minY, width: containerFrame.width, height: containerFrame.height) + } + } else { + self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.4) + self.containerView.layer.cornerRadius = 10.0 + + let verticalInset: CGFloat = 44.0 + + let maxSide = max(layout.size.width, layout.size.height) + let minSide = min(layout.size.width, layout.size.height) + let containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0) + clipFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize) + } + + transition.setFrame(view: self.containerView, frame: clipFrame) + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: clipFrame.size), completion: nil) + + let environment = ViewControllerComponentContainer.Environment( + statusBarHeight: 0.0, + navigationHeight: navigationHeight, + safeInsets: UIEdgeInsets(top: layout.intrinsicInsets.top + layout.safeInsets.top, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.safeInsets.right), + isVisible: self.currentIsVisible, + theme: self.theme ?? self.presentationData.theme, + strings: self.presentationData.strings, + controller: { [weak self] in + return self?.controller + } + ) + var contentSize = self.hostView.update( + transition: transition, + component: self.component, + environment: { + environment + }, + forceUpdate: true, + containerSize: CGSize(width: clipFrame.size.width, height: 10000.0) + ) + contentSize.height = max(layout.size.height - navigationHeight, contentSize.height) + transition.setFrame(view: self.hostView, frame: CGRect(origin: CGPoint(), size: contentSize), completion: nil) + + self.scrollView.contentSize = contentSize + } + + private var didPlayAppearAnimation = false + func updateIsVisible(isVisible: Bool) { + if self.currentIsVisible == isVisible { + return + } + self.currentIsVisible = isVisible + + guard let currentLayout = self.currentLayout else { + return + } + self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: .immediate) + + if !self.didPlayAppearAnimation { + self.didPlayAppearAnimation = true + self.animateIn() + } + } + + private var defaultTopInset: CGFloat { + return 390.0 +// guard let (layout, _) = self.currentLayout else{ +// return 210.0 +// } +// if case .compact = layout.metrics.widthClass { +// var factor: CGFloat = 0.2488 +// if layout.size.width <= 320.0 { +// factor = 0.15 +// } +// return floor(max(layout.size.width, layout.size.height) * factor) +// } else { +// return 210.0 +// } + } + + private func findScrollView(view: UIView?) -> (UIScrollView, ListView?)? { + if let view = view { + if let view = view as? UIScrollView { + return (view, nil) + } + if let node = view.asyncdisplaykit_node as? ListView { + return (node.scroller, node) + } + return findScrollView(view: view.superview) + } else { + return nil + } + } + + @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { + guard let (layout, navigationHeight) = self.currentLayout else { + return + } + + let isLandscape = layout.orientation == .landscape + let edgeTopInset = isLandscape ? 0.0 : defaultTopInset + + switch recognizer.state { + case .began: + let point = recognizer.location(in: self.view) + let currentHitView = self.hitTest(point, with: nil) + + var scrollViewAndListNode = self.findScrollView(view: currentHitView) + if scrollViewAndListNode?.0.frame.height == self.frame.width { + scrollViewAndListNode = nil + } + let scrollView = scrollViewAndListNode?.0 + let listNode = scrollViewAndListNode?.1 + + let topInset: CGFloat + if self.isExpanded { + topInset = 0.0 + } else { + topInset = edgeTopInset + } + + self.panGestureArguments = (topInset, 0.0, scrollView, listNode) + case .changed: + guard let (topInset, panOffset, scrollView, listNode) = self.panGestureArguments else { + return + } + let visibleContentOffset = listNode?.visibleContentOffset() + let contentOffset = scrollView?.contentOffset.y ?? 0.0 + + var translation = recognizer.translation(in: self.view).y + + var currentOffset = topInset + translation + + let epsilon = 1.0 + if case let .known(value) = visibleContentOffset, value <= epsilon { + if let scrollView = scrollView { + scrollView.bounces = false + scrollView.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: false) + } + } else if let scrollView = scrollView, contentOffset <= -scrollView.contentInset.top + epsilon { + scrollView.bounces = false + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } else if let scrollView = scrollView { + translation = panOffset + currentOffset = topInset + translation + if self.isExpanded { + recognizer.setTranslation(CGPoint(), in: self.view) + } else if currentOffset > 0.0 { + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } + } + + self.panGestureArguments = (topInset, translation, scrollView, listNode) + + if !self.isExpanded { + if currentOffset > 0.0, let scrollView = scrollView { + scrollView.panGestureRecognizer.setTranslation(CGPoint(), in: scrollView) + } + } + + var bounds = self.bounds + if self.isExpanded { + bounds.origin.y = -max(0.0, translation - edgeTopInset) + } else { + bounds.origin.y = -translation + } + bounds.origin.y = min(0.0, bounds.origin.y) + self.bounds = bounds + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate) + case .ended: + guard let (currentTopInset, panOffset, scrollView, listNode) = self.panGestureArguments else { + return + } + self.panGestureArguments = nil + + let visibleContentOffset = listNode?.visibleContentOffset() + let contentOffset = scrollView?.contentOffset.y ?? 0.0 + + let translation = recognizer.translation(in: self.view).y + var velocity = recognizer.velocity(in: self.view) + + if self.isExpanded { + if case let .known(value) = visibleContentOffset, value > 0.1 { + velocity = CGPoint() + } else if case .unknown = visibleContentOffset { + velocity = CGPoint() + } else if contentOffset > 0.1 { + velocity = CGPoint() + } + } + + var bounds = self.bounds + if self.isExpanded { + bounds.origin.y = -max(0.0, translation - edgeTopInset) + } else { + bounds.origin.y = -translation + } + bounds.origin.y = min(0.0, bounds.origin.y) + + scrollView?.bounces = true + + let offset = currentTopInset + panOffset + let topInset: CGFloat = edgeTopInset + + var dismissing = false + if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0) { + self.controller?.dismiss(animated: true, completion: nil) + dismissing = true + } else if self.isExpanded { + if velocity.y > 300.0 || offset > topInset / 2.0 { + self.isExpanded = false + if let listNode = listNode { + listNode.scroller.setContentOffset(CGPoint(), animated: false) + } else if let scrollView = scrollView { + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } + + let distance = topInset - offset + let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance) + let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + } else { + self.isExpanded = true + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + } + } else if (velocity.y < -300.0 || offset < topInset / 2.0) { + if velocity.y > -2200.0 && velocity.y < -300.0, let listNode = listNode { + DispatchQueue.main.async { + listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + } + } + + let initialVelocity: CGFloat = offset.isZero ? 0.0 : abs(velocity.y / offset) + let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) + self.isExpanded = true + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + } else { + if let listNode = listNode { + listNode.scroller.setContentOffset(CGPoint(), animated: false) + } else if let scrollView = scrollView { + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + } + + if !dismissing { + var bounds = self.bounds + let previousBounds = bounds + bounds.origin.y = 0.0 + self.bounds = bounds + self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + } + case .cancelled: + self.panGestureArguments = nil + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + default: + break + } + } + + func update(isExpanded: Bool, transition: ContainedViewLayoutTransition) { + guard isExpanded != self.isExpanded else { + return + } + self.isExpanded = isExpanded + + guard let (layout, navigationHeight) = self.currentLayout else { + return + } + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + } + } + + var node: Node { + return self.displayNode as! Node + } + + private let context: AccountContext + private let theme: PresentationTheme? + private let component: AnyComponent + private var isInitiallyExpanded = false + + private var currentLayout: ContainerViewLayout? + + public var pushController: (ViewController) -> Void = { _ in } + public var presentController: (ViewController) -> Void = { _ in } + + public enum Subject { + case folders + case chatsInFolder + case pins + } + + public convenience init(context: AccountContext, subject: Subject) { + self.init(context: context, component: LimitScreenComponent(context: context, subject: subject, proceed: {})) + } + + private init(context: AccountContext, component: C, theme: PresentationTheme? = nil) where C.EnvironmentType == ViewControllerComponentContainer.Environment { + self.context = context + self.component = AnyComponent(component) + self.theme = nil + + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: context.sharedContext.currentPresentationData.with { $0 })) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func cancelPressed() { + self.dismiss(animated: true, completion: nil) + } + + override open func loadDisplayNode() { + self.displayNode = Node(context: self.context, controller: self, component: self.component, theme: self.theme) + self.displayNodeDidLoad() + } + + public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + self.view.endEditing(true) + if flag { + self.node.animateOut(completion: { + super.dismiss(animated: false, completion: {}) + completion?() + }) + } else { + super.dismiss(animated: false, completion: {}) + completion?() + } + } + + override open func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.node.updateIsVisible(isVisible: true) + } + + override open func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + self.node.updateIsVisible(isVisible: false) + } + + override public func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + var navigationLayout = self.navigationLayout(layout: layout) + var navigationFrame = navigationLayout.navigationFrame + + var layout = layout + if case .regular = layout.metrics.widthClass { + let verticalInset: CGFloat = 44.0 + let maxSide = max(layout.size.width, layout.size.height) + let minSide = min(layout.size.width, layout.size.height) + let containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0) + let clipFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize) + navigationFrame.size.width = clipFrame.width + layout.size = clipFrame.size + } + + navigationFrame.size.height = 56.0 + navigationLayout.navigationFrame = navigationFrame + navigationLayout.defaultContentHeight = 56.0 + + layout.statusBarHeight = nil + + self.applyNavigationBarLayout(layout, navigationLayout: navigationLayout, additionalBackgroundHeight: 0.0, transition: transition) + } + + override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.currentLayout = layout + super.containerLayoutUpdated(layout, transition: transition) + + let navigationHeight: CGFloat = 56.0 + + self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + } +} diff --git a/submodules/PremiumUI/Sources/ReactionCarouselNode.swift b/submodules/PremiumUI/Sources/ReactionCarouselNode.swift index 97de4839b5..cfec7560c9 100644 --- a/submodules/PremiumUI/Sources/ReactionCarouselNode.swift +++ b/submodules/PremiumUI/Sources/ReactionCarouselNode.swift @@ -14,6 +14,7 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { private let context: AccountContext private let theme: PresentationTheme private let reactions: [AvailableReactions.Reaction] + private var itemContainerNodes: [ASDisplayNode] = [] private var itemNodes: [ReactionNode] = [] private let scrollNode: ASScrollNode private let tapNode: ASDisplayNode @@ -21,9 +22,14 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { private var standaloneReactionAnimation: StandaloneReactionAnimation? private var animator: DisplayLinkAnimator? private var currentPosition: CGFloat = 0.0 + private var currentIndex: Int = 0 private var validLayout: CGSize? + private var playingIndices = Set() + + private let positionDelta: Double + init(context: AccountContext, theme: PresentationTheme, reactions: [AvailableReactions.Reaction]) { self.context = context self.theme = theme @@ -32,6 +38,8 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { self.scrollNode = ASScrollNode() self.tapNode = ASDisplayNode() + self.positionDelta = 1.0 / CGFloat(self.reactions.count) + super.init() self.addSubnode(self.scrollNode) @@ -51,12 +59,12 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { } @objc private func reactionTapped(_ gestureRecognizer: UITapGestureRecognizer) { - guard self.animator == nil, self.standaloneReactionAnimation == nil, self.scrollStartPosition == nil else { + guard self.animator == nil, self.scrollStartPosition == nil else { return } let point = gestureRecognizer.location(in: self.view) - guard let index = self.itemNodes.firstIndex(where: { $0.frame.contains(point) }) else { + guard let index = self.itemContainerNodes.firstIndex(where: { $0.frame.contains(point) }) else { return } @@ -77,7 +85,8 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { guard index >= 0 && index < self.itemNodes.count else { return } - let delta = 1.0 / CGFloat(self.itemNodes.count) + self.currentIndex = index + let delta = self.positionDelta let startPosition = self.currentPosition let newPosition = delta * CGFloat(index) @@ -129,6 +138,7 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { guard let aroundAnimation = reaction.aroundAnimation else { continue } + let containerNode = ASDisplayNode() let itemNode = ReactionNode(context: self.context, theme: self.theme, item: ReactionItem( reaction: ReactionItem.Reaction(rawValue: reaction.value), appearAnimation: reaction.appearAnimation, @@ -138,9 +148,11 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { applicationAnimation: aroundAnimation, largeApplicationAnimation: reaction.effectAnimation ), hasAppearAnimation: false) - itemNode.isUserInteractionEnabled = false - self.addSubnode(itemNode) + containerNode.isUserInteractionEnabled = false + containerNode.addSubnode(itemNode) + self.addSubnode(containerNode) + self.itemContainerNodes.append(containerNode) self.itemNodes.append(itemNode) } } @@ -154,22 +166,17 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { } func playReaction() { - guard self.standaloneReactionAnimation == nil else { - return - } - - let delta = 1.0 / CGFloat(self.itemNodes.count) + let delta = self.positionDelta let index = max(0, min(self.itemNodes.count - 1, Int(round(self.currentPosition / delta)))) - let reaction = self.reactions[index] - let targetView = self.itemNodes[index].view - - let standaloneReactionAnimation = StandaloneReactionAnimation() - self.standaloneReactionAnimation = standaloneReactionAnimation - guard let supernode = self.supernode else { + guard !self.playingIndices.contains(index) else { return } + let reaction = self.reactions[index] + let targetContainerNode = self.itemContainerNodes[index] + let targetView = self.itemNodes[index].view + guard let centerAnimation = reaction.centerAnimation else { return } @@ -177,10 +184,15 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { return } - self.scrollNode.view.isScrollEnabled = false + self.playingIndices.insert(index) - supernode.addSubnode(standaloneReactionAnimation) - standaloneReactionAnimation.frame = supernode.bounds + targetContainerNode.view.superview?.bringSubviewToFront(targetContainerNode.view) + + let standaloneReactionAnimation = StandaloneReactionAnimation() + self.standaloneReactionAnimation = standaloneReactionAnimation + + targetContainerNode.addSubnode(standaloneReactionAnimation) + standaloneReactionAnimation.frame = targetContainerNode.bounds standaloneReactionAnimation.animateReactionSelection( context: self.context, theme: self.theme, reaction: ReactionItem( reaction: ReactionItem.Reaction(rawValue: reaction.value), @@ -200,7 +212,7 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { completion: { [weak standaloneReactionAnimation, weak self] in standaloneReactionAnimation?.removeFromSupernode() self?.standaloneReactionAnimation = nil - self?.scrollNode.view.isScrollEnabled = true + self?.playingIndices.remove(index) } ) } @@ -211,7 +223,7 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { self.scrollStartPosition = (scrollView.contentOffset.x, self.currentPosition) } } - + func scrollViewDidScroll(_ scrollView: UIScrollView) { guard !self.ignoreContentOffsetChange, let (startContentOffset, startPosition) = self.scrollStartPosition else { return @@ -227,6 +239,14 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { updatedPosition += 1.0 } self.currentPosition = updatedPosition + + let indexDelta = self.positionDelta + let index = max(0, min(self.itemNodes.count - 1, Int(round(self.currentPosition / indexDelta)))) + if index != self.currentIndex { + self.currentIndex = index + print(index) + } + if let size = self.validLayout { self.updateLayout(size: size, transition: .immediate) } @@ -237,7 +257,7 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { return } - let delta = 1.0 / CGFloat(self.itemNodes.count) + let delta = self.positionDelta let scrollDelta = targetContentOffset.pointee.x - startContentOffset let positionDelta = scrollDelta * -0.001 let positionCounts = round(positionDelta / delta) @@ -251,7 +271,7 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { if !decelerate { self.resetScrollPosition() - let delta = 1.0 / CGFloat(self.itemNodes.count) + let delta = self.positionDelta let index = max(0, min(self.itemNodes.count - 1, Int(round(self.currentPosition / delta)))) self.scrollTo(index, playReaction: true, duration: 0.2) } @@ -272,12 +292,14 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { self.resetScrollPosition() } - let delta = 1.0 / CGFloat(self.itemNodes.count) + let delta = self.positionDelta let areaSize = CGSize(width: floor(size.width * 0.7), height: size.height * 0.5) - - var i = 0 - for itemNode in self.itemNodes { + + for i in 0 ..< self.itemNodes.count { + let itemNode = self.itemNodes[i] + let containerNode = self.itemContainerNodes[i] + var angle = CGFloat.pi * 0.5 + CGFloat(i) * delta * CGFloat.pi * 2.0 - self.currentPosition * CGFloat.pi * 2.0 if angle < 0.0 { angle = CGFloat.pi * 2.0 + angle @@ -303,12 +325,13 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { ) let itemFrame = CGRect(origin: CGPoint(x: size.width * 0.5 + point.x * areaSize.width * 0.5 - itemSize.width * 0.5, y: size.height * 0.5 + point.y * areaSize.height * 0.5 - itemSize.height * 0.5), size: itemSize) - itemNode.bounds = CGRect(origin: CGPoint(), size: itemFrame.size) - itemNode.position = itemFrame.center - itemNode.updateLayout(size: itemFrame.size, isExpanded: false, largeExpanded: false, isPreviewing: false, transition: transition) - transition.updateTransformScale(node: itemNode, scale: 1.0 - distance * 0.45) + containerNode.bounds = CGRect(origin: CGPoint(), size: itemFrame.size) + containerNode.position = itemFrame.center + transition.updateTransformScale(node: containerNode, scale: 1.0 - distance * 0.45) + + itemNode.frame = CGRect(origin: CGPoint(), size: itemFrame.size) + itemNode.updateLayout(size: itemFrame.size, isExpanded: false, largeExpanded: false, isPreviewing: false, transition: transition) - i += 1 } } } diff --git a/submodules/PresentationDataUtils/Sources/SolidRoundedButtonNode.swift b/submodules/PresentationDataUtils/Sources/SolidRoundedButtonNode.swift index 73ebbf3357..d708784757 100644 --- a/submodules/PresentationDataUtils/Sources/SolidRoundedButtonNode.swift +++ b/submodules/PresentationDataUtils/Sources/SolidRoundedButtonNode.swift @@ -5,6 +5,6 @@ import TelegramPresentationData public extension SolidRoundedButtonTheme { convenience init(theme: PresentationTheme) { - self.init(backgroundColor: theme.list.itemCheckColors.fillColor, foregroundColor: theme.list.itemCheckColors.foregroundColor) + self.init(backgroundColor: theme.list.itemCheckColors.fillColor, backgroundColors: [], foregroundColor: theme.list.itemCheckColors.foregroundColor) } } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index 28ff9eb170..623b5b8085 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -299,6 +299,37 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { } } +private func generatePremiumReactionIcon() -> UIImage? { + return generateImage(CGSize(width: 32.0, height: 32.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + if let backgroundImage = UIImage(bundleImageName: "Premium/BackgroundIcon"), let foregroundImage = UIImage(bundleImageName: "Premium/ForegroundIcon") { + context.saveGState() + if let cgImage = backgroundImage.cgImage { + context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage) + } + + let colorsArray: [CGColor] = [ + UIColor(rgb: 0xa34ecf).cgColor, + UIColor(rgb: 0xa34ecf).cgColor, + UIColor(rgb: 0xff7923).cgColor, + UIColor(rgb: 0xff7923).cgColor + ] + var locations: [CGFloat] = [0.0, 0.15, 0.85, 1.0] + let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions()) + + context.restoreGState() + + if let cgImage = foregroundImage.cgImage { + context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage) + } + context.setFillColor(UIColor.white.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + } + }) +} + final class PremiumReactionsNode: ASDisplayNode, ReactionItemNode { var isExtracted: Bool = false @@ -309,23 +340,11 @@ final class PremiumReactionsNode: ASDisplayNode, ReactionItemNode { self.imageNode.contentMode = .center self.imageNode.displaysAsynchronously = false self.imageNode.isUserInteractionEnabled = false - self.imageNode.alpha = 0.5 + self.imageNode.image = generatePremiumReactionIcon() super.init() self.addSubnode(self.imageNode) - - if theme.overallDarkAppearance { - self.imageNode.image = generateTintedImage(image: UIImage(bundleImageName: "Premium/BackgroundIcon"), color: .white) - } else { - self.imageNode.image = UIImage(bundleImageName: "Premium/BackgroundIcon") - } - } - - override func didLoad() { - super.didLoad() - - self.imageNode.layer.compositingFilter = "softLightBlendMode" } func appear(animated: Bool) { diff --git a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift index 9e832cd22e..eaa8137460 100644 --- a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift +++ b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift @@ -19,12 +19,12 @@ private func generateIndefiniteActivityIndicatorImage(color: UIColor, diameter: public final class SolidRoundedButtonTheme: Equatable { public let backgroundColor: UIColor - public let gradientBackgroundColor: UIColor? + public let backgroundColors: [UIColor] public let foregroundColor: UIColor - public init(backgroundColor: UIColor, gradientBackgroundColor: UIColor? = nil, foregroundColor: UIColor) { + public init(backgroundColor: UIColor, backgroundColors: [UIColor] = [], foregroundColor: UIColor) { self.backgroundColor = backgroundColor - self.gradientBackgroundColor = gradientBackgroundColor + self.backgroundColors = backgroundColors self.foregroundColor = foregroundColor } @@ -32,7 +32,7 @@ public final class SolidRoundedButtonTheme: Equatable { if lhs.backgroundColor != rhs.backgroundColor { return false } - if lhs.gradientBackgroundColor != rhs.gradientBackgroundColor { + if lhs.backgroundColors != rhs.backgroundColors { return false } if lhs.foregroundColor != rhs.foregroundColor { @@ -47,13 +47,18 @@ public enum SolidRoundedButtonFont { case regular } +public enum SolidRoundedButtonIconPosition { + case left + case right +} + public final class SolidRoundedButtonNode: ASDisplayNode { private var theme: SolidRoundedButtonTheme private var font: SolidRoundedButtonFont private var fontSize: CGFloat private let gloss: Bool - private let buttonBackgroundNode: ASDisplayNode + private let buttonBackgroundNode: ASImageNode private var shimmerView: ShimmerEffectForegroundView? private var borderView: UIView? @@ -101,6 +106,14 @@ public final class SolidRoundedButtonNode: ASDisplayNode { } } } + + public var iconPosition: SolidRoundedButtonIconPosition = .left { + didSet { + if let width = self.validLayout { + _ = self.updateLayout(width: width, transition: .immediate) + } + } + } public init(title: String? = nil, icon: UIImage? = nil, theme: SolidRoundedButtonTheme, font: SolidRoundedButtonFont = .bold, fontSize: CGFloat = 17.0, height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0, gloss: Bool = false) { self.theme = theme @@ -111,9 +124,21 @@ public final class SolidRoundedButtonNode: ASDisplayNode { self.title = title self.gloss = gloss - self.buttonBackgroundNode = ASDisplayNode() + self.buttonBackgroundNode = ASImageNode() + self.buttonBackgroundNode.displaysAsynchronously = false self.buttonBackgroundNode.clipsToBounds = true - self.buttonBackgroundNode.backgroundColor = theme.backgroundColor + if theme.backgroundColors.count > 1 { + self.buttonBackgroundNode.backgroundColor = nil + + var locations: [CGFloat] = [] + let delta = 1.0 / CGFloat(theme.backgroundColors.count - 1) + for i in 0 ..< theme.backgroundColors.count { + locations.append(delta * CGFloat(i)) + } + self.buttonBackgroundNode.image = generateGradientImage(size: CGSize(width: 200.0, height: height), colors: theme.backgroundColors, locations: locations, direction: .horizontal) + } else { + self.buttonBackgroundNode.backgroundColor = theme.backgroundColor + } self.buttonBackgroundNode.cornerRadius = cornerRadius self.buttonNode = HighlightTrackingButtonNode() @@ -230,13 +255,34 @@ public final class SolidRoundedButtonNode: ASDisplayNode { borderShimmerView.layer.compositingFilter = compositingFilter } - public func updateTheme(_ theme: SolidRoundedButtonTheme) { + public func updateTheme(_ theme: SolidRoundedButtonTheme, animated: Bool = false) { guard theme !== self.theme else { return } self.theme = theme - self.buttonBackgroundNode.backgroundColor = theme.backgroundColor + if animated { + if let snapshotView = self.buttonBackgroundNode.view.snapshotView(afterScreenUpdates: false) { + self.view.insertSubview(snapshotView, aboveSubview: self.buttonBackgroundNode.view) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + } + if theme.backgroundColors.count > 1 { + self.buttonBackgroundNode.backgroundColor = nil + + var locations: [CGFloat] = [] + let delta = 1.0 / CGFloat(theme.backgroundColors.count - 1) + for i in 0 ..< theme.backgroundColors.count { + locations.append(delta * CGFloat(i)) + } + self.buttonBackgroundNode.image = generateGradientImage(size: CGSize(width: 200.0, height: self.buttonHeight), colors: theme.backgroundColors, locations: locations, direction: .horizontal) + } else { + self.buttonBackgroundNode.backgroundColor = theme.backgroundColor + self.buttonBackgroundNode.image = nil + } + self.titleNode.attributedText = NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: theme.foregroundColor) self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: theme.foregroundColor) @@ -284,6 +330,9 @@ public final class SolidRoundedButtonNode: ASDisplayNode { let iconSize = self.iconNode.image?.size ?? CGSize() let titleSize = self.titleNode.updateLayout(buttonSize) + let spacingOffset: CGFloat = 9.0 + let verticalInset: CGFloat = self.subtitle == nil ? floor((buttonFrame.height - titleSize.height) / 2.0) : floor((buttonFrame.height - titleSize.height) / 2.0) - spacingOffset + let iconSpacing: CGFloat = self.iconSpacing var contentWidth: CGFloat = titleSize.width @@ -291,15 +340,25 @@ public final class SolidRoundedButtonNode: ASDisplayNode { contentWidth += iconSize.width + iconSpacing } var nextContentOrigin = floor((buttonFrame.width - contentWidth) / 2.0) - transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: floor((buttonFrame.height - iconSize.height) / 2.0)), size: iconSize)) - if !iconSize.width.isZero { - nextContentOrigin += iconSize.width + iconSpacing + + let iconFrame: CGRect + let titleFrame: CGRect + switch self.iconPosition { + case .left: + iconFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: floor((buttonFrame.height - iconSize.height) / 2.0)), size: iconSize) + if !iconSize.width.isZero { + nextContentOrigin += iconSize.width + iconSpacing + } + titleFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: buttonFrame.minY + verticalInset), size: titleSize) + case .right: + titleFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: buttonFrame.minY + verticalInset), size: titleSize) + if !iconSize.width.isZero { + nextContentOrigin += titleFrame.width + iconSpacing + } + iconFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: floor((buttonFrame.height - iconSize.height) / 2.0)), size: iconSize) } - let spacingOffset: CGFloat = 9.0 - let verticalInset: CGFloat = self.subtitle == nil ? floor((buttonFrame.height - titleSize.height) / 2.0) : floor((buttonFrame.height - titleSize.height) / 2.0) - spacingOffset - - let titleFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: buttonFrame.minY + verticalInset), size: titleSize) + transition.updateFrame(node: self.iconNode, frame: iconFrame) transition.updateFrame(node: self.titleNode, frame: titleFrame) if self.subtitle != self.subtitleNode.attributedText?.string { @@ -374,7 +433,7 @@ public final class SolidRoundedButtonView: UIView { private var font: SolidRoundedButtonFont private var fontSize: CGFloat - private let buttonBackgroundNode: UIView + private let buttonBackgroundNode: UIImageView private let buttonGlossView: SolidRoundedButtonGlossView? private let buttonNode: HighlightTrackingButton private let titleNode: ImmediateTextView @@ -418,6 +477,14 @@ public final class SolidRoundedButtonView: UIView { } } + public var iconPosition: SolidRoundedButtonIconPosition = .left { + didSet { + if let width = self.validLayout { + _ = self.updateLayout(width: width, transition: .immediate) + } + } + } + public init(title: String? = nil, icon: UIImage? = nil, theme: SolidRoundedButtonTheme, font: SolidRoundedButtonFont = .bold, fontSize: CGFloat = 17.0, height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0, gloss: Bool = false) { self.theme = theme self.font = font @@ -426,11 +493,23 @@ public final class SolidRoundedButtonView: UIView { self.buttonCornerRadius = cornerRadius self.title = title - self.buttonBackgroundNode = UIView() + self.buttonBackgroundNode = UIImageView() self.buttonBackgroundNode.clipsToBounds = true - self.buttonBackgroundNode.backgroundColor = theme.backgroundColor self.buttonBackgroundNode.layer.cornerRadius = cornerRadius + if theme.backgroundColors.count > 1 { + self.buttonBackgroundNode.backgroundColor = nil + + var locations: [CGFloat] = [] + let delta = 1.0 / CGFloat(theme.backgroundColors.count - 1) + for i in 0 ..< theme.backgroundColors.count { + locations.append(delta * CGFloat(i)) + } + self.buttonBackgroundNode.image = generateGradientImage(size: CGSize(width: 200.0, height: height), colors: theme.backgroundColors, locations: locations, direction: .horizontal) + } else { + self.buttonBackgroundNode.backgroundColor = theme.backgroundColor + } + if gloss { self.buttonGlossView = SolidRoundedButtonGlossView(color: theme.foregroundColor, cornerRadius: cornerRadius) } else { @@ -545,7 +624,20 @@ public final class SolidRoundedButtonView: UIView { } self.theme = theme - self.buttonBackgroundNode.backgroundColor = theme.backgroundColor + if theme.backgroundColors.count > 1 { + self.buttonBackgroundNode.backgroundColor = nil + + var locations: [CGFloat] = [] + let delta = 1.0 / CGFloat(theme.backgroundColors.count - 1) + for i in 0 ..< theme.backgroundColors.count { + locations.append(delta * CGFloat(i)) + } + self.buttonBackgroundNode.image = generateGradientImage(size: CGSize(width: 200.0, height: self.buttonHeight), colors: theme.backgroundColors, locations: locations, direction: .horizontal) + } else { + self.buttonBackgroundNode.backgroundColor = theme.backgroundColor + self.buttonBackgroundNode.image = nil + } + self.buttonGlossView?.color = theme.foregroundColor self.titleNode.attributedText = NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: theme.foregroundColor) self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: theme.foregroundColor) @@ -579,6 +671,8 @@ public final class SolidRoundedButtonView: UIView { let iconSize = self.iconNode.image?.size ?? CGSize() let titleSize = self.titleNode.updateLayout(buttonSize) + let spacingOffset: CGFloat = 9.0 + let verticalInset: CGFloat = self.subtitle == nil ? floor((buttonFrame.height - titleSize.height) / 2.0) : floor((buttonFrame.height - titleSize.height) / 2.0) - spacingOffset let iconSpacing: CGFloat = self.iconSpacing var contentWidth: CGFloat = titleSize.width @@ -586,15 +680,25 @@ public final class SolidRoundedButtonView: UIView { contentWidth += iconSize.width + iconSpacing } var nextContentOrigin = floor((buttonFrame.width - contentWidth) / 2.0) - transition.updateFrame(view: self.iconNode, frame: CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: floor((buttonFrame.height - iconSize.height) / 2.0)), size: iconSize)) - if !iconSize.width.isZero { - nextContentOrigin += iconSize.width + iconSpacing + + let iconFrame: CGRect + let titleFrame: CGRect + switch self.iconPosition { + case .left: + iconFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: floor((buttonFrame.height - iconSize.height) / 2.0)), size: iconSize) + if !iconSize.width.isZero { + nextContentOrigin += iconSize.width + iconSpacing + } + titleFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: buttonFrame.minY + verticalInset), size: titleSize) + case .right: + titleFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: buttonFrame.minY + verticalInset), size: titleSize) + if !iconSize.width.isZero { + nextContentOrigin += titleFrame.width + iconSpacing + } + iconFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: floor((buttonFrame.height - iconSize.height) / 2.0)), size: iconSize) } - let spacingOffset: CGFloat = 9.0 - let verticalInset: CGFloat = self.subtitle == nil ? floor((buttonFrame.height - titleSize.height) / 2.0) : floor((buttonFrame.height - titleSize.height) / 2.0) - spacingOffset - - let titleFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: buttonFrame.minY + verticalInset), size: titleSize) + transition.updateFrame(view: self.iconNode, frame: iconFrame) transition.updateFrame(view: self.titleNode, frame: titleFrame) if self.subtitle != self.subtitleNode.attributedText?.string { diff --git a/submodules/TelegramCore/Sources/State/ManagedConfigurationUpdates.swift b/submodules/TelegramCore/Sources/State/ManagedConfigurationUpdates.swift index bbe40b75f0..1010cab2d0 100644 --- a/submodules/TelegramCore/Sources/State/ManagedConfigurationUpdates.swift +++ b/submodules/TelegramCore/Sources/State/ManagedConfigurationUpdates.swift @@ -12,7 +12,7 @@ func managedConfigurationUpdates(accountManager: AccountManager mapToSignal { result -> Signal in return postbox.transaction { transaction -> Signal in switch result { - case let .config(flags, _, _, _, _, dcOptions, _, chatSizeMax, megagroupSizeMax, forwardedCountMax, _, _, _, _, _, _, _, _, savedGifsLimit, editTimeLimit, revokeTimeLimit, revokePmTimeLimit, _, stickersRecentLimit, _, _, _, pinnedDialogsCountMax, pinnedInfolderCountMax, _, _, _, _, _, autoupdateUrlPrefix, gifSearchUsername, venueSearchUsername, imgSearchUsername, _, captionLengthMax, _, webfileDcId, suggestedLangCode, langPackVersion, baseLangPackVersion): + case let .config(flags, _, _, _, _, dcOptions, _, chatSizeMax, megagroupSizeMax, forwardedCountMax, _, _, _, _, _, _, _, _, savedGifsLimit, editTimeLimit, revokeTimeLimit, revokePmTimeLimit, _, stickersRecentLimit, stickersFavedLimit, _, _, pinnedDialogsCountMax, pinnedInfolderCountMax, _, _, _, _, _, autoupdateUrlPrefix, gifSearchUsername, venueSearchUsername, imgSearchUsername, _, captionLengthMax, _, webfileDcId, suggestedLangCode, langPackVersion, baseLangPackVersion): var addressList: [Int: [MTDatacenterAddress]] = [:] for option in dcOptions { switch option { @@ -59,7 +59,7 @@ func managedConfigurationUpdates(accountManager: AccountManager> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 2.000000 0.864380 cm +0.000000 0.000000 0.000000 scn +20.000000 11.044711 m +20.000000 16.065481 15.522848 20.135620 10.000000 20.135620 c +4.477152 20.135620 0.000000 16.065481 0.000000 11.044711 c +0.000000 8.181209 1.337573 5.834022 3.613619 4.167677 c +3.904685 3.954580 4.172771 2.770550 3.523984 1.775995 c +2.875197 0.781441 2.066323 0.326941 2.471971 0.156790 c +2.722059 0.051889 4.199766 -0.000002 5.266314 0.598131 c +6.791368 1.453400 7.217727 2.304844 7.545889 2.229574 c +8.331102 2.049473 9.153261 1.953802 10.000000 1.953802 c +15.522848 1.953802 20.000000 6.023941 20.000000 11.044711 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 668 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000758 00000 n +0000000780 00000 n +0000000953 00000 n +0000001027 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1086 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/ContextX2.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/ContextX2.imageset/Contents.json new file mode 100644 index 0000000000..6fe332016d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/ContextX2.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Folder.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Folder.imageset/Contents.json new file mode 100644 index 0000000000..ef196420e9 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Folder.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "badgefolder_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Folder.imageset/badgefolder_24.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Folder.imageset/badgefolder_24.pdf new file mode 100644 index 0000000000..3a148d6e05 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Folder.imageset/badgefolder_24.pdf @@ -0,0 +1,108 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 2.500000 3.500000 cm +0.000000 0.000000 0.000000 scn +0.326980 15.361972 m +0.000000 14.720236 0.000000 13.880157 0.000000 12.199999 c +0.000000 12.130000 l +0.000000 11.849974 0.000000 11.709961 0.054497 11.603004 c +0.102433 11.508924 0.178924 11.432433 0.273005 11.384497 c +0.379961 11.330000 0.519973 11.330000 0.799999 11.330000 c +18.266001 11.330000 l +18.483961 11.330000 18.592939 11.330000 18.679672 11.363398 c +18.811571 11.414187 18.915813 11.518429 18.966602 11.650328 c +19.000000 11.737060 19.000000 11.846040 19.000000 12.064000 c +19.000000 12.935841 19.000000 13.371761 18.866409 13.718690 c +18.663250 14.246285 18.246286 14.663251 17.718691 14.866409 c +17.371761 15.000000 16.935841 15.000000 16.064001 15.000000 c +10.414214 15.000000 l +10.245598 15.000000 10.161290 15.000000 10.080059 15.004409 c +9.397568 15.041451 8.748186 15.310432 8.239400 15.766834 c +8.178847 15.821153 8.119237 15.880764 8.000020 15.999980 c +8.000000 16.000000 l +7.880771 16.119228 7.821156 16.178844 7.760600 16.233166 c +7.251813 16.689568 6.602432 16.958549 5.919941 16.995592 c +5.838710 17.000000 5.754402 17.000000 5.585786 17.000000 c +4.800000 17.000000 l +3.119843 17.000000 2.279764 17.000000 1.638029 16.673019 c +1.073542 16.385399 0.614601 15.926457 0.326980 15.361972 c +h +0.163490 9.180986 m +0.000000 8.860118 0.000000 8.440079 0.000000 7.600000 c +0.000000 6.400000 l +0.000000 4.159790 0.000000 3.039685 0.435974 2.184038 c +0.819467 1.431390 1.431390 0.819468 2.184038 0.435974 c +3.039685 0.000000 4.159790 0.000000 6.399999 0.000000 c +12.599999 0.000000 l +14.840210 0.000000 15.960315 0.000000 16.815962 0.435974 c +17.568609 0.819468 18.180532 1.431390 18.564026 2.184038 c +19.000000 3.039685 19.000000 4.159790 19.000000 6.400001 c +19.000000 7.600000 l +19.000000 8.440079 19.000000 8.860118 18.836510 9.180986 c +18.692699 9.463228 18.463228 9.692699 18.180986 9.836510 c +17.860119 10.000000 17.440079 10.000000 16.600000 10.000000 c +2.400000 10.000000 l +1.559921 10.000000 1.139882 10.000000 0.819014 9.836510 c +0.536771 9.692699 0.307300 9.463228 0.163490 9.180986 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 2139 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002229 00000 n +0000002252 00000 n +0000002425 00000 n +0000002499 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2558 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Group.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Group.imageset/Contents.json new file mode 100644 index 0000000000..f638604085 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Group.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "badgegroup_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Group.imageset/badgegroup_24.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Group.imageset/badgegroup_24.pdf new file mode 100644 index 0000000000..1d7c6be790 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Group.imageset/badgegroup_24.pdf @@ -0,0 +1,96 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 1.000000 3.500000 cm +0.000000 0.000000 0.000000 scn +7.798214 10.500000 m +9.731211 10.500000 11.298214 12.067003 11.298214 14.000000 c +11.298214 15.932997 9.731211 17.500000 7.798214 17.500000 c +5.865218 17.500000 4.298214 15.932997 4.298214 14.000000 c +4.298214 12.067003 5.865218 10.500000 7.798214 10.500000 c +h +15.800003 10.500000 m +17.456858 10.500000 18.800003 11.843145 18.800003 13.500000 c +18.800003 15.156855 17.456858 16.500000 15.800003 16.500000 c +14.143148 16.500000 12.800003 15.156855 12.800003 13.500000 c +12.800003 11.843145 14.143148 10.500000 15.800003 10.500000 c +h +12.332236 6.972357 m +13.807444 6.168993 14.733925 4.998592 15.315786 3.845357 c +15.642039 3.198733 15.667471 2.562775 15.476417 1.999998 c +15.084723 0.846209 13.783087 0.000000 12.298215 0.000000 c +3.298214 0.000000 l +1.089075 0.000000 -0.714483 1.873044 0.280643 3.845357 c +1.315064 5.895553 3.438652 8.000000 7.798214 8.000000 c +9.529486 8.000000 10.908134 7.668119 12.005980 7.139552 c +12.117610 7.085807 12.226336 7.030027 12.332236 6.972357 c +h +13.528140 7.807037 m +14.969814 6.871095 15.903218 5.633628 16.503208 4.444468 c +16.923948 3.610569 17.012365 2.769079 16.852047 1.999998 c +19.298889 1.999998 l +20.955744 1.999998 22.306608 3.401897 21.573879 4.887923 c +20.792742 6.472124 19.169998 8.117645 15.798890 8.117645 c +14.925295 8.117645 14.169111 8.007141 13.514557 7.815837 c +13.528140 7.807037 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1470 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001560 00000 n +0000001583 00000 n +0000001756 00000 n +0000001830 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1889 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Link.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Link.imageset/Contents.json new file mode 100644 index 0000000000..bb753b82fe --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Link.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "badgelink_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Link.imageset/badgelink_24.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Link.imageset/badgelink_24.pdf new file mode 100644 index 0000000000..5bf6b1068a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Link.imageset/badgelink_24.pdf @@ -0,0 +1,101 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 1.760010 0.617157 cm +0.000000 0.000000 0.000000 scn +11.862618 18.488392 m +13.375206 20.000980 15.827595 20.000980 17.340183 18.488392 c +18.852770 16.975805 18.852770 14.523417 17.340183 13.010829 c +15.042923 10.713570 l +13.530336 9.200982 11.077947 9.200982 9.565359 10.713570 c +9.363936 10.914992 9.189997 11.132182 9.042994 11.360771 c +8.744267 11.825293 8.125531 11.959696 7.661010 11.660969 c +7.196488 11.362242 7.062085 10.743505 7.360812 10.278984 c +7.584530 9.931103 7.848157 9.602345 8.151146 9.299356 c +10.444782 7.005720 14.163501 7.005720 16.457138 9.299356 c +18.754395 11.596616 l +21.048031 13.890251 21.048031 17.608971 18.754395 19.902607 c +16.460758 22.196241 12.742041 22.196241 10.448404 19.902607 c +8.151146 17.605347 l +7.760622 17.214823 7.760622 16.581657 8.151146 16.191133 c +8.541670 15.800610 9.174835 15.800610 9.565359 16.191133 c +11.862618 18.488392 l +h +8.612004 4.281246 m +7.099417 2.768658 4.647028 2.768658 3.134441 4.281246 c +1.621853 5.793833 1.621853 8.246222 3.134441 9.758809 c +5.431700 12.056068 l +6.944287 13.568655 9.396676 13.568655 10.909264 12.056068 c +11.110686 11.854645 11.284627 11.637455 11.431628 11.408867 c +11.730356 10.944345 12.349092 10.809942 12.813613 11.108670 c +13.278135 11.407397 13.412538 12.026133 13.113811 12.490655 c +12.890092 12.838536 12.626466 13.167293 12.323477 13.470282 c +10.029840 15.763918 6.311122 15.763918 4.017486 13.470282 c +1.720227 11.173022 l +-0.573409 8.879387 -0.573409 5.160667 1.720227 2.867031 c +4.013863 0.573397 7.732582 0.573397 10.026217 2.867031 c +12.323477 5.164291 l +12.714001 5.554815 12.714001 6.187981 12.323477 6.578505 c +11.932953 6.969028 11.299788 6.969028 10.909264 6.578505 c +8.612004 4.281246 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1772 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001862 00000 n +0000001885 00000 n +0000002058 00000 n +0000002132 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2191 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Pin.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Pin.imageset/Contents.json new file mode 100644 index 0000000000..99fcfa8ddf --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Pin.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "badgepin_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Pin.imageset/badgepin_24.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Pin.imageset/badgepin_24.pdf new file mode 100644 index 0000000000..8b3e329b6b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Pin.imageset/badgepin_24.pdf @@ -0,0 +1,108 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 2.398682 1.870453 cm +0.000000 0.000000 0.000000 scn +10.791250 19.146320 m +10.723915 18.838600 10.918332 18.158140 11.307166 16.797222 c +11.307172 16.797207 l +11.385918 16.521591 11.425291 16.383781 11.434118 16.251194 c +11.453884 15.954321 11.367108 15.660133 11.189390 15.421513 c +11.110017 15.314939 11.002151 15.220556 10.786422 15.031794 c +9.300981 13.732033 l +9.153894 13.603332 9.080351 13.538980 9.000536 13.488085 c +8.840524 13.386050 8.659379 13.321789 8.470838 13.300173 c +8.376793 13.289391 8.279138 13.293007 8.083828 13.300241 c +5.466913 13.397163 l +5.466902 13.397163 l +3.994872 13.451683 3.258856 13.478943 2.872828 13.296442 c +2.081267 12.922220 1.688092 12.020587 1.952483 11.185896 c +2.081421 10.778834 2.602221 10.258035 3.643819 9.216436 c +3.643824 9.216431 l +5.596159 7.264095 l +0.292893 1.960831 l +-0.097631 1.570305 -0.097631 0.937141 0.292893 0.546616 c +0.683418 0.156092 1.316582 0.156092 1.707107 0.546616 c +7.010372 5.849882 l +8.962662 3.897593 l +10.004263 2.855991 10.525064 2.335190 10.932127 2.206251 c +11.766818 1.941860 12.668451 2.335035 13.042673 3.126596 c +13.225174 3.512627 13.197914 4.248645 13.143394 5.720681 c +13.046472 8.337596 l +13.046472 8.337616 l +13.039238 8.532912 13.035621 8.630565 13.046403 8.724607 c +13.068019 8.913148 13.132281 9.094293 13.234317 9.254305 c +13.285213 9.334120 13.349563 9.407663 13.478264 9.554749 c +14.778025 11.040191 l +14.778039 11.040205 l +14.966792 11.255926 15.061172 11.363788 15.167744 11.443159 c +15.406365 11.620877 15.700552 11.707653 15.997424 11.687887 c +16.130014 11.679060 16.267828 11.639685 16.543453 11.560935 c +17.904371 11.172101 18.584831 10.977684 18.892551 11.045019 c +19.603607 11.200610 20.031862 11.928437 19.822556 12.625574 c +19.731974 12.927271 19.231562 13.427683 18.230738 14.428506 c +14.174737 18.484509 l +14.174723 18.484522 l +13.173909 19.485336 12.673501 19.985743 12.371805 20.076324 c +11.674668 20.285631 10.946841 19.857376 10.791250 19.146320 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 2030 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002120 00000 n +0000002143 00000 n +0000002316 00000 n +0000002390 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2449 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Tmp.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Tmp.imageset/Contents.json new file mode 100644 index 0000000000..96acb63cc3 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Tmp.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Tmp.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Tmp.imageset/Tmp.png b/submodules/TelegramUI/Images.xcassets/Premium/Tmp.imageset/Tmp.png new file mode 100644 index 0000000000..460cf4ce24 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Tmp.imageset/Tmp.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Tmp2.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Tmp2.imageset/Contents.json new file mode 100644 index 0000000000..b538d05a51 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Tmp2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Tmp2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Tmp2.imageset/Tmp2.png b/submodules/TelegramUI/Images.xcassets/Premium/Tmp2.imageset/Tmp2.png new file mode 100644 index 0000000000..bbf29bcd92 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Tmp2.imageset/Tmp2.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/X2.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/X2.imageset/Contents.json new file mode 100644 index 0000000000..fee27e3b2c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/X2.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "x2.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/X2.imageset/x2.pdf b/submodules/TelegramUI/Images.xcassets/Premium/X2.imageset/x2.pdf new file mode 100644 index 0000000000..3bfc938925 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/X2.imageset/x2.pdf @@ -0,0 +1,129 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.000000 0.000000 0.000000 scn +0.435974 15.815962 m +0.000000 14.960315 0.000000 13.840210 0.000000 11.600000 c +0.000000 6.400001 l +0.000000 4.159790 0.000000 3.039685 0.435974 2.184038 c +0.819467 1.431389 1.431390 0.819468 2.184038 0.435974 c +3.039685 0.000000 4.159790 0.000000 6.400000 0.000000 c +17.600000 0.000000 l +19.840210 0.000000 20.960316 0.000000 21.815962 0.435974 c +22.568611 0.819468 23.180532 1.431389 23.564026 2.184038 c +24.000000 3.039685 24.000000 4.159790 24.000000 6.400000 c +24.000000 11.600000 l +24.000000 13.840210 24.000000 14.960315 23.564026 15.815962 c +23.180532 16.568611 22.568611 17.180532 21.815962 17.564026 c +20.960316 18.000000 19.840210 18.000000 17.600000 18.000000 c +6.400001 18.000000 l +4.159790 18.000000 3.039685 18.000000 2.184038 17.564026 c +1.431390 17.180532 0.819467 16.568611 0.435974 15.815962 c +h +5.505859 4.375977 m +5.246094 4.000000 5.027344 3.870117 4.637695 3.870117 c +4.090820 3.870117 3.660156 4.266602 3.660156 4.765625 c +3.660156 5.018555 3.742188 5.237305 3.940430 5.510742 c +6.401367 8.942383 l +6.401367 8.997070 l +4.022461 12.237305 l +3.796875 12.538086 3.721680 12.763672 3.721680 13.030273 c +3.721680 13.590820 4.166016 14.000977 4.767578 14.000977 c +5.157227 14.000977 5.430664 13.830078 5.697266 13.426758 c +7.809570 10.309570 l +7.864258 10.309570 l +10.044922 13.495117 l +10.284180 13.850586 10.489258 14.000977 10.885742 14.000977 c +11.425781 14.000977 11.890625 13.611328 11.890625 13.119141 c +11.890625 12.852539 11.815430 12.633789 11.617188 12.380859 c +9.053711 8.956055 l +9.053711 8.908203 l +11.541992 5.551758 l +11.733398 5.305664 11.815430 5.073242 11.815430 4.799805 c +11.815430 4.259766 11.398438 3.870117 10.824219 3.870117 c +10.448242 3.870117 10.195312 4.020508 9.928711 4.382812 c +7.727539 7.452148 l +7.672852 7.452148 l +5.505859 4.375977 l +h +19.690430 4.000000 m +14.228516 4.000000 l +13.579102 4.000000 13.257812 4.362305 13.257812 4.881836 c +13.257812 5.271484 13.408203 5.531250 13.825195 5.914062 c +16.764648 8.723633 l +17.981445 9.892578 18.295898 10.371094 18.295898 11.082031 c +18.295898 11.895508 17.666992 12.476562 16.785156 12.476562 c +15.992188 12.476562 15.445312 12.080078 15.089844 11.287109 c +14.857422 10.876953 14.618164 10.685547 14.173828 10.685547 c +13.620117 10.685547 13.291992 11.013672 13.291992 11.526367 c +13.291992 11.683594 13.319336 11.827148 13.367188 11.970703 c +13.702148 13.064453 14.939453 14.083008 16.805664 14.083008 c +18.897461 14.083008 20.305664 12.907227 20.305664 11.211914 c +20.305664 10.008789 19.717773 9.256836 18.104492 7.732422 c +15.971680 5.681641 l +15.971680 5.640625 l +19.690430 5.640625 l +20.223633 5.640625 20.544922 5.319336 20.544922 4.820312 c +20.544922 4.334961 20.223633 4.000000 19.690430 4.000000 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 2835 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 18.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002925 00000 n +0000002948 00000 n +0000003121 00000 n +0000003195 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +3254 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 08d4527f84..31d3baff12 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -3358,7 +3358,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(false))).start(next: { [weak self] responded in if let strongSelf = self { if !responded { - strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: file, text: strongSelf.presentationData.strings.Conversation_InteractiveEmojiSyncTip(EnginePeer(peer).compactDisplayTitle).string), elevatedLayout: false, action: { _ in return false }), in: .current) + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: file, title: nil, text: strongSelf.presentationData.strings.Conversation_InteractiveEmojiSyncTip(EnginePeer(peer).compactDisplayTitle).string), elevatedLayout: false, action: { _ in return false }), in: .current) let _ = ApplicationSpecificNotice.incrementInteractiveEmojiSyncTip(accountManager: strongSelf.context.sharedContext.accountManager, timestamp: currentTimestamp).start() } @@ -7945,7 +7945,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G |> switchToLatest |> deliverOnMainQueue).start(next: { [weak self] added in if let strongSelf = self { - strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: stickerFile, text: added ? strongSelf.presentationData.strings.Conversation_StickerAddedToFavorites : strongSelf.presentationData.strings.Conversation_StickerRemovedFromFavorites), elevatedLayout: true, action: { _ in return false }), with: nil) +// strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: stickerFile, title: added ? "The Limit of 5 Stickers Reached" : nil, text: added ? "An older sticker was replaced with this one. You can [increase the limit]() to 10 stickers." : strongSelf.presentationData.strings.Conversation_StickerRemovedFromFavorites), elevatedLayout: true, action: { _ in return false }), with: nil) + strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: stickerFile, title: nil, text: added ? strongSelf.presentationData.strings.Conversation_StickerAddedToFavorites : strongSelf.presentationData.strings.Conversation_StickerRemovedFromFavorites), elevatedLayout: true, action: { _ in return false }), with: nil) } }) } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 95423a59db..3e7a12e904 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -618,9 +618,39 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState if let starStatus = data.starStatus { actions.append(.action(ContextMenuActionItem(text: starStatus ? chatPresentationInterfaceState.strings.Stickers_RemoveFromFavorites : chatPresentationInterfaceState.strings.Stickers_AddToFavorites, icon: { theme in return generateTintedImage(image: starStatus ? UIImage(bundleImageName: "Chat/Context Menu/Unfave") : UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - interfaceInteraction.toggleMessageStickerStarred(messages[0].id) - f(.default) + }, action: { c, f in +// interfaceInteraction.toggleMessageStickerStarred(messages[0].id) + + var subItems: [ContextMenuItem] = [] + + subItems.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Common_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) + }, action: { c, _ in + c.popItems() + }))) + subItems.append(.separator) + + subItems.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Stickers_FaveLimitReachedInfo("5", "10").string, textLayout: .multiline, textFont: .small, parseMarkdown: true, icon: { _ in + return nil + }, action: nil as ((ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void)?))) + + subItems.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Stickers_FaveLimitReplaceOlder, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Replace"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.default) + interfaceInteraction.toggleMessageStickerStarred(messages[0].id) + }))) + + subItems.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Stickers_FaveLimitIncrease, icon: { _ in + return UIImage(bundleImageName: "Premium/Tmp2") + }, action: { _, f in + f(.default) + + }))) + + c.pushItems(items: .single(ContextController.Items(content: .list(subItems)))) +// +// f(.default) }))) } diff --git a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift index 2113f92279..5a6c14d730 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift @@ -1087,7 +1087,7 @@ final class ChatMediaInputNode: ChatInputNode { return false } }) - strongSelf.controllerInteraction.presentController(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + strongSelf.controllerInteraction.presentController(controller, nil) }, getItemIsPreviewed: { item in return getItemIsPreviewedImpl?(item) ?? false }, openSearch: { diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index c905ebf4b8..7759e02e7f 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -2306,10 +2306,10 @@ final class PeerInfoHeaderNode: ASDisplayNode { context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage) let colorsArray: [CGColor] = [ - UIColor(rgb: 0x418eff).cgColor, - UIColor(rgb: 0x418eff).cgColor, - UIColor(rgb: 0xfc7ebd).cgColor, - UIColor(rgb: 0xfc7ebd).cgColor + UIColor(rgb: 0xa34ecf).cgColor, + UIColor(rgb: 0xa34ecf).cgColor, + UIColor(rgb: 0xff7923).cgColor, + UIColor(rgb: 0xff7923).cgColor ] var locations: [CGFloat] = [0.0, 0.35, 0.65, 1.0] let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)! diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 7d5c6ae3d4..78c73bde49 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -16,8 +16,6 @@ import DatePickerNode import DebugSettingsUI import TabBarUI -import TelegramCallsUI - public final class TelegramRootController: NavigationController { private let context: AccountContext diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index b3e52fd0a5..b6b2cb8a69 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -34,7 +34,7 @@ public enum UndoOverlayContent { case voiceChatRecording(text: String) case voiceChatFlag(text: String) case voiceChatCanSpeak(text: String) - case sticker(context: AccountContext, file: TelegramMediaFile, text: String) + case sticker(context: AccountContext, file: TelegramMediaFile, title: String?, text: String) case copy(text: String) case mediaSaved(text: String) case paymentSent(currencyValue: String, itemTitle: String) diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index dd6df51216..87281dba91 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -408,7 +408,8 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) - let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural) + let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return nil }), textAlignment: .natural) self.textNode.attributedText = attributedText self.textNode.maximumNumberOfLines = 2 displayUndo = undo @@ -626,7 +627,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { displayUndo = false self.originalRemainingSeconds = 3 - case let .sticker(context, file, text): + case let .sticker(context, file, title, text): self.avatarNode = nil self.iconNode = nil self.iconCheckNode = nil @@ -680,10 +681,19 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { updatedImageSignal = .single({ _ in return nil }) updatedFetchSignal = .complete() } + + if let title = title { + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + } else { + self.titleNode.attributedText = nil + } let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) - let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural) + let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { contents in + return ("URL", contents) + }), textAlignment: .natural) self.textNode.attributedText = attributedText self.textNode.maximumNumberOfLines = 2