diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 4625f9d971..6443c3d4dc 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12466,3 +12466,9 @@ Sorry for the inconvenience."; "WebApp.MinimizedTitleFormat" = "%1$@ & %2$@"; "WebApp.MinimizedTitle.Others_1" = "%@ Other"; "WebApp.MinimizedTitle.Others_any" = "%@ Others"; + +"Stars.SendStars.Title" = "Send Stars"; +"Stars.SendStars.AmountTitle" = "ENTER AMOUNT"; +"Stars.SendStars.AmountPlaceholder" = "Stars Amount"; +"Stars.SendStars.AmountInfo" = "Send %@ or more to highlight your profile in the TOP 3 supporters of this message."; +"Stars.SendStars.SendStars" = "Confirm and Send"; diff --git a/submodules/Display/Source/Navigation/NavigationController.swift b/submodules/Display/Source/Navigation/NavigationController.swift index 6d4ea37b7d..0b75f59ac3 100644 --- a/submodules/Display/Source/Navigation/NavigationController.swift +++ b/submodules/Display/Source/Navigation/NavigationController.swift @@ -153,6 +153,13 @@ open class NavigationController: UINavigationController, ContainableController, open var minimizedContainer: MinimizedContainer? { didSet { self.minimizedContainer?.navigationController = self + self.minimizedContainer?.willMaximize = { [weak self] in + guard let self else { + return + } + self.isMaximizing = true + self.updateContainersNonReentrant(transition: .animated(duration: 0.4, curve: .spring)) + } } } @@ -1576,7 +1583,6 @@ open class NavigationController: UINavigationController, ContainableController, self.updateContainersNonReentrant(transition: .animated(duration: 0.4, curve: .spring)) } - self.minimizedContainer?.removeFromSupernode() self.minimizedContainer = minimizedContainer diff --git a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift index d3f44e84d0..c5cab76799 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift @@ -133,9 +133,10 @@ final class MediaPickerGridItemNode: GridItemNode { private struct SelectionState: Equatable { let selected: Bool + let index: Int? let count: Int } - private let selectionPromise = ValuePromise(SelectionState(selected: false, count: 0)) + private let selectionPromise = ValuePromise(SelectionState(selected: false, index: nil, count: 0)) private let spoilerDisposable = MetaDisposable() var spoilerNode: SpoilerOverlayNode? var priceNode: PriceNode? @@ -256,14 +257,16 @@ final class MediaPickerGridItemNode: GridItemNode { if let interaction = self.interaction, let selectionState = interaction.selectionState { let selected = selectionState.isIdentifierSelected(self.identifier) + var selectionIndex: Int? if let selectableItem = self.selectableItem { let index = selectionState.index(of: selectableItem) if index != NSNotFound { self.checkNode?.content = .counter(Int(index)) + selectionIndex = Int(index) } } self.checkNode?.setSelected(selected, animated: animated) - self.selectionPromise.set(SelectionState(selected: selected, count: selectionState.selectedItems().count)) + self.selectionPromise.set(SelectionState(selected: selected, index: selectionIndex, count: selectionState.selectedItems().count)) } } @@ -292,7 +295,7 @@ final class MediaPickerGridItemNode: GridItemNode { self.durationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) self.draftNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) self.priceNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - if animateSpoilerNode { + if animateSpoilerNode || self.priceNode != nil { self.spoilerNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) } } @@ -573,7 +576,7 @@ final class MediaPickerGridItemNode: GridItemNode { guard let strongSelf = self else { return } - strongSelf.updateHasSpoiler(hasSpoiler, price: selectionState.selected ? price : nil, isSingle: selectionState.count == 1) + strongSelf.updateHasSpoiler(hasSpoiler, price: selectionState.selected ? price : nil, isSingle: selectionState.count == 1 || selectionState.index == 1) })) if self.currentDraftState != nil { diff --git a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift index add3b3da13..78caaba0fe 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift @@ -185,7 +185,7 @@ private class MediaPickerSelectedItemNode: ASDisplayNode { self.didSetupSpoiler = true } - if hasSpoiler || price != nil { + if hasSpoiler { if self.spoilerNode == nil { let spoilerNode = SpoilerOverlayNode(enableAnimations: self.enableAnimations) self.insertSubnode(spoilerNode, aboveSubnode: self.imageNode) @@ -499,6 +499,8 @@ final class PriceNode: ASDisplayNode { super.init() + self.isUserInteractionEnabled = false + self.addSubnode(self.backgroundNode) self.backgroundNode.addSubnode(self.lockNode) self.backgroundNode.addSubnode(self.iconNode) @@ -890,11 +892,24 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS self.reorderFeedback = HapticFeedback() } self.reorderFeedback?.impact() + + let priceTransition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut) + for (_, node) in self.priceNodes { + priceTransition.updateAlpha(node: node, alpha: 0.0) + } } private func endReordering(point: CGPoint?) { if let reorderNode = self.reorderNode { self.reorderNode = nil + + let completion = { + let priceTransition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut) + for (_, node) in self.priceNodes { + node.supernode?.view.bringSubviewToFront(node.view) + priceTransition.updateAlpha(node: node, alpha: 1.0) + } + } if let itemNode = reorderNode.itemNode, let point = point { var targetNode: MediaPickerSelectedItemNode? @@ -910,11 +925,13 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS } reorderNode.animateCompletion(completion: { [weak reorderNode] in reorderNode?.removeFromSupernode() + completion() }) self.reorderFeedback?.tap() } else { reorderNode.removeFromSupernode() reorderNode.itemNode?.isHidden = false + completion() } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift index 1e2e7e9000..8ca472a617 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift @@ -22,6 +22,7 @@ public struct CachedChannelFlags: OptionSet { public static let translationHidden = CachedChannelFlags(rawValue: 1 << 8) public static let adsRestricted = CachedChannelFlags(rawValue: 1 << 9) public static let canViewRevenue = CachedChannelFlags(rawValue: 1 << 10) + public static let paidMediaAllowed = CachedChannelFlags(rawValue: 1 << 11) } public struct CachedChannelParticipantsSummary: PostboxCoding, Equatable { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index f8538240fb..0cef10c2f4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -1096,6 +1096,34 @@ public extension TelegramEngine.EngineData.Item { } } + public struct PaidMediaAllowed: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = Bool + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .cachedPeerData(peerId: self.id) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? CachedPeerDataView else { + preconditionFailure() + } + if let cachedData = view.cachedPeerData as? CachedChannelData { + return cachedData.flags.contains(.paidMediaAllowed) + } else { + return false + } + } + } + public struct BoostsToUnrestrict: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { public typealias Result = Int32? diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index 875d3ba68a..333ae2772a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -589,6 +589,9 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee if (flags2 & Int32(1 << 12)) != 0 { channelFlags.insert(.canViewRevenue) } + if (flags2 & Int32(1 << 14)) != 0 { + channelFlags.insert(.paidMediaAllowed) + } let sendAsPeerId = defaultSendAs?.peerId diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 44d2791a2d..87faa898ff 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -2948,7 +2948,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI contentSize.height += totalContentNodesHeight if let paidContent = item.message.media.first(where: { $0 is TelegramMediaPaidContent }) as? TelegramMediaPaidContent, let media = paidContent.extendedMedia.first { + var isLocked = false if case .preview = media { + isLocked = true + } else if item.presentationData.isPreview { + isLocked = true + } + if isLocked { let sizeAndApply = unlockButtonLayout(ChatMessageUnlockMediaNode.Arguments( presentationData: item.presentationData, strings: item.presentationData.strings, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index 3b9cd0b578..ad0ea85ac4 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -1140,11 +1140,21 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr if let extendedMedia { switch extendedMedia { - case let .preview(_, immediateThumbnailData, _): - let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) - media = thumbnailMedia - case let .full(fullMedia): + case let .preview(_, immediateThumbnailData, _): + let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) + media = thumbnailMedia + case let .full(fullMedia): + if presentationData.isPreview { + if let image = fullMedia as? TelegramMediaImage { + let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: []) + media = thumbnailMedia + } else if let video = fullMedia as? TelegramMediaFile { + let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: video.immediateThumbnailData, reference: nil, partialReference: nil, flags: []) + media = thumbnailMedia + } + } else { media = fullMedia + } } } @@ -1475,7 +1485,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr extendedMedia = paidContent.extendedMedia[selectedMediaIndex] } } - if let extendedMedia, case let .full(fullMedia) = extendedMedia { + if let extendedMedia, case let .full(fullMedia) = extendedMedia, !presentationData.isPreview { isExtendedMedia = true media = fullMedia } @@ -2366,6 +2376,11 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr icon = .lock } displaySpoiler = true + } else if let _ = extendedMedia, isPreview { + if let invoice, invoice.currency != "XTR" { + icon = .lock + } + displaySpoiler = true } else if message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) { displaySpoiler = true } else if isSecretMedia { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift index ab887d4bf4..f190635001 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift @@ -193,7 +193,7 @@ public final class DrawingMessageRenderer { mainRadius: presentationData.chatBubbleCorners.mainRadius, auxiliaryRadius: presentationData.chatBubbleCorners.auxiliaryRadius, mergeBubbleCorners: presentationData.chatBubbleCorners.mergeBubbleCorners, - hasTails: false + hasTails: !self.isLink ) let avatarHeaderItem: ListViewItemHeader? diff --git a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift index 1c975ef173..cacdc741ce 100644 --- a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift +++ b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift @@ -716,7 +716,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll itemFrame = effectiveItemFrame itemTransform = effectiveItemTransform - itemNode.isCovered = index == self.items.count - 2 + itemNode.isCovered = index <= self.items.count - 2 } itemNode.bounds = CGRect(origin: .zero, size: itemFrame.size) diff --git a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift index fa56c53adb..3e6c5fc93d 100644 --- a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift @@ -643,7 +643,7 @@ public final class StarsImageComponent: Component { } var totalLabelWidth: CGFloat = 0.0 - let labelSpacing: CGFloat = 3.0 + let labelSpacing: CGFloat = 4.0 let lockView: UIImageView if let current = self.lockView { lockView = current diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index 05869fb375..436374d47e 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -129,6 +129,14 @@ private final class SheetContent: CombinedComponent { minAmount = 1 maxAmount = configuration.maxPaidMediaAmount + case .reaction: + titleString = environment.strings.Stars_SendStars_Title + amountTitle = environment.strings.Stars_SendStars_AmountTitle + amountPlaceholder = environment.strings.Stars_SendStars_AmountPlaceholder + + minAmount = 1 + //TODO: + maxAmount = configuration.maxPaidMediaAmount } let title = title.update( @@ -142,7 +150,16 @@ private final class SheetContent: CombinedComponent { contentSize.height += title.size.height contentSize.height += 40.0 - if case let .withdraw(starsState) = component.mode { + let balance: Int64? + if case .reaction = component.mode { + balance = state.balance + } else if case let .withdraw(starsState) = component.mode { + balance = starsState.balances.availableBalance + } else { + balance = nil + } + + if let balance { let balanceTitle = balanceTitle.update( component: MultilineTextComponent( text: .plain(NSAttributedString( @@ -158,7 +175,7 @@ private final class SheetContent: CombinedComponent { let balanceValue = balanceValue.update( component: MultilineTextComponent( text: .plain(NSAttributedString( - string: presentationStringsFormattedNumber(Int32(starsState.balances.availableBalance), environment.dateTimeFormat.groupingSeparator), + string: presentationStringsFormattedNumber(Int32(balance), environment.dateTimeFormat.groupingSeparator), font: Font.semibold(16.0), textColor: theme.list.itemPrimaryTextColor )), @@ -185,17 +202,18 @@ private final class SheetContent: CombinedComponent { ) } + let amountFont = Font.regular(13.0) + let amountTextColor = theme.list.freeTextColor + let amountMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: amountFont, textColor: amountTextColor), bold: MarkdownAttributeSet(font: amountFont, textColor: amountTextColor), link: MarkdownAttributeSet(font: amountFont, textColor: theme.list.itemAccentColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme { + state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme) + } let amountFooter: AnyComponent? - if case .paidMedia = component.mode { - let amountFont = Font.regular(13.0) - let amountTextColor = theme.list.freeTextColor - let amountMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: amountFont, textColor: amountTextColor), bold: MarkdownAttributeSet(font: amountFont, textColor: amountTextColor), link: MarkdownAttributeSet(font: amountFont, textColor: theme.list.itemAccentColor), linkAttribute: { contents in - return (TelegramTextAttributes.URL, contents) - }) + switch component.mode { + case .paidMedia: let amountInfoString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Stars_PaidContent_AmountInfo, attributes: amountMarkdownAttributes, textAlignment: .natural)) - if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme { - state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme) - } if let range = amountInfoString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { amountInfoString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: amountInfoString.string)) } @@ -214,7 +232,13 @@ private final class SheetContent: CombinedComponent { component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_PaidContent_AmountInfo_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) } )) - } else { + case let .reaction(starsToTop): + let amountInfoString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Stars_SendStars_AmountInfo("\(starsToTop ?? 0)").string, attributes: amountMarkdownAttributes, textAlignment: .natural)) + amountFooter = AnyComponent(MultilineTextComponent( + text: .plain(amountInfoString), + maximumNumberOfLines: 0 + )) + default: amountFooter = nil } @@ -296,7 +320,7 @@ private final class SheetContent: CombinedComponent { id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) ), - isEnabled: true, + isEnabled: (state.amount ?? 0) > 0, displaysProgress: false, action: { [weak state] in if let controller = controller() as? StarsWithdrawScreen, let amount = state?.amount { @@ -328,33 +352,56 @@ private final class SheetContent: CombinedComponent { final class State: ComponentState { private let context: AccountContext + private let mode: StarsWithdrawScreen.Mode fileprivate var amount: Int64? + fileprivate var balance: Int64? + private var stateDisposable: Disposable? + var cachedCloseImage: (UIImage, PresentationTheme)? var cachedStarImage: (UIImage, PresentationTheme)? var cachedChevronImage: (UIImage, PresentationTheme)? init( context: AccountContext, - amount: Int64? + mode: StarsWithdrawScreen.Mode ) { self.context = context + self.mode = mode + + var amount: Int64? + switch mode { + case let .withdraw(stats): + amount = stats.balances.availableBalance + case let .paidMedia(initialValue): + amount = initialValue + case .reaction: + amount = nil + } + self.amount = amount super.init() + + if case .reaction = self.mode, let starsContext = context.starsContext { + self.stateDisposable = (starsContext.state + |> deliverOnMainQueue).startStrict(next: { [weak self] state in + if let self, let balance = state?.balance { + self.balance = balance + self.updated() + } + }) + } + } + + deinit { + self.stateDisposable?.dispose() } } func makeState() -> State { - var amount: Int64? - switch self.mode { - case let .withdraw(stats): - amount = stats.balances.availableBalance - case let .paidMedia(initialValue): - amount = initialValue - } - return State(context: self.context, amount: amount) + return State(context: self.context, mode: self.mode) } } @@ -449,6 +496,7 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer { public enum Mode: Equatable { case withdraw(StarsRevenueStats) case paidMedia(Int64?) + case reaction(Int64?) } private let context: AccountContext @@ -638,12 +686,11 @@ private final class AmountFieldComponent: Component { if let component = self.component { let amount: Int64? - if !newText.isEmpty, let value = Int64(newText) { + if !newText.isEmpty, let value = Int64(normalizeArabicNumeralString(newText, type: .western)) { amount = value } else { amount = nil } - if let amount, let maxAmount = component.maxValue, amount > maxAmount { textField.text = "\(maxAmount)" self.textChanged(self.textField)