diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 4517922891..2ea56abc86 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -10637,3 +10637,50 @@ Sorry for the inconvenience."; "BoostGift.WinnersInfo" = "Choose whether to make the list of winners public when the giveaway ends."; "Story.ViewList.TitleReactions" = "Reactions"; + +"Wallpaper.ApplyForChannel" = "Apply For This Channel"; + +"Notification.ChannelChangedWallpaper" = "Channel set a new wallpaper"; + +"Chat.Giveaway.Message.WinnersSelectedTitle.One" = "Winner Selected!"; +"Chat.Giveaway.Message.WinnersSelectedTitle.Many" = "Winners Selected!"; +"Chat.Giveaway.Message.WinnersSelectedText_1" = "**%@** winner of the [Giveaway]() was randomly selected by Telegram."; +"Chat.Giveaway.Message.WinnersSelectedText_any" = "**%@** winners of the [Giveaway]() were randomly selected by Telegram."; + +"Chat.Giveaway.Message.WinnersTitle.One" = "Winner"; +"Chat.Giveaway.Message.WinnersTitle.Many" = "Winners"; + +"Chat.Giveaway.Message.WinnersMore_1" = "and %@ more!"; +"Chat.Giveaway.Message.WinnersMore_any" = "and %@ more!"; + +"Chat.Giveaway.Message.WinnersInfo.One" = "The winner received their gift link in a private message."; +"Chat.Giveaway.Message.WinnersInfo.Many" = "All winners received gift links in private messages."; + +"ChannelBoost.Wallpaper" = "Set Wallpaper"; +"ChannelBoost.WallpaperText" = "Your channel needs **Level %1$@** to set custom wallpaper.\n\nAsk your **Premium** subscribers to boost your channel with this link:"; + +"Settings.PremiumGift" = "Premium Gifting"; + +"Story.ViewList.ContextSortChannelInfo" = "Choose the order for the list of reactions."; + +"Premium.Gift.MultipleDescription" = "Get **%1$@**%2$@ access to exclusive features with **Telegram Premium**."; + +"Premium.Gift.NamesAndMore_1" = " and %@ more"; +"Premium.Gift.NamesAndMore_any" = " and %@ more"; + +"Premium.Gift.YouWillReceiveBoosts_1" = "You will receive []() **%@ Boost**!"; +"Premium.Gift.YouWillReceiveBoosts_any" = "You will receive []() **%@ Boosts**!"; + +"Premium.Gift.ContactSelection.Title" = "Gift Premium"; +"Premium.Gift.ContactSelection.Placeholder" = "Search people to gift Premium to..."; +"Premium.Gift.ContactSelection.Proceed" = "Proceed"; +"Premium.Gift.ContactSelection.FrequentContacts" = "FREQUENT CONTACTS"; +"Premium.Gift.ContactSelection.MaximumReached" = "You can select up to %@ users."; + +"Premium.Gift.GiftMultipleSubscriptionsFormat" = "%1$@ for %2$@"; +"Premium.Gift.GiftMultipleSubscriptions_1" = "Gift %@ Subscription"; +"Premium.Gift.GiftMultipleSubscriptions_any" = "Gift %@ Subscriptions"; + +"Message.GiveawayEndedWinners_1" = "%@ winner of the giveaway was randomly selected by Telegram"; +"Message.GiveawayEndedWinners_any" = "%@ winners of the giveaway was randomly selected by Telegram"; +"Message.GiveawayEndedNoWinners" = "Due to the giveaway terms, no winners could be selected by Telegram"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index dde2bbe7ca..e2c2fdc8db 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1101,7 +1101,7 @@ public protocol AccountContext: AnyObject { public struct PremiumConfiguration { public static var defaultValue: PremiumConfiguration { - return PremiumConfiguration(isPremiumDisabled: false, showPremiumGiftInAttachMenu: false, showPremiumGiftInTextField: false, giveawayGiftsPurchaseAvailable: false, boostsPerGiftCount: 3, minChannelNameColorLevel: 5, audioTransciptionTrialMaxDuration: 300, audioTransciptionTrialCount: 2) + return PremiumConfiguration(isPremiumDisabled: false, showPremiumGiftInAttachMenu: false, showPremiumGiftInTextField: false, giveawayGiftsPurchaseAvailable: false, boostsPerGiftCount: 3, minChannelNameColorLevel: 5, audioTransciptionTrialMaxDuration: 300, audioTransciptionTrialCount: 2, minChannelWallpaperLevel: 5) } public let isPremiumDisabled: Bool @@ -1112,8 +1112,9 @@ public struct PremiumConfiguration { public let minChannelNameColorLevel: Int32 public let audioTransciptionTrialMaxDuration: Int32 public let audioTransciptionTrialCount: Int32 + public let minChannelWallpaperLevel: Int32 - fileprivate init(isPremiumDisabled: Bool, showPremiumGiftInAttachMenu: Bool, showPremiumGiftInTextField: Bool, giveawayGiftsPurchaseAvailable: Bool, boostsPerGiftCount: Int32, minChannelNameColorLevel: Int32, audioTransciptionTrialMaxDuration: Int32, audioTransciptionTrialCount: Int32) { + fileprivate init(isPremiumDisabled: Bool, showPremiumGiftInAttachMenu: Bool, showPremiumGiftInTextField: Bool, giveawayGiftsPurchaseAvailable: Bool, boostsPerGiftCount: Int32, minChannelNameColorLevel: Int32, audioTransciptionTrialMaxDuration: Int32, audioTransciptionTrialCount: Int32, minChannelWallpaperLevel: Int32) { self.isPremiumDisabled = isPremiumDisabled self.showPremiumGiftInAttachMenu = showPremiumGiftInAttachMenu self.showPremiumGiftInTextField = showPremiumGiftInTextField @@ -1122,6 +1123,7 @@ public struct PremiumConfiguration { self.minChannelNameColorLevel = minChannelNameColorLevel self.audioTransciptionTrialMaxDuration = audioTransciptionTrialMaxDuration self.audioTransciptionTrialCount = audioTransciptionTrialCount + self.minChannelWallpaperLevel = minChannelWallpaperLevel } public static func with(appConfiguration: AppConfiguration) -> PremiumConfiguration { @@ -1134,7 +1136,8 @@ public struct PremiumConfiguration { boostsPerGiftCount: Int32(data["boosts_per_sent_gift"] as? Double ?? 3), minChannelNameColorLevel: Int32(data["channel_color_level_min"] as? Double ?? 5), audioTransciptionTrialMaxDuration: Int32(data["transcribe_audio_trial_duration_max"] as? Double ?? 300), - audioTransciptionTrialCount: Int32(data["transcribe_audio_trial_weekly_number"] as? Double ?? 2) + audioTransciptionTrialCount: Int32(data["transcribe_audio_trial_weekly_number"] as? Double ?? 2), + minChannelWallpaperLevel: Int32(data["channel_wallpaper_level_min"] as? Double ?? 5) ) } else { return .defaultValue diff --git a/submodules/AccountContext/Sources/ContactMultiselectionController.swift b/submodules/AccountContext/Sources/ContactMultiselectionController.swift index f1bb36cc4e..84ac96014b 100644 --- a/submodules/AccountContext/Sources/ContactMultiselectionController.swift +++ b/submodules/AccountContext/Sources/ContactMultiselectionController.swift @@ -69,6 +69,7 @@ public enum ContactMultiselectionControllerMode { case peerSelection(searchChatList: Bool, searchGroups: Bool, searchChannels: Bool) case channelCreation case chatSelection(ChatSelection) + case premiumGifting } public enum ContactListFilter { diff --git a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift index 4778982f7b..20df040754 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift @@ -311,6 +311,12 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: } else { messageText = strings.Message_GiveawayStarted } + case let results as TelegramMediaGiveawayResults: + if results.winnersCount == 0 { + messageText = strings.Message_GiveawayEndedNoWinners + } else { + messageText = strings.Message_GiveawayEndedWinners(results.winnersCount) + } case let webpage as TelegramMediaWebpage: if messageText.isEmpty, case let .Loaded(content) = webpage.content { messageText = content.displayUrl diff --git a/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift b/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift index 3f51ef98bf..32e0c7edfe 100644 --- a/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift +++ b/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift @@ -11,16 +11,18 @@ public final class RoundedRectangle: Component { public let cornerRadius: CGFloat public let gradientDirection: GradientDirection public let stroke: CGFloat? + public let strokeColor: UIColor? - public convenience init(color: UIColor, cornerRadius: CGFloat, stroke: CGFloat? = nil) { - self.init(colors: [color], cornerRadius: cornerRadius, stroke: stroke) + public convenience init(color: UIColor, cornerRadius: CGFloat, stroke: CGFloat? = nil, strokeColor: UIColor? = nil) { + self.init(colors: [color], cornerRadius: cornerRadius, stroke: stroke, strokeColor: strokeColor) } - public init(colors: [UIColor], cornerRadius: CGFloat, gradientDirection: GradientDirection = .horizontal, stroke: CGFloat? = nil) { + public init(colors: [UIColor], cornerRadius: CGFloat, gradientDirection: GradientDirection = .horizontal, stroke: CGFloat? = nil, strokeColor: UIColor? = nil) { self.colors = colors self.cornerRadius = cornerRadius self.gradientDirection = gradientDirection self.stroke = stroke + self.strokeColor = strokeColor } public static func ==(lhs: RoundedRectangle, rhs: RoundedRectangle) -> Bool { @@ -36,6 +38,9 @@ public final class RoundedRectangle: Component { if lhs.stroke != rhs.stroke { return false } + if lhs.strokeColor != rhs.strokeColor { + return false + } return true } @@ -48,11 +53,19 @@ public final class RoundedRectangle: Component { let imageSize = CGSize(width: max(component.stroke ?? 0.0, component.cornerRadius) * 2.0, height: max(component.stroke ?? 0.0, component.cornerRadius) * 2.0) UIGraphicsBeginImageContextWithOptions(imageSize, false, 0.0) if let context = UIGraphicsGetCurrentContext() { - context.setFillColor(color.cgColor) + if let strokeColor = component.strokeColor { + context.setFillColor(strokeColor.cgColor) + } else { + context.setFillColor(color.cgColor) + } context.fillEllipse(in: CGRect(origin: CGPoint(), size: imageSize)) if let stroke = component.stroke, stroke > 0.0 { - context.setBlendMode(.clear) + if let _ = component.strokeColor { + context.setFillColor(color.cgColor) + } else { + context.setBlendMode(.clear) + } context.fillEllipse(in: CGRect(origin: CGPoint(), size: imageSize).insetBy(dx: stroke, dy: stroke)) } } diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index d1cbb233b5..e0191f68d5 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -358,7 +358,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { } } -private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactListPeer], presences: [EnginePeer.Id: EnginePeer.Presence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, disabledPeerIds: Set, authorizationStatus: AccessType, warningSuppressed: (Bool, Bool), displaySortOptions: Bool, displayCallIcons: Bool, storySubscriptions: EngineStorySubscriptions?) -> [ContactListNodeEntry] { +private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactListPeer], presences: [EnginePeer.Id: EnginePeer.Presence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, disabledPeerIds: Set, authorizationStatus: AccessType, warningSuppressed: (Bool, Bool), displaySortOptions: Bool, displayCallIcons: Bool, storySubscriptions: EngineStorySubscriptions?, topPeers: [EnginePeer]) -> [ContactListNodeEntry] { var entries: [ContactListNodeEntry] = [] var commonHeader: ListViewItemHeader? @@ -421,7 +421,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis for i in 0 ..< options.count { entries.append(.option(i, options[i], commonHeader, theme, strings)) } - case let .natural(options, _): + case let .natural(options, _, _): let sortedPeers = peers.sorted(by: { lhs, rhs in let result = EnginePeer.IndexName(lhs.indexName).isLessThan(other: EnginePeer.IndexName(rhs.indexName), ordering: sortOrder) if result == .orderedSame { @@ -513,6 +513,27 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis } } + var existingPeerIds = Set() + if !topPeers.isEmpty { + let header: ListViewItemHeader? = ChatListSearchItemHeader(type: .text(strings.Premium_Gift_ContactSelection_FrequentContacts, AnyHashable(0)), theme: theme, strings: strings) + + var index: Int = 0 + for peer in topPeers.prefix(15) { + existingPeerIds.insert(.peer(peer.id)) + + let selection: ContactsPeerItemSelection + if let selectionState = selectionState { + selection = .selectable(selected: selectionState.selectedPeerIndices[.peer(peer.id)] != nil) + } else { + selection = .none + } + + let presence = presences[peer.id] + entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, true, nil)) + + index += 1 + } + } if let storySubscriptions { let _ = storySubscriptions @@ -528,7 +549,6 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis var index: Int = 0 - var existingPeerIds = Set() if let selectionState = selectionState { for peer in selectionState.foundPeers { if existingPeerIds.contains(peer.id) { @@ -657,7 +677,7 @@ private struct ContactsListNodeTransition { public enum ContactListPresentation { case orderedByPresence(options: [ContactListAdditionalOption]) - case natural(options: [ContactListAdditionalOption], includeChatList: Bool) + case natural(options: [ContactListAdditionalOption], includeChatList: Bool, topPeers: Bool) case search(signal: Signal, searchChatList: Bool, searchDeviceContacts: Bool, searchGroups: Bool, searchChannels: Bool, globalSearch: Bool) public var sortOrder: ContactsSortOrder? { @@ -983,9 +1003,11 @@ public final class ContactListNode: ASDisplayNode { |> mapToSignal { presentation in var generateSections = false var includeChatList = false - if case let .natural(_, includeChatListValue) = presentation { + var displayTopPeers = false + if case let .natural(_, includeChatListValue, displayTopPeersValue) = presentation { generateSections = true includeChatList = includeChatListValue + displayTopPeers = displayTopPeersValue } if case let .search(query, searchChatList, searchDeviceContacts, searchGroups, searchChannels, globalSearch) = presentation { @@ -1214,7 +1236,7 @@ public final class ContactListNode: ASDisplayNode { peers.append(.deviceContact(stableId, contact.0)) } - let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, authorizationStatus: .allowed, warningSuppressed: (true, true), displaySortOptions: false, displayCallIcons: displayCallIcons, storySubscriptions: nil) + let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, authorizationStatus: .allowed, warningSuppressed: (true, true), displaySortOptions: false, displayCallIcons: displayCallIcons, storySubscriptions: nil, topPeers: []) let previous = previousEntries.swap(entries) return .single(preparedContactListNodeTransition(context: context, presentationData: presentationData, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, isEmpty: false, generateIndexSections: generateSections, animation: .none, isSearch: isSearch)) } @@ -1274,8 +1296,24 @@ public final class ContactListNode: ASDisplayNode { chatListSignal = .single([]) } - return (combineLatest(self.contactPeersViewPromise.get(), chatListSignal, selectionStateSignal, presentationDataPromise.get(), contactsAuthorization.get(), contactsWarningSuppressed.get(), self.storySubscriptions.get()) - |> mapToQueue { view, chatListPeers, selectionState, presentationData, authorizationStatus, warningSuppressed, storySubscriptions -> Signal in + let recentPeers: Signal + if displayTopPeers { + recentPeers = context.engine.peers.recentPeers() + } else { + recentPeers = .single(.disabled) + } + + return (combineLatest( + self.contactPeersViewPromise.get(), + chatListSignal, + selectionStateSignal, + presentationDataPromise.get(), + contactsAuthorization.get(), + contactsWarningSuppressed.get(), + self.storySubscriptions.get(), + recentPeers + ) + |> mapToQueue { view, chatListPeers, selectionState, presentationData, authorizationStatus, warningSuppressed, storySubscriptions, recentPeers -> Signal in let signal = deferred { () -> Signal in var peers = view.0.peers.map({ ContactListPeer.peer(peer: $0._asPeer(), isGlobal: false, participantCount: nil) }) for (peer, memberCount) in chatListPeers { @@ -1312,11 +1350,16 @@ public final class ContactListNode: ASDisplayNode { } } + var topPeers: [EnginePeer] = [] + if case let .peers(peers) = recentPeers { + topPeers = peers.map(EnginePeer.init) + } + var isEmpty = false if (authorizationStatus == .notDetermined || authorizationStatus == .denied) && peers.isEmpty { isEmpty = true } - let entries = contactListNodeEntries(accountPeer: view.1, peers: peers, presences: view.0.presences, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, authorizationStatus: authorizationStatus, warningSuppressed: warningSuppressed, displaySortOptions: displaySortOptions, displayCallIcons: displayCallIcons, storySubscriptions: storySubscriptions) + let entries = contactListNodeEntries(accountPeer: view.1, peers: peers, presences: view.0.presences, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, authorizationStatus: authorizationStatus, warningSuppressed: warningSuppressed, displaySortOptions: displaySortOptions, displayCallIcons: displayCallIcons, storySubscriptions: storySubscriptions, topPeers: topPeers) let previous = previousEntries.swap(entries) let previousSelection = previousSelectionState.swap(selectionState) diff --git a/submodules/ContactListUI/Sources/ContactsControllerNode.swift b/submodules/ContactListUI/Sources/ContactsControllerNode.swift index ade1b4a403..5be7b54cec 100644 --- a/submodules/ContactListUI/Sources/ContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsControllerNode.swift @@ -107,7 +107,7 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { case .presence: return .orderedByPresence(options: options) case .natural: - return .natural(options: options, includeChatList: false) + return .natural(options: options, includeChatList: false, topPeers: false) } } diff --git a/submodules/PremiumUI/BUILD b/submodules/PremiumUI/BUILD index b6cff081e2..83a13b2ba8 100644 --- a/submodules/PremiumUI/BUILD +++ b/submodules/PremiumUI/BUILD @@ -110,6 +110,7 @@ swift_library( "//submodules/TelegramUI/Components/Stories/PeerListItemComponent", "//submodules/InvisibleInkDustNode", "//submodules/AlertUI", + "//submodules/TelegramUI/Components/Chat/MergedAvatarsNode", ], visibility = [ "//visibility:public", diff --git a/submodules/PremiumUI/Sources/GiftAvatarComponent.swift b/submodules/PremiumUI/Sources/GiftAvatarComponent.swift index 094a06bee5..5a21c535cd 100644 --- a/submodules/PremiumUI/Sources/GiftAvatarComponent.swift +++ b/submodules/PremiumUI/Sources/GiftAvatarComponent.swift @@ -10,24 +10,29 @@ import LegacyComponents import AvatarNode import AccountContext import TelegramCore +import MergedAvatarsNode +import MultilineTextComponent +import TelegramPresentationData private let sceneVersion: Int = 3 class GiftAvatarComponent: Component { let context: AccountContext - let peer: EnginePeer? + let theme: PresentationTheme + let peers: [EnginePeer] let isVisible: Bool let hasIdleAnimations: Bool - init(context: AccountContext, peer: EnginePeer?, isVisible: Bool, hasIdleAnimations: Bool) { + init(context: AccountContext, theme: PresentationTheme, peers: [EnginePeer], isVisible: Bool, hasIdleAnimations: Bool) { self.context = context - self.peer = peer + self.theme = theme + self.peers = peers self.isVisible = isVisible self.hasIdleAnimations = hasIdleAnimations } static func ==(lhs: GiftAvatarComponent, rhs: GiftAvatarComponent) -> Bool { - return lhs.peer == rhs.peer && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations + return lhs.peers == rhs.peers && lhs.theme === rhs.theme && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations } final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView { @@ -52,6 +57,10 @@ class GiftAvatarComponent: Component { private let sceneView: SCNView private let avatarNode: ImageNode + private var mergedAvatarsNode: MergedAvatarsNode? + + private let badgeBackground = ComponentView() + private let badge = ComponentView() private var previousInteractionTimestamp: Double = 0.0 private var timer: SwiftSignalKit.Timer? @@ -242,11 +251,75 @@ class GiftAvatarComponent: Component { } self.hasIdleAnimations = component.hasIdleAnimations - let avatarSize = CGSize(width: 100.0, height: 100.0) - if let peer = component.peer { - self.avatarNode.setSignal(peerAvatarCompleteImage(account: component.context.account, peer: peer, size: avatarSize, font: avatarPlaceholderFont(size: 43.0), fullSize: true)) + + if component.peers.count > 1 { + let avatarSize = CGSize(width: 60.0, height: 60.0) + + let mergedAvatarsNode: MergedAvatarsNode + if let current = self.mergedAvatarsNode { + mergedAvatarsNode = current + } else { + mergedAvatarsNode = MergedAvatarsNode() + mergedAvatarsNode.isUserInteractionEnabled = false + self.addSubview(mergedAvatarsNode.view) + self.mergedAvatarsNode = mergedAvatarsNode + } + + mergedAvatarsNode.update(context: component.context, peers: Array(component.peers.map { $0._asPeer() }.prefix(3)), synchronousLoad: false, imageSize: avatarSize.width, imageSpacing: 30.0, borderWidth: 2.0, avatarFontSize: 26.0) + let avatarsSize = CGSize(width: avatarSize.width + 60.0, height: avatarSize.height) + mergedAvatarsNode.updateLayout(size: avatarsSize) + mergedAvatarsNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - avatarsSize.width) / 2.0), y: 113.0 - avatarSize.height / 2.0), size: avatarsSize) + self.avatarNode.isHidden = true + } else { + self.mergedAvatarsNode?.view.removeFromSuperview() + self.mergedAvatarsNode = nil + self.avatarNode.isHidden = false + + let avatarSize = CGSize(width: 100.0, height: 100.0) + if let peer = component.peers.first { + self.avatarNode.setSignal(peerAvatarCompleteImage(account: component.context.account, peer: peer, size: avatarSize, font: avatarPlaceholderFont(size: 43.0), fullSize: true)) + } + self.avatarNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - avatarSize.width) / 2.0), y: 113.0 - avatarSize.height / 2.0), size: avatarSize) + } + + if component.peers.count > 3 { + let badgeTextSize = self.badge.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: "+\(component.peers.count - 3)", font: Font.with(size: 10.0, design: .round, weight: .semibold), textColor: component.theme.list.itemCheckColors.foregroundColor)) + ) + ), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + let lineWidth = 1.0 + UIScreenPixel + let badgeSize = CGSize(width: max(17.0, badgeTextSize.width + 7.0) + lineWidth * 2.0, height: 17.0 + lineWidth * 2.0) + let _ = self.badgeBackground.update( + transition: .immediate, + component: AnyComponent( + RoundedRectangle(color: component.theme.list.itemCheckColors.fillColor, cornerRadius: badgeSize.height / 2.0, stroke: lineWidth, strokeColor: component.theme.list.blocksBackgroundColor) + ), + environment: {}, + containerSize: badgeSize + ) + + if let badgeTextView = self.badge.view, let badgeBackgroundView = self.badgeBackground.view { + if badgeBackgroundView.superview == nil { + self.addSubview(badgeBackgroundView) + self.addSubview(badgeTextView) + } + + let avatarsSize = CGSize(width: 120.0, height: 60.0) + let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width + avatarsSize.width) / 2.0) - 19.0 - lineWidth, y: 113.0 + avatarsSize.height / 2.0 - 15.0 - lineWidth), size: badgeSize) + badgeBackgroundView.frame = backgroundFrame + badgeTextView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - badgeTextSize.width / 2.0), y: floorToScreenPixels(backgroundFrame.midY - badgeTextSize.height / 2.0) - UIScreenPixel), size: badgeTextSize) + } + } else { + self.badge.view?.removeFromSuperview() + self.badgeBackground.view?.removeFromSuperview() } - self.avatarNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - avatarSize.width) / 2.0), y: 63.0), size: avatarSize) return availableSize } diff --git a/submodules/PremiumUI/Sources/GiveawayInfoController.swift b/submodules/PremiumUI/Sources/GiveawayInfoController.swift index 2b5c5b776f..0fe808602a 100644 --- a/submodules/PremiumUI/Sources/GiveawayInfoController.swift +++ b/submodules/PremiumUI/Sources/GiveawayInfoController.swift @@ -31,19 +31,66 @@ public func presentGiveawayInfoController( guard let message else { return } - guard let giveaway = message.media.first(where: { $0 is TelegramMediaGiveaway }) as? TelegramMediaGiveaway else { - return + + let giveaway = message.media.first(where: { $0 is TelegramMediaGiveaway }) as? TelegramMediaGiveaway + let giveawayResults = message.media.first(where: { $0 is TelegramMediaGiveawayResults }) as? TelegramMediaGiveawayResults + + var channelPeerId: EnginePeer.Id? + if let giveaway { + if let peerId = giveaway.channelPeerIds.first { + channelPeerId = peerId + } + } else if let _ = giveawayResults { + channelPeerId = message.author?.id + } + + var quantity: Int32 = 0 + if let giveaway { + quantity = giveaway.quantity + } else if let giveawayResults { + quantity = giveawayResults.winnersCount + giveawayResults.unclaimedCount + } + + var months: Int32 = 0 + if let giveaway { + months = giveaway.months + } else if let giveawayResults { + months = giveawayResults.months + } + + var prizeDescription: String? + if let giveaway { + prizeDescription = giveaway.prizeDescription + } else if let giveawayResults { + prizeDescription = giveawayResults.prizeDescription + } + + var untilDateValue: Int32 = 0 + if let giveaway { + untilDateValue = giveaway.untilDate + } else if let _ = giveawayResults { + untilDateValue = message.timestamp + } + + var onlyNewSubscribers = false + if let giveaway, giveaway.flags.contains(.onlyNewSubscribers) { + onlyNewSubscribers = true + } + + var channelsCount = 1 + if let giveaway { + channelsCount = giveaway.channelPeerIds.count } let presentationData = context.sharedContext.currentPresentationData.with { $0 } var peerName = "" - if let peerId = giveaway.channelPeerIds.first, let peer = message.peers[peerId] { + if let peerId = channelPeerId, let peer = message.peers[peerId] { peerName = EnginePeer(peer).compactDisplayTitle } let timeZone = TimeZone.current - let untilDate = stringForDate(timestamp: giveaway.untilDate, timeZone: timeZone, strings: presentationData.strings) + let untilDate = stringForDate(timestamp: untilDateValue, timeZone: timeZone, strings: presentationData.strings) let title: String let text: String @@ -56,8 +103,8 @@ public func presentGiveawayInfoController( })] var additionalPrizes = "" - if let prizeDescription = giveaway.prizeDescription, !prizeDescription.isEmpty { - additionalPrizes = "\n\n" + presentationData.strings.Chat_Giveaway_Info_AdditionalPrizes(peerName, "\(giveaway.quantity) \(prizeDescription)").string + if let prizeDescription, !prizeDescription.isEmpty { + additionalPrizes = "\n\n" + presentationData.strings.Chat_Giveaway_Info_AdditionalPrizes(peerName, "\(quantity) \(prizeDescription)").string } switch giveawayInfo { @@ -71,23 +118,23 @@ public func presentGiveawayInfoController( let intro: String if case .almostOver = status { - intro = presentationData.strings.Chat_Giveaway_Info_EndedIntro(peerName, presentationData.strings.Chat_Giveaway_Info_Subscriptions(giveaway.quantity), presentationData.strings.Chat_Giveaway_Info_Months(giveaway.months)).string + intro = presentationData.strings.Chat_Giveaway_Info_EndedIntro(peerName, presentationData.strings.Chat_Giveaway_Info_Subscriptions(quantity), presentationData.strings.Chat_Giveaway_Info_Months(months)).string } else { - intro = presentationData.strings.Chat_Giveaway_Info_OngoingIntro(peerName, presentationData.strings.Chat_Giveaway_Info_Subscriptions(giveaway.quantity), presentationData.strings.Chat_Giveaway_Info_Months(giveaway.months)).string + intro = presentationData.strings.Chat_Giveaway_Info_OngoingIntro(peerName, presentationData.strings.Chat_Giveaway_Info_Subscriptions(quantity), presentationData.strings.Chat_Giveaway_Info_Months(months)).string } let ending: String - if giveaway.flags.contains(.onlyNewSubscribers) { - let randomUsers = presentationData.strings.Chat_Giveaway_Info_RandomUsers(giveaway.quantity) - if giveaway.channelPeerIds.count > 1 { - ending = presentationData.strings.Chat_Giveaway_Info_OngoingNewMany(untilDate, randomUsers, peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(giveaway.channelPeerIds.count - 1)), startDate).string + if onlyNewSubscribers { + let randomUsers = presentationData.strings.Chat_Giveaway_Info_RandomUsers(quantity) + if channelsCount > 1 { + ending = presentationData.strings.Chat_Giveaway_Info_OngoingNewMany(untilDate, randomUsers, peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(channelsCount - 1)), startDate).string } else { ending = presentationData.strings.Chat_Giveaway_Info_OngoingNew(untilDate, randomUsers, peerName, startDate).string } } else { - let randomSubscribers = presentationData.strings.Chat_Giveaway_Info_RandomSubscribers(giveaway.quantity) - if giveaway.channelPeerIds.count > 1 { - ending = presentationData.strings.Chat_Giveaway_Info_OngoingMany(untilDate, randomSubscribers, peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(giveaway.channelPeerIds.count - 1))).string + let randomSubscribers = presentationData.strings.Chat_Giveaway_Info_RandomSubscribers(quantity) + if channelsCount > 1 { + ending = presentationData.strings.Chat_Giveaway_Info_OngoingMany(untilDate, randomSubscribers, peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(channelsCount - 1))).string } else { ending = presentationData.strings.Chat_Giveaway_Info_Ongoing(untilDate, randomSubscribers, peerName).string } @@ -96,8 +143,8 @@ public func presentGiveawayInfoController( var participation: String switch status { case .notQualified: - if giveaway.channelPeerIds.count > 1 { - participation = presentationData.strings.Chat_Giveaway_Info_NotQualifiedMany(peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(giveaway.channelPeerIds.count - 1)), untilDate).string + if channelsCount > 1 { + participation = presentationData.strings.Chat_Giveaway_Info_NotQualifiedMany(peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(channelsCount - 1)), untilDate).string } else { participation = presentationData.strings.Chat_Giveaway_Info_NotQualified(peerName, untilDate).string } @@ -116,8 +163,8 @@ public func presentGiveawayInfoController( participation = presentationData.strings.Chat_Giveaway_Info_NotAllowedCountry } case .participating: - if giveaway.channelPeerIds.count > 1 { - participation = presentationData.strings.Chat_Giveaway_Info_ParticipatingMany(peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(giveaway.channelPeerIds.count - 1))).string + if channelsCount > 1 { + participation = presentationData.strings.Chat_Giveaway_Info_ParticipatingMany(peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(channelsCount - 1))).string } else { participation = presentationData.strings.Chat_Giveaway_Info_Participating(peerName).string } @@ -139,19 +186,19 @@ public func presentGiveawayInfoController( let finishDate = stringForDate(timestamp: finish, timeZone: timeZone, strings: presentationData.strings) title = presentationData.strings.Chat_Giveaway_Info_EndedTitle - let intro = presentationData.strings.Chat_Giveaway_Info_EndedIntro(peerName, presentationData.strings.Chat_Giveaway_Info_Subscriptions(giveaway.quantity), presentationData.strings.Chat_Giveaway_Info_Months(giveaway.months)).string + let intro = presentationData.strings.Chat_Giveaway_Info_EndedIntro(peerName, presentationData.strings.Chat_Giveaway_Info_Subscriptions(quantity), presentationData.strings.Chat_Giveaway_Info_Months(months)).string var ending: String - if giveaway.flags.contains(.onlyNewSubscribers) { - let randomUsers = presentationData.strings.Chat_Giveaway_Info_RandomUsers(giveaway.quantity) - if giveaway.channelPeerIds.count > 1 { + if onlyNewSubscribers { + let randomUsers = presentationData.strings.Chat_Giveaway_Info_RandomUsers(quantity) + if channelsCount > 1 { ending = presentationData.strings.Chat_Giveaway_Info_EndedNewMany(finishDate, randomUsers, peerName, startDate).string } else { ending = presentationData.strings.Chat_Giveaway_Info_EndedNew(finishDate, randomUsers, peerName, startDate).string } } else { - let randomSubscribers = presentationData.strings.Chat_Giveaway_Info_RandomSubscribers(giveaway.quantity) - if giveaway.channelPeerIds.count > 1 { + let randomSubscribers = presentationData.strings.Chat_Giveaway_Info_RandomSubscribers(quantity) + if channelsCount > 1 { ending = presentationData.strings.Chat_Giveaway_Info_EndedMany(finishDate, randomSubscribers, peerName).string } else { ending = presentationData.strings.Chat_Giveaway_Info_Ended(finishDate, randomSubscribers, peerName).string diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift index 88aacf28a8..bb06a80b32 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -23,6 +23,7 @@ import UniversalMediaPlayer public enum PremiumGiftSource: Equatable { case profile case attachMenu + case settings var identifier: String? { switch self { @@ -30,6 +31,8 @@ public enum PremiumGiftSource: Equatable { return "profile" case .attachMenu: return "attach" + case .settings: + return "settings" } } } @@ -39,7 +42,7 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { let context: AccountContext let source: PremiumGiftSource - let peer: EnginePeer? + let peers: [EnginePeer] let products: [PremiumGiftProduct]? let selectedProductId: String? @@ -47,10 +50,10 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { let selectProduct: (String) -> Void let buy: () -> Void - init(context: AccountContext, source: PremiumGiftSource, peer: EnginePeer?, products: [PremiumGiftProduct]?, selectedProductId: String?, present: @escaping (ViewController) -> Void, selectProduct: @escaping (String) -> Void, buy: @escaping () -> Void) { + init(context: AccountContext, source: PremiumGiftSource, peers: [EnginePeer], products: [PremiumGiftProduct]?, selectedProductId: String?, present: @escaping (ViewController) -> Void, selectProduct: @escaping (String) -> Void, buy: @escaping () -> Void) { self.context = context self.source = source - self.peer = peer + self.peers = peers self.products = products self.selectedProductId = selectedProductId self.present = present @@ -65,7 +68,7 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { if lhs.source != rhs.source { return false } - if lhs.peer != rhs.peer { + if lhs.peers != rhs.peers { return false } if lhs.products != rhs.products { @@ -88,6 +91,8 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { private var stickersDisposable: Disposable? private var preloadDisposableSet = DisposableSet() + var cachedBoostIcon: UIImage? + var price: String? init(context: AccountContext, source: PremiumGiftSource) { @@ -186,7 +191,7 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { var size = CGSize(width: context.availableSize.width, height: 0.0) let overscroll = overscroll.update( - component: Rectangle(color: theme.list.plainBackgroundColor), + component: Rectangle(color: theme.list.blocksBackgroundColor), availableSize: CGSize(width: context.availableSize.width, height: 1000), transition: context.transition ) @@ -205,15 +210,63 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { let textFont = Font.regular(15.0) let boldTextFont = Font.semibold(15.0) - let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: textColor), linkAttribute: { _ in + + var descriptionString: String = "" + if context.component.peers.count > 1 { + if context.component.peers.count < 4 { + var names = "" + for i in 0 ..< context.component.peers.count { + if i == 0 { + } else if i < context.component.peers.count - 1 { + names.append(environment.strings.CreateGroup_PeersTitleDelimeter) + } else { + names.append(environment.strings.CreateGroup_PeersTitleLastDelimeter) + } + names.append(context.component.peers[i].compactDisplayTitle) + } + descriptionString = environment.strings.Premium_Gift_MultipleDescription(names, "").string + } else { + var names = "" + for i in 0 ..< min(3, context.component.peers.count) { + if i == 0 { + + } else { + names.append(environment.strings.CreateGroup_PeersTitleDelimeter) + } + names.append(context.component.peers[i].compactDisplayTitle) + } + let more = environment.strings.Premium_Gift_NamesAndMore(Int32(context.component.peers.count - 3)) + + descriptionString = environment.strings.Premium_Gift_MultipleDescription(names, more).string + } + } else { + descriptionString = strings.Premium_Gift_Description(component.peers.first?.compactDisplayTitle ?? "").string + } + + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.component.context.currentAppConfiguration.with { $0 }) + + descriptionString += "\n\n" + descriptionString += environment.strings.Premium_Gift_YouWillReceiveBoosts(Int32(context.component.peers.count) * premiumConfiguration.boostsPerGiftCount).replacingOccurrences(of: "[]()", with: " [ ]() ") + + let boostIcon: UIImage + if let current = context.state.cachedBoostIcon { + boostIcon = current + } else { + boostIcon = generateImage(CGSize(width: 14.0, height: 20.0), rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + if let cgImage = UIImage(bundleImageName: "Premium/BoostChannel")?.cgImage { + context.draw(cgImage, in: CGRect(origin: .zero, size: size), byTiling: false) + } + })! + context.state.cachedBoostIcon = boostIcon + } + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: environment.theme.list.itemAccentColor, additionalAttributes: [NSAttributedString.Key.attachment.rawValue: boostIcon]), linkAttribute: { _ in return nil }) + let descriptionText = parseMarkdownIntoAttributedString(descriptionString, attributes: markdownAttributes, textAlignment: .center) let text = text.update( component: MultilineTextComponent( - text: .markdown( - text: strings.Premium_Gift_Description(component.peer?.compactDisplayTitle ?? "").string, - attributes: markdownAttributes - ), + text: .plain(descriptionText), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2 @@ -489,7 +542,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext - let peerId: PeerId + let peerIds: [EnginePeer.Id] let options: [CachedPremiumGiftOption] let source: PremiumGiftSource let buttonStatePromise: Promise @@ -502,7 +555,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent { init( context: AccountContext, - peerId: PeerId, + peerIds: [EnginePeer.Id], options: [CachedPremiumGiftOption], source: PremiumGiftSource, buttonStatePromise: Promise, @@ -514,7 +567,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent { completion: @escaping (Int32) -> Void) { self.context = context - self.peerId = peerId + self.peerIds = peerIds self.options = options self.source = source self.buttonStatePromise = buttonStatePromise @@ -530,7 +583,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent { if lhs.context !== rhs.context { return false } - if lhs.peerId != rhs.peerId { + if lhs.peerIds != rhs.peerIds { return false } if lhs.options != rhs.options { @@ -544,7 +597,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent { final class State: ComponentState { private let context: AccountContext - private let peerId: PeerId + private let peerIds: [EnginePeer.Id] private let options: [CachedPremiumGiftOption] private let source: PremiumGiftSource private let buttonStatePromise: Promise @@ -564,7 +617,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent { } } - var peer: EnginePeer? + var peers: [EnginePeer.Id: EnginePeer] = [:] var products: [PremiumGiftProduct]? var selectedProductId: String? @@ -574,7 +627,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent { init( context: AccountContext, - peerId: PeerId, + peerIds: [EnginePeer.Id], options: [CachedPremiumGiftOption], source: PremiumGiftSource, buttonStatePromise: Promise, @@ -584,7 +637,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent { completion: @escaping (Int32) -> Void) { self.context = context - self.peerId = peerId + self.peerIds = peerIds self.options = options self.source = source self.buttonAction = buttonAction @@ -605,8 +658,10 @@ private final class PremiumGiftScreenComponent: CombinedComponent { self.disposable = combineLatest( queue: Queue.mainQueue(), availableProducts, - context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - ).start(next: { [weak self] products, peer in + context.engine.data.get( + EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) + ) + ).start(next: { [weak self] products, peers in if let strongSelf = self { var gifts: [PremiumGiftProduct] = [] for option in strongSelf.options { @@ -619,7 +674,15 @@ private final class PremiumGiftScreenComponent: CombinedComponent { if strongSelf.selectedProductId == nil && strongSelf.source != .attachMenu { strongSelf.selectedProductId = strongSelf.products?.first?.id } - strongSelf.peer = peer + + var unwrappedPeers: [EnginePeer.Id: EnginePeer] = [:] + for (peerId, maybePeer) in peers { + if let peer = maybePeer { + unwrappedPeers[peerId] = peer + } + } + + strongSelf.peers = unwrappedPeers strongSelf.updated(transition: .immediate) } }) @@ -675,7 +738,9 @@ private final class PremiumGiftScreenComponent: CombinedComponent { self.updateInProgress(true) self.updated(transition: .immediate) - let purpose: AppStoreTransactionPurpose = .gift(peerId: self.peerId, currency: currency, amount: amount) + //TODO:update buy when api arrives + + let purpose: AppStoreTransactionPurpose = .gift(peerId: self.peerIds.first!, currency: currency, amount: amount) let _ = (self.context.engine.payments.canPurchasePremium(purpose: purpose) |> deliverOnMainQueue).start(next: { [weak self] available in if let strongSelf = self { @@ -741,7 +806,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent { func makeState() -> State { return State( context: self.context, - peerId: self.peerId, + peerIds: self.peerIds, options: self.options, source: self.source, buttonStatePromise: self.buttonStatePromise, @@ -823,12 +888,19 @@ private final class PremiumGiftScreenComponent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) ) + var peers: [EnginePeer] = [] + for peerId in context.component.peerIds { + if let peer = state.peers[peerId] { + peers.append(peer) + } + } + let scrollContent = scrollContent.update( component: ScrollComponent( content: AnyComponent(PremiumGiftScreenContentComponent( context: context.component.context, source: context.component.source, - peer: state.peer, + peers: peers, products: state.products, selectedProductId: state.selectedProductId, present: context.component.present, @@ -887,7 +959,8 @@ private final class PremiumGiftScreenComponent: CombinedComponent { let star = star.update( component: GiftAvatarComponent( context: context.component.context, - peer: context.state.peer, + theme: environment.theme, + peers: peers, isVisible: starIsVisible, hasIdleAnimations: state.hasIdleAnimations ), @@ -939,9 +1012,18 @@ private final class PremiumGiftScreenComponent: CombinedComponent { context.component.updateTabBarAlpha(bottomPanelAlpha, .immediate) } else { let sideInset: CGFloat = 16.0 + + let buttonText: String + if context.component.peerIds.count > 1 { + let subscriptions = environment.strings.Premium_Gift_GiftMultipleSubscriptions(Int32(context.component.peerIds.count)) + buttonText = environment.strings.Premium_Gift_GiftMultipleSubscriptionsFormat(subscriptions, price ?? "—").string + } else { + buttonText = environment.strings.Premium_Gift_GiftSubscription(price ?? "—").string + } + let button = button.update( component: SolidRoundedButtonComponent( - title: environment.strings.Premium_Gift_GiftSubscription(price ?? "—").string, + title: buttonText, theme: SolidRoundedButtonComponent.Theme( backgroundColor: UIColor(rgb: 0x8878ff), backgroundColors: [ @@ -1015,7 +1097,7 @@ open class PremiumGiftScreen: ViewControllerComponentContainer { public let mainButtonStatePromise = Promise(nil) private let mainButtonActionSlot = ActionSlot() - public init(context: AccountContext, peerId: PeerId, options: [CachedPremiumGiftOption], source: PremiumGiftSource, pushController: @escaping (ViewController) -> Void, completion: @escaping () -> Void) { + public init(context: AccountContext, peerIds: [EnginePeer.Id], options: [CachedPremiumGiftOption], source: PremiumGiftSource, pushController: @escaping (ViewController) -> Void, completion: @escaping () -> Void) { self.context = context var updateInProgressImpl: ((Bool) -> Void)? @@ -1025,7 +1107,7 @@ open class PremiumGiftScreen: ViewControllerComponentContainer { var updateTabBarAlphaImpl: ((CGFloat, ContainedViewLayoutTransition) -> Void)? super.init(context: context, component: PremiumGiftScreenComponent( context: context, - peerId: peerId, + peerIds: peerIds, options: options, source: source, buttonStatePromise: self.mainButtonStatePromise, diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index 29573caeb3..e086e73e25 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -1195,6 +1195,9 @@ private final class LimitSheetContent: CombinedComponent { case let .channelReactions(reactionCount): titleText = strings.ChannelBoost_CustomReactions string = strings.ChannelBoost_CustomReactionsText("\(reactionCount)", "\(reactionCount)").string + case .wallpaper: + titleText = strings.ChannelBoost_Wallpaper + string = strings.ChannelBoost_WallpaperText("\(premiumConfiguration.minChannelWallpaperLevel)").string } } else { let storiesString = strings.ChannelBoost_StoriesPerDay(level) @@ -1780,6 +1783,7 @@ public class PremiumLimitScreen: ViewControllerComponentContainer { case stories case nameColors case channelReactions(reactionCount: Int) + case wallpaper } case storiesChannelBoost(peer: EnginePeer, boostSubject: BoostSubject, isCurrent: Bool, level: Int32, currentLevelBoosts: Int32, nextLevelBoosts: Int32?, link: String?, myBoostCount: Int32, canBoostAgain: Bool) diff --git a/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift b/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift index b87262f2c0..64ce650d95 100644 --- a/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift +++ b/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift @@ -229,7 +229,7 @@ private final class ReplaceBoostScreenComponent: CombinedComponent { selectionPosition: .right, isEnabled: isEnabled, hasNext: i != occupiedBoosts.count - 1, - action: { [weak state] _ in + action: { [weak state] _, _ in guard let state, hasSelection else { return } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift index 192e02a402..aff01e5072 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift @@ -308,7 +308,11 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate if case .edit(_, _, _, _, _, true, _) = self.mode { doneButtonType = .proceed } else if case let .peer(peer) = resultMode { - doneButtonType = .setPeer(peer.compactDisplayTitle, context.isPremium) + if peer.id.namespace == Namespaces.Peer.CloudUser { + doneButtonType = .setPeer(peer.compactDisplayTitle, context.isPremium) + } else { + doneButtonType = .setChannel + } } else { doneButtonType = .set } @@ -775,7 +779,11 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate if case .edit(_, _, _, _, _, true, _) = self.mode { doneButtonType = .proceed } else if case let .peer(peer) = self.resultMode { - doneButtonType = .setPeer(peer.compactDisplayTitle, self.context.isPremium) + if peer.id.namespace == Namespaces.Peer.CloudUser { + doneButtonType = .setPeer(peer.compactDisplayTitle, self.context.isPremium) + } else { + doneButtonType = .setChannel + } } else { doneButtonType = .set } @@ -1182,7 +1190,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate if case .background = self.mode, toolbarBottomInset.isZero { toolbarBottomInset = 16.0 } - if case .peer = self.resultMode, !self.state.displayPatternPanel { + if case let .peer(peer) = self.resultMode, case .user = peer, !self.state.displayPatternPanel { toolbarBottomInset += 58.0 } let toolbarHeight = 49.0 + toolbarBottomInset diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryController.swift b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryController.swift index 18c55a2f02..15813a0268 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryController.swift +++ b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryController.swift @@ -498,7 +498,11 @@ public class WallpaperGalleryController: ViewController { break } if case let .peer(peer, _) = self.mode { - doneButtonType = .setPeer(peer.compactDisplayTitle, self.context.isPremium) + if case .user = peer { + doneButtonType = .setPeer(peer.compactDisplayTitle, self.context.isPremium) + } else { + doneButtonType = .setChannel + } } let toolbarNode = WallpaperGalleryToolbarNode(theme: presentationData.theme, strings: presentationData.strings, doneButtonType: doneButtonType) @@ -969,7 +973,7 @@ public class WallpaperGalleryController: ViewController { self.overlayNode?.frame = self.galleryNode.bounds var toolbarHeight: CGFloat = 66.0 - if case .peer = self.mode { + if case let .peer(peer, _) = self.mode, case .user = peer { toolbarHeight += 58.0 } transition.updateFrame(node: self.toolbarNode!, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarHeight - layout.intrinsicInsets.bottom), size: CGSize(width: layout.size.width, height: toolbarHeight + layout.intrinsicInsets.bottom))) diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift index 01fb70c683..b3561b5e4e 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift +++ b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift @@ -1330,7 +1330,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { let buttonSpacing: CGFloat = 18.0 var toolbarHeight: CGFloat = 66.0 - if let mode = self.mode, case .peer = mode { + if let mode = self.mode, case let .peer(peer, _) = mode, case .user = peer { toolbarHeight += 58.0 } @@ -1482,7 +1482,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.nativeNode.updateBubbleTheme(bubbleTheme: self.presentationData.theme, bubbleCorners: self.presentationData.chatBubbleCorners) var bottomInset: CGFloat = 132.0 - if let mode = self.mode, case .peer = mode { + if let mode = self.mode, case let .peer(peer, _) = mode, case .user = peer { bottomInset += 58.0 } diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryToolbarNode.swift b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryToolbarNode.swift index 876f1325bb..851d9e7198 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryToolbarNode.swift +++ b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryToolbarNode.swift @@ -13,6 +13,7 @@ enum WallpaperGalleryToolbarCancelButtonType { enum WallpaperGalleryToolbarDoneButtonType { case set case setPeer(String, Bool) + case setChannel case proceed case apply case none @@ -284,6 +285,8 @@ final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryToolbar applyTitle = strings.Wallpaper_ApplyForMe applyForBothTitle = strings.Wallpaper_ApplyForBoth(name).string applyForBothLocked = !isPremium + case .setChannel: + applyTitle = strings.Wallpaper_ApplyForChannel case .proceed: applyTitle = strings.Theme_Colors_Proceed case .apply: @@ -423,7 +426,7 @@ final class WallpaperGalleryOldToolbarNode: ASDisplayNode, WallpaperGalleryToolb } let doneTitle: String switch self.doneButtonType { - case .set, .setPeer: + case .set, .setPeer, .setChannel: doneTitle = strings.Wallpaper_Set case .proceed: doneTitle = strings.Theme_Colors_Proceed diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index 8a0fa81ad0..3af5413605 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -2612,3 +2612,148 @@ private func restrictedSendingContentsText(peer: EnginePeer, presentationData: P return presentationData.strings.Chat_SendAllowedContentPeerText(peer.compactDisplayTitle, itemListString).string } + +public final class MessageStoryRenderer { + private let context: AccountContext + private let presentationData: PresentationData + private let messages: [Message] + + let containerNode: ASDisplayNode + private let wallpaperBackgroundNode: WallpaperBackgroundNode + private let messagesContainerNode: ASDisplayNode + private var dateHeaderNode: ListViewItemHeaderNode? + private var messageNodes: [ListViewItemNode]? + private let addressNode: ImmediateTextNode + + private let _ready = Promise() + var isReady: Signal { + return self._ready.get() + } + + public init(context: AccountContext, messages: [Message]) { + self.context = context + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.messages = messages + + self.containerNode = ASDisplayNode() + + self.wallpaperBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: true, useSharedAnimationPhase: false) + self.wallpaperBackgroundNode.displaysAsynchronously = false + + self.messagesContainerNode = ASDisplayNode() + self.messagesContainerNode.clipsToBounds = true + self.messagesContainerNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) + + let message = messages.first! + let addressName = message.peers[message.id.peerId]?.addressName ?? "" + + self.addressNode = ImmediateTextNode() + self.addressNode.displaysAsynchronously = false + self.addressNode.attributedText = NSAttributedString(string: "t.me/\(addressName)/\(message.id.id)", font: Font.medium(14.0), textColor: UIColor(rgb: 0xffffff)) + self.addressNode.textShadowColor = UIColor(rgb: 0x929292, alpha: 0.8) + + self.containerNode.addSubnode(self.wallpaperBackgroundNode) + self.containerNode.addSubnode(self.messagesContainerNode) + self.containerNode.addSubnode(self.addressNode) + + let wallpaper = context.sharedContext.currentPresentationData.with { $0 }.chatWallpaper + self.wallpaperBackgroundNode.update(wallpaper: wallpaper, animated: false) + + self._ready.set(self.wallpaperBackgroundNode.isReady) + } + + public func update(layout: ContainerViewLayout, completion: @escaping (UIImage?) -> Void) { + self.updateMessagesLayout(layout: layout) + + Queue.mainQueue().after(0.55) { + UIGraphicsBeginImageContextWithOptions(layout.size, false, 3.0) + self.containerNode.view.drawHierarchy(in: CGRect(origin: CGPoint(), size: layout.size), afterScreenUpdates: true) + let img = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + let finalImage = generateImage(CGSize(width: 1080, height: 1920), contextGenerator: { size, context in + if let cgImage = img?.cgImage { + context.draw(cgImage, in: CGRect(origin: .zero, size: size), byTiling: false) + } + }, opaque: true, scale: 1.0) + if let finalImage { + completion(finalImage) + } + } + } + + private func updateMessagesLayout(layout: ContainerViewLayout) { + let size = layout.size + self.containerNode.frame = CGRect(origin: CGPoint(), size: layout.size) + self.wallpaperBackgroundNode.frame = CGRect(origin: CGPoint(), size: layout.size) + self.wallpaperBackgroundNode.updateLayout(size: size, displayMode: .aspectFill, transition: .immediate) + self.messagesContainerNode.frame = CGRect(origin: CGPoint(), size: layout.size) + + let addressLayout = self.addressNode.updateLayout(size) + + let theme = self.presentationData.theme.withUpdated(preview: true) + let headerItem = self.context.sharedContext.makeChatMessageDateHeaderItem(context: self.context, timestamp: self.messages.first?.timestamp ?? 0, theme: theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder) + + let items: [ListViewItem] = [self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: self.messages, theme: theme, strings: self.presentationData.strings, wallpaper: self.presentationData.theme.chat.defaultWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: nil, availableReactions: nil, accountPeer: nil, isCentered: false)] + + let inset: CGFloat = 16.0 + let width = layout.size.width - inset * 2.0 + let params = ListViewItemLayoutParams(width: width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height) + if let messageNodes = self.messageNodes { + for i in 0 ..< items.count { + let itemNode = messageNodes[i] + items[i].updateNode(async: { $0() }, node: { + return itemNode + }, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in + let nodeFrame = CGRect(origin: CGPoint(x: 0.0, y: floor((size.height - layout.size.height) / 2.0)), size: CGSize(width: width, height: layout.size.height)) + + itemNode.contentSize = layout.contentSize + itemNode.insets = layout.insets + itemNode.frame = nodeFrame + itemNode.isUserInteractionEnabled = false + + apply(ListViewItemApply(isOnScreen: true)) + }) + } + } else { + var messageNodes: [ListViewItemNode] = [] + for i in 0 ..< items.count { + var itemNode: ListViewItemNode? + items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: true, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in + itemNode = node + apply().1(ListViewItemApply(isOnScreen: true)) + }) + itemNode!.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + itemNode!.isUserInteractionEnabled = false + messageNodes.append(itemNode!) + self.messagesContainerNode.addSubnode(itemNode!) + } + self.messageNodes = messageNodes + } + + var bottomOffset: CGFloat = 0.0 + if let messageNodes = self.messageNodes { + for itemNode in messageNodes { + itemNode.frame = CGRect(origin: CGPoint(x: inset, y: floor((size.height - itemNode.frame.height) / 2.0)), size: itemNode.frame.size) + bottomOffset += itemNode.frame.maxY + itemNode.updateFrame(itemNode.frame, within: layout.size) + } + } + + self.addressNode.frame = CGRect(origin: CGPoint(x: inset + 16.0, y: bottomOffset + 3.0), size: CGSize(width: addressLayout.width, height: addressLayout.height + 3.0)) + + let dateHeaderNode: ListViewItemHeaderNode + if let currentDateHeaderNode = self.dateHeaderNode { + dateHeaderNode = currentDateHeaderNode + headerItem.updateNode(dateHeaderNode, previous: nil, next: headerItem) + } else { + dateHeaderNode = headerItem.node(synchronousLoad: true) + dateHeaderNode.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + self.messagesContainerNode.addSubnode(dateHeaderNode) + self.dateHeaderNode = dateHeaderNode + } + + dateHeaderNode.frame = CGRect(origin: CGPoint(x: 0.0, y: bottomOffset), size: CGSize(width: layout.size.width, height: headerItem.height)) + dateHeaderNode.updateLayout(size: self.containerNode.frame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right) + } +} diff --git a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift index 31a05c84d9..47765cad02 100644 --- a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift +++ b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift @@ -1461,11 +1461,33 @@ public final class SolidRoundedButtonView: UIView { 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 + let badgeSpacing: CGFloat = 6.0 var contentWidth: CGFloat = titleSize.width if !iconSize.width.isZero { contentWidth += iconSize.width + iconSpacing } + + var badgeSize: CGSize = .zero + if let badge = self.badge { + let badgeNode: BadgeNode + if let current = self.badgeNode { + badgeNode = current + } else { + badgeNode = BadgeNode(fillColor: self.theme.foregroundColor, strokeColor: .clear, textColor: self.theme.backgroundColor) + badgeNode.alpha = self.titleNode.alpha == 0.0 ? 0.0 : 1.0 + self.badgeNode = badgeNode + self.addSubnode(badgeNode) + } + badgeNode.text = badge + badgeSize = badgeNode.update(CGSize(width: 100.0, height: 100.0)) + + contentWidth += badgeSize.width + badgeSpacing + } else if let badgeNode = self.badgeNode { + self.badgeNode = nil + badgeNode.removeFromSupernode() + } + var nextContentOrigin = floor((buttonFrame.width - contentWidth) / 2.0) let iconFrame: CGRect @@ -1484,6 +1506,7 @@ public final class SolidRoundedButtonView: UIView { } iconFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: floor((buttonFrame.height - iconSize.height) / 2.0)), size: iconSize) } + let badgeFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + badgeSpacing, y: titleFrame.minY + floor((titleFrame.height - badgeSize.height) * 0.5)), size: badgeSize) transition.updateFrame(view: self.iconNode, frame: iconFrame) if let animationNode = self.animationNode { @@ -1491,22 +1514,8 @@ public final class SolidRoundedButtonView: UIView { } transition.updateFrame(view: self.titleNode, frame: titleFrame) - if let badge = self.badge { - let badgeNode: BadgeNode - if let current = self.badgeNode { - badgeNode = current - } else { - badgeNode = BadgeNode(fillColor: self.theme.foregroundColor, strokeColor: .clear, textColor: self.theme.backgroundColor) - badgeNode.alpha = self.titleNode.alpha == 0.0 ? 0.0 : 1.0 - self.badgeNode = badgeNode - self.addSubnode(badgeNode) - } - badgeNode.text = badge - let badgeSize = badgeNode.update(CGSize(width: 100.0, height: 100.0)) - transition.updateFrame(node: badgeNode, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: titleFrame.minY + floor((titleFrame.height - badgeSize.height) * 0.5)), size: badgeSize)) - } else if let badgeNode = self.badgeNode { - self.badgeNode = nil - badgeNode.removeFromSupernode() + if let badgeNode = self.badgeNode { + transition.updateFrame(node: badgeNode, frame: badgeFrame) } if self.subtitle != self.subtitleNode.attributedText?.string { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift index 57f7f623d8..49c7d41aa7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift @@ -69,7 +69,7 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran flags |= (1 << 0) } if showWinners { - flags |= (1 << 0) + flags |= (1 << 3) } var additionalPeers: [Api.InputPeer] = [] if !additionalPeerIds.isEmpty { diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift index c122f261ae..8c2797cb2c 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift @@ -64,6 +64,33 @@ public struct PresentationResourcesSettings { drawBorder(context: context, rect: bounds) }) + + public static let premiumGift = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let path = UIBezierPath(roundedRect: bounds, cornerRadius: 7.0) + context.addPath(path.cgPath) + context.clip() + + let colorsArray: [CGColor] = [ + UIColor(rgb: 0x3da3f4).cgColor, + UIColor(rgb: 0x3da3f4).cgColor, + UIColor(rgb: 0x39b3b9).cgColor, + UIColor(rgb: 0x35c37c).cgColor, + UIColor(rgb: 0x35c37c).cgColor + ] + var locations: [CGFloat] = [0.0, 0.15, 0.5, 0.85, 1.0] + let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions()) + + if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Gift"), color: UIColor(rgb: 0xffffff)), let cgImage = image.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - image.size.width) / 2.0), y: floorToScreenPixels((bounds.height - image.size.height) / 2.0)), size: image.size)) + } + + drawBorder(context: context, rect: bounds) + }) public static let passport = renderIcon(name: "Settings/Menu/Passport") public static let watch = renderIcon(name: "Settings/Menu/Watch") diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 072dbe48e0..44fc8545cc 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -894,8 +894,12 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = NSAttributedString(string: strings.Notification_YouChangedWallpaper, font: titleFont, textColor: primaryTextColor) } } else { - let resultTitleString = strings.Notification_ChangedWallpaper(compactAuthorName) - attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) + if message.id.peerId.isGroupOrChannel { + attributedString = NSAttributedString(string: strings.Notification_ChannelChangedWallpaper, font: titleFont, textColor: primaryTextColor) + } else { + let resultTitleString = strings.Notification_ChangedWallpaper(compactAuthorName) + attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) + } } case .setSameChatWallpaper: if message.author?.id == accountPeerId { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift index 080df0704b..294d1865cc 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift @@ -150,7 +150,11 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode guard let strongSelf = self, let item = strongSelf.item else { return } - item.controllerInteraction.openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default) + if case .user = peer { + item.controllerInteraction.openPeer(peer, .info(nil), nil, .default) + } else { + item.controllerInteraction.openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default) + } } } @@ -252,9 +256,28 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode updatedBadgeImage = generateStretchableFilledCircleImage(diameter: 21.0, color: accentColor, strokeColor: backgroundColor, strokeWidth: 1.0 + UIScreenPixel, backgroundColor: nil) } - let badgeString = NSAttributedString(string: "X\(giveaway?.quantity ?? 2)", font: Font.with(size: 10.0, design: .round , weight: .bold, traits: .monospacedNumbers), textColor: badgeTextColor) + let badgeText: String + if let giveaway { + badgeText = "X\(giveaway.quantity)" + } else if let giveawayResults { + badgeText = "X\(giveawayResults.winnersCount)" + } else { + badgeText = "" + } + let badgeString = NSAttributedString(string: badgeText, font: Font.with(size: 10.0, design: .round , weight: .bold, traits: .monospacedNumbers), textColor: badgeTextColor) - let prizeTitleString = NSAttributedString(string: giveawayResults != nil ? "Winners Selected!" : item.presentationData.strings.Chat_Giveaway_Message_PrizeTitle, font: titleFont, textColor: textColor) + let prizeTitleText: String + if let giveawayResults { + if giveawayResults.winnersCount > 1 { + prizeTitleText = item.presentationData.strings.Chat_Giveaway_Message_WinnersSelectedTitle_Many + } else { + prizeTitleText = item.presentationData.strings.Chat_Giveaway_Message_WinnersSelectedTitle_One + } + } else { + prizeTitleText = item.presentationData.strings.Chat_Giveaway_Message_PrizeTitle + } + + let prizeTitleString = NSAttributedString(string: prizeTitleText, font: titleFont, textColor: textColor) var prizeTextString: NSAttributedString? var additionalPrizeSeparatorString: NSAttributedString? var additionalPrizeTextString: NSAttributedString? @@ -294,7 +317,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode } ), textAlignment: .center) } else if let giveawayResults { - prizeTextString = parseMarkdownIntoAttributedString("**\(giveawayResults.winnersCount)** winners of the [Giveaway]() were randomly selected by Telegram.", attributes: MarkdownAttributes( + prizeTextString = parseMarkdownIntoAttributedString(item.presentationData.strings.Chat_Giveaway_Message_WinnersSelectedText(giveawayResults.winnersCount), attributes: MarkdownAttributes( body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: accentColor), @@ -304,7 +327,18 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode ), textAlignment: .center) } - let participantsTitleString = NSAttributedString(string: giveawayResults != nil ? "Winners" : item.presentationData.strings.Chat_Giveaway_Message_ParticipantsTitle, font: titleFont, textColor: textColor) + let participantsTitleText: String + if let giveawayResults { + if giveawayResults.winnersCount > 1 { + participantsTitleText = item.presentationData.strings.Chat_Giveaway_Message_WinnersTitle_Many + } else { + participantsTitleText = item.presentationData.strings.Chat_Giveaway_Message_WinnersTitle_One + } + } else { + participantsTitleText = item.presentationData.strings.Chat_Giveaway_Message_ParticipantsTitle + } + + let participantsTitleString = NSAttributedString(string: participantsTitleText, font: titleFont, textColor: textColor) let participantsText: String let countriesText: String @@ -360,7 +394,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode if let giveawayResults { if giveawayResults.winnersCount > giveawayResults.winnersPeerIds.count { let moreCount = giveawayResults.winnersCount - Int32(giveawayResults.winnersPeerIds.count) - dateTitleText = "and \(moreCount) more!" + dateTitleText = item.presentationData.strings.Chat_Giveaway_Message_WinnersMore(moreCount) } else { dateTitleText = "" } @@ -370,8 +404,8 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode var dateTextString: NSAttributedString? if let giveaway { dateTextString = NSAttributedString(string: stringForFullDate(timestamp: giveaway.untilDate, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat), font: textFont, textColor: textColor) - } else if let _ = giveawayResults { - dateTextString = NSAttributedString(string: "All winners received gift links in private messages.", font: textFont, textColor: textColor) + } else if let giveawayResults { + dateTextString = NSAttributedString(string: giveawayResults.winnersCount > 1 ? item.presentationData.strings.Chat_Giveaway_Message_WinnersInfo_Many : item.presentationData.strings.Chat_Giveaway_Message_WinnersInfo_One, font: textFont, textColor: textColor) } let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none, hidesHeaders: true) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/BUILD index 9f0577a650..8f73fb8736 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/BUILD @@ -36,6 +36,7 @@ swift_library( "//submodules/ChatMessageBackground", "//submodules/ContextUI", "//submodules/UndoUI", + "//submodules/TelegramUI/Components/Chat/MergedAvatarsNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/Sources/ChatMessageJoinedChannelBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/Sources/ChatMessageJoinedChannelBubbleContentNode.swift index a8bb5342c6..fc3c136f6f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/Sources/ChatMessageJoinedChannelBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/Sources/ChatMessageJoinedChannelBubbleContentNode.swift @@ -27,6 +27,7 @@ import BundleIconComponent import ChatMessageBackground import ContextUI import UndoUI +import MergedAvatarsNode private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id) -> NSAttributedString? { return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false) @@ -776,7 +777,7 @@ private final class ChannelItemComponent: Component { self.mergedAvatarsNode = mergedAvatarsNode } - mergedAvatarsNode.update(context: component.context, peers: component.peers.map { $0._asPeer() }, synchronousLoad: false, imageSize: 60.0, imageSpacing: 10.0, borderWidth: 2.0) + mergedAvatarsNode.update(context: component.context, peers: component.peers.map { $0._asPeer() }, synchronousLoad: false, imageSize: 60.0, imageSpacing: 10.0, borderWidth: 2.0, avatarFontSize: 26.0) let avatarsSize = CGSize(width: avatarSize.width + 20.0, height: avatarSize.height) mergedAvatarsNode.updateLayout(size: avatarsSize) mergedAvatarsNode.frame = CGRect(origin: CGPoint(x: avatarFrame.midX - avatarsSize.width / 2.0, y: avatarFrame.minY), size: avatarsSize) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/BUILD index 175efd024c..cbea42ae83 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/BUILD @@ -25,6 +25,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", "//submodules/TelegramUI/Components/Chat/PollBubbleTimerNode", + "//submodules/TelegramUI/Components/Chat/MergedAvatarsNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index da7d147bd5..15d189abae 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -15,6 +15,7 @@ import ChatMessageDateAndStatusNode import ChatMessageBubbleContentNode import ChatMessageItemCommon import PollBubbleTimerNode +import MergedAvatarsNode private final class ChatMessagePollOptionRadioNodeParameters: NSObject { let timestamp: Double @@ -1440,10 +1441,10 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { timerTransition.updateAlpha(node: strongSelf.solutionButtonNode, alpha: 0.0) } - let avatarsFrame = CGRect(origin: CGPoint(x: typeFrame.maxX + 6.0, y: typeFrame.minY + floor((typeFrame.height - defaultMergedImageSize) / 2.0)), size: CGSize(width: defaultMergedImageSize + defaultMergedImageSpacing * 2.0, height: defaultMergedImageSize)) + let avatarsFrame = CGRect(origin: CGPoint(x: typeFrame.maxX + 6.0, y: typeFrame.minY + floor((typeFrame.height - MergedAvatarsNode.defaultMergedImageSize) / 2.0)), size: CGSize(width: MergedAvatarsNode.defaultMergedImageSize + MergedAvatarsNode.defaultMergedImageSpacing * 2.0, height: MergedAvatarsNode.defaultMergedImageSize)) strongSelf.avatarsNode.frame = avatarsFrame strongSelf.avatarsNode.updateLayout(size: avatarsFrame.size) - strongSelf.avatarsNode.update(context: item.context, peers: avatarPeers, synchronousLoad: synchronousLoad, imageSize: defaultMergedImageSize, imageSpacing: defaultMergedImageSpacing, borderWidth: defaultBorderWidth) + strongSelf.avatarsNode.update(context: item.context, peers: avatarPeers, synchronousLoad: synchronousLoad, imageSize: MergedAvatarsNode.defaultMergedImageSize, imageSpacing: MergedAvatarsNode.defaultMergedImageSpacing, borderWidth: MergedAvatarsNode.defaultBorderWidth) strongSelf.avatarsNode.isHidden = isBotChat let alphaTransition: ContainedViewLayoutTransition if animation.isAnimated { @@ -1706,202 +1707,3 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { return nil } } - -private enum PeerAvatarReference: Equatable { - case letters(PeerId, PeerNameColor?, [String]) - case image(PeerReference, TelegramMediaImageRepresentation) - - var peerId: PeerId { - switch self { - case let .letters(value, _, _): - return value - case let .image(value, _): - return value.id - } - } -} - -private extension PeerAvatarReference { - init(peer: Peer) { - if let photo = peer.smallProfileImage, let peerReference = PeerReference(peer) { - self = .image(peerReference, photo) - } else { - self = .letters(peer.id, peer.nameColor, peer.displayLetters) - } - } -} - -private final class MergedAvatarsNodeArguments: NSObject { - let peers: [PeerAvatarReference] - let images: [PeerId: UIImage] - let imageSize: CGFloat - let imageSpacing: CGFloat - let borderWidth: CGFloat - - init(peers: [PeerAvatarReference], images: [PeerId: UIImage], imageSize: CGFloat, imageSpacing: CGFloat, borderWidth: CGFloat) { - self.peers = peers - self.images = images - self.imageSize = imageSize - self.imageSpacing = imageSpacing - self.borderWidth = borderWidth - } -} - -private let defaultMergedImageSize: CGFloat = 16.0 -private let defaultMergedImageSpacing: CGFloat = 15.0 -private let defaultBorderWidth: CGFloat = 1.0 - -private let avatarFont = avatarPlaceholderFont(size: 8.0) - -public final class MergedAvatarsNode: ASDisplayNode { - private var peers: [PeerAvatarReference] = [] - private var images: [PeerId: UIImage] = [:] - private var disposables: [PeerId: Disposable] = [:] - private let buttonNode: HighlightTrackingButtonNode - private var imageSize: CGFloat = defaultMergedImageSize - private var imageSpacing: CGFloat = defaultMergedImageSpacing - private var borderWidthValue: CGFloat = defaultBorderWidth - - public var pressed: (() -> Void)? - - override public init() { - self.buttonNode = HighlightTrackingButtonNode() - - super.init() - - self.isOpaque = false - self.displaysAsynchronously = true - self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) - self.addSubnode(self.buttonNode) - } - - deinit { - for (_, disposable) in self.disposables { - disposable.dispose() - } - } - - @objc private func buttonPressed() { - self.pressed?() - } - - public func updateLayout(size: CGSize) { - self.buttonNode.frame = CGRect(origin: CGPoint(), size: size) - } - - public func update(context: AccountContext, peers: [Peer], synchronousLoad: Bool, imageSize: CGFloat, imageSpacing: CGFloat, borderWidth: CGFloat) { - self.imageSize = imageSize - self.imageSpacing = imageSpacing - self.borderWidthValue = borderWidth - var filteredPeers = peers.map(PeerAvatarReference.init) - if filteredPeers.count > 3 { - filteredPeers = filteredPeers.dropLast(filteredPeers.count - 3) - } - if filteredPeers != self.peers { - self.peers = filteredPeers - - var validImageIds: [PeerId] = [] - for peer in filteredPeers { - if case .image = peer { - validImageIds.append(peer.peerId) - } - } - - var removedImageIds: [PeerId] = [] - for (id, _) in self.images { - if !validImageIds.contains(id) { - removedImageIds.append(id) - } - } - var removedDisposableIds: [PeerId] = [] - for (id, disposable) in self.disposables { - if !validImageIds.contains(id) { - disposable.dispose() - removedDisposableIds.append(id) - } - } - for id in removedImageIds { - self.images.removeValue(forKey: id) - } - for id in removedDisposableIds { - self.disposables.removeValue(forKey: id) - } - for peer in filteredPeers { - switch peer { - case let .image(peerReference, representation): - if self.disposables[peer.peerId] == nil { - if let signal = peerAvatarImage(account: context.account, peerReference: peerReference, authorOfMessage: nil, representation: representation, displayDimensions: CGSize(width: imageSize, height: imageSize), synchronousLoad: synchronousLoad) { - let disposable = (signal - |> deliverOnMainQueue).startStrict(next: { [weak self] imageVersions in - guard let strongSelf = self else { - return - } - let image = imageVersions?.0 - if let image = image { - strongSelf.images[peer.peerId] = image - strongSelf.setNeedsDisplay() - } - }) - self.disposables[peer.peerId] = disposable - } - } - case .letters: - break - } - } - self.setNeedsDisplay() - } - } - - override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol { - return MergedAvatarsNodeArguments(peers: self.peers, images: self.images, imageSize: self.imageSize, imageSpacing: self.imageSpacing, borderWidth: self.borderWidthValue) - } - - @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { - assertNotOnMainThread() - - let context = UIGraphicsGetCurrentContext()! - - if !isRasterizing { - context.setBlendMode(.copy) - context.setFillColor(UIColor.clear.cgColor) - context.fill(bounds) - } - - guard let parameters = parameters as? MergedAvatarsNodeArguments else { - return - } - - let mergedImageSize = parameters.imageSize - let mergedImageSpacing = parameters.imageSpacing - - var currentX = mergedImageSize + mergedImageSpacing * CGFloat(parameters.peers.count - 1) - mergedImageSize - for i in (0 ..< parameters.peers.count).reversed() { - let imageRect = CGRect(origin: CGPoint(x: currentX, y: 0.0), size: CGSize(width: mergedImageSize, height: mergedImageSize)) - context.setBlendMode(.copy) - context.setFillColor(UIColor.clear.cgColor) - context.fillEllipse(in: imageRect.insetBy(dx: -parameters.borderWidth, dy: -parameters.borderWidth)) - context.setBlendMode(.normal) - - context.saveGState() - switch parameters.peers[i] { - case let .letters(peerId, nameColor, letters): - context.translateBy(x: currentX, y: 0.0) - drawPeerAvatarLetters(context: context, size: CGSize(width: mergedImageSize, height: mergedImageSize), font: avatarFont, letters: letters, peerId: peerId, nameColor: nameColor) - context.translateBy(x: -currentX, y: 0.0) - case .image: - if let image = parameters.images[parameters.peers[i].peerId] { - context.translateBy(x: imageRect.midX, y: imageRect.midY) - context.scaleBy(x: 1.0, y: -1.0) - context.translateBy(x: -imageRect.midX, y: -imageRect.midY) - context.draw(image.cgImage!, in: imageRect) - } else { - context.setFillColor(UIColor.gray.cgColor) - context.fillEllipse(in: imageRect) - } - } - context.restoreGState() - currentX -= mergedImageSpacing - } - } -} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index 3e9e8bfe85..026e4e2170 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -588,6 +588,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.cachedChatMessageText = updatedCachedChatMessageText } + strongSelf.textNode.textNode.displaysAsynchronously = !item.presentationData.isPreview strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: boundingSize) let cachedLayout = strongSelf.textNode.textNode.cachedLayout diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode/Sources/ChatMessageWallpaperBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode/Sources/ChatMessageWallpaperBubbleContentNode.swift index dc835683d2..01d593eb0f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode/Sources/ChatMessageWallpaperBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode/Sources/ChatMessageWallpaperBubbleContentNode.swift @@ -265,6 +265,7 @@ public class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode } let fromYou = item.message.author?.id == item.context.account.peerId + let isChannel = item.message.id.peerId.isGroupOrChannel let peerName = item.message.peers[item.message.id.peerId].flatMap { EnginePeer($0).compactDisplayTitle } ?? "" let text: String @@ -281,7 +282,11 @@ public class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode } } } else { - text = item.presentationData.strings.Notification_ChangedWallpaper(peerName).string + if item.message.id.peerId.isGroupOrChannel { + text = item.presentationData.strings.Notification_ChannelChangedWallpaper + } else { + text = item.presentationData.strings.Notification_ChangedWallpaper(peerName).string + } } let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor) @@ -311,15 +316,15 @@ public class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode if displayTrailingAnimatedDots { textHeight += subtitleLayout.size.height } - let backgroundSize = CGSize(width: width, height: textHeight + 140.0 + (fromYou ? 0.0 : 42.0)) + let backgroundSize = CGSize(width: width, height: textHeight + 140.0 + (fromYou || isChannel ? 0.0 : 42.0)) return (backgroundSize.width, { boundingWidth in return (backgroundSize, { [weak self] animation, synchronousLoads, _ in if let strongSelf = self { strongSelf.item = item - strongSelf.buttonNode.isHidden = fromYou - strongSelf.buttonTitleNode.isHidden = fromYou + strongSelf.buttonNode.isHidden = fromYou || isChannel + strongSelf.buttonTitleNode.isHidden = fromYou || isChannel let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - imageSize.width) / 2.0), y: 13.0), size: imageSize) if let media, mediaUpdated { diff --git a/submodules/TelegramUI/Components/Chat/MergedAvatarsNode/BUILD b/submodules/TelegramUI/Components/Chat/MergedAvatarsNode/BUILD new file mode 100644 index 0000000000..1328ab6234 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/MergedAvatarsNode/BUILD @@ -0,0 +1,25 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "MergedAvatarsNode", + module_name = "MergedAvatarsNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/Display", + "//submodules/TelegramPresentationData", + "//submodules/AvatarNode", + "//submodules/AccountContext", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/MergedAvatarsNode/Sources/MergedAvatarsNode.swift b/submodules/TelegramUI/Components/Chat/MergedAvatarsNode/Sources/MergedAvatarsNode.swift new file mode 100644 index 0000000000..184f571429 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/MergedAvatarsNode/Sources/MergedAvatarsNode.swift @@ -0,0 +1,217 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import TelegramPresentationData +import Postbox +import TelegramCore +import AvatarNode +import AccountContext + +private enum PeerAvatarReference: Equatable { + case letters(PeerId, PeerNameColor?, [String]) + case image(PeerReference, TelegramMediaImageRepresentation) + + var peerId: PeerId { + switch self { + case let .letters(value, _, _): + return value + case let .image(value, _): + return value.id + } + } +} + +private extension PeerAvatarReference { + init(peer: Peer) { + if let photo = peer.smallProfileImage, let peerReference = PeerReference(peer) { + self = .image(peerReference, photo) + } else { + self = .letters(peer.id, peer.nameColor, peer.displayLetters) + } + } +} + +private final class MergedAvatarsNodeArguments: NSObject { + let peers: [PeerAvatarReference] + let images: [PeerId: UIImage] + let imageSize: CGFloat + let imageSpacing: CGFloat + let borderWidth: CGFloat + let avatarFontSize: CGFloat + + init(peers: [PeerAvatarReference], images: [PeerId: UIImage], imageSize: CGFloat, imageSpacing: CGFloat, borderWidth: CGFloat, avatarFontSize: CGFloat) { + self.peers = peers + self.images = images + self.imageSize = imageSize + self.imageSpacing = imageSpacing + self.borderWidth = borderWidth + self.avatarFontSize = avatarFontSize + } +} + +private let defaultMergedImageSize: CGFloat = 16.0 +private let defaultMergedImageSpacing: CGFloat = 15.0 +private let defaultBorderWidth: CGFloat = 1.0 + +public final class MergedAvatarsNode: ASDisplayNode { + public static let defaultMergedImageSize: CGFloat = 16.0 + public static let defaultMergedImageSpacing: CGFloat = 15.0 + public static let defaultBorderWidth: CGFloat = 1.0 + public static let defaultAvatarFontSize: CGFloat = 8.0 + + private var peers: [PeerAvatarReference] = [] + private var images: [PeerId: UIImage] = [:] + private var disposables: [PeerId: Disposable] = [:] + private let buttonNode: HighlightTrackingButtonNode + private var imageSize: CGFloat = defaultMergedImageSize + private var imageSpacing: CGFloat = defaultMergedImageSpacing + private var borderWidthValue: CGFloat = defaultBorderWidth + private var avatarFontSize: CGFloat = defaultAvatarFontSize + + public var pressed: (() -> Void)? + + override public init() { + self.buttonNode = HighlightTrackingButtonNode() + + super.init() + + self.isOpaque = false + self.displaysAsynchronously = true + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + self.addSubnode(self.buttonNode) + } + + deinit { + for (_, disposable) in self.disposables { + disposable.dispose() + } + } + + @objc private func buttonPressed() { + self.pressed?() + } + + public func updateLayout(size: CGSize) { + self.buttonNode.frame = CGRect(origin: CGPoint(), size: size) + } + + public func update(context: AccountContext, peers: [Peer], synchronousLoad: Bool, imageSize: CGFloat, imageSpacing: CGFloat, borderWidth: CGFloat, avatarFontSize: CGFloat = 8.0) { + self.imageSize = imageSize + self.imageSpacing = imageSpacing + self.borderWidthValue = borderWidth + self.avatarFontSize = avatarFontSize + + var filteredPeers = peers.map(PeerAvatarReference.init) + if filteredPeers.count > 3 { + filteredPeers = filteredPeers.dropLast(filteredPeers.count - 3) + } + if filteredPeers != self.peers { + self.peers = filteredPeers + + var validImageIds: [PeerId] = [] + for peer in filteredPeers { + if case .image = peer { + validImageIds.append(peer.peerId) + } + } + + var removedImageIds: [PeerId] = [] + for (id, _) in self.images { + if !validImageIds.contains(id) { + removedImageIds.append(id) + } + } + var removedDisposableIds: [PeerId] = [] + for (id, disposable) in self.disposables { + if !validImageIds.contains(id) { + disposable.dispose() + removedDisposableIds.append(id) + } + } + for id in removedImageIds { + self.images.removeValue(forKey: id) + } + for id in removedDisposableIds { + self.disposables.removeValue(forKey: id) + } + for peer in filteredPeers { + switch peer { + case let .image(peerReference, representation): + if self.disposables[peer.peerId] == nil { + if let signal = peerAvatarImage(account: context.account, peerReference: peerReference, authorOfMessage: nil, representation: representation, displayDimensions: CGSize(width: imageSize, height: imageSize), synchronousLoad: synchronousLoad) { + let disposable = (signal + |> deliverOnMainQueue).startStrict(next: { [weak self] imageVersions in + guard let strongSelf = self else { + return + } + let image = imageVersions?.0 + if let image = image { + strongSelf.images[peer.peerId] = image + strongSelf.setNeedsDisplay() + } + }) + self.disposables[peer.peerId] = disposable + } + } + case .letters: + break + } + } + self.setNeedsDisplay() + } + } + + override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol { + return MergedAvatarsNodeArguments(peers: self.peers, images: self.images, imageSize: self.imageSize, imageSpacing: self.imageSpacing, borderWidth: self.borderWidthValue, avatarFontSize: self.avatarFontSize) + } + + @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { + assertNotOnMainThread() + + let context = UIGraphicsGetCurrentContext()! + + if !isRasterizing { + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fill(bounds) + } + + guard let parameters = parameters as? MergedAvatarsNodeArguments else { + return + } + + let mergedImageSize = parameters.imageSize + let mergedImageSpacing = parameters.imageSpacing + + var currentX = mergedImageSize + mergedImageSpacing * CGFloat(parameters.peers.count - 1) - mergedImageSize + for i in (0 ..< parameters.peers.count).reversed() { + let imageRect = CGRect(origin: CGPoint(x: currentX, y: 0.0), size: CGSize(width: mergedImageSize, height: mergedImageSize)) + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: imageRect.insetBy(dx: -parameters.borderWidth, dy: -parameters.borderWidth)) + context.setBlendMode(.normal) + + context.saveGState() + switch parameters.peers[i] { + case let .letters(peerId, nameColor, letters): + context.translateBy(x: currentX, y: 0.0) + drawPeerAvatarLetters(context: context, size: CGSize(width: mergedImageSize, height: mergedImageSize), font: avatarPlaceholderFont(size: parameters.avatarFontSize), letters: letters, peerId: peerId, nameColor: nameColor) + context.translateBy(x: -currentX, y: 0.0) + case .image: + if let image = parameters.images[parameters.peers[i].peerId] { + context.translateBy(x: imageRect.midX, y: imageRect.midY) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -imageRect.midX, y: -imageRect.midY) + context.draw(image.cgImage!, in: imageRect) + } else { + context.setFillColor(UIColor.gray.cgColor) + context.fillEllipse(in: imageRect) + } + } + context.restoreGState() + currentX -= mergedImageSpacing + } + } +} diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift index 2d96d9672c..118426c4fb 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift @@ -230,7 +230,7 @@ final class ContextResultPanelComponent: Component { presence: nil, selectionState: .none, hasNext: index != peers.count - 1, - action: { [weak self] peer in + action: { [weak self] peer, _ in guard let self, let component = self.component else { return } @@ -299,7 +299,7 @@ final class ContextResultPanelComponent: Component { presence: nil, selectionState: .none, hasNext: true, - action: { _ in + action: { _, _ in } )), environment: {}, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD index c87a7f3705..8a18efaa92 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD @@ -131,6 +131,8 @@ swift_library( "//submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent", "//submodules/TelegramUI/Components/LottieComponent", "//submodules/SolidRoundedButtonNode", + "//submodules/MediaPickerUI", + "//submodules/AttachmentUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index dbba3c72ac..57621e4c3d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -98,6 +98,8 @@ import PeerNameColorScreen import PeerAllowedReactionsScreen import ChatMessageSelectionInputPanelNode import ChatHistorySearchContainerNode +import MediaPickerUI +import AttachmentUI public enum PeerInfoAvatarEditingMode { case generic @@ -483,6 +485,7 @@ private enum PeerInfoSettingsSection { case language case stickers case premium + case premiumGift case passport case watch case support @@ -527,6 +530,7 @@ private final class PeerInfoInteraction { let performBotCommand: (PeerInfoBotCommand) -> Void let editingOpenPublicLinkSetup: () -> Void let editingOpenNameColorSetup: () -> Void + let editingOpenPeerWallpaperSetup: () -> Void let editingOpenInviteLinksSetup: () -> Void let editingOpenDiscussionGroupSetup: () -> Void let editingToggleMessageSignatures: (Bool) -> Void @@ -582,6 +586,7 @@ private final class PeerInfoInteraction { performBotCommand: @escaping (PeerInfoBotCommand) -> Void, editingOpenPublicLinkSetup: @escaping () -> Void, editingOpenNameColorSetup: @escaping () -> Void, + editingOpenPeerWallpaperSetup: @escaping () -> Void, editingOpenInviteLinksSetup: @escaping () -> Void, editingOpenDiscussionGroupSetup: @escaping () -> Void, editingToggleMessageSignatures: @escaping (Bool) -> Void, @@ -636,6 +641,7 @@ private final class PeerInfoInteraction { self.performBotCommand = performBotCommand self.editingOpenPublicLinkSetup = editingOpenPublicLinkSetup self.editingOpenNameColorSetup = editingOpenNameColorSetup + self.editingOpenPeerWallpaperSetup = editingOpenPeerWallpaperSetup self.editingOpenInviteLinksSetup = editingOpenInviteLinksSetup self.editingOpenDiscussionGroupSetup = editingOpenDiscussionGroupSetup self.editingToggleMessageSignatures = editingToggleMessageSignatures @@ -910,6 +916,9 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 100, label: .text(""), text: presentationData.strings.Settings_Premium, icon: PresentationResourcesSettings.premium, action: { interaction.openSettings(.premium) })) + items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 101, label: .text(""), additionalBadgeLabel: presentationData.strings.Settings_New, text: presentationData.strings.Settings_PremiumGift, icon: PresentationResourcesSettings.premiumGift, action: { + interaction.openSettings(.premiumGift) + })) } /*items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 100, label: .text(""), text: "Payment Method", icon: PresentationResourcesSettings.language, action: { @@ -1570,19 +1579,20 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL switch channel.info { case .broadcast: let ItemUsername = 1 - let ItemNameColor = 2 - let ItemInviteLinks = 3 - let ItemDiscussionGroup = 4 - let ItemSignMessages = 5 - let ItemSignMessagesHelp = 6 - let ItemDeleteChannel = 7 - let ItemReactions = 8 - let ItemAdmins = 9 - let ItemMembers = 10 - let ItemMemberRequests = 11 - let ItemStats = 12 - let ItemBanned = 13 - let ItemRecentActions = 14 + let ItemPeerColor = 2 + let ItemPeerWallpaper = 3 + let ItemInviteLinks = 4 + let ItemDiscussionGroup = 5 + let ItemSignMessages = 6 + let ItemSignMessagesHelp = 7 + let ItemDeleteChannel = 8 + let ItemReactions = 9 + let ItemAdmins = 10 + let ItemMembers = 11 + let ItemMemberRequests = 12 + let ItemStats = 13 + let ItemBanned = 14 + let ItemRecentActions = 15 let isCreator = channel.flags.contains(.isCreator) @@ -1655,10 +1665,22 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL } if isCreator || (channel.adminRights?.rights.contains(.canChangeInfo) == true) { - let colors = context.peerNameColors.get(data.peer?.nameColor ?? .blue, dark: presentationData.theme.overallDarkAppearance) - items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemNameColor, label: .semitransparentBadge(EnginePeer(channel).compactDisplayTitle, colors.main), text: presentationData.strings.Channel_ChannelColor, icon: UIImage(bundleImageName: "Chat/Info/NameColorIcon"), action: { + var colors: [PeerNameColors.Colors] = [] + if let nameColor = channel.nameColor.flatMap({ context.peerNameColors.get($0, dark: presentationData.theme.overallDarkAppearance) }) { + colors.append(nameColor) + } + if let profileColor = channel.profileColor.flatMap({ context.peerNameColors.getProfile($0, dark: presentationData.theme.overallDarkAppearance, subject: .palette) }) { + colors.append(profileColor) + } + let colorImage = generateSettingsMenuPeerColorsLabelIcon(colors: colors) + + items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerColor, label: .image(colorImage, colorImage.size), text: presentationData.strings.Channel_ChannelColor, icon: UIImage(bundleImageName: "Chat/Info/NameColorIcon"), action: { interaction.editingOpenNameColorSetup() })) + + items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerWallpaper, label: .none, text: "Wallpaper", icon: UIImage(bundleImageName: "Settings/Menu/Appearance"), action: { + interaction.editingOpenPeerWallpaperSetup() + })) } if isCreator || (channel.adminRights != nil && channel.hasPermission(.sendSomething)) { @@ -2336,6 +2358,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro editingOpenNameColorSetup: { [weak self] in self?.editingOpenNameColorSetup() }, + editingOpenPeerWallpaperSetup: { [weak self] in + self?.editingOpenPeerWallpaperSetup() + }, editingOpenInviteLinksSetup: { [weak self] in self?.editingOpenInviteLinksSetup() }, @@ -5412,7 +5437,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if let strongSelf = self { var pushControllerImpl: ((ViewController) -> Void)? - let controller = PremiumGiftScreen(context: strongSelf.context, peerId: strongSelf.peerId, options: cachedData.premiumGiftOptions, source: .profile, pushController: { c in + let controller = PremiumGiftScreen(context: strongSelf.context, peerIds: [strongSelf.peerId], options: cachedData.premiumGiftOptions, source: .profile, pushController: { c in pushControllerImpl?(c) }, completion: { [weak self] in if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { @@ -7180,6 +7205,79 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } + private func editingOpenPeerWallpaperSetup() { +// let link = status.url +// let controller = PremiumLimitScreen(context: context, subject: .storiesChannelBoost(peer: peer, boostSubject: .nameColors, isCurrent: true, level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), link: link, myBoostCount: 0, canBoostAgain: false), count: Int32(status.boosts), action: { +// UIPasteboard.general.string = link +// let presentationData = context.sharedContext.currentPresentationData.with { $0 } +// presentImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.ChannelBoost_BoostLinkCopied), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false })) +// return true +// }, openStats: nil, openGift: premiumConfiguration.giveawayGiftsPurchaseAvailable ? { +// let controller = createGiveawayController(context: context, peerId: peerId, subject: .generic) +// pushImpl?(controller) +// } : nil) +// pushImpl?(controller) + + let dismissControllers = { [weak self] in + if let self, let navigationController = self.controller?.navigationController as? NavigationController { + let controllers = navigationController.viewControllers.filter({ controller in + if controller is WallpaperGalleryController || controller is AttachmentController || controller is PeerInfoScreenImpl { + return false + } + return true + }) + navigationController.setViewControllers(controllers, animated: true) + } + } + var openWallpaperPickerImpl: ((Bool) -> Void)? + let openWallpaperPicker: (Bool) -> Void = { [weak self] animateAppearance in + guard let self, let peer = self.data?.peer else { + return + } + let controller = wallpaperMediaPickerController( + context: self.context, + updatedPresentationData: self.controller?.updatedPresentationData, + peer: EnginePeer(peer), + animateAppearance: true, + completion: { [weak self] _, result in + guard let strongSelf = self, let asset = result as? PHAsset else { + return + } + let controller = WallpaperGalleryController(context: strongSelf.context, source: .asset(asset), mode: .peer(EnginePeer(peer), false)) + controller.navigationPresentation = .modal + controller.apply = { [weak self] wallpaper, options, editedImage, cropRect, brightness, forBoth in + if let strongSelf = self { + uploadCustomPeerWallpaper(context: strongSelf.context, wallpaper: wallpaper, mode: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness, peerId: peer.id, forBoth: forBoth, completion: { + Queue.mainQueue().after(0.3, { + dismissControllers() + }) + }) + } + } + strongSelf.controller?.push(controller) + }, + openColors: { [weak self] in + guard let strongSelf = self else { + return + } + let controller = standaloneColorPickerController(context: strongSelf.context, peer: EnginePeer(peer), push: { [weak self] controller in + if let strongSelf = self { + strongSelf.controller?.push(controller) + } + }, openGallery: { + openWallpaperPickerImpl?(false) + }) + controller.navigationPresentation = .flatModal + strongSelf.controller?.push(controller) + } + ) + controller.navigationPresentation = .flatModal + self.controller?.push(controller) + } + openWallpaperPickerImpl = openWallpaperPicker + openWallpaperPicker(true) + } + private func editingOpenInviteLinksSetup() { self.controller?.push(inviteLinkListController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId, admin: nil)) } @@ -8626,6 +8724,63 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro push(LocalizationListController(context: self.context)) case .premium: self.controller?.push(PremiumIntroScreen(context: self.context, modal: false, source: .settings)) + case .premiumGift: + let controller = self.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: self.context, mode: .premiumGifting, options: [], isPeerEnabled: { peer in + if case let .user(user) = peer, user.botInfo == nil { + return true + } else { + return false + } + })) + self.controller?.push(controller) + self.activeActionDisposable.set((controller.result + |> deliverOnMainQueue).startStrict(next: { [weak self, weak controller] result in + guard let self, let controller else { + return + } + var peerIds: [PeerId] = [] + if case let .result(peerIdsValue, _) = result { + peerIds = peerIdsValue.compactMap({ peerId in + if case let .peer(peerId) = peerId { + return peerId + } else { + return nil + } + }) + } + + let maxCount = 10 + if peerIds.count > maxCount { + self.hapticFeedback.error() + + controller.present(UndoOverlayController(presentationData: self.presentationData, content: .info(title: nil, text: self.presentationData.strings.Premium_Gift_ContactSelection_MaximumReached("\(maxCount)").string, timeout: nil, customUndoText: nil), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current) + return + } + + let giftController = PremiumGiftScreen(context: self.context, peerIds: peerIds, options: [], source: .settings, pushController: { [weak self] c in + self?.controller?.push(c) + }, completion: { [weak self] in + if let self, let navigationController = self.controller?.navigationController as? NavigationController, peerIds.count == 1, let peerId = peerIds.first { + var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is PeerInfoScreen) && !($0 is PremiumGiftScreen) } + var foundController = false + for controller in controllers.reversed() { + if let chatController = controller as? ChatController, case .peer(id: peerId) = chatController.chatLocation { + chatController.hintPlayNextOutgoingGift() + foundController = true + break + } + } + if !foundController { + let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(previewing: false)) + chatController.hintPlayNextOutgoingGift() + controllers.append(chatController) + } + navigationController.setViewControllers(controllers, animated: true) + } + }) + controller.push(giftController) + })) case .stickers: if let settings = self.data?.globalSettings { push(installedStickerPacksController(context: self.context, mode: .general, archivedPacks: settings.archivedStickerPacks, updatedPacks: { [weak self] packs in diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index 0d4dc66f96..5bc0c12a51 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -1337,7 +1337,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.controller?.containerLayoutUpdated(layout, transition: .immediate) } } else { - let contactListNode = ContactListNode(context: self.context, updatedPresentationData: self.updatedPresentationData, presentation: .single(.natural(options: [], includeChatList: false))) + let contactListNode = ContactListNode(context: self.context, updatedPresentationData: self.updatedPresentationData, presentation: .single(.natural(options: [], includeChatList: false, topPeers: false))) self.contactListNode = contactListNode contactListNode.enableUpdates = true contactListNode.selectionStateUpdated = { [weak self] selectionState in diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index a14f0398f9..8a4af47593 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -1066,7 +1066,7 @@ final class ShareWithPeersScreenComponent: Component { rightAccessory: accessory, selectionState: .none, hasNext: i < peers.count - 1, - action: { [weak self] peer in + action: { [weak self] peer, _ in guard let self, let component = self.component else { return } @@ -1414,7 +1414,7 @@ final class ShareWithPeersScreenComponent: Component { presence: stateValue.presences[peer.id], selectionState: .editing(isSelected: isSelected, isTinted: false), hasNext: true, - action: { [weak self] peer in + action: { [weak self] peer, _ in guard let self, let environment = self.environment, let controller = environment.controller() as? ShareWithPeersScreen else { return } @@ -2143,7 +2143,7 @@ final class ShareWithPeersScreenComponent: Component { presence: nil, selectionState: .editing(isSelected: false, isTinted: false), hasNext: true, - action: { _ in + action: { _, _ in } )), environment: {}, diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift index 3aa2b497d0..e69c895c20 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift @@ -23,7 +23,7 @@ import PhotoResources private let avatarFont = avatarPlaceholderFont(size: 15.0) private let readIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/MenuReadIcon"), color: .white)?.withRenderingMode(.alwaysTemplate) private let repostIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Stories/HeaderRepost"), color: .white)?.withRenderingMode(.alwaysTemplate) -private let forwardIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Stories/HeaderRepost"), color: .white)?.withRenderingMode(.alwaysTemplate) +private let forwardIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Stories/HeaderForward"), color: .white)?.withRenderingMode(.alwaysTemplate) private let checkImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white)?.withRenderingMode(.alwaysTemplate) private let disclosureImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Item List/DisclosureArrow"), color: .white)?.withRenderingMode(.alwaysTemplate) @@ -116,7 +116,7 @@ public final class PeerListItemComponent: Component { let selectionPosition: SelectionPosition let isEnabled: Bool let hasNext: Bool - let action: (EnginePeer) -> Void + let action: (EnginePeer, EngineMessage.Id?) -> Void let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? let openStories: ((EnginePeer, AvatarNode) -> Void)? let openStory: ((EnginePeer, Int32, UIView) -> Void)? @@ -141,7 +141,7 @@ public final class PeerListItemComponent: Component { selectionPosition: SelectionPosition = .left, isEnabled: Bool = true, hasNext: Bool, - action: @escaping (EnginePeer) -> Void, + action: @escaping (EnginePeer, EngineMessage.Id?) -> Void, contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? = nil, openStories: ((EnginePeer, AvatarNode) -> Void)? = nil, openStory: ((EnginePeer, Int32, UIView) -> Void)? = nil @@ -353,7 +353,7 @@ public final class PeerListItemComponent: Component { guard let component = self.component, let peer = component.peer else { return } - component.action(peer) + component.action(peer, component.message?.id) } @objc private func avatarButtonPressed() { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 7c08f2724b..cebd413a35 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -1707,6 +1707,7 @@ public final class StoryItemSetContainerComponent: Component { context: component.context, theme: component.theme, strings: component.strings, + peer: component.slice.peer, storyItem: item.storyItem, myReaction: item.storyItem.myReaction.flatMap { value -> StoryFooterPanelComponent.MyReaction? in var centerAnimation: TelegramMediaFile? @@ -3327,6 +3328,12 @@ public final class StoryItemSetContainerComponent: Component { } self.navigateToPeer(peer: peer, chat: false) }, + openMessage: { [weak self] peer, messageId in + guard let self else { + return + } + self.navigateToPeer(peer: peer, chat: true, subject: .message(id: .id(messageId), highlight: nil, timecode: nil)) + }, peerContextAction: { [weak self] peer, sourceView, gesture in guard let self, let component = self.component else { return diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift index 15c0de7f7d..af0a684267 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift @@ -69,6 +69,7 @@ final class StoryItemSetViewListComponent: Component { let deleteAction: () -> Void let moreAction: (UIView, ContextGesture?) -> Void let openPeer: (EnginePeer) -> Void + let openMessage: (EnginePeer, EngineMessage.Id) -> Void let peerContextAction: (EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void let openPeerStories: (EnginePeer, AvatarNode) -> Void let openStory: (EnginePeer, Int32, [(EnginePeer, EngineStoryItem)], UIView) -> Void @@ -94,6 +95,7 @@ final class StoryItemSetViewListComponent: Component { deleteAction: @escaping () -> Void, moreAction: @escaping (UIView, ContextGesture?) -> Void, openPeer: @escaping (EnginePeer) -> Void, + openMessage: @escaping (EnginePeer, EngineMessage.Id) -> Void, peerContextAction: @escaping (EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void, openPeerStories: @escaping (EnginePeer, AvatarNode) -> Void, openStory: @escaping (EnginePeer, Int32, [(EnginePeer, EngineStoryItem)], UIView) -> Void, @@ -118,6 +120,7 @@ final class StoryItemSetViewListComponent: Component { self.deleteAction = deleteAction self.moreAction = moreAction self.openPeer = openPeer + self.openMessage = openMessage self.peerContextAction = peerContextAction self.openPeerStories = openPeerStories self.openStory = openStory @@ -237,6 +240,17 @@ final class StoryItemSetViewListComponent: Component { case repostsFirst = 0 case reactionsFirst = 1 case recentFirst = 2 + + var sortMode: EngineStoryViewListContext.SortMode { + switch self { + case .repostsFirst: + return .repostsFirst + case .reactionsFirst: + return .reactionsFirst + case .recentFirst: + return .recentFirst + } + } } private struct ContentConfigurationKey: Equatable { @@ -527,11 +541,15 @@ final class StoryItemSetViewListComponent: Component { message: item.message, selectionState: .none, hasNext: index != viewListState.totalCount - 1 || itemLayout.premiumFooterSize != nil, - action: { [weak self] peer in + action: { [weak self] peer, messageId in guard let self, let component = self.component else { return } - component.openPeer(peer) + if let messageId { + component.openMessage(peer, messageId) + } else { + component.openPeer(peer) + } }, contextAction: { peer, view, gesture in component.peerContextAction(peer, view, gesture) @@ -727,15 +745,6 @@ final class StoryItemSetViewListComponent: Component { case .contacts: mappedListMode = .contacts } - let mappedSortMode: EngineStoryViewListContext.SortMode - switch self.configuration.sortMode { - case .repostsFirst: - mappedSortMode = .repostsFirst - case .reactionsFirst: - mappedSortMode = .reactionsFirst - case .recentFirst: - mappedSortMode = .recentFirst - } var parentSource: EngineStoryViewListContext? if let baseContentView, baseContentView.configuration == self.configuration, baseContentView.query == nil { @@ -745,16 +754,22 @@ final class StoryItemSetViewListComponent: Component { parentSource = nil } - self.viewList = component.context.engine.messages.storyViewList(peerId: component.peerId, id: component.storyItem.id, views: views, listMode: mappedListMode, sortMode: mappedSortMode, searchQuery: query, parentSource: parentSource) + self.viewList = component.context.engine.messages.storyViewList(peerId: component.peerId, id: component.storyItem.id, views: views, listMode: mappedListMode, sortMode: self.configuration.sortMode.sortMode, searchQuery: query, parentSource: parentSource) } } } else { - if self.configuration == ContentConfigurationKey(listMode: .everyone, sortMode: .reactionsFirst) { + let defaultSortMode: SortMode + if component.peerId.isGroupOrChannel { + defaultSortMode = .recentFirst + } else { + defaultSortMode = .reactionsFirst + } + if self.configuration == ContentConfigurationKey(listMode: .everyone, sortMode: defaultSortMode) { let viewList: EngineStoryViewListContext if let current = component.sharedListsContext.viewLists[StoryId(peerId: component.peerId, id: component.storyItem.id)] { viewList = current } else { - viewList = component.context.engine.messages.storyViewList(peerId: component.peerId, id: component.storyItem.id, views: views, listMode: .everyone, sortMode: .reactionsFirst) + viewList = component.context.engine.messages.storyViewList(peerId: component.peerId, id: component.storyItem.id, views: views, listMode: .everyone, sortMode: defaultSortMode.sortMode) component.sharedListsContext.viewLists[StoryId(peerId: component.peerId, id: component.storyItem.id)] = viewList } self.viewList = viewList @@ -766,16 +781,7 @@ final class StoryItemSetViewListComponent: Component { case .contacts: mappedListMode = .contacts } - let mappedSortMode: EngineStoryViewListContext.SortMode - switch self.configuration.sortMode { - case .repostsFirst: - mappedSortMode = .repostsFirst - case .reactionsFirst: - mappedSortMode = .reactionsFirst - case .recentFirst: - mappedSortMode = .recentFirst - } - self.viewList = component.context.engine.messages.storyViewList(peerId: component.peerId, id: component.storyItem.id, views: views, listMode: mappedListMode, sortMode: mappedSortMode, parentSource: component.sharedListsContext.viewLists[StoryId(peerId: component.peerId, id: component.storyItem.id)]) + self.viewList = component.context.engine.messages.storyViewList(peerId: component.peerId, id: component.storyItem.id, views: views, listMode: mappedListMode, sortMode: self.configuration.sortMode.sortMode, parentSource: component.sharedListsContext.viewLists[StoryId(peerId: component.peerId, id: component.storyItem.id)]) } } } @@ -886,7 +892,7 @@ final class StoryItemSetViewListComponent: Component { presence: nil, selectionState: .none, hasNext: true, - action: { _ in + action: { _, _ in } )), environment: {}, @@ -1351,25 +1357,26 @@ final class StoryItemSetViewListComponent: Component { self.state?.updated(transition: .immediate) } }))) + } else { + items.append(.action(ContextMenuActionItem(text: component.strings.Story_ViewList_ContextSortReactions, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reactions"), color: theme.contextMenu.primaryColor) + }, additionalLeftIcon: { theme in + if sortMode != .reactionsFirst { + return nil + } + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + if self.sortMode != .reactionsFirst { + self.sortMode = .reactionsFirst + self.state?.updated(transition: .immediate) + } + }))) } - items.append(.action(ContextMenuActionItem(text: component.strings.Story_ViewList_ContextSortReactions, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reactions"), color: theme.contextMenu.primaryColor) - }, additionalLeftIcon: { theme in - if sortMode != .reactionsFirst { - return nil - } - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self else { - return - } - if self.sortMode != .reactionsFirst { - self.sortMode = .reactionsFirst - self.state?.updated(transition: .immediate) - } - }))) items.append(.action(ContextMenuActionItem(text: component.strings.Story_ViewList_ContextSortRecent, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Time"), color: theme.contextMenu.primaryColor) }, additionalLeftIcon: { theme in @@ -1392,7 +1399,8 @@ final class StoryItemSetViewListComponent: Component { items.append(.separator) let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil - items.append(.action(ContextMenuActionItem(text: component.strings.Story_ViewList_ContextSortInfo, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction))) + + items.append(.action(ContextMenuActionItem(text: component.peerId.isGroupOrChannel ? component.strings.Story_ViewList_ContextSortChannelInfo : component.strings.Story_ViewList_ContextSortInfo, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction))) let contextItems = ContextController.Items(content: .list(items)) @@ -1432,6 +1440,12 @@ final class StoryItemSetViewListComponent: Component { var updateSubState = false if self.mainViewList == nil { + if component.peerId.isGroupOrChannel { + self.sortMode = .recentFirst + } else { + self.sortMode = .reactionsFirst + } + self.mainViewListDisposable?.dispose() self.mainViewListDisposable = nil diff --git a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift index 1af28ee73c..ba2e76e557 100644 --- a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift @@ -38,6 +38,7 @@ public final class StoryFooterPanelComponent: Component { public let context: AccountContext public let theme: PresentationTheme public let strings: PresentationStrings + public let peer: EnginePeer public let storyItem: EngineStoryItem public let myReaction: MyReaction? public let isChannel: Bool @@ -56,6 +57,7 @@ public final class StoryFooterPanelComponent: Component { context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, + peer: EnginePeer, storyItem: EngineStoryItem, myReaction: MyReaction?, isChannel: Bool, @@ -73,6 +75,7 @@ public final class StoryFooterPanelComponent: Component { self.context = context self.theme = theme self.strings = strings + self.peer = peer self.storyItem = storyItem self.myReaction = myReaction self.isChannel = isChannel @@ -98,6 +101,9 @@ public final class StoryFooterPanelComponent: Component { if lhs.strings !== rhs.strings { return false } + if lhs.peer != rhs.peer { + return false + } if lhs.storyItem != rhs.storyItem { return false } @@ -186,6 +192,7 @@ public final class StoryFooterPanelComponent: Component { self.avatarsView.alpha = 0.7 self.viewStatsCountText.alpha = 0.7 self.viewStatsLabelText.view?.alpha = 0.7 + self.viewsIconView.alpha = 0.7 self.reactionStatsIcon?.alpha = 0.7 self.reactionStatsText?.alpha = 0.7 self.repostStatsIcon?.alpha = 0.7 @@ -194,6 +201,7 @@ public final class StoryFooterPanelComponent: Component { self.avatarsView.alpha = 1.0 self.viewStatsCountText.alpha = 1.0 self.viewStatsLabelText.view?.alpha = 1.0 + self.viewsIconView.alpha = 1.0 self.reactionStatsIcon?.alpha = 1.0 self.reactionStatsText?.alpha = 1.0 self.repostStatsIcon?.alpha = 1.0 @@ -202,6 +210,7 @@ public final class StoryFooterPanelComponent: Component { self.avatarsView.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2) self.viewStatsCountText.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2) self.viewStatsLabelText.view?.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2) + self.viewsIconView.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2) self.reactionStatsIcon?.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2) self.reactionStatsText?.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2) self.repostStatsIcon?.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2) @@ -383,7 +392,14 @@ public final class StoryFooterPanelComponent: Component { } } - self.viewStatsButton.isEnabled = viewCount != 0 && !component.isChannel + var displayViewLists = false + if case let .channel(channel) = component.peer, channel.flags.contains(.isCreator) || channel.adminRights?.rights.contains(.canPostStories) == true { + displayViewLists = reactionCount != 0 || forwardCount != 0 + } else { + displayViewLists = viewCount != 0 && !component.isChannel + } + + self.viewStatsButton.isEnabled = displayViewLists var rightContentOffset: CGFloat = availableSize.width - 12.0 diff --git a/submodules/TelegramUI/Images.xcassets/Stories/HeaderForward.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Stories/HeaderForward.imageset/Contents.json new file mode 100644 index 0000000000..7d40464891 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Stories/HeaderForward.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "arrow.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Stories/HeaderForward.imageset/arrow.pdf b/submodules/TelegramUI/Images.xcassets/Stories/HeaderForward.imageset/arrow.pdf new file mode 100644 index 0000000000..92b05022c2 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Stories/HeaderForward.imageset/arrow.pdf @@ -0,0 +1,351 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 13.000000 12.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +1.000000 0.000000 -0.000000 1.000000 1.330078 -0.721191 cm +5.315626 8.533691 m +5.315626 10.192076 l +5.315626 10.556320 5.315626 10.738442 5.388546 10.825513 c +5.451862 10.901115 5.547148 10.942396 5.645606 10.936879 c +5.759001 10.930527 5.891866 10.805965 6.157596 10.556844 c +9.926538 7.023460 l +10.061858 6.896598 10.129518 6.833168 10.154650 6.759085 c +10.176737 6.693978 10.176737 6.623405 10.154650 6.558298 c +10.129518 6.484215 10.061858 6.420784 9.926539 6.293923 c +6.157597 2.760540 l +5.891867 2.511417 5.759001 2.386856 5.645606 2.380504 c +5.547148 2.374987 5.451862 2.416267 5.388546 2.491870 c +5.315626 2.578940 5.315626 2.761063 5.315626 3.125307 c +5.315626 4.783691 l +2.581792 4.783691 1.121608 3.510042 0.458909 2.676931 c +0.280517 2.452666 0.191321 2.340534 0.141585 2.333771 c +0.096309 2.327615 0.057156 2.342980 0.028153 2.378287 c +-0.003707 2.417074 0.006321 2.548758 0.026378 2.812124 c +0.161226 4.582819 0.951126 8.533691 5.315626 8.533691 c +h +0.000000 0.000000 0.000000 scn +f* +n + +endstream +endobj + +2 0 obj + 1028 +endobj + +3 0 obj + << /Length 4 0 R + /FunctionType 4 + /Domain [ 0.000000 1.000000 ] + /Range [ 0.000000 1.000000 ] + >> +stream +{ 0 gt { 0 } { 1 } ifelse } +endstream +endobj + +4 0 obj + 27 +endobj + +5 0 obj + << /ExtGState << /E1 << /SMask << /Type /Mask + /G 1 0 R + /S /Alpha + /TR 3 0 R + >> + /Type /ExtGState + >> >> >> +endobj + +6 0 obj + << /Length 7 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +q +1.000000 0.000000 -0.000000 1.000000 1.330078 -0.721191 cm +0.000000 0.000000 0.000000 scn +5.315626 8.533691 m +5.315626 7.203691 l +6.050164 7.203691 6.645626 7.799152 6.645626 8.533691 c +5.315626 8.533691 l +h +5.315626 4.783691 m +6.645626 4.783691 l +6.645626 5.136429 6.505501 5.474720 6.256078 5.724144 c +6.006654 5.973567 5.668364 6.113691 5.315626 6.113691 c +5.315626 4.783691 l +h +0.458909 2.676931 m +-0.581952 3.504886 l +0.458909 2.676931 l +h +0.141585 2.333771 m +-0.037609 3.651644 l +0.141585 2.333771 l +h +0.028153 2.378287 m +1.055869 3.222504 l +0.028153 2.378287 l +h +5.645606 2.380504 m +5.571209 3.708421 l +5.645606 2.380504 l +h +5.388546 2.491870 m +4.368898 1.637928 l +5.388546 2.491870 l +h +9.926538 7.023460 m +9.016897 6.053176 l +9.926538 7.023460 l +h +10.154650 6.759085 m +8.895151 6.331811 l +10.154650 6.759085 l +h +9.926539 6.293923 m +10.836181 5.323638 l +9.926539 6.293923 l +h +10.154650 6.558298 m +11.414148 6.131024 l +10.154650 6.558298 l +h +5.388546 10.825513 m +6.408195 9.971571 l +5.388546 10.825513 l +h +5.645606 10.936879 m +5.720003 12.264797 l +5.645606 10.936879 l +h +6.645626 8.533691 m +6.645626 10.192076 l +3.985626 10.192076 l +3.985626 8.533691 l +6.645626 8.533691 l +h +5.247954 9.586559 m +9.016897 6.053176 l +10.836181 7.993745 l +7.067238 11.527128 l +5.247954 9.586559 l +h +9.016897 7.264207 m +5.247955 3.730824 l +7.067239 1.790256 l +10.836181 5.323638 l +9.016897 7.264207 l +h +6.645626 3.125307 m +6.645626 4.783691 l +3.985626 4.783691 l +3.985626 3.125307 l +6.645626 3.125307 l +h +5.315626 6.113691 m +2.097498 6.113691 0.279446 4.587790 -0.581952 3.504886 c +1.499771 1.848978 l +1.963770 2.432294 3.066086 3.453691 5.315626 3.453691 c +5.315626 6.113691 l +h +1.352538 2.711129 m +1.413560 3.512409 1.620141 4.689168 2.210945 5.629186 c +2.747699 6.483207 3.627555 7.203691 5.315626 7.203691 c +5.315626 9.863691 l +2.639197 9.863691 0.941854 8.608740 -0.041179 7.044650 c +-0.970163 5.566558 -1.225956 3.882534 -1.299782 2.913118 c +1.352538 2.711129 l +h +-0.581952 3.504886 m +-0.682445 3.378551 -0.720813 3.331185 -0.740086 3.309600 c +-0.750286 3.298176 -0.718989 3.335414 -0.659294 3.384659 c +-0.627593 3.410811 -0.566207 3.458532 -0.478809 3.506605 c +-0.394197 3.553145 -0.242563 3.623776 -0.037609 3.651644 c +0.320778 1.015898 l +0.717179 1.069798 0.973219 1.283091 1.033420 1.332752 c +1.129403 1.411933 1.203473 1.492488 1.244089 1.537976 c +1.326445 1.630214 1.421872 1.751047 1.499771 1.848978 c +-0.581952 3.504886 l +h +-1.299782 2.913118 m +-1.308334 2.800830 -1.320285 2.652165 -1.322062 2.530396 c +-1.322950 2.469575 -1.322850 2.358324 -1.304198 2.231652 c +-1.290535 2.138864 -1.241301 1.828356 -0.999563 1.534072 c +1.055869 3.222504 l +1.177860 3.073997 1.240899 2.929724 1.273622 2.836102 c +1.306749 2.741327 1.320904 2.663443 1.327426 2.619150 c +1.334278 2.572617 1.336326 2.537593 1.337061 2.520598 c +1.337841 2.502546 1.337658 2.491820 1.337655 2.491581 c +1.337645 2.490877 1.337704 2.495204 1.338235 2.506417 c +1.338756 2.517421 1.339593 2.532197 1.340889 2.552054 c +1.343585 2.593356 1.347357 2.643099 1.352538 2.711129 c +-1.299782 2.913118 l +h +-0.037609 3.651644 m +0.141479 3.675995 0.351744 3.662107 0.564653 3.578549 c +0.777561 3.494993 0.941147 3.362162 1.055869 3.222504 c +-0.999563 1.534072 l +-0.677662 1.142203 -0.181727 0.947571 0.320778 1.015898 c +-0.037609 3.651644 l +h +5.247955 3.730824 m +5.179151 3.666320 5.129662 3.619975 5.087789 3.581986 c +5.067758 3.563814 5.052710 3.550512 5.041348 3.540738 c +5.029866 3.530859 5.024831 3.526900 5.024859 3.526922 c +5.025069 3.527086 5.027243 3.528785 5.031198 3.531670 c +5.035140 3.534546 5.041450 3.539039 5.049938 3.544694 c +5.066150 3.555496 5.095299 3.573881 5.135852 3.594389 c +5.216256 3.635052 5.367123 3.696987 5.571209 3.708421 c +5.720003 1.052586 l +6.213777 1.080250 6.554987 1.346669 6.664093 1.432044 c +6.802816 1.540595 6.953648 1.683764 7.067239 1.790256 c +5.247955 3.730824 l +h +3.985626 3.125307 m +3.985626 2.969605 3.984338 2.761649 4.000025 2.586203 c +4.012363 2.448214 4.051368 2.017074 4.368898 1.637928 c +6.408195 3.345812 l +6.539436 3.189104 6.597436 3.036680 6.622764 2.950212 c +6.635538 2.906600 6.642061 2.872761 6.645269 2.853546 c +6.646949 2.843485 6.647987 2.835810 6.648585 2.830967 c +6.649184 2.826108 6.649432 2.823360 6.649456 2.823094 c +6.649459 2.823059 6.648904 2.829440 6.648258 2.844573 c +6.647618 2.859546 6.647029 2.879622 6.646587 2.906664 c +6.645662 2.963193 6.645626 3.030995 6.645626 3.125307 c +3.985626 3.125307 l +h +5.571209 3.708421 m +5.891789 3.726381 6.202040 3.591971 6.408195 3.345812 c +4.368898 1.637928 l +4.701684 1.240564 5.202506 1.023593 5.720003 1.052586 c +5.571209 3.708421 l +h +9.016897 6.053176 m +9.052996 6.019333 9.074108 5.999492 9.090629 5.983498 c +9.107223 5.967433 9.104925 5.968908 9.094450 5.980518 c +9.075613 6.001396 8.965572 6.124225 8.895151 6.331811 c +11.414148 7.186358 l +11.318595 7.468027 11.162159 7.659614 11.069380 7.762442 c +10.986407 7.854402 10.887315 7.945806 10.836181 7.993745 c +9.016897 6.053176 l +h +10.836181 5.323638 m +10.887316 5.371577 10.986407 5.462982 11.069380 5.554941 c +11.162159 5.657770 11.318595 5.849356 11.414148 6.131024 c +8.895151 6.985572 l +8.965572 7.193157 9.075612 7.315987 9.094449 7.336864 c +9.104925 7.348475 9.107223 7.349950 9.090629 7.333885 c +9.074109 7.317891 9.052996 7.298050 9.016897 7.264207 c +10.836181 5.323638 l +h +8.895151 6.331811 m +8.823236 6.543798 8.823236 6.773584 8.895151 6.985572 c +11.414148 6.131024 l +11.530237 6.473225 11.530237 6.844158 11.414148 7.186358 c +8.895151 6.331811 l +h +6.645626 10.192076 m +6.645626 10.286387 6.645662 10.354190 6.646587 10.410719 c +6.647029 10.437760 6.647618 10.457837 6.648258 10.472810 c +6.648904 10.487943 6.649459 10.494324 6.649456 10.494288 c +6.649432 10.494022 6.649184 10.491275 6.648585 10.486416 c +6.647987 10.481573 6.646949 10.473898 6.645269 10.463837 c +6.642061 10.444622 6.635538 10.410783 6.622764 10.367170 c +6.597436 10.280703 6.539436 10.128279 6.408195 9.971571 c +4.368898 11.679455 l +4.051368 11.300309 4.012363 10.869169 4.000025 10.731180 c +3.984338 10.555734 3.985626 10.347777 3.985626 10.192076 c +6.645626 10.192076 l +h +7.067238 11.527128 m +6.953648 11.633619 6.802816 11.776789 6.664093 11.885339 c +6.554986 11.970715 6.213776 12.237133 5.720003 12.264797 c +5.571209 9.608962 l +5.367124 9.620396 5.216256 9.682331 5.135852 9.722993 c +5.095299 9.743502 5.066151 9.761887 5.049939 9.772688 c +5.041450 9.778344 5.035140 9.782837 5.031198 9.785712 c +5.027243 9.788598 5.025069 9.790297 5.024859 9.790462 c +5.024831 9.790483 5.029865 9.786524 5.041348 9.776645 c +5.052709 9.766871 5.067758 9.753570 5.087788 9.735397 c +5.129661 9.697409 5.179150 9.651064 5.247954 9.586559 c +7.067238 11.527128 l +h +6.408195 9.971571 m +6.202040 9.725411 5.891789 9.591002 5.571209 9.608962 c +5.720003 12.264797 l +5.202506 12.293790 4.701684 12.076818 4.368898 11.679455 c +6.408195 9.971571 l +h +f +n +Q +Q + +endstream +endobj + +7 0 obj + 6868 +endobj + +8 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 13.000000 12.000000 ] + /Resources 5 0 R + /Contents 6 0 R + /Parent 9 0 R + >> +endobj + +9 0 obj + << /Kids [ 8 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +10 0 obj + << /Pages 9 0 R + /Type /Catalog + >> +endobj + +xref +0 11 +0000000000 65535 f +0000000010 00000 n +0000001286 00000 n +0000001309 00000 n +0000001484 00000 n +0000001505 00000 n +0000001817 00000 n +0000008741 00000 n +0000008764 00000 n +0000008937 00000 n +0000009011 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +9071 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 3aaa1f0fdc..ba94c89938 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -7310,8 +7310,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let chatWallpaper = self.context.account.viewTracker.peerView(peerId) |> take(1) |> map { view -> TelegramWallpaper? in - if let cachedUserData = view.cachedData as? CachedUserData { - return cachedUserData.wallpaper + if let cachedData = view.cachedData as? CachedUserData { + return cachedData.wallpaper + } else if let cachedData = view.cachedData as? CachedChannelData { + return cachedData.wallpaper } else { return nil } @@ -7463,6 +7465,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G themeEmoticon = cachedData.themeEmoticon } else if let cachedData = cachedData as? CachedChannelData { themeEmoticon = cachedData.themeEmoticon + chatWallpaper = cachedData.wallpaper } strongSelf.chatThemeEmoticonPromise.set(.single(themeEmoticon)) @@ -13274,7 +13277,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .gift: let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions if !premiumGiftOptions.isEmpty { - let controller = PremiumGiftAttachmentScreen(context: context, peerId: peer.id, options: premiumGiftOptions, source: .attachMenu, pushController: { [weak self] c in + let controller = PremiumGiftAttachmentScreen(context: context, peerIds: [peer.id], options: premiumGiftOptions, source: .attachMenu, pushController: { [weak self] c in if let strongSelf = self { strongSelf.push(c) } diff --git a/submodules/TelegramUI/Sources/ComposeControllerNode.swift b/submodules/TelegramUI/Sources/ComposeControllerNode.swift index 380d2f4ec8..897ad34313 100644 --- a/submodules/TelegramUI/Sources/ComposeControllerNode.swift +++ b/submodules/TelegramUI/Sources/ComposeControllerNode.swift @@ -51,7 +51,7 @@ final class ComposeControllerNode: ASDisplayNode { ContactListAdditionalOption(title: self.presentationData.strings.Compose_NewChannel, icon: .generic(UIImage(bundleImageName: "Contact List/CreateChannelActionIcon")!), action: { openCreateNewChannelImpl?() }) - ], includeChatList: false)), displayPermissionPlaceholder: false) + ], includeChatList: false, topPeers: false)), displayPermissionPlaceholder: false) super.init() diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift index 888cc24934..17cd8ae523 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift @@ -201,6 +201,13 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection self.rightNavigationButton = rightNavigationButton self.navigationItem.rightBarButtonItem = self.rightNavigationButton rightNavigationButton.isEnabled = true //count != 0 || self.params.alwaysEnabled + case .premiumGifting: + let maxCount: Int32 = 10 + var count = 0 + if case let .contacts(contactsNode) = self.contactsNode.contentNode { + count = contactsNode.selectionState?.selectedPeerIndices.count ?? 0 + } + self.titleView.title = CounterContollerTitle(title: self.presentationData.strings.Premium_Gift_ContactSelection_Title, counter: "\(count)/\(maxCount)") case .channelCreation: self.titleView.title = CounterContollerTitle(title: self.presentationData.strings.GroupInfo_AddParticipantTitle, counter: "") let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed)) @@ -310,7 +317,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection switch strongSelf.mode { case .groupCreation, .peerSelection, .chatSelection: strongSelf.rightNavigationButton?.isEnabled = updatedCount != 0 || strongSelf.params.alwaysEnabled - case .channelCreation: + case .channelCreation, .premiumGifting: break } @@ -318,6 +325,9 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection case .groupCreation: let maxCount: Int32 = strongSelf.limitsConfiguration?.maxSupergroupMemberCount ?? 5000 strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.Compose_NewGroupTitle, counter: "\(updatedCount)/\(maxCount)") + case .premiumGifting: + let maxCount: Int32 = 10 + strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.Premium_Gift_ContactSelection_Title, counter: "\(updatedCount)/\(maxCount)") case .peerSelection, .channelCreation, .chatSelection: break } @@ -389,13 +399,16 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection switch strongSelf.mode { case .groupCreation, .peerSelection, .chatSelection: strongSelf.rightNavigationButton?.isEnabled = updatedCount != 0 || strongSelf.params.alwaysEnabled - case .channelCreation: + case .channelCreation, .premiumGifting: break } switch strongSelf.mode { case .groupCreation: let maxCount: Int32 = strongSelf.limitsConfiguration?.maxSupergroupMemberCount ?? 5000 strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.Compose_NewGroupTitle, counter: "\(updatedCount)/\(maxCount)") + case .premiumGifting: + let maxCount: Int32 = 10 + strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.Premium_Gift_ContactSelection_Title, counter: "\(updatedCount)/\(maxCount)") case .peerSelection, .channelCreation, .chatSelection: break } @@ -500,8 +513,14 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection } } self.contactsNode.complete = { [weak self] in - if let strongSelf = self, let rightBarButtonItem = strongSelf.navigationItem.rightBarButtonItem, rightBarButtonItem.isEnabled { - strongSelf.rightNavigationButtonPressed() + if let strongSelf = self { + var available = true + if let rightBarButtonItem = strongSelf.navigationItem.rightBarButtonItem { + available = rightBarButtonItem.isEnabled + } + if available { + strongSelf.rightNavigationButtonPressed() + } } } diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift index 725d1259e5..b47a2a5eb7 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift @@ -12,6 +12,7 @@ import ChatListUI import AnimationCache import MultiAnimationRenderer import EditableTokenListNode +import SolidRoundedButtonNode private struct SearchResultEntry: Identifiable { let index: Int @@ -72,6 +73,8 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { private let animationCache: AnimationCache private let animationRenderer: MultiAnimationRenderer + private let footerPanelNode: FooterPanelNode? + private let isPeerEnabled: ((EnginePeer) -> Bool)? init(navigationBar: NavigationBar?, context: AccountContext, presentationData: PresentationData, mode: ContactMultiselectionControllerMode, isPeerEnabled: ((EnginePeer) -> Bool)?, attemptDisabledItemSelection: ((EnginePeer) -> Void)?, options: [ContactListAdditionalOption], filters: [ContactListFilter], limit: Int32?, reachedSelectionLimit: ((Int32) -> Void)?) { @@ -85,18 +88,28 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { self.isPeerEnabled = isPeerEnabled + var proceedImpl: (() -> Void)? + var placeholder: String var includeChatList = false switch mode { - case let .peerSelection(_, searchGroups, searchChannels): - includeChatList = searchGroups || searchChannels - if searchGroups { - placeholder = self.presentationData.strings.Contacts_SearchUsersAndGroupsLabel - } else { - placeholder = self.presentationData.strings.Contacts_SearchLabel - } - default: - placeholder = self.presentationData.strings.Compose_TokenListPlaceholder + case let .peerSelection(_, searchGroups, searchChannels): + includeChatList = searchGroups || searchChannels + if searchGroups { + placeholder = self.presentationData.strings.Contacts_SearchUsersAndGroupsLabel + } else { + placeholder = self.presentationData.strings.Contacts_SearchLabel + } + self.footerPanelNode = nil + case .premiumGifting: + placeholder = self.presentationData.strings.Premium_Gift_ContactSelection_Placeholder + + self.footerPanelNode = FooterPanelNode(theme: self.presentationData.theme, strings: self.presentationData.strings, action: { + proceedImpl?() + }) + default: + placeholder = self.presentationData.strings.Compose_TokenListPlaceholder + self.footerPanelNode = nil } if case let .chatSelection(chatSelection) = mode { @@ -132,7 +145,11 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { } self.contentNode = .chats(chatListNode) } else { - self.contentNode = .contacts(ContactListNode(context: context, presentation: .single(.natural(options: options, includeChatList: includeChatList)), filters: filters, selectionState: ContactListNodeGroupSelectionState())) + var displayTopPeers = false + if case .premiumGifting = mode { + displayTopPeers = true + } + self.contentNode = .contacts(ContactListNode(context: context, presentation: .single(.natural(options: options, includeChatList: includeChatList, topPeers: displayTopPeers)), filters: filters, selectionState: ContactListNodeGroupSelectionState())) } self.tokenListNode = EditableTokenListNode(context: self.context, presentationTheme: self.presentationData.theme, theme: EditableTokenListNodeTheme(backgroundColor: .clear, separatorColor: self.presentationData.theme.rootController.navigationBar.separatorColor, placeholderTextColor: self.presentationData.theme.list.itemPlaceholderTextColor, primaryTextColor: self.presentationData.theme.list.itemPrimaryTextColor, tokenBackgroundColor: self.presentationData.theme.list.itemCheckColors.strokeColor.withAlphaComponent(0.25), selectedTextColor: self.presentationData.theme.list.itemCheckColors.foregroundColor, selectedBackgroundColor: self.presentationData.theme.list.itemCheckColors.fillColor, accentColor: self.presentationData.theme.list.itemAccentColor, keyboardColor: self.presentationData.theme.rootController.keyboardColor), placeholder: placeholder) @@ -215,6 +232,8 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { searchGroups = true searchChannels = true globalSearch = false + case .premiumGifting: + searchChatList = true } let searchResultsNode = ContactListNode(context: context, presentation: .single(.search(signal: searchText.get(), searchChatList: searchChatList, searchDeviceContacts: false, searchGroups: searchGroups, searchChannels: searchChannels, globalSearch: globalSearch)), filters: filters, isPeerEnabled: strongSelf.isPeerEnabled, selectionState: selectionState, isSearch: true) searchResultsNode.openPeer = { peer, _ in @@ -248,6 +267,13 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { self.tokenListNode.textReturned = { [weak self] in self?.complete?() } + + if let footerPanelNode = self.footerPanelNode { + proceedImpl = { [weak self] in + self?.complete?() + } + self.addSubnode(footerPanelNode) + } } deinit { @@ -284,6 +310,21 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { insets.top += tokenListHeight headerInsets.top += tokenListHeight + if let footerPanelNode = self.footerPanelNode { + var count = 0 + if case let .contacts(contactListNode) = self.contentNode { + count = contactListNode.selectionState?.selectedPeerIndices.count ?? 0 + } + footerPanelNode.count = count + let panelHeight = footerPanelNode.updateLayout(width: layout.size.width, sideInset: layout.safeInsets.left, bottomInset: layout.intrinsicInsets.bottom, transition: transition) + if count == 0 { + transition.updateFrame(node: footerPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height), size: CGSize(width: layout.size.width, height: panelHeight))) + } else { + insets.bottom += panelHeight + transition.updateFrame(node: footerPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - panelHeight), size: CGSize(width: layout.size.width, height: panelHeight))) + } + } + switch self.contentNode { case let .contacts(contactsNode): contactsNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), headerInsets: headerInsets, storiesInset: 0.0, transition: transition) @@ -318,3 +359,68 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { }) } } + + +private final class FooterPanelNode: ASDisplayNode { + private let theme: PresentationTheme + private let strings: PresentationStrings + + private let separatorNode: ASDisplayNode + private let button: SolidRoundedButtonView + + private var validLayout: (CGFloat, CGFloat, CGFloat)? + + var count: Int = 0 { + didSet { + if self.count != oldValue && self.count > 0 { + self.button.title = self.strings.Premium_Gift_ContactSelection_Proceed + self.button.badge = "\(self.count)" + + if let (width, sideInset, bottomInset) = self.validLayout { + let _ = self.updateLayout(width: width, sideInset: sideInset, bottomInset: bottomInset, transition: .immediate) + } + } + } + } + + init(theme: PresentationTheme, strings: PresentationStrings, action: @escaping () -> Void) { + self.theme = theme + self.strings = strings + + self.separatorNode = ASDisplayNode() + self.separatorNode.backgroundColor = theme.rootController.navigationBar.separatorColor + + self.button = SolidRoundedButtonView(theme: SolidRoundedButtonTheme(theme: theme), height: 48.0, cornerRadius: 10.0) + + super.init() + + self.backgroundColor = theme.rootController.navigationBar.opaqueBackgroundColor + + self.addSubnode(self.separatorNode) + + self.button.pressed = { + action() + } + } + + override func didLoad() { + super.didLoad() + self.view.addSubview(self.button) + } + + func updateLayout(width: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + self.validLayout = (width, sideInset, bottomInset) + let topInset: CGFloat = 9.0 + var bottomInset = bottomInset + bottomInset += topInset - (bottomInset.isZero ? 0.0 : 4.0) + + let buttonInset: CGFloat = 16.0 + sideInset + let buttonWidth = width - buttonInset * 2.0 + let buttonHeight = self.button.updateLayout(width: buttonWidth, transition: transition) + transition.updateFrame(view: self.button, frame: CGRect(x: buttonInset, y: topInset, width: buttonWidth, height: buttonHeight)) + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) + + return topInset + buttonHeight + bottomInset + } +} diff --git a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift index ad60e31dd4..762797102a 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift @@ -67,7 +67,7 @@ final class ContactSelectionControllerNode: ASDisplayNode { self.filters = filters var contextActionImpl: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? - self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: .single(.natural(options: options, includeChatList: false)), filters: filters, displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _, _ in + self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: .single(.natural(options: options, includeChatList: false, topPeers: false)), filters: filters, displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _, _ in contextActionImpl?(peer, node, gesture, nil) } : nil, multipleSelection: multipleSelection)