diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index c0de01da10..19b0a1241e 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -10364,6 +10364,8 @@ Sorry for the inconvenience."; "Chat.Giveaway.Info.DidntWin" = "You didn't win a prize in this giveaway."; "Chat.Giveaway.Info.ViewPrize" = "View My Prize"; +"Chat.Giveaway.Info.FullDate" = "**%1$@** on **%2$@**"; + "Chat.Giveaway.Toast.NotAllowed" = "You can't participate in this giveaway."; "Chat.Giveaway.Toast.Participating" = "You are participating in this giveaway."; "Chat.Giveaway.Toast.NotQualified" = "You are not qualified for this giveaway yet."; @@ -10423,3 +10425,10 @@ Sorry for the inconvenience."; "Channel.ChannelColor" = "Channel Color"; "TextFormat.Code" = "Code"; + +"Notification.ChannelJoinedByYou" = "You joined the channel"; + +"CountriesList.SelectCountries" = "Select Countries"; +"CountriesList.SaveCountries" = "Save Countries"; +"CountriesList.SelectUpTo_1" = "select up to %@ country"; +"CountriesList.SelectUpTo_any" = "select up to %@ countries"; diff --git a/submodules/DatePickerNode/Sources/DatePickerNode.swift b/submodules/DatePickerNode/Sources/DatePickerNode.swift index 06fb60598f..61ca6ce416 100644 --- a/submodules/DatePickerNode/Sources/DatePickerNode.swift +++ b/submodules/DatePickerNode/Sources/DatePickerNode.swift @@ -666,6 +666,13 @@ public final class DatePickerNode: ASDisplayNode { } } + if let date = calendar.date(from: dateComponents), date > self.maximumDate { + let maximumDateComponents = calendar.dateComponents([.hour, .minute, .day, .month, .year], from: self.maximumDate) + if let hour = maximumDateComponents.hour { + dateComponents.hour = hour - 1 + } + } + if let date = calendar.date(from: dateComponents), date >= self.minimumDate && date < self.maximumDate { let updatedState = State(minDate: self.state.minDate, maxDate: self.state.maxDate, date: date, displayingMonthSelection: self.state.displayingMonthSelection, displayingDateSelection: self.state.displayingDateSelection, displayingTimeSelection: self.state.displayingTimeSelection, selectedMonth: monthNode.month) self.updateState(updatedState, animated: false) diff --git a/submodules/PremiumUI/Sources/CreateGiveawayController.swift b/submodules/PremiumUI/Sources/CreateGiveawayController.swift index 6b9b7dbb02..6e53d10a43 100644 --- a/submodules/PremiumUI/Sources/CreateGiveawayController.swift +++ b/submodules/PremiumUI/Sources/CreateGiveawayController.swift @@ -896,50 +896,9 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio buyActionImpl = { [weak controller] in let state = stateValue.with { $0 } - guard let products = productsValue.with({ $0 }), !products.isEmpty else { - return - } - + let presentationData = context.sharedContext.currentPresentationData.with { $0 } - - var selectedProduct: PremiumGiftProduct? - let selectedMonths = state.selectedMonths ?? 12 - switch state.mode { - case .giveaway: - if let product = products.first(where: { $0.months == selectedMonths && $0.giftOption.users == state.subscriptions }) { - selectedProduct = product - } - case .gift: - if let product = products.first(where: { $0.months == selectedMonths && $0.giftOption.users == 1 }) { - selectedProduct = product - } - } - - guard let selectedProduct else { - let alertController = textAlertController(context: context, title: presentationData.strings.BoostGift_ReduceQuantity_Title, text: presentationData.strings.BoostGift_ReduceQuantity_Text("\(state.subscriptions)", "\(selectedMonths)", "\(25)").string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.BoostGift_ReduceQuantity_Reduce, action: { - updateState { state in - var updatedState = state - updatedState.subscriptions = 25 - return updatedState - } - })], parseMarkdown: true) - presentControllerImpl?(alertController) - return - } - - let (currency, amount) = selectedProduct.storeProduct.priceCurrencyAndAmount - - let purpose: AppStoreTransactionPurpose - let quantity: Int32 - switch state.mode { - case .giveaway: - purpose = .giveaway(boostPeer: peerId, additionalPeerIds: state.channels.filter { $0 != peerId }, countries: state.countries, onlyNewSubscribers: state.onlyNewEligible, randomId: Int64.random(in: .min ..< .max), untilDate: state.time, currency: currency, amount: amount) - quantity = selectedProduct.giftOption.storeQuantity - case .gift: - purpose = .giftCode(peerIds: state.peers, boostPeer: peerId, currency: currency, amount: amount) - quantity = Int32(state.peers.count) - } - + updateState { state in var updatedState = state updatedState.updating = true @@ -948,6 +907,47 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio switch subject { case .generic: + guard let products = productsValue.with({ $0 }), !products.isEmpty else { + return + } + var selectedProduct: PremiumGiftProduct? + let selectedMonths = state.selectedMonths ?? 12 + switch state.mode { + case .giveaway: + if let product = products.first(where: { $0.months == selectedMonths && $0.giftOption.users == state.subscriptions }) { + selectedProduct = product + } + case .gift: + if let product = products.first(where: { $0.months == selectedMonths && $0.giftOption.users == 1 }) { + selectedProduct = product + } + } + + guard let selectedProduct else { + let alertController = textAlertController(context: context, title: presentationData.strings.BoostGift_ReduceQuantity_Title, text: presentationData.strings.BoostGift_ReduceQuantity_Text("\(state.subscriptions)", "\(selectedMonths)", "\(25)").string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.BoostGift_ReduceQuantity_Reduce, action: { + updateState { state in + var updatedState = state + updatedState.subscriptions = 25 + return updatedState + } + })], parseMarkdown: true) + presentControllerImpl?(alertController) + return + } + + let (currency, amount) = selectedProduct.storeProduct.priceCurrencyAndAmount + + let purpose: AppStoreTransactionPurpose + let quantity: Int32 + switch state.mode { + case .giveaway: + purpose = .giveaway(boostPeer: peerId, additionalPeerIds: state.channels.filter { $0 != peerId }, countries: state.countries, onlyNewSubscribers: state.onlyNewEligible, randomId: Int64.random(in: .min ..< .max), untilDate: state.time, currency: currency, amount: amount) + quantity = selectedProduct.giftOption.storeQuantity + case .gift: + purpose = .giftCode(peerIds: state.peers, boostPeer: peerId, currency: currency, amount: amount) + quantity = Int32(state.peers.count) + } + let _ = (context.engine.payments.canPurchasePremium(purpose: purpose) |> deliverOnMainQueue).startStandalone(next: { [weak controller] available in if available, let inAppPurchaseManager = context.inAppPurchaseManager { diff --git a/submodules/PremiumUI/Sources/GiveawayInfoController.swift b/submodules/PremiumUI/Sources/GiveawayInfoController.swift index 981c4a7ada..390a02f78b 100644 --- a/submodules/PremiumUI/Sources/GiveawayInfoController.swift +++ b/submodules/PremiumUI/Sources/GiveawayInfoController.swift @@ -57,7 +57,10 @@ public func presentGiveawayInfoController( switch giveawayInfo { case let .ongoing(start, status): - let startDate = stringForDate(timestamp: start, timeZone: timeZone, strings: presentationData.strings) + let startDate = presentationData.strings.Chat_Giveaway_Info_FullDate( + stringForMessageTimestamp(timestamp: start, dateTimeFormat: presentationData.dateTimeFormat), + stringForDate(timestamp: start, timeZone: timeZone, strings: presentationData.strings) + ).string.trimmingCharacters(in: CharacterSet(charactersIn: "*")) title = presentationData.strings.Chat_Giveaway_Info_Title @@ -123,7 +126,11 @@ public func presentGiveawayInfoController( text = "\(intro)\n\n\(ending)\(participation)" case let .finished(status, start, finish, _, activatedCount): - let startDate = stringForDate(timestamp: start, timeZone: timeZone, strings: presentationData.strings) + let startDate = presentationData.strings.Chat_Giveaway_Info_FullDate( + stringForMessageTimestamp(timestamp: start, dateTimeFormat: presentationData.dateTimeFormat), + stringForDate(timestamp: start, timeZone: timeZone, strings: presentationData.strings) + ).string.trimmingCharacters(in: CharacterSet(charactersIn: "*")) + let finishDate = stringForDate(timestamp: finish, timeZone: timeZone, strings: presentationData.strings) title = presentationData.strings.Chat_Giveaway_Info_EndedTitle diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index de36e834e4..82e7b01c0e 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -22,7 +22,7 @@ import ShareController import ItemListPeerActionItem import PremiumUI -private let maxUsersDisplayedLimit: Int32 = 5 +private let initialBoostersDisplayedLimit: Int32 = 5 private final class ChannelStatsControllerArguments { let context: AccountContext @@ -652,17 +652,20 @@ public enum ChannelStatsSection { private struct ChannelStatsControllerState: Equatable { let section: ChannelStatsSection let boostersExpanded: Bool + let moreBoostersDisplayed: Int32 let giftsSelected: Bool init() { self.section = .stats self.boostersExpanded = false + self.moreBoostersDisplayed = 0 self.giftsSelected = false } - init(section: ChannelStatsSection, boostersExpanded: Bool, giftsSelected: Bool) { + init(section: ChannelStatsSection, boostersExpanded: Bool, moreBoostersDisplayed: Int32, giftsSelected: Bool) { self.section = section self.boostersExpanded = boostersExpanded + self.moreBoostersDisplayed = moreBoostersDisplayed self.giftsSelected = giftsSelected } @@ -673,6 +676,9 @@ private struct ChannelStatsControllerState: Equatable { if lhs.boostersExpanded != rhs.boostersExpanded { return false } + if lhs.moreBoostersDisplayed != rhs.moreBoostersDisplayed { + return false + } if lhs.giftsSelected != rhs.giftsSelected { return false } @@ -680,15 +686,19 @@ private struct ChannelStatsControllerState: Equatable { } func withUpdatedSection(_ section: ChannelStatsSection) -> ChannelStatsControllerState { - return ChannelStatsControllerState(section: section, boostersExpanded: self.boostersExpanded, giftsSelected: self.giftsSelected) + return ChannelStatsControllerState(section: section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected) } func withUpdatedBoostersExpanded(_ boostersExpanded: Bool) -> ChannelStatsControllerState { - return ChannelStatsControllerState(section: self.section, boostersExpanded: boostersExpanded, giftsSelected: self.giftsSelected) + return ChannelStatsControllerState(section: self.section, boostersExpanded: boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected) + } + + func withUpdatedMoreBoostersDisplayed(_ moreBoostersDisplayed: Int32) -> ChannelStatsControllerState { + return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: moreBoostersDisplayed, giftsSelected: self.giftsSelected) } func withUpdatedGiftsSelected(_ giftsSelected: Bool) -> ChannelStatsControllerState { - return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, giftsSelected: giftsSelected) + return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: giftsSelected) } } @@ -826,20 +836,32 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p var boosterIndex: Int32 = 0 var boosters: [ChannelBoostersContext.State.Boost] = selectedState.boosts - var effectiveExpanded = state.boostersExpanded - if boosters.count > maxUsersDisplayedLimit && !state.boostersExpanded { - boosters = Array(boosters.prefix(Int(maxUsersDisplayedLimit))) + + var limit: Int32 + if state.boostersExpanded { + limit = 25 + state.moreBoostersDisplayed } else { - effectiveExpanded = true + limit = initialBoostersDisplayedLimit } + boosters = Array(boosters.prefix(Int(limit))) for booster in boosters { entries.append(.booster(boosterIndex, presentationData.theme, presentationData.dateTimeFormat, booster)) boosterIndex += 1 } - if !effectiveExpanded { - entries.append(.boostersExpand(presentationData.theme, presentationData.strings.Stats_Boosts_ShowMoreBoosts(Int32(selectedState.count) - maxUsersDisplayedLimit))) + let totalBoostsCount = boosters.reduce(Int32(0)) { partialResult, boost in + return partialResult + boost.multiplier + } + + if totalBoostsCount < selectedState.count { + let moreCount: Int32 + if !state.boostersExpanded { + moreCount = min(80, selectedState.count - totalBoostsCount) + } else { + moreCount = min(200, selectedState.count - totalBoostsCount) + } + entries.append(.boostersExpand(presentationData.theme, presentationData.strings.Stats_Boosts_ShowMoreBoosts(moreCount))) } } @@ -862,8 +884,8 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p } public func channelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: PeerId, section: ChannelStatsSection = .stats, boostStatus: ChannelBoostStatus? = nil, statsDatacenterId: Int32?) -> ViewController { - let statePromise = ValuePromise(ChannelStatsControllerState(section: section, boostersExpanded: false, giftsSelected: false), ignoreRepeated: true) - let stateValue = Atomic(value: ChannelStatsControllerState(section: section, boostersExpanded: false, giftsSelected: false)) + let statePromise = ValuePromise(ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false), ignoreRepeated: true) + let stateValue = Atomic(value: ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false)) let updateState: ((ChannelStatsControllerState) -> ChannelStatsControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } @@ -993,7 +1015,20 @@ public func channelStatsController(context: AccountContext, updatedPresentationD pushImpl?(controller) }, expandBoosters: { - updateState { $0.withUpdatedBoostersExpanded(true) } + var giftsSelected = false + updateState { state in + giftsSelected = state.giftsSelected + if state.boostersExpanded { + return state.withUpdatedMoreBoostersDisplayed(state.moreBoostersDisplayed + 50) + } else { + return state.withUpdatedBoostersExpanded(true) + } + } + if giftsSelected { + giftsContext.loadMore() + } else { + boostsContext.loadMore() + } }, openGifts: { let controller = createGiveawayController(context: context, peerId: peerId, subject: .generic) @@ -1075,12 +1110,6 @@ public func channelStatsController(context: AccountContext, updatedPresentationD } }) } - controller.visibleBottomContentOffsetChanged = { offset in - let state = stateValue.with { $0 } - if case let .known(value) = offset, value < 510.0, case .boosts = state.section, state.boostersExpanded { - boostsContext.loadMore() - } - } controller.titleControlValueChanged = { value in updateState { $0.withUpdatedSection(value == 1 ? .boosts : .stats) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 5f870a77b0..d3930bf96e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -535,9 +535,16 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI private var swipeToReplyFeedback: HapticFeedback? private var nameNode: TextNode? + private var nameButtonNode: HighlightTrackingButtonNode? + private var nameHighlightNode: ASImageNode? + private var adminBadgeNode: TextNode? private var credibilityIconView: ComponentHostView? private var credibilityIconComponent: EmojiStatusComponent? + private var credibilityIconContent: EmojiStatusComponent.Content? + private var credibilityButtonNode: HighlightTrackingButtonNode? + private var credibilityHighlightNode: ASImageNode? + private var closeButtonNode: HighlightTrackingButtonNode? private var closeIconNode: ASImageNode? @@ -1064,7 +1071,15 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return .fail } } - + + if let nameButtonNode = strongSelf.nameButtonNode, nameButtonNode.frame.contains(point) { + return .fail + } + + if let credibilityButtonNode = strongSelf.credibilityButtonNode, credibilityButtonNode.frame.contains(point) { + return .fail + } + if let nameNode = strongSelf.nameNode, nameNode.frame.contains(point) { if let item = strongSelf.item { for attribute in item.message.attributes { @@ -1540,6 +1555,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI var hasInstantVideo = false for contentNodeItemValue in contentNodeMessagesAndClasses { let contentNodeItem = contentNodeItemValue as (message: Message, type: AnyClass, attributes: ChatMessageEntryAttributes, bubbleAttributes: BubbleItemAttributes) + if contentNodeItem.type == ChatMessageGiveawayBubbleContentNode.self { + maximumContentWidth = 210.0 + break + } if contentNodeItem.type == ChatMessageInstantVideoBubbleContentNode.self, !contentNodeItem.bubbleAttributes.isAttachment { maximumContentWidth = baseWidth - 20.0 hasInstantVideo = true @@ -2939,6 +2958,44 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI animation.animator.updateFrame(layer: nameNode.layer, frame: nameNodeFrame, completion: nil) } + let nameButtonNode: HighlightTrackingButtonNode + let nameHighlightNode: ASImageNode + if let currentButton = strongSelf.nameButtonNode, let currentHighlight = strongSelf.nameHighlightNode { + nameButtonNode = currentButton + nameHighlightNode = currentHighlight + } else { + nameHighlightNode = ASImageNode() + nameHighlightNode.alpha = 0.0 + nameHighlightNode.displaysAsynchronously = false + nameHighlightNode.isUserInteractionEnabled = false + strongSelf.clippingNode.addSubnode(nameHighlightNode) + strongSelf.nameHighlightNode = nameHighlightNode + + nameButtonNode = HighlightTrackingButtonNode() + nameButtonNode.highligthedChanged = { [weak nameHighlightNode] highlighted in + guard let nameHighlightNode else { + return + } + if highlighted { + nameHighlightNode.layer.removeAnimation(forKey: "opacity") + nameHighlightNode.alpha = 1.0 + } else { + nameHighlightNode.alpha = 0.0 + nameHighlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } + nameButtonNode.addTarget(strongSelf, action: #selector(strongSelf.nameButtonPressed), forControlEvents: .touchUpInside) + strongSelf.clippingNode.addSubnode(nameButtonNode) + strongSelf.nameButtonNode = nameButtonNode + } + nameHighlightNode.frame = nameNodeFrame.insetBy(dx: -2.0, dy: -1.0) + nameButtonNode.frame = nameNodeFrame.insetBy(dx: -2.0, dy: -3.0) + + let nameColor = authorNameColor ?? item.presentationData.theme.theme.chat.message.outgoing.accentTextColor + if "".isEmpty { + nameHighlightNode.image = generateFilledRoundedRectImage(size: CGSize(width: 8.0, height: 8.0), cornerRadius: 4.0, color: nameColor.withAlphaComponent(0.1))?.stretchableImage(withLeftCapWidth: 4, topCapHeight: 4) + } + if let currentCredibilityIcon = currentCredibilityIcon { let credibilityIconView: ComponentHostView if let current = strongSelf.credibilityIconView { @@ -2963,6 +3020,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI action: nil ) strongSelf.credibilityIconComponent = credibilityIconComponent + strongSelf.credibilityIconContent = currentCredibilityIcon let credibilityIconSize = credibilityIconView.update( transition: .immediate, @@ -2971,10 +3029,49 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI containerSize: CGSize(width: 20.0, height: 20.0) ) - credibilityIconView.frame = CGRect(origin: CGPoint(x: nameNode.frame.maxX + 3.0, y: nameNode.frame.minY + floor((nameNode.bounds.height - credibilityIconSize.height) / 2.0)), size: credibilityIconSize) + let credibilityIconFrame = CGRect(origin: CGPoint(x: nameNode.frame.maxX + 3.0, y: nameNode.frame.minY + floor((nameNode.bounds.height - credibilityIconSize.height) / 2.0)), size: credibilityIconSize) + credibilityIconView.frame = credibilityIconFrame + + let credibilityButtonNode: HighlightTrackingButtonNode + let credibilityHighlightNode: ASImageNode + if let currentButton = strongSelf.credibilityButtonNode, let currentHighlight = strongSelf.credibilityHighlightNode { + credibilityButtonNode = currentButton + credibilityHighlightNode = currentHighlight + } else { + credibilityHighlightNode = ASImageNode() + credibilityHighlightNode.alpha = 0.0 + credibilityHighlightNode.displaysAsynchronously = false + credibilityHighlightNode.isUserInteractionEnabled = false + strongSelf.clippingNode.addSubnode(credibilityHighlightNode) + strongSelf.credibilityHighlightNode = credibilityHighlightNode + + credibilityButtonNode = HighlightTrackingButtonNode() + credibilityButtonNode.highligthedChanged = { [weak credibilityHighlightNode] highlighted in + guard let credibilityHighlightNode else { + return + } + if highlighted { + credibilityHighlightNode.layer.removeAnimation(forKey: "opacity") + credibilityHighlightNode.alpha = 1.0 + } else { + credibilityHighlightNode.alpha = 0.0 + credibilityHighlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } + credibilityButtonNode.addTarget(strongSelf, action: #selector(strongSelf.credibilityButtonPressed), forControlEvents: .touchUpInside) + strongSelf.clippingNode.addSubnode(credibilityButtonNode) + strongSelf.credibilityButtonNode = credibilityButtonNode + } + credibilityHighlightNode.frame = credibilityIconFrame.insetBy(dx: -1.0, dy: -1.0) + credibilityButtonNode.frame = credibilityIconFrame.insetBy(dx: -2.0, dy: -3.0) + + if "".isEmpty { + credibilityHighlightNode.image = generateFilledRoundedRectImage(size: CGSize(width: 8.0, height: 8.0), cornerRadius: 4.0, color: nameColor.withAlphaComponent(0.1))?.stretchableImage(withLeftCapWidth: 4, topCapHeight: 4) + } } else { strongSelf.credibilityIconView?.removeFromSuperview() strongSelf.credibilityIconView = nil + strongSelf.credibilityIconContent = nil } if let adminBadgeNode = adminNodeSizeApply.1() { @@ -3074,6 +3171,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI strongSelf.adminBadgeNode = nil strongSelf.credibilityIconView?.removeFromSuperview() strongSelf.credibilityIconView = nil + strongSelf.nameButtonNode?.removeFromSupernode() + strongSelf.nameButtonNode = nil + strongSelf.nameHighlightNode?.removeFromSupernode() + strongSelf.nameHighlightNode = nil + strongSelf.credibilityButtonNode?.removeFromSupernode() + strongSelf.credibilityButtonNode = nil + strongSelf.credibilityHighlightNode?.removeFromSupernode() + strongSelf.credibilityHighlightNode = nil } } @@ -4420,6 +4525,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return result } + if let nameButtonNode = self.nameButtonNode, nameButtonNode.frame.contains(point) { + return nameButtonNode.view + } + + if let credibilityButtonNode = self.credibilityButtonNode, credibilityButtonNode.frame.contains(point) { + return credibilityButtonNode.view + } + if let shareButtonNode = self.shareButtonNode, shareButtonNode.frame.contains(point) { return shareButtonNode.view } @@ -4827,6 +4940,28 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } } + @objc private func nameButtonPressed() { + if let item = self.item, let peer = item.message.author { + let messageReference = MessageReference(item.message) + if let channel = peer as? TelegramChannel, case .broadcast = channel.info { + item.controllerInteraction.openPeer(EnginePeer(peer), .chat(textInputState: nil, subject: nil, peekData: nil), messageReference, .default) + } else { + item.controllerInteraction.openPeer(EnginePeer(peer), .info, messageReference, .groupParticipant(storyStats: nil, avatarHeaderNode: nil)) + } + } + } + + @objc private func credibilityButtonPressed() { + if let item = self.item, let credibilityIconView = self.credibilityIconView, let iconContent = self.credibilityIconContent, let peer = item.message.author { + var emojiFileId: Int64? + if case let .animation(content, _, _, _, _) = iconContent { + emojiFileId = content.fileId.id + } + + item.controllerInteraction.openPremiumStatusInfo(peer.id, credibilityIconView, emojiFileId, peer.nameColor ?? .blue) + } + } + private var playedSwipeToReplyHaptic = false @objc private func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { var offset: CGFloat = 0.0 diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift index 9679cac715..2c1b52aba4 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift @@ -433,7 +433,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode } } } - let (channelsWidth, continueChannelLayout) = makeChannelsLayout(item.context, 240.0, channelPeers, accentColor, accentColor.withAlphaComponent(0.1)) + let (channelsWidth, continueChannelLayout) = makeChannelsLayout(item.context, 220.0, channelPeers, accentColor, accentColor.withAlphaComponent(0.1)) maxContentWidth = max(maxContentWidth, channelsWidth) maxContentWidth += 30.0 diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index 047d39e37d..ea1badefea 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -569,6 +569,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, saveMediaToFiles: { _ in }, openNoAdsDemo: { }, displayGiveawayParticipationStatus: { _ in + }, openPremiumStatusInfo: { _, _, _, _ in }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index a1857ed653..bfe67334c6 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -209,6 +209,7 @@ public final class ChatControllerInteraction { public let saveMediaToFiles: (EngineMessage.Id) -> Void public let openNoAdsDemo: () -> Void public let displayGiveawayParticipationStatus: (EngineMessage.Id) -> Void + public let openPremiumStatusInfo: (EnginePeer.Id, UIView, Int64?, PeerNameColor) -> Void public let requestMessageUpdate: (MessageId, Bool) -> Void public let cancelInteractiveKeyboardGestures: () -> Void @@ -327,6 +328,7 @@ public final class ChatControllerInteraction { saveMediaToFiles: @escaping (EngineMessage.Id) -> Void, openNoAdsDemo: @escaping () -> Void, displayGiveawayParticipationStatus: @escaping (EngineMessage.Id) -> Void, + openPremiumStatusInfo: @escaping (EnginePeer.Id, UIView, Int64?, PeerNameColor) -> Void, requestMessageUpdate: @escaping (MessageId, Bool) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, dismissTextInput: @escaping () -> Void, @@ -427,6 +429,7 @@ public final class ChatControllerInteraction { self.saveMediaToFiles = saveMediaToFiles self.openNoAdsDemo = openNoAdsDemo self.displayGiveawayParticipationStatus = displayGiveawayParticipationStatus + self.openPremiumStatusInfo = openPremiumStatusInfo self.requestMessageUpdate = requestMessageUpdate self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures self.dismissTextInput = dismissTextInput diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift index a4d93ce24f..db68431ac0 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift @@ -829,9 +829,9 @@ final class CountriesMultiselectionScreenComponent: Component { } navigationButtonsWidth += navigationLeftButtonSize.width + navigationSideInset - let actionButtonTitle = "Save Countries" - let title = "Select Countries" - let subtitle = "select up to \(component.context.userLimits.maxGiveawayCountriesCount) countries" + let actionButtonTitle = environment.strings.CountriesList_SaveCountries + let title = environment.strings.CountriesList_SelectCountries + let subtitle = environment.strings.CountriesList_SelectUpTo(component.context.userLimits.maxGiveawayCountriesCount) let titleComponent = AnyComponent( List([ diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift index 0a2bde11db..241ee1d82d 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift @@ -588,17 +588,22 @@ public extension ShareWithPeersScreen { case let .channels(excludePeerIds, searchQuery): self.stateDisposable = (combineLatest( context.engine.messages.chatList(group: .root, count: 500) |> take(1), + searchQuery.flatMap { context.engine.contacts.searchLocalPeers(query: $0) } ?? .single([]), context.engine.data.get(EngineDataMap(Array(self.initialPeerIds).map(TelegramEngine.EngineData.Item.Peer.Peer.init))) ) - |> mapToSignal { chatList, initialPeers -> Signal<(EngineChatList, [EnginePeer.Id: Optional], [EnginePeer.Id: Optional]), NoError> in + |> mapToSignal { chatList, searchResults, initialPeers -> Signal<(EngineChatList, [EngineRenderedPeer], [EnginePeer.Id: Optional], [EnginePeer.Id: Optional]), NoError> in + var peerIds: [EnginePeer.Id] = [] + peerIds.append(contentsOf: chatList.items.map(\.renderedPeer.peerId)) + peerIds.append(contentsOf: searchResults.map(\.peerId)) + peerIds.append(contentsOf: initialPeers.compactMap(\.value?.id)) return context.engine.data.subscribe( EngineDataMap(chatList.items.map(\.renderedPeer.peerId).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init)) ) - |> map { participantCountMap -> (EngineChatList, [EnginePeer.Id: Optional], [EnginePeer.Id: Optional]) in - return (chatList, initialPeers, participantCountMap) + |> map { participantCountMap -> (EngineChatList, [EngineRenderedPeer], [EnginePeer.Id: Optional], [EnginePeer.Id: Optional]) in + return (chatList, searchResults, initialPeers, participantCountMap) } } - |> deliverOnMainQueue).start(next: { [weak self] chatList, initialPeers, participantCounts in + |> deliverOnMainQueue).start(next: { [weak self] chatList, searchResults, initialPeers, participantCounts in guard let self else { return } @@ -612,7 +617,7 @@ public extension ShareWithPeersScreen { var existingIds = Set() var selectedPeers: [EnginePeer] = [] - + for item in chatList.items.reversed() { if let peer = item.renderedPeer.peer { if self.initialPeerIds.contains(peer.id) { @@ -628,6 +633,13 @@ public extension ShareWithPeersScreen { existingIds.insert(peerId) } } + + for item in searchResults { + if let peer = item.peer, case let .channel(channel) = peer, case .broadcast = channel.info { + selectedPeers.append(peer) + existingIds.insert(peer.id) + } + } let queryTokens = stringIndexTokens(searchQuery ?? "", transliteration: .combined) func peerMatchesTokens(peer: EnginePeer, tokens: [ValueBoxKey]) -> Bool { @@ -640,9 +652,15 @@ public extension ShareWithPeersScreen { var peers: [EnginePeer] = [] peers = chatList.items.filter { peer in if let peer = peer.renderedPeer.peer { + if existingIds.contains(peer.id) { + return false + } if excludePeerIds.contains(peer.id) { return false } + if peer.isFake || peer.isScam { + return false + } if let _ = searchQuery, !peerMatchesTokens(peer: peer, tokens: queryTokens) { return false } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index bf05960376..29150c715c 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -4852,6 +4852,65 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.present(controller, in: .current) })) + }, openPremiumStatusInfo: { [weak self] peerId, sourceView, peerStatus, nameColor in + guard let self else { + return + } + + let context = self.context + let source: Signal + if let peerStatus { + source = context.engine.stickers.resolveInlineStickers(fileIds: [peerStatus]) + |> mapToSignal { files in + if let file = files[peerStatus] { + var reference: StickerPackReference? + for attribute in file.attributes { + if case let .CustomEmoji(_, _, _, packReference) = attribute, let packReference = packReference { + reference = packReference + break + } + } + + if let reference { + return context.engine.stickers.loadedStickerPack(reference: reference, forceActualized: false) + |> filter { result in + if case .result = result { + return true + } else { + return false + } + } + |> take(1) + |> mapToSignal { result -> Signal in + if case let .result(_, items, _) = result { + return .single(.emojiStatus(peerId, peerStatus, items.first?.file, result)) + } else { + return .single(.emojiStatus(peerId, peerStatus, nil, nil)) + } + } + } else { + return .single(.emojiStatus(peerId, peerStatus, nil, nil)) + } + } else { + return .single(.emojiStatus(peerId, peerStatus, nil, nil)) + } + } + } else { + source = .single(.profile(peerId)) + } + + let _ = (source + |> deliverOnMainQueue).startStandalone(next: { [weak self] source in + guard let self else { + return + } + let controller = PremiumIntroScreen(context: self.context, source: source) + controller.sourceView = sourceView + controller.containerView = self.navigationController?.view + controller.animationColor = self.context.peerNameColors.get(nameColor, dark: self.presentationData.theme.overallDarkAppearance).main + self.push(controller) + }) + }, requestMessageUpdate: { [weak self] id, scroll in if let self { self.chatDisplayNode.historyNode.requestMessageUpdate(id, andScrollToItem: scroll) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 3efb7f2db5..df161c0d6c 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -565,6 +565,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState var loadStickerSaveStatus: MediaId? var loadCopyMediaResource: MediaResource? var isAction = false + var isGiveawayLaunch = false var diceEmoji: String? if messages.count == 1 { for media in messages[0].media { @@ -579,6 +580,9 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } else if media is TelegramMediaAction || media is TelegramMediaExpiredContent { isAction = true + if let action = media as? TelegramMediaAction, case .giveawayLaunched = action.action { + isGiveawayLaunch = true + } } else if let image = media as? TelegramMediaImage { if !messages[0].containsSecretMedia { loadCopyMediaResource = largestImageRepresentation(image.representations)?.resource @@ -639,6 +643,10 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState canPin = false } + if isGiveawayLaunch { + canReply = false + } + if let peer = messages[0].peers[messages[0].id.peerId] { if peer.isDeleted { canPin = false @@ -925,7 +933,6 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState resourceAvailable = false } - if !isPremium && isDownloading { var isLargeFile = false for media in message.media { diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 6a0ef98092..4a656b7a49 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -170,6 +170,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, saveMediaToFiles: { _ in }, openNoAdsDemo: { }, displayGiveawayParticipationStatus: { _ in + }, openPremiumStatusInfo: { _, _, _, _ in }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index b43f14cf92..05f743cb9d 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -2921,6 +2921,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, saveMediaToFiles: { _ in }, openNoAdsDemo: { }, displayGiveawayParticipationStatus: { _ in + }, openPremiumStatusInfo: { _, _, _, _ in }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { @@ -3794,37 +3795,31 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return } - let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId)) - |> deliverOnMainQueue).startStandalone(next: { [weak self] _ in + let source: Signal + if let peerStatus = peerStatus { + source = emojiStatusFileAndPack + |> take(1) + |> mapToSignal { emojiStatusFileAndPack -> Signal in + if let (file, pack) = emojiStatusFileAndPack { + return .single(.emojiStatus(strongSelf.peerId, peerStatus.fileId, file, pack)) + } else { + return .complete() + } + } + } else { + source = .single(.profile(strongSelf.peerId)) + } + + let _ = (source + |> deliverOnMainQueue).startStandalone(next: { [weak self] source in guard let strongSelf = self else { return } - let source: Signal - if let peerStatus = peerStatus { - source = emojiStatusFileAndPack - |> take(1) - |> mapToSignal { emojiStatusFileAndPack -> Signal in - if let (file, pack) = emojiStatusFileAndPack { - return .single(.emojiStatus(strongSelf.peerId, peerStatus.fileId, file, pack)) - } else { - return .complete() - } - } - } else { - source = .single(.profile(strongSelf.peerId)) - } - - let _ = (source - |> deliverOnMainQueue).startStandalone(next: { [weak self] source in - guard let strongSelf = self else { - return - } - let controller = PremiumIntroScreen(context: strongSelf.context, source: source) - controller.sourceView = sourceView - controller.containerView = strongSelf.controller?.navigationController?.view - controller.animationColor = white ? .white : strongSelf.presentationData.theme.list.itemAccentColor - strongSelf.controller?.push(controller) - }) + let controller = PremiumIntroScreen(context: strongSelf.context, source: source) + controller.sourceView = sourceView + controller.containerView = strongSelf.controller?.navigationController?.view + controller.animationColor = white ? .white : strongSelf.presentationData.theme.list.itemAccentColor + strongSelf.controller?.push(controller) }) } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 3da93fdb92..3a3ad0197f 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1559,6 +1559,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, saveMediaToFiles: { _ in }, openNoAdsDemo: { }, displayGiveawayParticipationStatus: { _ in + }, openPremiumStatusInfo: { _, _, _, _ in }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: {