diff --git a/Telegram/Telegram-iOS/Resources/CraftFail.tgs b/Telegram/Telegram-iOS/Resources/CraftFail.tgs deleted file mode 100644 index eaf5e1db3f..0000000000 Binary files a/Telegram/Telegram-iOS/Resources/CraftFail.tgs and /dev/null differ diff --git a/Telegram/Telegram-iOS/Resources/CraftFailOverlay.tgs b/Telegram/Telegram-iOS/Resources/CraftFailOverlay.tgs deleted file mode 100644 index 983d067102..0000000000 Binary files a/Telegram/Telegram-iOS/Resources/CraftFailOverlay.tgs and /dev/null differ diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 16b14dc7b2..db3598e081 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15669,3 +15669,50 @@ Error: %8$@"; "Conversation.Summary.Limit.Title" = "AI Summary"; "Conversation.Summary.Limit.Text" = "Summarize large messages with AI – unlimited with Telegram Premium."; + +"Appearance.SendWithCmdEnter" = "Send Messages with ⌘+Enter"; + +"Notification.StarsGift.Crafted" = "You crafted a new unique collectible"; + +"Gift.Craft.Title" = "Craft Gift"; +"Gift.Craft.Description" = "Add up to **4 gifts** to craft new\n**$ %@**.\n\nIf crafting fails, all used gifts\nwill be lost."; +"Gift.Craft.ViewVariants" = "View all new variants"; +"Gift.Craft.BackdropTooltip" = "There's **%1$@%** chance the crafted gift will have **%2$@** backdrop."; +"Gift.Craft.SymbolTooltip" = "There's **%1$@%** chance the crafted gift will have **%2$@** symbol."; +"Gift.Craft.SuccessChanceSuffix" = "% Success Chance"; +"Gift.Craft.Craft" = "Craft %@"; +"Gift.Craft.Crafting.Title" = "Crafting"; +"Gift.Craft.Crafting.Description" = "If crafting fails, all used gifts\nwill be lost."; +"Gift.Craft.Crafting.SuccessChance" = "%@% Success Chance"; +"Gift.Craft.CraftingFailed.Title" = "Crafting Failed"; +"Gift.Craft.CraftingFailed.Text_1" = "This crafting attempt was unsuccessful.\n**%@ gift** was lost."; +"Gift.Craft.CraftingFailed.Text_any" = "This crafting attempt was unsuccessful.\n**%@ gifts** were lost."; +"Gift.Craft.CraftingFailed.CraftAnotherGift" = "Craft Another Gift"; +"Gift.Craft.Info.Title" = "Gift Crafting"; +"Gift.Craft.Info.Description" = "Turn your gifts intro rare, epic,\nuncommon and legendary versions."; +"Gift.Craft.Info.Paragraph1.Title" = "Get Rare Models"; +"Gift.Craft.Info.Paragraph1.Text" = "Select up to 4 gifts to craft a new exclusive model."; +"Gift.Craft.Info.Paragraph2.Title" = "Maximize Chances"; +"Gift.Craft.Info.Paragraph2.Text" = "Combine more gifts to increase your odds of success."; +"Gift.Craft.Info.Paragraph3.Title" = "Affect the Result"; +"Gift.Craft.Info.Paragraph3.Text" = "Use gifts with the same attribute to boost it's chance."; +"Gift.Craft.Info.Understood" = "Understood"; +"Gift.Craft.Unavailable.Title" = "Try Later"; +"Gift.Craft.Unavailable.Text" = "You will be able to craft this gift on %@."; + +"Gift.Attribute.Rare" = "rare"; +"Gift.Attribute.Legendary" = "legendary"; +"Gift.Attribute.Epic" = "epic"; +"Gift.Attribute.Uncommon" = "uncommon"; + +"SocksProxySetup.CheckStatus" = "Check Status"; +"SocksProxySetup.Warning.Title" = "Warning"; +"SocksProxySetup.Warning.Text" = "This action will expose your IP address to the admin of the proxy server."; +"SocksProxySetup.Warning.Proceed" = "Proceed"; + +"Gift.Variants.CollectionInfo.CraftableModel_1" = "**%@** craftable model"; +"Gift.Variants.CollectionInfo.CraftableModel_any" = "**%@** craftable models"; +"Gift.Variants.ViewCraftableModels" = "View Craftable Models"; +"Gift.Variants.ViewPrimaryModels" = "View Primary Models"; + +"Gift.View.OnSale" = "On sale for %@"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index efab15b20f..1b40090279 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1294,6 +1294,7 @@ public protocol SharedAccountContext: AnyObject { var currentMediaInputSettings: Atomic { get } var currentStickerSettings: Atomic { get } var currentMediaDisplaySettings: Atomic { get } + var currentChatSettings: Atomic { get } var energyUsageSettings: EnergyUsageSettings { get } @@ -1453,13 +1454,13 @@ public protocol SharedAccountContext: AnyObject { func makeGiftViewScreen(context: AccountContext, message: EngineMessage, shareStory: ((StarGift.UniqueGift) -> Void)?) -> ViewController func makeGiftViewScreen(context: AccountContext, gift: StarGift.UniqueGift, shareStory: ((StarGift.UniqueGift) -> Void)?, openChatTheme: (() -> Void)?, dismissed: (() -> Void)?) -> ViewController func makeGiftWearPreviewScreen(context: AccountContext, gift: StarGift, attributes: [StarGift.UniqueGift.Attribute]?) -> ViewController - func makeGiftUpgradePreviewScreen(context: AccountContext, attributes: [StarGift.UniqueGift.Attribute], peerName: String) -> ViewController + func makeGiftUpgradePreviewScreen(context: AccountContext, gift: StarGift.Gift, attributes: [StarGift.UniqueGift.Attribute], peerName: String) -> ViewController func makeGiftAuctionInfoScreen(context: AccountContext, auctionContext: GiftAuctionContext, completion: (() -> Void)?) -> ViewController func makeGiftAuctionBidScreen(context: AccountContext, toPeerId: EnginePeer.Id, text: String?, entities: [MessageTextEntity]?, hideName: Bool, auctionContext: GiftAuctionContext, acquiredGifts: Signal<[GiftAuctionAcquiredGift], NoError>?) -> ViewController func makeGiftAuctionViewScreen(context: AccountContext, auctionContext: GiftAuctionContext, peerId: EnginePeer.Id?, completion: @escaping (Signal<[GiftAuctionAcquiredGift], NoError>, [StarGift.UniqueGift.Attribute]?) -> Void) -> ViewController func makeGiftAuctionActiveBidsScreen(context: AccountContext) -> ViewController func makeGiftOfferScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, gift: StarGift.UniqueGift, peer: EnginePeer, amount: CurrencyAmount, commit: @escaping () -> Void) -> ViewController - func makeGiftUpgradeVariantsScreen(context: AccountContext, gift: StarGift, onlyCrafted: Bool, attributes: [StarGift.UniqueGift.Attribute], selectedAttributes: [StarGift.UniqueGift.Attribute]?, focusedAttribute: StarGift.UniqueGift.Attribute?) -> ViewController + func makeGiftUpgradeVariantsScreen(context: AccountContext, gift: StarGift, crafted: Bool, attributes: [StarGift.UniqueGift.Attribute], selectedAttributes: [StarGift.UniqueGift.Attribute]?, focusedAttribute: StarGift.UniqueGift.Attribute?) -> ViewController func makeGiftAuctionWearPreviewScreen(context: AccountContext, auctionContext: GiftAuctionContext, acquiredGifts: Signal<[GiftAuctionAcquiredGift], NoError>?, attributes: [StarGift.UniqueGift.Attribute], completion: @escaping () -> Void) -> ViewController func makeGiftCraftScreen(context: AccountContext, gift: StarGift.UniqueGift) -> ViewController func makeGiftDemoScreen(context: AccountContext) -> ViewController diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift index f74fde4062..ca3e2e75dd 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift @@ -1489,7 +1489,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } } - public func chatInputTextNodeShouldReturn() -> Bool { + public func chatInputTextNodeShouldReturn(modifierFlags: UIKeyModifierFlags) -> Bool { if self.actionButtons.sendButton.supernode != nil && !self.actionButtons.sendButton.isHidden && !self.actionButtons.sendButton.alpha.isZero { self.sendButtonPressed() } @@ -1497,7 +1497,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } @objc public func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool { - return self.chatInputTextNodeShouldReturn() + return self.chatInputTextNodeShouldReturn(modifierFlags: []) } private func applyUpdateSendButtonIcon() { diff --git a/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift b/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift index e638c51cb7..dbb51b9746 100644 --- a/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift +++ b/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift @@ -530,6 +530,11 @@ public final class ResizableSheetComponent if let current = self.bottomItemView { bottomItemView = current } else { + bottomItemTransition = .immediate bottomItemView = ComponentView() self.bottomItemView = bottomItemView } let bottomInsets = ContainerViewLayout.concentricInsets(bottomInset: sheetEnvironment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) let bottomItemSize = bottomItemView.update( - transition: transition, + transition: bottomItemTransition, component: bottomItem, environment: {}, containerSize: CGSize(width: containerSize.width - bottomInsets.left - bottomInsets.right, height: 52.0) @@ -606,12 +613,24 @@ public final class ResizableSheetComponent: C self.scrollView = ScrollView() self.scrollView.delaysContentTouches = false - if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { - self.scrollView.contentInsetAdjustmentBehavior = .never - } + self.scrollView.contentInsetAdjustmentBehavior = .never self.scrollView.showsVerticalScrollIndicator = false self.scrollView.showsHorizontalScrollIndicator = false self.scrollView.alwaysBounceVertical = true @@ -351,6 +349,9 @@ public final class SheetComponent: C }) } else { var targetOffset: CGFloat = self.scrollView.contentSize.height + abs(contentView.frame.minY) + 6.0 + if self.currentHasInputHeight { + targetOffset += 330.0 + } if self.isCentered { targetOffset = self.frame.height + self.scrollView.frame.height * 0.5 } diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index 290c218763..3b70e74503 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -35,6 +35,8 @@ import GlassControls import ComponentFlow import ComponentDisplayAdapters import EdgeEffect +import RasterizedCompositionComponent +import BadgeComponent private let deleteImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: .white) private let actionImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: .white) @@ -45,25 +47,6 @@ private let forwardImage = generateTintedImage(image: UIImage(bundleImageName: " private let cloudFetchIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/FileCloudFetch"), color: UIColor.white) -private let fullscreenOnImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Expand"), color: .white) -private let fullscreenOffImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Collapse"), color: .white) - -private let captionMaskImage = generateImage(CGSize(width: 1.0, height: 17.0), opaque: false, rotatedContext: { size, context in - let bounds = CGRect(origin: CGPoint(), size: size) - context.clear(bounds) - - let gradientColors = [UIColor.white.withAlphaComponent(1.0).cgColor, UIColor.white.withAlphaComponent(0.0).cgColor] as CFArray - - var locations: [CGFloat] = [0.0, 1.0] - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! - - context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: 17.0), options: CGGradientDrawingOptions()) -}) - -private let titleFont = Font.medium(15.0) -private let dateFont = Font.regular(14.0) - enum ChatItemGalleryFooterContent: Equatable { case info case fetch(status: MediaResourceStatus, seekable: Bool) @@ -128,6 +111,26 @@ class CaptionScrollWrapperNode: ASDisplayNode { } final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScrollViewDelegate { + public final class SettingsButtonState: Equatable { + public let speed: String? + public let quality: String? + + public init(speed: String?, quality: String?) { + self.speed = speed + self.quality = quality + } + + public static func ==(lhs: SettingsButtonState, rhs: SettingsButtonState) -> Bool { + if lhs.speed != rhs.speed { + return false + } + if lhs.quality != rhs.quality { + return false + } + return true + } + } + private let context: AccountContext private var presentationData: PresentationData private var theme: PresentationTheme @@ -140,6 +143,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll private let textSelectionKnobContainer: UIView private let textSelectionKnobSurface: UIView private let scrollWrapperNode: CaptionScrollWrapperNode + private let scrollWrapperMask: EdgeMaskView private let scrollWrapperEffect: VariableBlurEffect private let scrollNode: ASScrollNode @@ -178,7 +182,9 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll displayActionButton: Bool, displayEditButton: Bool, displayPictureInPictureButton: Bool, - displaySettingsButton: Bool + settingsButtonState: SettingsButtonState?, + displayTextRecognitionButton: Bool, + displayStickersButton: Bool )? private var codeHighlightState: (id: EngineMessage.Id, specs: [CachedMessageSyntaxHighlight.Spec], disposable: Disposable)? @@ -304,7 +310,6 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } didSet { if let scrubberView = self.scrubberView { - scrubberView.setCollapsed(self.visibilityAlpha < 1.0, animated: false) self.view.addSubview(scrubberView) scrubberView.updateScrubbingVisual = { [weak self] value in guard let strongSelf = self else { @@ -338,8 +343,16 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll override func setVisibilityAlpha(_ alpha: CGFloat, animated: Bool) { self.visibilityAlpha = alpha - self.contentNode.alpha = alpha - self.scrubberView?.setCollapsed(alpha < 1.0, animated: animated) + + let transition: ComponentTransition = animated ? .easeInOut(duration: 0.2) : .immediate + transition.animateView { + self.contentNode.alpha = alpha + } + //transition.setAlpha(view: self.contentNode.view, alpha: alpha) + + if let validLayout = self.validLayout { + let _ = self.updateLayout(size: validLayout.0, metrics: validLayout.1, leftInset: validLayout.2, rightInset: validLayout.3, bottomInset: validLayout.4, contentInset: validLayout.5, transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate) + } } private var hasExpandedCaptionPromise = ValuePromise(false) @@ -368,6 +381,9 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll self.scrollWrapperNode.backgroundColor = .clear self.scrollWrapperNode.isOpaque = false + self.scrollWrapperMask = EdgeMaskView() + //self.scrollWrapperNode.view.addSubview(self.scrollWrapperMask) + self.scrollNode = ASScrollNode() self.scrollNode.clipsToBounds = false @@ -795,7 +811,9 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll displayActionButton: false, displayEditButton: false, displayPictureInPictureButton: false, - displaySettingsButton: false + settingsButtonState: nil, + displayTextRecognitionButton: false, + displayStickersButton: false ) } @@ -815,7 +833,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } } - func setMessage(_ message: Message, displayInfo: Bool = true, translateToLanguage: String? = nil, peerIsCopyProtected: Bool = false, displayPictureInPictureButton: Bool = false, displaySettingsButton: Bool = false) { + func setMessage(_ message: Message, displayInfo: Bool = true, translateToLanguage: String? = nil, peerIsCopyProtected: Bool = false, displayPictureInPictureButton: Bool = false, settingsButtonState: SettingsButtonState? = nil, displayTextRecognitionButton: Bool = false, displayStickersButton: Bool = false, animated: Bool = false) { self.currentMessage = message var displayInfo = displayInfo @@ -1061,10 +1079,12 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll displayActionButton: displayActionButton, displayEditButton: displayEditButton, displayPictureInPictureButton: displayPictureInPictureButton, - displaySettingsButton: displaySettingsButton + settingsButtonState: settingsButtonState, + displayTextRecognitionButton: displayTextRecognitionButton, + displayStickersButton: displayStickersButton ) - self.requestLayout?(.immediate) + self.requestLayout?(animated ? .animated(duration: 0.4, curve: .spring) : .immediate) } } @@ -1169,6 +1189,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll let textSize = self.textNode.updateLayout(constrainSize) var textOffset: CGFloat = 0.0 + var additionalTextHeight: CGFloat = 0.0 if displayCaption { visibleTextHeight = textSize.height if visibleTextHeight > 100.0 { @@ -1190,7 +1211,9 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll var maxTextOffset: CGFloat = size.height - bottomInset - 238.0 - UIScreenPixel if let _ = self.scrubberView { - maxTextOffset -= 44.0 + //maxTextOffset -= 100.0 + maxTextOffset += 0.0 + additionalTextHeight += 44.0 } textOffset = min(maxTextOffset, self.scrollNode.view.contentOffset.y) let originalPanelHeight = panelHeight @@ -1198,47 +1221,22 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll if self.scrollNode.view.isScrollEnabled { if self.scrollWrapperNode.view.mask == nil { - self.scrollWrapperNode.layer.rasterizationScale = UIScreenScale - let maskView = UIImageView() - - let height: CGFloat = 70.0 - let baseGradientAlpha: CGFloat = 1.0 - let numSteps = 8 - let firstStep = 0 - let firstLocation = 0.0 - let colors = (0 ..< numSteps).map { i -> UIColor in - if i < firstStep { - return UIColor(white: 1.0, alpha: 1.0) - } else { - let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1) - let value: CGFloat = 1.0 - bezierPoint(0.42, 0.0, 0.58, 1.0, step) - return UIColor(white: 0.0, alpha: baseGradientAlpha * value) - } - } - let locations = (0 ..< numSteps).map { i -> CGFloat in - if i < firstStep { - return 0.0 - } else { - let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1) - return (firstLocation + (1.0 - firstLocation) * step) - } - } - - maskView.image = generateGradientImage(size: CGSize(width: 8.0, height: height), colors: colors, locations: locations)!.stretchableImage(withLeftCapWidth: 0, topCapHeight: 1) - - //maskView.image = EdgeEffectView.generateEdgeGradient(baseHeight: originalPanelHeight + 20.0, isInverted: false) - self.scrollWrapperNode.view.mask = maskView + self.scrollWrapperNode.view.mask = self.scrollWrapperMask } } else { self.scrollWrapperNode.view.mask = nil } - let scrollWrapperNodeFrame = CGRect(x: 0.0, y: 0.0, width: width, height: max(0.0, visibleTextPanelHeight + textOffset + originalPanelHeight - 14.0)) + let scrollWrapperNodeFrame = CGRect(x: 0.0, y: 0.0, width: width, height: max(0.0, visibleTextPanelHeight + textOffset + originalPanelHeight - 14.0 + additionalTextHeight)) if self.scrollWrapperNode.frame != scrollWrapperNodeFrame { self.scrollWrapperNode.frame = scrollWrapperNodeFrame - self.scrollWrapperNode.view.mask?.frame = self.scrollWrapperNode.bounds - self.scrollWrapperNode.view.mask?.layer.removeAllAnimations() - self.scrollWrapperEffect.update(size: scrollWrapperNodeFrame.size, constantHeight: 80.0, placement: VariableBlurEffect.Placement(position: .bottom, extendsInwards: true), gradient: EdgeEffectView.generateEdgeGradientData(baseHeight: 80.0), transition: transition) + do { + let mask = self.scrollWrapperMask + mask.update(size: self.scrollWrapperNode.bounds.size, color: UIColor.blue, gradientHeight: 90.0 + additionalTextHeight + contentInset, extensionHeight: 0.0, transition: ComponentTransition(transition)) + mask.frame = self.scrollWrapperNode.bounds + mask.layer.removeAllAnimations() + } + self.scrollWrapperEffect.update(size: CGSize(width: scrollWrapperNodeFrame.width, height: scrollWrapperNodeFrame.height), constantHeight: 90.0, placement: VariableBlurEffect.Placement(position: .bottom, inwardsExtension: additionalTextHeight + contentInset), gradient: EdgeEffectView.generateEdgeGradientData(baseHeight: 90.0), transition: transition) } if let buttonNode = self.buttonNode { @@ -1295,7 +1293,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } let scrubberFrame = CGRect(origin: CGPoint(x: buttonPanelInsets.left, y: scrubberY), size: CGSize(width: width - buttonPanelInsets.left - buttonPanelInsets.right, height: 44.0)) - scrubberView.updateLayout(size: scrubberFrame.size, leftInset: 0.0, rightInset: 0.0, transition: .immediate) + scrubberView.updateLayout(size: scrubberFrame.size, leftInset: 0.0, rightInset: 0.0, isCollapsed: self.visibilityAlpha < 1.0, transition: transition) transition.updateBounds(layer: scrubberView.layer, bounds: CGRect(origin: CGPoint(), size: scrubberFrame.size)) transition.updatePosition(layer: scrubberView.layer, position: CGPoint(x: scrubberFrame.midX, y: scrubberFrame.midY)) } @@ -1330,10 +1328,14 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } )) } - if buttonsState.displaySettingsButton { + if let settingsButtonState = buttonsState.settingsButtonState { centerControlItems.append(GlassControlGroupComponent.Item( id: AnyHashable("settings"), - content: .icon("Chat/Context Menu/Settings"), + content: .customIcon(id: AnyHashable("settings"), component: AnyComponent(SettingsNavigationIconComponent( + speed: settingsButtonState.speed, + quality: settingsButtonState.quality, + isOpen: false + ))), action: { [weak self] in guard let self, let buttonPanelView = self.buttonPanel.view as? GlassControlPanelComponent.View, let centerItemView = buttonPanelView.centerItemView else { return @@ -1357,6 +1359,30 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } )) } + if buttonsState.displayTextRecognitionButton { + centerControlItems.append(GlassControlGroupComponent.Item( + id: AnyHashable("textRecognition"), + content: .icon("Media Gallery/LiveTextIcon"), + action: { [weak self] in + guard let self else { + return + } + self.textRecognitionButtonPressed() + } + )) + } + if buttonsState.displayStickersButton { + centerControlItems.append(GlassControlGroupComponent.Item( + id: AnyHashable("stickers"), + content: .icon("Media Gallery/Stickers"), + action: { [weak self] in + guard let self else { + return + } + self.stickersButtonPressed() + } + )) + } if buttonsState.displayFullscreenButton && !metrics.isTablet { centerControlItems.append(GlassControlGroupComponent.Item( id: AnyHashable("fullscreen"), @@ -2126,6 +2152,21 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll self.controllerInteraction?.editMedia(message.id) } + private func textRecognitionButtonPressed() { + guard let currentItemNode = self.controllerInteraction?.currentItemNode() as? ChatImageGalleryItemNode else { + return + } + currentItemNode.textRecognitionButtonPressed() + } + + private func stickersButtonPressed() { + if let currentItemNode = self.controllerInteraction?.currentItemNode() as? ChatImageGalleryItemNode { + currentItemNode.openStickersButtonPressed() + } else if let currentItemNode = self.controllerInteraction?.currentItemNode() as? UniversalVideoGalleryItemNode { + currentItemNode.openStickersButtonPressed() + } + } + private func pipButtonPressed() { guard let currentItemNode = self.controllerInteraction?.currentItemNode() as? UniversalVideoGalleryItemNode else { return @@ -2333,3 +2374,225 @@ private final class PlaybackButtonNode: HighlightTrackingButtonNode { self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: floorToScreenPixels((size.height - textSize.height) / 2.0) + UIScreenPixel), size: textSize) } } + +final class SettingsNavigationIconComponent: Component { + let speed: String? + let quality: String? + let isOpen: Bool + + init(speed: String?, quality: String?, isOpen: Bool) { + self.speed = speed + self.quality = quality + self.isOpen = isOpen + } + + static func ==(lhs: SettingsNavigationIconComponent, rhs: SettingsNavigationIconComponent) -> Bool { + if lhs.speed != rhs.speed { + return false + } + if lhs.quality != rhs.quality { + return false + } + if lhs.isOpen != rhs.isOpen { + return false + } + return true + } + + final class View: UIView { + private let iconLayer: RasterizedCompositionMonochromeLayer + + private let gearsLayer: RasterizedCompositionImageLayer + private let dotLayer: RasterizedCompositionImageLayer + + private var speedBadge: ComponentView? + private var qualityBadge: ComponentView? + + private var speedBadgeText: String? + private var qualityBadgeText: String? + + private let badgeFont: UIFont + + private var isMenuOpen: Bool = false + + override init(frame: CGRect) { + self.iconLayer = RasterizedCompositionMonochromeLayer() + + self.gearsLayer = RasterizedCompositionImageLayer() + self.gearsLayer.image = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsNoDot"), color: .white) + + self.dotLayer = RasterizedCompositionImageLayer() + self.dotLayer.image = generateFilledCircleImage(diameter: 4.0, color: .white) + + self.iconLayer.contentsLayer.addSublayer(self.gearsLayer) + self.iconLayer.contentsLayer.addSublayer(self.dotLayer) + + self.badgeFont = Font.with(size: 8.0, design: .round, weight: .bold) + + super.init(frame: frame) + + self.layer.addSublayer(self.iconLayer) + + let size = CGSize(width: 44.0, height: 44.0) + + if let image = self.gearsLayer.image { + let iconInnerInsets = UIEdgeInsets(top: 4.0, left: 8.0, bottom: 4.0, right: 6.0) + let iconSize = CGSize(width: image.size.width + iconInnerInsets.left + iconInnerInsets.right, height: image.size.height + iconInnerInsets.top + iconInnerInsets.bottom) + let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize) + self.iconLayer.position = iconFrame.center + self.iconLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) + + self.iconLayer.contentsLayer.position = CGRect(origin: CGPoint(), size: iconFrame.size).center + self.iconLayer.contentsLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) + + self.iconLayer.maskedLayer.position = CGRect(origin: CGPoint(), size: iconFrame.size).center + self.iconLayer.maskedLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) + self.iconLayer.maskedLayer.backgroundColor = UIColor.white.cgColor + + let gearsFrame = CGRect(origin: CGPoint(x: floor((iconSize.width - image.size.width) * 0.5), y: floor((iconSize.height - image.size.height) * 0.5)), size: image.size) + self.gearsLayer.position = gearsFrame.center + self.gearsLayer.bounds = CGRect(origin: CGPoint(), size: gearsFrame.size) + + if let dotImage = self.dotLayer.image { + let dotFrame = CGRect(origin: CGPoint(x: gearsFrame.minX + floorToScreenPixels((gearsFrame.width - dotImage.size.width) * 0.5), y: gearsFrame.minY + floorToScreenPixels((gearsFrame.height - dotImage.size.height) * 0.5)), size: dotImage.size) + self.dotLayer.position = dotFrame.center + self.dotLayer.bounds = CGRect(origin: CGPoint(), size: dotFrame.size) + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setIsMenuOpen(isMenuOpen: Bool) { + if self.isMenuOpen == isMenuOpen { + return + } + self.isMenuOpen = isMenuOpen + + let rotationTransition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring) + rotationTransition.updateTransform(layer: self.gearsLayer, transform: CGAffineTransformMakeRotation(isMenuOpen ? (CGFloat.pi * 2.0 / 6.0) : 0.0)) + self.gearsLayer.animateScale(from: 1.0, to: 1.07, duration: 0.1, removeOnCompletion: false, completion: { [weak self] finished in + guard let self, finished else { + return + } + self.gearsLayer.animateScale(from: 1.07, to: 1.0, duration: 0.1, removeOnCompletion: true) + }) + + self.dotLayer.animateScale(from: 1.0, to: 0.8, duration: 0.1, removeOnCompletion: false, completion: { [weak self] finished in + guard let self, finished else { + return + } + self.dotLayer.animateScale(from: 0.8, to: 1.0, duration: 0.1, removeOnCompletion: true) + }) + } + + func setBadges(speed: String?, quality: String?, transition: ComponentTransition) { + if self.speedBadgeText == speed && self.qualityBadgeText == quality { + return + } + self.speedBadgeText = speed + self.qualityBadgeText = quality + + if let badgeText = speed { + var badgeTransition = transition + let speedBadge: ComponentView + if let current = self.speedBadge { + speedBadge = current + } else { + speedBadge = ComponentView() + self.speedBadge = speedBadge + badgeTransition = badgeTransition.withAnimation(.none) + } + let badgeSize = speedBadge.update( + transition: badgeTransition, + component: AnyComponent(BadgeComponent( + text: badgeText, + font: self.badgeFont, + cornerRadius: .custom(3.0), + insets: UIEdgeInsets(top: 1.33, left: 1.66, bottom: 1.33, right: 1.66), + outerInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0) + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + if let speedBadgeView = speedBadge.view { + if speedBadgeView.layer.superlayer == nil { + self.iconLayer.contentsLayer.addSublayer(speedBadgeView.layer) + + transition.animateAlpha(layer: speedBadgeView.layer, from: 0.0, to: 1.0) + transition.animateScale(layer: speedBadgeView.layer, from: 0.001, to: 1.0) + } + badgeTransition.setFrame(layer: speedBadgeView.layer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: badgeSize)) + } + } else if let speedBadge = self.speedBadge { + self.speedBadge = nil + if let speedBadgeView = speedBadge.view { + let speedBadgeLayer = speedBadgeView.layer + transition.setAlpha(layer: speedBadgeLayer, alpha: 0.0, completion: { [weak speedBadgeLayer] _ in + speedBadgeLayer?.removeFromSuperlayer() + }) + transition.setScale(layer: speedBadgeLayer, scale: 0.001) + } + } + + if let badgeText = quality { + var badgeTransition = transition + let qualityBadge: ComponentView + if let current = self.qualityBadge { + qualityBadge = current + } else { + qualityBadge = ComponentView() + self.qualityBadge = qualityBadge + badgeTransition = badgeTransition.withAnimation(.none) + } + let badgeSize = qualityBadge.update( + transition: badgeTransition, + component: AnyComponent(BadgeComponent( + text: badgeText, + font: self.badgeFont, + cornerRadius: .custom(3.0), + insets: UIEdgeInsets(top: 1.33, left: 1.66, bottom: 1.33, right: 1.66), + outerInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0) + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + if let qualityBadgeView = qualityBadge.view { + if qualityBadgeView.layer.superlayer == nil { + self.iconLayer.contentsLayer.addSublayer(qualityBadgeView.layer) + + transition.animateAlpha(layer: qualityBadgeView.layer, from: 0.0, to: 1.0) + transition.animateScale(layer: qualityBadgeView.layer, from: 0.001, to: 1.0) + } + badgeTransition.setFrame(layer: qualityBadgeView.layer, frame: CGRect(origin: CGPoint(x: self.iconLayer.bounds.width - badgeSize.width, y: self.iconLayer.bounds.height - badgeSize.height), size: badgeSize)) + } + } else if let qualityBadge = self.qualityBadge { + self.qualityBadge = nil + if let qualityBadgeView = qualityBadge.view { + let qualityBadgeLayer = qualityBadgeView.layer + transition.setAlpha(layer: qualityBadgeLayer, alpha: 0.0, completion: { [weak qualityBadgeLayer] _ in + qualityBadgeLayer?.removeFromSuperlayer() + }) + transition.setScale(layer: qualityBadgeLayer, scale: 0.001) + } + } + } + + func update(component: SettingsNavigationIconComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { + self.setBadges(speed: component.speed, quality: component.quality, transition: transition) + self.setIsMenuOpen(isMenuOpen: component.isOpen) + + return CGSize(width: 44.0, height: 44.0) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift b/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift index 4eb20f145c..af13b1fc83 100644 --- a/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift +++ b/submodules/GalleryUI/Sources/ChatVideoGalleryItemScrubberView.swift @@ -21,7 +21,7 @@ private let scrubberForegroundColor = UIColor.white private let scrubberBufferingColor = UIColor(rgb: 0xffffff, alpha: 0.5) final class ChatVideoGalleryItemScrubberView: UIView { - private var containerLayout: (size: CGSize, leftInset: CGFloat, rightInset: CGFloat)? + private var containerLayout: (size: CGSize, leftInset: CGFloat, rightInset: CGFloat, isCollapsed: Bool)? private let backgroundContainer: GlassBackgroundContainerView private let backgroundView: GlassBackgroundView @@ -80,6 +80,7 @@ final class ChatVideoGalleryItemScrubberView: UIView { self.chapters = chapters self.scrubberNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 8.0, lineCap: .round, scrubberHandle: .none, backgroundColor: scrubberBackgroundColor, foregroundColor: scrubberForegroundColor, bufferingColor: scrubberBufferingColor, chapters: chapters)) + self.scrubberNode.layer.allowsGroupOpacity = true self.shimmerEffectNode = ShimmerEffectForegroundNode() self.leftTimestampNode = MediaPlayerTimeTextNode(textColor: .white) @@ -127,9 +128,9 @@ final class ChatVideoGalleryItemScrubberView: UIView { } } - self.backgroundView.contentView.addSubview(self.scrubberNode.view) self.backgroundView.contentView.addSubview(self.leftTimestampNode.view) self.backgroundView.contentView.addSubview(self.rightTimestampNode.view) + self.addSubview(self.scrubberNode.view) //self.backgroundView.contentView.addSubview(self.infoNode.view) } @@ -146,20 +147,6 @@ final class ChatVideoGalleryItemScrubberView: UIView { var isLoading = false var isCollapsed: Bool? - func setCollapsed(_ collapsed: Bool, animated: Bool) { - guard self.isCollapsed != collapsed else { - return - } - - self.isCollapsed = collapsed - - self.updateTimestampsVisibility(animated: animated) - self.updateScrubberVisibility(animated: animated) - - if let (size, _, _) = self.containerLayout { - self.infoNode.alpha = size.width < size.height && !collapsed ? 1.0 : 0.0 - } - } func updateTimestampsVisibility(animated: Bool) { if self.isAnimatedOut { @@ -174,15 +161,20 @@ final class ChatVideoGalleryItemScrubberView: UIView { private func updateScrubberVisibility(animated: Bool) { var collapsed = self.isCollapsed var alpha: CGFloat = 1.0 + var controlAlpha: CGFloat = 1.0 if let playbackStatus = self.playbackStatus, playbackStatus.duration <= 30.0 { + alpha = self.isCollapsed == true ? 0.0 : 1.0 + controlAlpha = 1.0 } else { alpha = self.isCollapsed == true ? 0.0 : 1.0 + controlAlpha = alpha collapsed = false } self.scrubberNode.setCollapsed(collapsed == true, animated: animated) let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .linear) : .immediate ComponentTransition(transition).setAlpha(view: self.backgroundContainer, alpha: alpha) + ComponentTransition(transition).setAlpha(view: self.scrubberNode.view, alpha: controlAlpha) } func animateTo(_ timestamp: Double) { @@ -232,8 +224,8 @@ final class ChatVideoGalleryItemScrubberView: UIView { strongSelf.currentLeftString = leftString strongSelf.currentRightString = rightString - if let (size, leftInset, rightInset) = strongSelf.containerLayout { - strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate) + if let (size, leftInset, rightInset, isCollapsed) = strongSelf.containerLayout { + strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, isCollapsed: isCollapsed, transition: .immediate) } } } @@ -284,8 +276,8 @@ final class ChatVideoGalleryItemScrubberView: UIView { } strongSelf.infoNode.attributedText = NSAttributedString(string: chapter.title, font: textFont, textColor: .white) - if let (size, leftInset, rightInset) = strongSelf.containerLayout { - strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate) + if let (size, leftInset, rightInset, isCollapsed) = strongSelf.containerLayout { + strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, isCollapsed: isCollapsed, transition: .immediate) } } } @@ -315,7 +307,7 @@ final class ChatVideoGalleryItemScrubberView: UIView { strongSelf.infoNodePushed = infoNodePushed if let layout = strongSelf.containerLayout { - strongSelf.updateLayout(size: layout.0, leftInset: layout.1, rightInset: layout.2, transition: .animated(duration: 0.35, curve: .spring)) + strongSelf.updateLayout(size: layout.0, leftInset: layout.1, rightInset: layout.2, isCollapsed: layout.3, transition: .animated(duration: 0.35, curve: .spring)) } } })) @@ -343,8 +335,8 @@ final class ChatVideoGalleryItemScrubberView: UIView { } strongSelf.infoNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: .white) - if let (size, leftInset, rightInset) = strongSelf.containerLayout { - strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate) + if let (size, leftInset, rightInset, isCollapsed) = strongSelf.containerLayout { + strongSelf.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, isCollapsed: isCollapsed, transition: .immediate) } } })) @@ -356,8 +348,10 @@ final class ChatVideoGalleryItemScrubberView: UIView { } } - func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { - self.containerLayout = (size, leftInset, rightInset) + func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, isCollapsed: Bool, transition: ContainedViewLayoutTransition) { + self.containerLayout = (size, leftInset, rightInset, isCollapsed) + + self.isCollapsed = isCollapsed let transition = ComponentTransition(transition) @@ -386,6 +380,11 @@ final class ChatVideoGalleryItemScrubberView: UIView { rightTimestampOffset = 14.0 infoOffset = 0.0 + if isCollapsed { + scrubberLeftInset = 0.0 + scrubberRightInset = 0.0 + } + transition.setFrame(view: self.leftTimestampNode.view, frame: CGRect(origin: CGPoint(x: 16.0, y: leftTimestampOffset), size: CGSize(width: 60.0, height: 20.0))) transition.setFrame(view: self.rightTimestampNode.view, frame: CGRect(origin: CGPoint(x: size.width - leftInset - rightInset - 60.0 - 16.0, y: rightTimestampOffset), size: CGSize(width: 60.0, height: 20.0))) @@ -397,8 +396,21 @@ final class ChatVideoGalleryItemScrubberView: UIView { transition.setPosition(view: self.infoNode.view, position: CGPoint(x: size.width / 2.0, y: infoOffset + infoSize.height / 2.0)) self.infoNode.alpha = size.width < size.height && self.isCollapsed == false ? 1.0 : 0.0 - let scrubberFrame = CGRect(origin: CGPoint(x: scrubberLeftInset, y: 15.0), size: CGSize(width: size.width - leftInset - rightInset - scrubberLeftInset - scrubberRightInset, height: scrubberHeight)) - self.scrubberNode.frame = scrubberFrame + var scrubberFrame = CGRect(origin: CGPoint(x: scrubberLeftInset, y: 15.0), size: CGSize(width: size.width - leftInset - rightInset - scrubberLeftInset - scrubberRightInset, height: scrubberHeight)) + if isCollapsed { + scrubberFrame.origin.y = size.height - scrubberHeight + } + transition.setFrame(view: self.scrubberNode.view, frame: scrubberFrame) + + let scrubberTransition: ControlledTransition + switch transition.animation { + case let .curve(duration, curve): + scrubberTransition = ControlledTransition(duration: duration, curve: curve.containedViewLayoutTransitionCurve, interactive: false) + default: + scrubberTransition = ControlledTransition(duration: 0.0, curve: .linear, interactive: false) + } + + self.scrubberNode.update(size: scrubberFrame.size, animator: scrubberTransition.legacyAnimator) self.shimmerEffectNode.updateAbsoluteRect(CGRect(origin: .zero, size: scrubberFrame.size), within: scrubberFrame.size) self.shimmerEffectNode.update(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.75), horizontal: true, effectSize: nil, globalTimeOffset: false, duration: nil) self.shimmerEffectNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 4.0), size: CGSize(width: scrubberFrame.size.width, height: 5.0)) @@ -409,6 +421,9 @@ final class ChatVideoGalleryItemScrubberView: UIView { transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) self.backgroundView.update(size: size, cornerRadius: min(44.0 * 0.5, size.height * 0.5), isDark: true, tintColor: .init(kind: .panel), transition: transition) + + self.updateTimestampsVisibility(animated: !transition.animation.isImmediate) + self.updateScrubberVisibility(animated: !transition.animation.isImmediate) } override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { @@ -452,6 +467,9 @@ final class ChatVideoGalleryItemScrubberView: UIView { self.backgroundContainer.alpha = 0.0 ComponentTransition.easeInOut(duration: 0.2).setAlpha(view: self.backgroundContainer, alpha: 1.0) + + self.scrubberNode.alpha = 0.0 + ComponentTransition.easeInOut(duration: 0.2).setAlpha(view: self.scrubberNode.view, alpha: 1.0) } func animateOut(to scrubberTransition: GalleryItemScrubberTransition?, transition: ContainedViewLayoutTransition) { @@ -488,5 +506,6 @@ final class ChatVideoGalleryItemScrubberView: UIView { } ComponentTransition.easeInOut(duration: 0.2).setAlpha(view: self.backgroundContainer, alpha: 0.0) + ComponentTransition.easeInOut(duration: 0.2).setAlpha(view: self.scrubberNode.view, alpha: 0.0) } } diff --git a/submodules/GalleryUI/Sources/GalleryControllerNode.swift b/submodules/GalleryUI/Sources/GalleryControllerNode.swift index 10ec3ffd9e..5003d2fb80 100644 --- a/submodules/GalleryUI/Sources/GalleryControllerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryControllerNode.swift @@ -75,6 +75,7 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture } self.headerEdgeEffectView = EdgeEffectView() + self.headerEdgeEffectView.isUserInteractionEnabled = false self.pager = GalleryPagerNode(pageGap: pageGap, disableTapNavigation: disableTapNavigation) self.footerNode = GalleryFooterNode(controllerInteraction: controllerInteraction) @@ -303,7 +304,7 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture edgeEffectFrame.origin.y -= navigationBarHeight } transition.updateFrame(view: self.headerEdgeEffectView, frame: edgeEffectFrame) - self.headerEdgeEffectView.update(content: .black, alpha: 0.35, rect: edgeEffectFrame, edge: .top, edgeSize: min(edgeEffectHeight, edgeEffectFrame.height), transition: ComponentTransition(transition)) + self.headerEdgeEffectView.update(content: .black, alpha: 0.65, rect: edgeEffectFrame, edge: .top, edgeSize: min(edgeEffectHeight, edgeEffectFrame.height), transition: ComponentTransition(transition)) transition.updateAlpha(layer: self.headerEdgeEffectView.layer, alpha: self.areControlsHidden ? 0.0 : 1.0) if let navigationBar = self.navigationBar { @@ -363,16 +364,16 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture self.areControlsHidden = hidden self.controlsVisibilityChanged?(!hidden) if animated { + let alpha: CGFloat = self.areControlsHidden ? 0.0 : 1.0 UIView.animate(withDuration: 0.3, animations: { - let alpha: CGFloat = self.areControlsHidden ? 0.0 : 1.0 self.navigationBar?.alpha = alpha self.statusBar?.updateAlpha(alpha, transition: .animated(duration: 0.3, curve: .easeInOut)) - self.footerNode.setVisibilityAlpha(alpha, animated: animated) - self.updateThumbnailContainerNodeAlpha(.immediate) }) + self.footerNode.setVisibilityAlpha(alpha, animated: animated) + self.updateThumbnailContainerNodeAlpha(.immediate) if let (navigationBarHeight, layout) = self.containerLayout { - self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.4, curve: .spring)) } } else { let alpha: CGFloat = self.areControlsHidden ? 0.0 : 1.0 diff --git a/submodules/GalleryUI/Sources/GalleryFooterNode.swift b/submodules/GalleryUI/Sources/GalleryFooterNode.swift index beff9b0fac..5239b91f55 100644 --- a/submodules/GalleryUI/Sources/GalleryFooterNode.swift +++ b/submodules/GalleryUI/Sources/GalleryFooterNode.swift @@ -8,6 +8,7 @@ import ComponentDisplayAdapters public final class GalleryFooterNode: ASDisplayNode { private let edgeEffectView: EdgeEffectView + private var contentsFrame = CGRect() private var currentThumbnailPanelHeight: CGFloat? private var currentFooterContentNode: GalleryFooterContentNode? @@ -20,6 +21,7 @@ public final class GalleryFooterNode: ASDisplayNode { self.controllerInteraction = controllerInteraction self.edgeEffectView = EdgeEffectView() + self.edgeEffectView.isUserInteractionEnabled = false super.init() @@ -29,7 +31,8 @@ public final class GalleryFooterNode: ASDisplayNode { private var visibilityAlpha: CGFloat = 1.0 public func setVisibilityAlpha(_ alpha: CGFloat, animated: Bool) { self.visibilityAlpha = alpha - self.edgeEffectView.alpha = alpha + let transition: ComponentTransition = animated ? .easeInOut(duration: 0.2) : .immediate + transition.setAlpha(view: self.edgeEffectView, alpha: alpha) self.currentFooterContentNode?.setVisibilityAlpha(alpha, animated: true) self.currentOverlayContentNode?.setVisibilityAlpha(alpha) } @@ -132,13 +135,15 @@ public final class GalleryFooterNode: ASDisplayNode { backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - backgroundHeight + verticalOffset), size: CGSize(width: layout.size.width, height: backgroundHeight)) } + self.contentsFrame = backgroundFrame + var edgeEffectFrame = backgroundFrame let edgeEffectHeight: CGFloat = 120.0 let edgeEffectOffset: CGFloat = 70.0 edgeEffectFrame.origin.y -= edgeEffectOffset edgeEffectFrame.size.height += edgeEffectOffset edgeEffectTransition.setFrame(view: self.edgeEffectView, frame: edgeEffectFrame) - self.edgeEffectView.update(content: .black, alpha: 0.35, rect: edgeEffectFrame, edge: .bottom, edgeSize: min(edgeEffectHeight, edgeEffectFrame.height), transition: edgeEffectTransition) + self.edgeEffectView.update(content: .black, alpha: 0.65, rect: edgeEffectFrame, edge: .bottom, edgeSize: min(edgeEffectHeight, edgeEffectFrame.height), transition: edgeEffectTransition) let contentTransition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) if let overlayContentNode = self.currentOverlayContentNode { @@ -171,7 +176,7 @@ public final class GalleryFooterNode: ASDisplayNode { if let overlayResult = self.currentOverlayContentNode?.hitTest(point, with: event) { return overlayResult } - if !self.edgeEffectView.frame.contains(point) || self.visibilityAlpha < 1.0 { + if !self.contentsFrame.contains(point) || self.visibilityAlpha < 1.0 { return nil } let result = super.hitTest(point, with: event) diff --git a/submodules/GalleryUI/Sources/GalleryTitleView.swift b/submodules/GalleryUI/Sources/GalleryTitleView.swift index 6d008ac4da..883b460487 100644 --- a/submodules/GalleryUI/Sources/GalleryTitleView.swift +++ b/submodules/GalleryUI/Sources/GalleryTitleView.swift @@ -53,7 +53,7 @@ final class GalleryTitleView: UIView, NavigationBarTitleView { } func setMessage(_ message: Message, presentationData: PresentationData, accountPeerId: PeerId, title: String?) { - let authorNameText = stringForFullAuthorName(message: EngineMessage(message), strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: accountPeerId)[0]//.joined(separator: " → ") + let authorNameText = stringForFullAuthorName(message: EngineMessage(message), strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeerId: accountPeerId).first ?? "" let dateText = humanReadableStringForTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, timestamp: message.timestamp).string self.authorNameNode.attributedText = NSAttributedString(string: authorNameText, font: titleFont, textColor: .white) diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index 11d0b7daf0..3fdc1ac8d5 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -232,6 +232,7 @@ class ChatImageGalleryItem: GalleryItem { final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { private let context: AccountContext private var message: Message? + private var displayInfo: Bool = false private var translateToLanguage: String? private var peerIsCopyProtected: Bool = false private var isSecret: Bool = false @@ -242,6 +243,9 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { private let recognitionOverlayContentNode: ImageRecognitionOverlayContentNode + private var displayTextRecognitionButton: Bool = false + private var displayStickersButton: Bool = false + private let moreBarButton: MoreHeaderButton private var tilingNode: TilingNode? @@ -445,6 +449,11 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { strongSelf.imageNode.addSubnode(recognizedContentNode) strongSelf.recognizedContentNode = recognizedContentNode strongSelf.recognitionOverlayContentNode.transitionIn() + + if !strongSelf.displayTextRecognitionButton { + strongSelf.displayTextRecognitionButton = true + strongSelf.updateFooter(animated: true) + } } } })) @@ -452,11 +461,19 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { fileprivate func setMessage(_ message: Message, displayInfo: Bool, translateToLanguage: String?, peerIsCopyProtected: Bool, isSecret: Bool) { self.message = message + self.displayInfo = displayInfo self.translateToLanguage = translateToLanguage self.peerIsCopyProtected = peerIsCopyProtected self.isSecret = isSecret self.imageNode.captureProtected = message.id.peerId.namespace == Namespaces.Peer.SecretChat || message.isCopyProtected() || peerIsCopyProtected || isSecret || message.paidContent != nil - self.footerContentNode.setMessage(message, displayInfo: displayInfo, translateToLanguage: translateToLanguage, peerIsCopyProtected: peerIsCopyProtected) + self.updateFooter(animated: false) + } + + private func updateFooter(animated: Bool) { + guard let message = self.message else { + return + } + self.footerContentNode.setMessage(message, displayInfo: self.displayInfo, translateToLanguage: self.translateToLanguage, peerIsCopyProtected: self.peerIsCopyProtected, displayTextRecognitionButton: self.displayTextRecognitionButton, displayStickersButton: self.displayStickersButton, animated: animated) } fileprivate func setImage(userLocation: MediaResourceUserLocation, imageReference: ImageMediaReference) { @@ -497,9 +514,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { var barButtonItems: [UIBarButtonItem] = [] if imageReference.media.flags.contains(.hasStickers) { - let rightBarButtonItem = UIBarButtonItem(image: generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/Stickers"), color: .white), style: .plain, target: self, action: #selector(self.openStickersButtonPressed)) - rightBarButtonItem.accessibilityLabel = self.presentationData.strings.Gallery_VoiceOver_Stickers - barButtonItems.append(rightBarButtonItem) + self.displayStickersButton = true } if self.message != nil { let moreMenuItem = UIBarButtonItem(customDisplayNode: self.moreBarButton)! @@ -509,6 +524,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { self._rightBarButtonItems.set(.single(barButtonItems)) } self.contextAndMedia = (self.context, imageReference.abstract) + self.updateFooter(animated: false) } private func updateImageFromFile(path: String) { @@ -768,11 +784,16 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { controller.presentInGlobalOverlay(contextController) } + func textRecognitionButtonPressed() { + self.recognitionOverlayContentNode.isSelected = true + self.recognitionOverlayContentNode.action?(true) + } + @objc func openStickersButtonPressed() { guard let (context, media) = self.contextAndMedia else { return } - let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme) let topController = (self.baseNavigationController()?.topViewController as? ViewController) let progressSignal = Signal { subscriber in let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) @@ -800,7 +821,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { } let baseNavigationController = strongSelf.baseNavigationController() baseNavigationController?.view.endEditing(true) - let controller = StickerPackScreen(context: context, mainStickerPack: packs[0], stickerPacks: packs, sendSticker: nil, actionPerformed: { actions in + let controller = StickerPackScreen(context: context, updatedPresentationData: (initial: presentationData, signal: .single(presentationData)), mainStickerPack: packs[0], stickerPacks: packs, sendSticker: nil, actionPerformed: { actions in if let (info, items, action) = actions.first { let animateInAsReplacement = false switch action { @@ -1562,7 +1583,7 @@ private class ImageRecognitionOverlayContentNode: GalleryOverlayContentNode { private var validLayout: (CGSize, LayoutMetrics, UIEdgeInsets)? private var interfaceIsHidden: Bool = false - private var isSelected: Bool = false + var isSelected: Bool = false init(theme: PresentationTheme) { self.backgroundContainer = GlassBackgroundContainerView() @@ -1619,33 +1640,24 @@ private class ImageRecognitionOverlayContentNode: GalleryOverlayContentNode { self.backgroundContainer.update(size: buttonSize, isDark: true, transition: transition) self.backgroundView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: buttonSize) - let tintColor: GlassBackgroundView.TintColor - if self.isSelected { - tintColor = .init(kind: .custom(style: .default, color: UIColor(white: 1.0, alpha: 1.0))) - } else { - tintColor = .init(kind: .panel) - } + let tintColor: GlassBackgroundView.TintColor = .init(kind: .custom(style: .default, color: UIColor(white: 1.0, alpha: 1.0))) self.backgroundView.update(size: buttonSize, cornerRadius: buttonSize.height * 0.5, isDark: true, tintColor: tintColor, isInteractive: true, transition: transition) self.iconView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: buttonSize) - transition.setTintColor(view: self.iconView, color: self.isSelected ? .black : .white) + transition.setTintColor(view: self.iconView, color: .black) if self.appeared { if !self.isSelected && isHidden { transition.setAlpha(view: self.backgroundContainer, alpha: 0.0) } else { - transition.setAlpha(view: self.backgroundContainer, alpha: 1.0) + transition.setAlpha(view: self.backgroundContainer, alpha: self.isSelected ? 1.0 : 0.0) } } else { transition.setAlpha(view: self.backgroundContainer, alpha: 0.0) } var buttonPosition: CGPoint - if isHidden && !self.isSelected { - buttonPosition = CGPoint(x: size.width - insets.right - buttonSize.width - 66.0 - 10.0, y: -52.0) - } else { - buttonPosition = CGPoint(x: size.width - insets.right - buttonSize.width - (self.isSelected ? 24.0 : 70.0), y: insets.top - 50.0) - } + buttonPosition = CGPoint(x: size.width - insets.right - buttonSize.width - 16.0, y: insets.top - 50.0) transition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: buttonPosition, size: buttonSize)) } diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 4cf2ba9756..48a8fd5786 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -611,226 +611,6 @@ final class MoreHeaderButton: HighlightableButtonNode { } } -final class SettingsHeaderButton: HighlightableButtonNode { - let referenceNode: ContextReferenceContentNode - let containerNode: ContextControllerSourceNode - - private let iconLayer: RasterizedCompositionMonochromeLayer - - private let gearsLayer: RasterizedCompositionImageLayer - private let dotLayer: RasterizedCompositionImageLayer - - private var speedBadge: ComponentView? - private var qualityBadge: ComponentView? - - private var speedBadgeText: String? - private var qualityBadgeText: String? - - private let badgeFont: UIFont - - private var isMenuOpen: Bool = false - - var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? - - private let wide: Bool - - init(wide: Bool = false) { - self.wide = wide - - self.referenceNode = ContextReferenceContentNode() - self.containerNode = ContextControllerSourceNode() - self.containerNode.animateScale = false - - self.iconLayer = RasterizedCompositionMonochromeLayer() - //self.iconLayer.backgroundColor = UIColor.green.cgColor - - self.gearsLayer = RasterizedCompositionImageLayer() - self.gearsLayer.image = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsNoDot"), color: .white) - - self.dotLayer = RasterizedCompositionImageLayer() - self.dotLayer.image = generateFilledCircleImage(diameter: 4.0, color: .white) - - self.iconLayer.contentsLayer.addSublayer(self.gearsLayer) - self.iconLayer.contentsLayer.addSublayer(self.dotLayer) - - self.badgeFont = Font.with(size: 8.0, design: .round, weight: .bold) - - super.init() - - self.containerNode.addSubnode(self.referenceNode) - self.referenceNode.layer.addSublayer(self.iconLayer) - self.addSubnode(self.containerNode) - - self.containerNode.shouldBegin = { [weak self] location in - guard let strongSelf = self, let _ = strongSelf.contextAction else { - return false - } - return true - } - self.containerNode.activated = { [weak self] gesture, _ in - guard let strongSelf = self else { - return - } - strongSelf.contextAction?(strongSelf.containerNode, gesture) - } - - self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 44.0, height: 44.0)) - self.referenceNode.frame = self.containerNode.bounds - - if let image = self.gearsLayer.image { - let iconInnerInsets = UIEdgeInsets(top: 4.0, left: 8.0, bottom: 4.0, right: 6.0) - let iconSize = CGSize(width: image.size.width + iconInnerInsets.left + iconInnerInsets.right, height: image.size.height + iconInnerInsets.top + iconInnerInsets.bottom) - let iconFrame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - iconSize.width) / 2.0), y: floor((self.containerNode.bounds.height - iconSize.height) / 2.0)), size: iconSize) - self.iconLayer.position = iconFrame.center - self.iconLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) - - self.iconLayer.contentsLayer.position = CGRect(origin: CGPoint(), size: iconFrame.size).center - self.iconLayer.contentsLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) - - self.iconLayer.maskedLayer.position = CGRect(origin: CGPoint(), size: iconFrame.size).center - self.iconLayer.maskedLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) - self.iconLayer.maskedLayer.backgroundColor = UIColor.white.cgColor - - let gearsFrame = CGRect(origin: CGPoint(x: floor((iconSize.width - image.size.width) * 0.5), y: floor((iconSize.height - image.size.height) * 0.5)), size: image.size) - self.gearsLayer.position = gearsFrame.center - self.gearsLayer.bounds = CGRect(origin: CGPoint(), size: gearsFrame.size) - - if let dotImage = self.dotLayer.image { - let dotFrame = CGRect(origin: CGPoint(x: gearsFrame.minX + floorToScreenPixels((gearsFrame.width - dotImage.size.width) * 0.5), y: gearsFrame.minY + floorToScreenPixels((gearsFrame.height - dotImage.size.height) * 0.5)), size: dotImage.size) - self.dotLayer.position = dotFrame.center - self.dotLayer.bounds = CGRect(origin: CGPoint(), size: dotFrame.size) - } - } - } - - override func didLoad() { - super.didLoad() - self.view.isOpaque = false - } - - override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { - return CGSize(width: 44.0, height: 44.0) - } - - func onLayout() { - } - - func setIsMenuOpen(isMenuOpen: Bool) { - if self.isMenuOpen == isMenuOpen { - return - } - self.isMenuOpen = isMenuOpen - - let rotationTransition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring) - rotationTransition.updateTransform(layer: self.gearsLayer, transform: CGAffineTransformMakeRotation(isMenuOpen ? (CGFloat.pi * 2.0 / 6.0) : 0.0)) - self.gearsLayer.animateScale(from: 1.0, to: 1.07, duration: 0.1, removeOnCompletion: false, completion: { [weak self] finished in - guard let self, finished else { - return - } - self.gearsLayer.animateScale(from: 1.07, to: 1.0, duration: 0.1, removeOnCompletion: true) - }) - - self.dotLayer.animateScale(from: 1.0, to: 0.8, duration: 0.1, removeOnCompletion: false, completion: { [weak self] finished in - guard let self, finished else { - return - } - self.dotLayer.animateScale(from: 0.8, to: 1.0, duration: 0.1, removeOnCompletion: true) - }) - } - - func setBadges(speed: String?, quality: String?, transition: ComponentTransition) { - if self.speedBadgeText == speed && self.qualityBadgeText == quality { - return - } - self.speedBadgeText = speed - self.qualityBadgeText = quality - - if let badgeText = speed { - var badgeTransition = transition - let speedBadge: ComponentView - if let current = self.speedBadge { - speedBadge = current - } else { - speedBadge = ComponentView() - self.speedBadge = speedBadge - badgeTransition = badgeTransition.withAnimation(.none) - } - let badgeSize = speedBadge.update( - transition: badgeTransition, - component: AnyComponent(BadgeComponent( - text: badgeText, - font: self.badgeFont, - cornerRadius: .custom(3.0), - insets: UIEdgeInsets(top: 1.33, left: 1.66, bottom: 1.33, right: 1.66), - outerInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0) - )), - environment: {}, - containerSize: CGSize(width: 100.0, height: 100.0) - ) - if let speedBadgeView = speedBadge.view { - if speedBadgeView.layer.superlayer == nil { - self.iconLayer.contentsLayer.addSublayer(speedBadgeView.layer) - - transition.animateAlpha(layer: speedBadgeView.layer, from: 0.0, to: 1.0) - transition.animateScale(layer: speedBadgeView.layer, from: 0.001, to: 1.0) - } - badgeTransition.setFrame(layer: speedBadgeView.layer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: badgeSize)) - } - } else if let speedBadge = self.speedBadge { - self.speedBadge = nil - if let speedBadgeView = speedBadge.view { - let speedBadgeLayer = speedBadgeView.layer - transition.setAlpha(layer: speedBadgeLayer, alpha: 0.0, completion: { [weak speedBadgeLayer] _ in - speedBadgeLayer?.removeFromSuperlayer() - }) - transition.setScale(layer: speedBadgeLayer, scale: 0.001) - } - } - - if let badgeText = quality { - var badgeTransition = transition - let qualityBadge: ComponentView - if let current = self.qualityBadge { - qualityBadge = current - } else { - qualityBadge = ComponentView() - self.qualityBadge = qualityBadge - badgeTransition = badgeTransition.withAnimation(.none) - } - let badgeSize = qualityBadge.update( - transition: badgeTransition, - component: AnyComponent(BadgeComponent( - text: badgeText, - font: self.badgeFont, - cornerRadius: .custom(3.0), - insets: UIEdgeInsets(top: 1.33, left: 1.66, bottom: 1.33, right: 1.66), - outerInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0) - )), - environment: {}, - containerSize: CGSize(width: 100.0, height: 100.0) - ) - if let qualityBadgeView = qualityBadge.view { - if qualityBadgeView.layer.superlayer == nil { - self.iconLayer.contentsLayer.addSublayer(qualityBadgeView.layer) - - transition.animateAlpha(layer: qualityBadgeView.layer, from: 0.0, to: 1.0) - transition.animateScale(layer: qualityBadgeView.layer, from: 0.001, to: 1.0) - } - badgeTransition.setFrame(layer: qualityBadgeView.layer, frame: CGRect(origin: CGPoint(x: self.iconLayer.bounds.width - badgeSize.width, y: self.iconLayer.bounds.height - badgeSize.height), size: badgeSize)) - } - } else if let qualityBadge = self.qualityBadge { - self.qualityBadge = nil - if let qualityBadgeView = qualityBadge.view { - let qualityBadgeLayer = qualityBadgeView.layer - transition.setAlpha(layer: qualityBadgeLayer, alpha: 0.0, completion: { [weak qualityBadgeLayer] _ in - qualityBadgeLayer?.removeFromSuperlayer() - }) - transition.setScale(layer: qualityBadgeLayer, scale: 0.001) - } - } - } -} - @available(iOS 15.0, *) private final class NativePictureInPictureContentImpl: NSObject, AVPictureInPictureControllerDelegate { private final class PlaybackDelegate: NSObject, AVPictureInPictureSampleBufferPlaybackDelegate { @@ -1113,8 +893,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var moreBarButtonRate: Double = 1.0 private var moreBarButtonRateTimestamp: Double? - private let settingsBarButton: SettingsHeaderButton - private var videoNode: UniversalVideoNode? private var videoNodeUserInteractionEnabled: Bool = false private var videoFramePreview: FramePreview? @@ -1139,7 +917,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var dismissOnOrientationChange = false private var keepSoundOnDismiss = false private var hasPictureInPicture = false - private var hasSettingsButton = false + private var displayStickersButton: Bool = false + private var settingsButtonState: ChatItemGalleryFooterContentNode.SettingsButtonState? private var pictureInPictureButton: UIBarButtonItem? @@ -1155,7 +934,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private let statusDisposable = MetaDisposable() private let moreButtonStateDisposable = MetaDisposable() - private let settingsButtonStateDisposable = MetaDisposable() private let mediaPlaybackStateDisposable = MetaDisposable() private let fetchDisposable = MetaDisposable() @@ -1215,9 +993,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.moreBarButton.isUserInteractionEnabled = true self.moreBarButton.setContent(.more(optionsCircleImage(dark: false))) - self.settingsBarButton = SettingsHeaderButton() - self.settingsBarButton.isUserInteractionEnabled = true - super.init() self.clipsToBounds = true @@ -1247,7 +1022,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } self.moreBarButton.addTarget(self, action: #selector(self.moreButtonPressed), forControlEvents: .touchUpInside) - //self.settingsBarButton.addTarget(self, action: #selector(self.settingsButtonPressed), forControlEvents: .touchUpInside) self.footerContentNode.interacting = { [weak self] value in self?.isInteractingPromise.set(value) @@ -1256,8 +1030,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.statusButtonNode.addSubnode(self.statusNode) self.statusButtonNode.addTarget(self, action: #selector(self.statusButtonPressed), forControlEvents: .touchUpInside) - //self.addSubnode(self.statusButtonNode) - self.footerContentNode.playbackControl = { [weak self] in if let strongSelf = self { if !strongSelf.isPaused { @@ -1391,7 +1163,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { deinit { self.statusDisposable.dispose() self.moreButtonStateDisposable.dispose() - self.settingsButtonStateDisposable.dispose() self.mediaPlaybackStateDisposable.dispose() self.scrubbingFrameDisposable?.dispose() self.hideControlsDisposable?.dispose() @@ -1864,15 +1635,27 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } - self.settingsBarButton.setBadges(speed: rateString, quality: qualityString, transition: .spring(duration: 0.35)) - })) - - self.settingsButtonStateDisposable.set((self.isShowingSettingsMenuPromise.get() - |> deliverOnMainQueue).start(next: { [weak self] isShowingSettingsMenu in - guard let self else { - return + if self.settingsButtonState != nil { + let settingsButtonState = ChatItemGalleryFooterContentNode.SettingsButtonState( + speed: rateString, + quality: qualityString + ) + if self.settingsButtonState != settingsButtonState { + self.settingsButtonState = settingsButtonState + if let validLayout = self.validLayout { + if let contentInfo = item.contentInfo { + switch contentInfo { + case let .message(message, _): + self.footerContentNode.setMessage(message, displayInfo: !item.displayInfoOnTop, peerIsCopyProtected: item.peerIsCopyProtected, displayPictureInPictureButton: self.hasPictureInPicture, settingsButtonState: self.settingsButtonState, displayStickersButton: self.displayStickersButton, animated: true) + case let .webPage(webPage, media, _): + self.footerContentNode.setWebPage(webPage, media: media) + } + } + + self.containerLayoutUpdated(validLayout.layout, navigationBarHeight: validLayout.navigationBarHeight, transition: .animated(duration: 0.4, curve: .spring)) + } + } } - self.settingsBarButton.setIsMenuOpen(isMenuOpen: isShowingSettingsMenu) })) self.statusDisposable.set((combineLatest(queue: .mainQueue(), videoNode.status, mediaFileStatus) @@ -1936,11 +1719,13 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { isPaused = false } } else if strongSelf.actionAtEnd == .stop { - strongSelf.isPlayingPromise.set(false) - strongSelf.isPlaying = false - if strongSelf.isCentral == true { - if !item.isSecret && !strongSelf.playOnDismiss { - strongSelf.updateControlsVisibility(true) + if strongSelf.isPlaying { + strongSelf.isPlayingPromise.set(false) + strongSelf.isPlaying = false + if strongSelf.isCentral == true { + if !item.isSecret && !strongSelf.playOnDismiss { + strongSelf.updateControlsVisibility(true) + } } } } @@ -2030,9 +1815,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { var barButtonItems: [UIBarButtonItem] = [] if hasLinkedStickers { - let rightBarButtonItem = UIBarButtonItem(image: generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/Stickers"), color: .white), style: .plain, target: self, action: #selector(self.openStickersButtonPressed)) - rightBarButtonItem.accessibilityLabel = self.presentationData.strings.Gallery_VoiceOver_Stickers - barButtonItems.append(rightBarButtonItem) + self.displayStickersButton = true } if forceEnablePiP || (!isAnimated && !disablePlayerControls && !disablePictureInPicture) { @@ -2080,9 +1863,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } if !isAnimated && !disablePlayerControls { - /*let settingsMenuItem = UIBarButtonItem(customDisplayNode: self.settingsBarButton)! - settingsMenuItem.accessibilityLabel = self.presentationData.strings.Settings_Title - barButtonItems.append(settingsMenuItem)*/ hasSettingsButton = true } @@ -2093,7 +1873,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } - self.hasSettingsButton = hasSettingsButton + if hasSettingsButton { + self.settingsButtonState = ChatItemGalleryFooterContentNode.SettingsButtonState( + speed: nil, + quality: nil + ) + } self._rightBarButtonItems.set(.single(barButtonItems)) videoNode.playbackCompleted = { [weak self, weak videoNode] in @@ -2148,7 +1933,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { switch contentInfo { case let .message(message, _): isAd = message.adAttribute != nil - self.footerContentNode.setMessage(message, displayInfo: !item.displayInfoOnTop, peerIsCopyProtected: item.peerIsCopyProtected, displayPictureInPictureButton: self.hasPictureInPicture, displaySettingsButton: self.hasSettingsButton) + self.footerContentNode.setMessage(message, displayInfo: !item.displayInfoOnTop, peerIsCopyProtected: item.peerIsCopyProtected, displayPictureInPictureButton: self.hasPictureInPicture, settingsButtonState: self.settingsButtonState, displayStickersButton: self.displayStickersButton) case let .webPage(webPage, media, _): self.footerContentNode.setWebPage(webPage, media: media) } @@ -3111,7 +2896,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { switch contentInfo { case let .message(message, _): isAd = message.adAttribute != nil - self.footerContentNode.setMessage(message, displayInfo: !item.displayInfoOnTop, peerIsCopyProtected: item.peerIsCopyProtected, displayPictureInPictureButton: self.hasPictureInPicture, displaySettingsButton: self.hasSettingsButton) + self.footerContentNode.setMessage(message, displayInfo: !item.displayInfoOnTop, peerIsCopyProtected: item.peerIsCopyProtected, displayPictureInPictureButton: self.hasPictureInPicture, settingsButtonState: self.settingsButtonState, displayStickersButton: self.displayStickersButton) case let .webPage(webPage, media, _): self.footerContentNode.setWebPage(webPage, media: media) } diff --git a/submodules/ItemListUI/Sources/Items/ItemListActionItem.swift b/submodules/ItemListUI/Sources/Items/ItemListActionItem.swift index c60eccaeb0..3b877e48e4 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListActionItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListActionItem.swift @@ -89,6 +89,7 @@ public class ItemListActionItem: ListViewItem, ItemListItem { public class ItemListActionItemNode: ListViewItemNode, ItemListItemNode { private let backgroundNode: ASDisplayNode + private let highlightNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode @@ -107,7 +108,10 @@ public class ItemListActionItemNode: ListViewItemNode, ItemListItemNode { public init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true - self.backgroundNode.backgroundColor = .white + + self.highlightNode = ASDisplayNode() + self.highlightNode.isLayerBacked = true + self.maskNode = ASImageNode() self.topStripeNode = ASDisplayNode() self.topStripeNode.isLayerBacked = true @@ -132,6 +136,20 @@ public class ItemListActionItemNode: ListViewItemNode, ItemListItemNode { self.addSubnode(self.activateArea) } + public func displayHighlight() { + if self.backgroundNode.supernode != nil { + self.insertSubnode(self.highlightNode, aboveSubnode: self.backgroundNode) + } else { + self.insertSubnode(self.highlightNode, at: 0) + } + + Queue.mainQueue().after(1.2, { + self.highlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + self.highlightNode.removeFromSupernode() + }) + }) + } + public func asyncLayout() -> (_ item: ItemListActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) @@ -211,6 +229,7 @@ public class ItemListActionItemNode: ListViewItemNode, ItemListItemNode { strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor strongSelf.backgroundNode.backgroundColor = itemBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor + strongSelf.highlightNode.backgroundColor = item.presentationData.theme.list.itemSearchHighlightColor } let _ = titleApply() @@ -273,6 +292,7 @@ public class ItemListActionItemNode: ListViewItemNode, ItemListItemNode { strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.highlightNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: params.width - params.rightInset - bottomStripeInset - separatorRightInset, height: separatorHeight)) diff --git a/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift b/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift index ab98ca0550..d5ee06bab1 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift @@ -891,8 +891,13 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: floor((bounds.size.height - node.lineHeight) / 2.0)), size: CGSize(width: bounds.size.width, height: node.lineHeight)) let foregroundContentFrame = CGRect(origin: CGPoint(), size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height)) - node.backgroundNode.position = CGPoint(x: backgroundFrame.midX, y: backgroundFrame.midY) - node.backgroundNode.bounds = CGRect(origin: CGPoint(), size: backgroundFrame.size) + if let animator { + animator.updatePosition(layer: node.backgroundNode.layer, position: CGPoint(x: backgroundFrame.midX, y: backgroundFrame.midY), completion: nil) + animator.updateBounds(layer: node.backgroundNode.layer, bounds: CGRect(origin: CGPoint(), size: backgroundFrame.size), completion: nil) + } else { + node.backgroundNode.position = CGPoint(x: backgroundFrame.midX, y: backgroundFrame.midY) + node.backgroundNode.bounds = CGRect(origin: CGPoint(), size: backgroundFrame.size) + } node.foregroundContentNode.position = CGPoint(x: foregroundContentFrame.midX, y: foregroundContentFrame.midY) node.foregroundContentNode.bounds = CGRect(origin: CGPoint(), size: foregroundContentFrame.size) @@ -1090,10 +1095,14 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { switch self.contentNodes { case let .standard(node): if let handleNodeContainer = node.handleNodeContainer, handleNodeContainer.isUserInteractionEnabled, handleNodeContainer.frame.insetBy(dx: 0.0, dy: -16.0).contains(point) { - if let handleNode = node.handleNode, handleNode.convert(handleNode.bounds, to: self).insetBy(dx: -32.0, dy: -16.0).contains(point) { + if case .none = node.handle { return handleNodeContainer.view } else { - return nil + if let handleNode = node.handleNode, handleNode.convert(handleNode.bounds, to: self).insetBy(dx: -32.0, dy: -16.0).contains(point) { + return handleNodeContainer.view + } else { + return nil + } } } else { return nil diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift index 167e3e4471..5a5f721893 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift @@ -40,9 +40,11 @@ private func generateBubbleShadowImage(shadow: UIColor, diameter: CGFloat, shado final class ReactionContextBackgroundNode: ASDisplayNode { struct GlassParams { + var isDark: Bool var isTinted: Bool - init(isTinted: Bool) { + init(isDark: Bool, isTinted: Bool) { + self.isDark = isDark self.isTinted = isTinted } } @@ -244,7 +246,7 @@ final class ReactionContextBackgroundNode: ASDisplayNode { transition.updateFrame(view: self.backgroundView, frame: contentBounds, beginWithCurrentState: true) self.backgroundView.update(size: contentBounds.size, transition: transition) - if let glassBackgroundView = self.glassBackgroundView { + if let glass = self.glass, let glassBackgroundView = self.glassBackgroundView { var glassBackgroundFrame = contentBounds.insetBy(dx: 10.0, dy: 10.0) glassBackgroundFrame.size.height -= 8.0 transition.updateFrame(view: glassBackgroundView.container, frame: glassBackgroundFrame, beginWithCurrentState: true) @@ -255,7 +257,7 @@ final class ReactionContextBackgroundNode: ASDisplayNode { } else { glassTintColor = .init(kind: .panel) } - glassBackgroundView.container.update(size: glassBackgroundFrame.size, isDark: true, transition: ComponentTransition(transition)) + glassBackgroundView.container.update(size: glassBackgroundFrame.size, isDark: glass.isDark, transition: ComponentTransition(transition)) glassBackgroundView.view.update(size: glassBackgroundFrame.size, cornerRadius: 23.0, isDark: true, tintColor: glassTintColor, transition: ComponentTransition(transition)) transition.updateFrame(view: self.backgroundTintView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentBounds.width, height: contentBounds.height)).insetBy(dx: -10.0, dy: -10.0)) @@ -283,6 +285,13 @@ final class ReactionContextBackgroundNode: ASDisplayNode { self.backgroundClippingLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.01, delay: mainCircleDelay) self.backgroundClippingLayer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: mainCircleDuration, delay: mainCircleDelay) self.backgroundShadowLayer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: mainCircleDuration, delay: mainCircleDelay) + + if let glassBackgroundView = self.glassBackgroundView { + glassBackgroundView.container.alpha = 0.0 + + let transition: ComponentTransition = .easeInOut(duration: 0.2) + transition.setAlpha(view: glassBackgroundView.container, alpha: 1.0) + } } func animateInFromAnchorRect(size: CGSize, sourceBackgroundFrame: CGRect) { @@ -304,6 +313,13 @@ final class ReactionContextBackgroundNode: ASDisplayNode { self.backgroundClippingLayer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: visualSourceBackgroundFrame.size)), to: NSValue(cgRect: self.backgroundClippingLayer.bounds), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping) self.backgroundShadowLayer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceShadowFrame.midX - size.width / 2.0, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping, additive: true) self.backgroundShadowLayer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: sourceShadowFrame.size)), to: NSValue(cgRect: self.backgroundShadowLayer.bounds), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping) + + if let glassBackgroundView = self.glassBackgroundView { + glassBackgroundView.container.alpha = 0.0 + + let transition: ComponentTransition = .easeInOut(duration: 0.2) + transition.setAlpha(view: glassBackgroundView.container, alpha: 1.0) + } } func animateOut() { @@ -314,5 +330,10 @@ final class ReactionContextBackgroundNode: ASDisplayNode { self.largeCircleShadowLayer.animateAlpha(from: CGFloat(self.largeCircleShadowLayer.opacity), to: 0.0, duration: 0.1, removeOnCompletion: false) //self.smallCircleLayer.animateAlpha(from: CGFloat(self.smallCircleLayer.opacity), to: 0.0, duration: 0.2, removeOnCompletion: false) self.smallCircleShadowLayer.animateAlpha(from: CGFloat(self.smallCircleShadowLayer.opacity), to: 0.0, duration: 0.1, removeOnCompletion: false) + + if let glassBackgroundView = self.glassBackgroundView { + let transition: ComponentTransition = .easeInOut(duration: 0.2) + transition.setAlpha(view: glassBackgroundView.container, alpha: 0.0) + } } } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 4b752d3e6a..45045f3107 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -510,7 +510,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { self.backgroundMaskNode = ASDisplayNode() var backgroundGlassParams: ReactionContextBackgroundNode.GlassParams? if case let .glass(isTinted) = style { - backgroundGlassParams = ReactionContextBackgroundNode.GlassParams(isTinted: isTinted) + backgroundGlassParams = ReactionContextBackgroundNode.GlassParams(isDark: presentationData.theme.overallDarkAppearance, isTinted: isTinted) } self.backgroundNode = ReactionContextBackgroundNode(glass: backgroundGlassParams, largeCircleSize: largeCircleSize, smallCircleSize: smallCircleSize, maskNode: self.backgroundMaskNode) self.leftBackgroundMaskNode = ASDisplayNode() diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index 8a40750407..b0ecc4368e 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -139,6 +139,7 @@ swift_library( "//submodules/TelegramUI/Components/Settings/PeerSelectionScreen", "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/Utils/DeviceModel", ], visibility = [ "//visibility:public", diff --git a/submodules/SettingsUI/Sources/LogoutOptionsController.swift b/submodules/SettingsUI/Sources/LogoutOptionsController.swift index 862a3054cd..51c2e728fc 100644 --- a/submodules/SettingsUI/Sources/LogoutOptionsController.swift +++ b/submodules/SettingsUI/Sources/LogoutOptionsController.swift @@ -224,7 +224,7 @@ public func logoutOptionsController(context: AccountContext, navigationControlle context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { controller, arguments in - pushControllerImpl?(controller) + presentControllerImpl?(controller, nil) }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) }) } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift index d7d5c84346..c20613b23e 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift @@ -623,15 +623,12 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont let removeSessionImpl: (Int64, @escaping () -> Void) -> Void = { sessionId, completion in let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationData: presentationData) - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() - } - controller.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: presentationData.strings.AuthSessions_TerminateSessionText), - ActionSheetButtonItem(title: presentationData.strings.AuthSessions_TerminateSession, color: .destructive, action: { - dismissAction() + let controller = textAlertController( + context: context, + title: nil, + text: presentationData.strings.AuthSessions_TerminateSessionText, + actions: [ + TextAlertAction(type: .defaultDestructiveAction, title: presentationData.strings.AuthSessions_TerminateSession, action: { completion() updateState { @@ -649,11 +646,12 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont } context.sharedContext.updateNotificationTokensRegistration() })) - }) - ]), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) - ]) - presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }), + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}) + ], + actionLayout: .vertical + ) + presentControllerImpl?(controller, nil) } let removeWebSessionImpl: (Int64) -> Void = { sessionId in @@ -690,16 +688,12 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont removeSessionImpl(sessionId, {}) }, terminateOtherSessions: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationData: presentationData) - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() - } - controller.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: presentationData.strings.AuthSessions_TerminateOtherSessionsText), - ActionSheetButtonItem(title: presentationData.strings.AuthSessions_TerminateOtherSessions, color: .destructive, action: { - dismissAction() - + let controller = textAlertController( + context: context, + title: nil, + text: presentationData.strings.AuthSessions_TerminateOtherSessionsText, + actions: [ + TextAlertAction(type: .defaultDestructiveAction, title: presentationData.strings.AuthSessions_TerminateOtherSessions, action: { updateState { return $0.withUpdatedTerminatingOtherSessions(true) } @@ -715,11 +709,12 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont } context.sharedContext.updateNotificationTokensRegistration() })) - }) - ]), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) - ]) - presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }), + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}) + ], + actionLayout: .vertical + ) + presentControllerImpl?(controller, nil) }, openSession: { session in let controller = RecentSessionScreen(context: context, subject: .session(session), updateAcceptSecretChats: { value in updateSessionDisposable.set(activeSessionsContext.updateSessionAcceptsSecretChats(session, accepts: value).start()) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift b/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift index ef326ba14f..d79684a91d 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift @@ -5,359 +5,272 @@ import AsyncDisplayKit import TelegramCore import SwiftSignalKit import AccountContext -import SolidRoundedButtonNode import TelegramPresentationData -import TelegramUIPreferences -import TelegramStringFormatting +import ComponentFlow +import ViewControllerComponent +import SheetComponent +import MultilineTextComponent +import BalancedTextComponent +import GlassBarButtonComponent +import ButtonComponent +import TableComponent import PresentationDataUtils -import AnimationUI -import MergeLists -import MediaResources -import StickerResources -import AnimatedStickerNode -import TelegramAnimatedStickerNode -import AvatarNode -import UndoUI +import BundleIconComponent +import LottieAnimationComponent +import ListSectionComponent +import ListActionItemComponent +import AvatarComponent +import TelegramStringFormatting +import Markdown -private func closeButtonImage(theme: PresentationTheme) -> UIImage? { - return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - - context.setFillColor(UIColor(rgb: 0x808084, alpha: 0.1).cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - - context.setLineWidth(2.0) - context.setLineCap(.round) - context.setStrokeColor(theme.actionSheet.inputClearButtonColor.cgColor) - - context.move(to: CGPoint(x: 10.0, y: 10.0)) - context.addLine(to: CGPoint(x: 20.0, y: 20.0)) - context.strokePath() - - context.move(to: CGPoint(x: 20.0, y: 10.0)) - context.addLine(to: CGPoint(x: 10.0, y: 20.0)) - context.strokePath() - }) -} - -final class RecentSessionScreen: ViewController { - enum Subject { - case session(RecentAccountSession) - case website(WebAuthorization, EnginePeer?) - } - private var controllerNode: RecentSessionScreenNode { - return self.displayNode as! RecentSessionScreenNode - } +private final class RecentSessionSheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment - private var animatedIn = false + let context: AccountContext + let subject: RecentSessionScreen.Subject + let cancel: (Bool) -> Void - private let context: AccountContext - private let subject: RecentSessionScreen.Subject - private let remove: (@escaping () -> Void) -> Void - private let updateAcceptSecretChats: (Bool) -> Void - private let updateAcceptIncomingCalls: (Bool) -> Void - - private var presentationData: PresentationData - private var presentationDataDisposable: Disposable? - - var dismissed: (() -> Void)? - - var passthroughHitTestImpl: ((CGPoint) -> UIView?)? { - didSet { - if self.isNodeLoaded { - self.controllerNode.passthroughHitTestImpl = self.passthroughHitTestImpl - } - } - } - - init(context: AccountContext, subject: RecentSessionScreen.Subject, updateAcceptSecretChats: @escaping (Bool) -> Void, updateAcceptIncomingCalls: @escaping (Bool) -> Void, remove: @escaping (@escaping () -> Void) -> Void) { + init( + context: AccountContext, + subject: RecentSessionScreen.Subject, + cancel: @escaping (Bool) -> Void + ) { self.context = context - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.subject = subject - self.remove = remove - self.updateAcceptSecretChats = updateAcceptSecretChats - self.updateAcceptIncomingCalls = updateAcceptIncomingCalls + self.cancel = cancel + } + + static func ==(lhs: RecentSessionSheetContent, rhs: RecentSessionSheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + final class State: ComponentState { + var allowSecretChats: Bool? + var allowIncomingCalls: Bool? - super.init(navigationBarPresentationData: nil) + weak var controller: RecentSessionScreen? - self.statusBar.statusBarStyle = .Ignore - - self.blocksBackgroundWhenInOverlay = true - - self.presentationDataDisposable = (context.sharedContext.presentationData - |> deliverOnMainQueue).start(next: { [weak self] presentationData in - if let strongSelf = self { - strongSelf.presentationData = presentationData - strongSelf.controllerNode.updatePresentationData(presentationData) + init(subject: RecentSessionScreen.Subject) { + super.init() + + switch subject { + case let .session(session): + if !session.flags.contains(.passwordPending) && session.apiId != 22 { + self.allowIncomingCalls = session.flags.contains(.acceptsIncomingCalls) + + if ![2040, 2496].contains(session.apiId) { + self.allowSecretChats = session.flags.contains(.acceptsSecretChats) + } + } + case .website: + break } - }) + } - self.statusBar.statusBarStyle = .Ignore - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.presentationDataDisposable?.dispose() - } - - override public func loadDisplayNode() { - self.displayNode = RecentSessionScreenNode(context: self.context, presentationData: self.presentationData, controller: self, subject: self.subject) - self.controllerNode.passthroughHitTestImpl = self.passthroughHitTestImpl - self.controllerNode.present = { [weak self] c in - self?.present(c, in: .current) + func toggleAllowSecretChats() { + guard let controller = self.controller else { + return + } + + if let allowSecretChats = self.allowSecretChats { + let newValue = !allowSecretChats + self.allowSecretChats = newValue + controller.updateAcceptSecretChats(newValue) + } + + self.updated() } - self.controllerNode.dismiss = { [weak self] in - self?.presentingViewController?.dismiss(animated: false, completion: nil) + + func toggleAllowIncomingCalls() { + guard let controller = self.controller else { + return + } + + if let allowIncomingCalls = self.allowIncomingCalls { + let newValue = !allowIncomingCalls + self.allowIncomingCalls = newValue + controller.updateAcceptIncomingCalls(newValue) + } + + self.updated() } - self.controllerNode.remove = { [weak self] in - self?.remove({ - self?.controllerNode.animateOut() + + func terminate() { + guard let controller = self.controller else { + return + } + self.updated() + + controller.remove({ [weak controller] in + controller?.dismissAnimated() }) } - self.controllerNode.updateAcceptSecretChats = { [weak self] value in - self?.updateAcceptSecretChats(value) - } - self.controllerNode.updateAcceptIncomingCalls = { [weak self] value in - self?.updateAcceptIncomingCalls(value) - } } - override public func loadView() { - super.loadView() - - self.view.disablesInteractiveTransitionGestureRecognizer = true + func makeState() -> State { + return State(subject: self.subject) } - override public func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) + static var body: Body { + let closeButton = Child(GlassBarButtonComponent.self) + let icon = Child(ZStack.self) + let avatar = Child(AvatarComponent.self) + let title = Child(BalancedTextComponent.self) + let description = Child(MultilineTextComponent.self) + let clientSection = Child(ListSectionComponent.self) + let optionsSection = Child(ListSectionComponent.self) + let button = Child(ButtonComponent.self) - if !self.animatedIn { - self.animatedIn = true - self.controllerNode.animateIn() - } - } - - override public func dismiss(completion: (() -> Void)? = nil) { - self.controllerNode.animateOut(completion: completion) - - self.dismissed?() - } - - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) - } -} - -private class RecentSessionScreenNode: ViewControllerTracingNode, ASScrollViewDelegate { - private let context: AccountContext - private var presentationData: PresentationData - private weak var controller: RecentSessionScreen? - private let subject: RecentSessionScreen.Subject - - private let dimNode: ASDisplayNode - private let wrappingScrollNode: ASScrollNode - private let contentContainerNode: ASDisplayNode - private let topContentContainerNode: SparseNode - private let backgroundNode: ASDisplayNode - private let contentBackgroundNode: ASDisplayNode - private var iconNode: ASImageNode? - private var animationBackgroundNode: ASDisplayNode? - private var animationNode: AnimationNode? - private var avatarNode: AvatarNode? - private let titleNode: ImmediateTextNode - private let textNode: ImmediateTextNode - private let fieldBackgroundNode: ASDisplayNode - private let deviceTitleNode: ImmediateTextNode - private let deviceValueNode: ImmediateTextNode - private let firstSeparatorNode: ASDisplayNode - private let ipTitleNode: ImmediateTextNode - private let ipValueNode: ImmediateTextNode - private let secondSeparatorNode: ASDisplayNode - private let locationTitleNode: ImmediateTextNode - private let locationValueNode: ImmediateTextNode - private let locationInfoNode: ImmediateTextNode - - private let acceptBackgroundNode: ASDisplayNode - private let acceptHeaderNode: ImmediateTextNode - private let secretChatsTitleNode: ImmediateTextNode - private let secretChatsSwitchNode: SwitchNode - private let secretChatsActivateAreaNode: AccessibilityAreaNode - private let incomingCallsTitleNode: ImmediateTextNode - private let incomingCallsSwitchNode: SwitchNode - private let incomingCallsActivateAreaNode: AccessibilityAreaNode - private let acceptSeparatorNode: ASDisplayNode - - private let cancelButton: HighlightableButtonNode - private let terminateButton: SolidRoundedButtonNode - - private var containerLayout: (ContainerViewLayout, CGFloat)? - - var present: ((ViewController) -> Void)? - var remove: (() -> Void)? - var dismiss: (() -> Void)? - var updateAcceptSecretChats: ((Bool) -> Void)? - var updateAcceptIncomingCalls: ((Bool) -> Void)? - - init(context: AccountContext, presentationData: PresentationData, controller: RecentSessionScreen, subject: RecentSessionScreen.Subject) { - self.context = context - self.controller = controller - self.presentationData = presentationData - self.subject = subject - - self.wrappingScrollNode = ASScrollNode() - self.wrappingScrollNode.view.alwaysBounceVertical = true - self.wrappingScrollNode.view.delaysContentTouches = false - self.wrappingScrollNode.view.showsVerticalScrollIndicator = false - self.wrappingScrollNode.view.canCancelContentTouches = true - - self.dimNode = ASDisplayNode() - self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) - - self.contentContainerNode = ASDisplayNode() - self.contentContainerNode.isOpaque = false - - self.topContentContainerNode = SparseNode() - self.topContentContainerNode.isOpaque = false - - self.backgroundNode = ASDisplayNode() - self.backgroundNode.clipsToBounds = true - self.backgroundNode.cornerRadius = 16.0 - - let backgroundColor = self.presentationData.theme.list.blocksBackgroundColor - let textColor = self.presentationData.theme.list.itemPrimaryTextColor - let accentColor = self.presentationData.theme.list.itemAccentColor - let secondaryTextColor = self.presentationData.theme.list.itemSecondaryTextColor - - self.contentBackgroundNode = ASDisplayNode() - self.contentBackgroundNode.backgroundColor = backgroundColor - - self.titleNode = ImmediateTextNode() - self.titleNode.maximumNumberOfLines = 2 - self.titleNode.textAlignment = .center - - self.textNode = ImmediateTextNode() - self.textNode.maximumNumberOfLines = 1 - self.textNode.textAlignment = .center - - self.fieldBackgroundNode = ASDisplayNode() - self.fieldBackgroundNode.clipsToBounds = true - self.fieldBackgroundNode.cornerRadius = 11 - self.fieldBackgroundNode.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor - - self.deviceTitleNode = ImmediateTextNode() - self.deviceValueNode = ImmediateTextNode() - - self.ipTitleNode = ImmediateTextNode() - self.ipValueNode = ImmediateTextNode() - - self.locationTitleNode = ImmediateTextNode() - self.locationValueNode = ImmediateTextNode() - self.locationInfoNode = ImmediateTextNode() - - self.acceptHeaderNode = ImmediateTextNode() - self.secretChatsTitleNode = ImmediateTextNode() - self.secretChatsSwitchNode = SwitchNode() - self.incomingCallsTitleNode = ImmediateTextNode() - self.incomingCallsSwitchNode = SwitchNode() - - self.secretChatsActivateAreaNode = AccessibilityAreaNode() - self.incomingCallsActivateAreaNode = AccessibilityAreaNode() - - self.cancelButton = HighlightableButtonNode() - self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) - self.cancelButton.accessibilityLabel = presentationData.strings.Common_Close - self.cancelButton.accessibilityTraits = [.button] - - self.terminateButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: self.presentationData.theme.list.itemBlocksBackgroundColor, foregroundColor: self.presentationData.theme.list.itemDestructiveColor), font: .regular, height: 44.0, cornerRadius: 11.0) - - var hasSecretChats = false - var hasIncomingCalls = false - - let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - let title: String - let subtitle: String - let subtitleActive: Bool - let device: String - let deviceTitle: String - let location: String - let ip: String - switch subject { + return { context in + let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + let component = context.component + let theme = environment.theme + let strings = environment.strings + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let state = context.state + if state.controller == nil { + state.controller = environment.controller() as? RecentSessionScreen + } + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + + let closeButton = closeButton.update( + component: GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: theme.chat.inputPanel.panelControlColor + ) + )), + action: { _ in + component.cancel(true) + } + ), + availableSize: CGSize(width: 44.0, height: 44.0), + transition: .immediate + ) + context.add(closeButton + .position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0)) + ) + + var contentHeight: CGFloat = 32.0 + switch component.subject { case let .session(session): - self.terminateButton.title = self.presentationData.strings.AuthSessions_View_TerminateSession - var appVersion = session.appVersion - appVersion = appVersion.replacingOccurrences(of: "APPSTORE", with: "").replacingOccurrences(of: "BETA", with: "Beta").trimmingTrailingSpaces() + let (image, backgroundColor, animationName, colorsArray) = iconForSession(session) - if session.isCurrent { - subtitle = presentationData.strings.Presence_online - subtitleActive = true - } else { - subtitle = stringForRelativeActivityTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, relativeTimestamp: session.activityDate, relativeTo: timestamp) - subtitleActive = false - } - deviceTitle = presentationData.strings.AuthSessions_View_Application - - var deviceString = "" - if !session.deviceModel.isEmpty { - deviceString = session.deviceModel - } - title = deviceString - device = "\(session.appName) \(appVersion)" - location = session.country - ip = session.ip - - let (icon, backgroundColor, animationName, colorsArray) = iconForSession(session) - if let animationName = animationName { + var items: [AnyComponentWithIdentity] = [] + items.append( + AnyComponentWithIdentity( + id: "background", + component: AnyComponent( + FilledRoundedRectangleComponent( + color: backgroundColor ?? .clear, + cornerRadius: .value(20.0), + smoothCorners: true + ) + ) + ) + ) + if let animationName { var colors: [String: UIColor] = [:] - if let colorsArray = colorsArray { + if let colorsArray { for color in colorsArray { colors[color] = backgroundColor } } - let animationNode = AnimationNode(animation: animationName, colors: colors, scale: 1.0) - self.animationNode = animationNode - - let animationBackgroundNode = ASDisplayNode() - animationBackgroundNode.cornerRadius = 20.0 - animationBackgroundNode.backgroundColor = backgroundColor - self.animationBackgroundNode = animationBackgroundNode - } else if let icon = icon { - let iconNode = ASImageNode() - iconNode.displaysAsynchronously = false - iconNode.image = icon - self.iconNode = iconNode + items.append( + AnyComponentWithIdentity( + id: "animation", + component: AnyComponent( + LottieAnimationComponent( + animation: .init(name: animationName, mode: .animating(loop: false)), + colors: colors, + size: CGSize(width: 92.0, height: 92.0) + ) + ) + ) + ) + } else if let image { + items.append( + AnyComponentWithIdentity( + id: "icon", + component: AnyComponent( + Image(image: image) + ) + ) + ) } - - self.secretChatsSwitchNode.isOn = session.flags.contains(.acceptsSecretChats) - self.incomingCallsSwitchNode.isOn = session.flags.contains(.acceptsIncomingCalls) - - self.secretChatsActivateAreaNode.accessibilityValue = self.secretChatsSwitchNode.isOn ? presentationData.strings.VoiceOver_Common_On : presentationData.strings.VoiceOver_Common_Off - self.incomingCallsActivateAreaNode.accessibilityValue = self.incomingCallsSwitchNode.isOn ? presentationData.strings.VoiceOver_Common_On : presentationData.strings.VoiceOver_Common_Off - - if !session.flags.contains(.passwordPending) && session.apiId != 22 { - hasIncomingCalls = true - if ![2040, 2496].contains(session.apiId) { - hasSecretChats = true - } + + let icon = icon.update( + component: ZStack(items), + availableSize: CGSize(width: 92.0, height: 92.0), + transition: .immediate + ) + context.add(icon + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + icon.size.height / 2.0)) + ) + contentHeight += icon.size.height + contentHeight += 18.0 + case let .website(_, peer): + if let peer { + let avatar = avatar.update( + component: AvatarComponent( + context: component.context, + theme: environment.theme, + peer: peer, + clipStyle: .roundedRect + ), + availableSize: CGSize(width: 92.0, height: 92.0), + transition: .immediate + ) + context.add(avatar + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + avatar.size.height / 2.0)) + ) + contentHeight += avatar.size.height + contentHeight += 18.0 } - case let .website(website, peer): - self.terminateButton.title = self.presentationData.strings.AuthSessions_View_Logout + } - if let peer = peer { - title = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + let titleString: String + let subtitleString: String + let subtitleActive: Bool + let applicationTitle: String + let applicationString: String + let ipString: String? + let locationString: String + let buttonString: String? + + switch component.subject { + case let .session(session): + titleString = session.deviceModel + if session.isCurrent { + subtitleString = strings.Presence_online + subtitleActive = true } else { - title = "" + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + subtitleString = stringForRelativeActivityTimestamp(strings: strings, dateTimeFormat: presentationData.dateTimeFormat, relativeTimestamp: session.activityDate, relativeTo: timestamp) + subtitleActive = false } - - subtitle = website.domain + var appVersion = session.appVersion + appVersion = appVersion.replacingOccurrences(of: "APPSTORE", with: "").replacingOccurrences(of: "BETA", with: "Beta").trimmingTrailingSpaces() + applicationTitle = strings.AuthSessions_View_Application + applicationString = "\(session.appName) \(appVersion)" + ipString = nil + locationString = session.country + + buttonString = !session.isCurrent ? strings.AuthSessions_View_TerminateSession : nil + case let .website(website, peer): + titleString = peer?.compactDisplayTitle ?? "" + subtitleString = website.domain subtitleActive = false - - deviceTitle = presentationData.strings.AuthSessions_View_Browser - + var deviceString = "" if !website.browser.isEmpty { deviceString += website.browser @@ -368,558 +281,418 @@ private class RecentSessionScreenNode: ViewControllerTracingNode, ASScrollViewDe } deviceString += website.platform } - device = deviceString - location = website.region - ip = website.ip - - let avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 12.0)) - avatarNode.clipsToBounds = true - avatarNode.cornerRadius = 17.0 - if let peer { - avatarNode.setPeer(context: context, theme: presentationData.theme, peer: peer, authorOfMessage: nil, overrideImage: nil, emptyColor: nil, clipStyle: .none, synchronousLoad: false, displayDimensions: CGSize(width: 72.0, height: 72.0), storeUnrounded: false) - } - self.avatarNode = avatarNode - } - - self.titleNode.attributedText = NSAttributedString(string: title, font: Font.regular(30.0), textColor: textColor) - self.titleNode.accessibilityLabel = title - self.titleNode.isAccessibilityElement = true - - self.textNode.attributedText = NSAttributedString(string: subtitle, font: Font.regular(17.0), textColor: subtitleActive ? accentColor : secondaryTextColor) - self.textNode.accessibilityLabel = subtitle - self.textNode.isAccessibilityElement = true - - self.deviceTitleNode.attributedText = NSAttributedString(string: deviceTitle, font: Font.regular(17.0), textColor: textColor) - self.deviceValueNode.attributedText = NSAttributedString(string: device, font: Font.regular(17.0), textColor: secondaryTextColor) - self.deviceValueNode.accessibilityLabel = deviceTitle - self.deviceValueNode.accessibilityValue = device - self.deviceValueNode.isAccessibilityElement = true - - self.firstSeparatorNode = ASDisplayNode() - self.firstSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor - - self.ipTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_IP, font: Font.regular(17.0), textColor: textColor) - self.ipValueNode.attributedText = NSAttributedString(string: ip, font: Font.regular(17.0), textColor: secondaryTextColor) - self.ipValueNode.accessibilityLabel = self.presentationData.strings.AuthSessions_View_IP - self.ipValueNode.accessibilityValue = ip - self.ipValueNode.isAccessibilityElement = true - - self.secondSeparatorNode = ASDisplayNode() - self.secondSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor - - self.locationTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_Location, font: Font.regular(17.0), textColor: textColor) - - self.locationValueNode.attributedText = NSAttributedString(string: location, font: Font.regular(17.0), textColor: secondaryTextColor) - self.locationValueNode.accessibilityLabel = self.presentationData.strings.AuthSessions_View_Location - self.locationValueNode.accessibilityValue = location - self.locationValueNode.isAccessibilityElement = true - - self.locationInfoNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_LocationInfo, font: Font.regular(13.0), textColor: secondaryTextColor) - self.locationInfoNode.maximumNumberOfLines = 4 - self.locationInfoNode.accessibilityLabel = self.presentationData.strings.AuthSessions_View_LocationInfo - self.locationInfoNode.isAccessibilityElement = true - - self.acceptBackgroundNode = ASDisplayNode() - self.acceptBackgroundNode.clipsToBounds = true - self.acceptBackgroundNode.cornerRadius = 11 - self.acceptBackgroundNode.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor - - self.acceptHeaderNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_AcceptTitle.uppercased(), font: Font.regular(17.0), textColor: textColor) - self.acceptHeaderNode.accessibilityLabel = self.presentationData.strings.AuthSessions_View_AcceptTitle - self.acceptHeaderNode.isAccessibilityElement = true - - self.secretChatsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_AcceptSecretChats, font: Font.regular(17.0), textColor: textColor) - self.incomingCallsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_AcceptIncomingCalls, font: Font.regular(17.0), textColor: textColor) - - self.secretChatsActivateAreaNode.accessibilityLabel = self.presentationData.strings.AuthSessions_View_AcceptSecretChats - self.secretChatsActivateAreaNode.accessibilityHint = self.presentationData.strings.VoiceOver_Common_SwitchHint - - self.incomingCallsActivateAreaNode.accessibilityLabel = self.presentationData.strings.AuthSessions_View_AcceptIncomingCalls - self.incomingCallsActivateAreaNode.accessibilityHint = self.presentationData.strings.VoiceOver_Common_SwitchHint - - self.acceptSeparatorNode = ASDisplayNode() - self.acceptSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor - - super.init() - - self.backgroundColor = nil - self.isOpaque = false - - self.addSubnode(self.dimNode) - - self.wrappingScrollNode.view.delegate = self.wrappedScrollViewDelegate - self.addSubnode(self.wrappingScrollNode) - - self.wrappingScrollNode.addSubnode(self.backgroundNode) - self.wrappingScrollNode.addSubnode(self.contentContainerNode) - self.wrappingScrollNode.addSubnode(self.topContentContainerNode) - - self.backgroundNode.addSubnode(self.contentBackgroundNode) - self.contentContainerNode.addSubnode(self.titleNode) - self.contentContainerNode.addSubnode(self.textNode) - - self.contentContainerNode.addSubnode(self.fieldBackgroundNode) - - self.contentContainerNode.addSubnode(self.deviceTitleNode) - self.contentContainerNode.addSubnode(self.deviceValueNode) - - self.contentContainerNode.addSubnode(self.ipTitleNode) - self.contentContainerNode.addSubnode(self.ipValueNode) - - self.contentContainerNode.addSubnode(self.locationTitleNode) - self.contentContainerNode.addSubnode(self.locationValueNode) - self.contentContainerNode.addSubnode(self.locationInfoNode) - - self.contentContainerNode.addSubnode(self.firstSeparatorNode) - self.contentContainerNode.addSubnode(self.secondSeparatorNode) - - self.contentContainerNode.addSubnode(self.terminateButton) - self.topContentContainerNode.addSubnode(self.cancelButton) - - self.iconNode.flatMap { self.contentContainerNode.addSubnode($0) } - self.animationBackgroundNode.flatMap { self.contentContainerNode.addSubnode($0) } - self.animationNode.flatMap { self.contentContainerNode.addSubnode($0) } - self.avatarNode.flatMap { self.contentContainerNode.addSubnode($0) } - - if hasIncomingCalls { - self.contentContainerNode.addSubnode(self.acceptBackgroundNode) - self.contentContainerNode.addSubnode(self.acceptHeaderNode) - if hasSecretChats { - self.contentContainerNode.addSubnode(self.secretChatsTitleNode) - self.contentContainerNode.addSubnode(self.secretChatsSwitchNode) - self.contentContainerNode.addSubnode(self.secretChatsActivateAreaNode) + applicationTitle = strings.AuthSessions_View_Browser + applicationString = deviceString + ipString = website.ip + locationString = website.region - self.secretChatsSwitchNode.valueUpdated = { [weak self] value in - if let strongSelf = self { - strongSelf.updateAcceptSecretChats?(value) - - strongSelf.secretChatsActivateAreaNode.accessibilityValue = value ? presentationData.strings.VoiceOver_Common_On : presentationData.strings.VoiceOver_Common_Off - } - } - - self.secretChatsActivateAreaNode.activate = { [weak self] in - guard let strongSelf = self else { - return false - } - let value = !strongSelf.secretChatsSwitchNode.isOn - strongSelf.updateAcceptSecretChats?(value) - strongSelf.secretChatsActivateAreaNode.accessibilityValue = value ? presentationData.strings.VoiceOver_Common_On : presentationData.strings.VoiceOver_Common_Off - return true - } - - self.contentContainerNode.addSubnode(self.acceptSeparatorNode) - } - self.contentContainerNode.addSubnode(self.incomingCallsTitleNode) - self.contentContainerNode.addSubnode(self.incomingCallsSwitchNode) - self.contentContainerNode.addSubnode(self.incomingCallsActivateAreaNode) - - self.incomingCallsSwitchNode.valueUpdated = { [weak self] value in - if let strongSelf = self { - strongSelf.updateAcceptIncomingCalls?(value) - - strongSelf.incomingCallsActivateAreaNode.accessibilityValue = value ? presentationData.strings.VoiceOver_Common_On : presentationData.strings.VoiceOver_Common_Off - } + buttonString = strings.AuthSessions_View_Logout } - self.incomingCallsActivateAreaNode.activate = { [weak self] in - guard let strongSelf = self else { - return false - } - let value = !strongSelf.incomingCallsSwitchNode.isOn - strongSelf.updateAcceptIncomingCalls?(value) - strongSelf.incomingCallsActivateAreaNode.accessibilityValue = value ? presentationData.strings.VoiceOver_Common_On : presentationData.strings.VoiceOver_Common_Off - return true - } - } - - self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) - self.terminateButton.pressed = { [weak self] in - if let strongSelf = self { - strongSelf.remove?() - } - } - } - - override func didLoad() { - super.didLoad() - - if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { - self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never - } - - self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture))) - - let titleGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleTitleLongPress(_:))) - self.titleNode.view.addGestureRecognizer(titleGestureRecognizer) - - let deviceGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleDeviceLongPress(_:))) - self.deviceValueNode.view.addGestureRecognizer(deviceGestureRecognizer) - - let locationGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLocationLongPress(_:))) - self.locationValueNode.view.addGestureRecognizer(locationGestureRecognizer) - - let ipGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleIpLongPress(_:))) - self.ipValueNode.view.addGestureRecognizer(ipGestureRecognizer) - - if let animationNode = self.animationNode { - animationNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.animationPressed))) - } - } - - @objc private func handleTitleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { - if gestureRecognizer.state == .began { - self.displayCopyContextMenu(self.titleNode, self.titleNode.attributedText?.string ?? "") - } - } - - @objc private func handleDeviceLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { - if gestureRecognizer.state == .began { - self.displayCopyContextMenu(self.deviceValueNode, self.deviceValueNode.attributedText?.string ?? "") - } - } - - @objc private func handleLocationLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { - if gestureRecognizer.state == .began { - self.displayCopyContextMenu(self.locationValueNode, self.locationValueNode.attributedText?.string ?? "") - } - } - - @objc private func handleIpLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { - if gestureRecognizer.state == .began { - self.displayCopyContextMenu(self.ipValueNode, self.ipValueNode.attributedText?.string ?? "") - } - } - - private func displayCopyContextMenu(_ node: ASDisplayNode, _ string: String) { - if !string.isEmpty { - var actions: [ContextMenuAction] = [] - actions.append(ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { [weak self] in - UIPasteboard.general.string = string - - if let strongSelf = self { - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) - } - })) - let contextMenuController = makeContextMenuController(actions: actions) - self.controller?.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in - if let strongSelf = self { - return (node, node.bounds.insetBy(dx: 0.0, dy: -2.0), strongSelf, strongSelf.view.bounds) - } else { - return nil - } - })) - } - } - - func updatePresentationData(_ presentationData: PresentationData) { - guard !self.animatedOut else { - return - } - let previousTheme = self.presentationData.theme - self.presentationData = presentationData - - self.contentBackgroundNode.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor - - self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.regular(30.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) - - let subtitleColor: UIColor - if case let .session(session) = self.subject, session.isCurrent { - subtitleColor = self.presentationData.theme.list.itemAccentColor - } else { - subtitleColor = self.presentationData.theme.list.itemSecondaryTextColor - } - self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: subtitleColor) - - self.fieldBackgroundNode.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor - self.firstSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor - self.secondSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor - self.acceptSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor - - self.deviceTitleNode.attributedText = NSAttributedString(string: self.deviceTitleNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) - self.locationTitleNode.attributedText = NSAttributedString(string: self.locationTitleNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) - self.ipTitleNode.attributedText = NSAttributedString(string: self.ipTitleNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) - - self.deviceValueNode.attributedText = NSAttributedString(string: self.deviceValueNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) - self.locationValueNode.attributedText = NSAttributedString(string: self.locationValueNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) - self.ipValueNode.attributedText = NSAttributedString(string: self.ipValueNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) - self.locationInfoNode.attributedText = NSAttributedString(string: self.locationInfoNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) - - self.acceptHeaderNode.attributedText = NSAttributedString(string: self.acceptHeaderNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) - self.secretChatsTitleNode.attributedText = NSAttributedString(string: self.secretChatsTitleNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) - self.incomingCallsTitleNode.attributedText = NSAttributedString(string: self.incomingCallsTitleNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) - self.acceptBackgroundNode.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor - - if previousTheme !== presentationData.theme, let (layout, navigationBarHeight) = self.containerLayout { - self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) - } - - self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) - self.terminateButton.updateTheme(SolidRoundedButtonTheme(backgroundColor: self.presentationData.theme.list.itemBlocksBackgroundColor, foregroundColor: self.presentationData.theme.list.itemDestructiveColor)) - } - - @objc func animationPressed() { - if let animationNode = self.animationNode, !animationNode.isPlaying { - animationNode.playOnce() - } - } - - @objc func cancelButtonPressed() { - self.animateOut() - } - - @objc func dimTapGesture() { - self.cancelButtonPressed() - } - - private var animatedOut = false - func animateIn() { - self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) - - let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY - let dimPosition = self.dimNode.layer.position - - let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) - let targetBounds = self.bounds - self.bounds = self.bounds.offsetBy(dx: 0.0, dy: -offset) - self.dimNode.position = CGPoint(x: dimPosition.x, y: dimPosition.y - offset) - transition.animateView({ - self.bounds = targetBounds - self.dimNode.position = dimPosition - }) - } - - func animateOut(completion: (() -> Void)? = nil) { - self.animatedOut = true - - var dimCompleted = false - var offsetCompleted = false - - let internalCompletion: () -> Void = { [weak self] in - if let strongSelf = self, dimCompleted && offsetCompleted { - strongSelf.dismiss?() - } - completion?() - } - - self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in - dimCompleted = true - internalCompletion() - }) - - let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY - self.wrappingScrollNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in - offsetCompleted = true - internalCompletion() - }) - - - self.controller?.window?.forEachController { c in - if let c = c as? UndoOverlayController { - c.dismiss() - } - } - } - - var passthroughHitTestImpl: ((CGPoint) -> UIView?)? - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if self.bounds.contains(point) { - if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) { - return self.dimNode.view - } - } - return super.hitTest(point, with: event) - } - - func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { - let contentOffset = scrollView.contentOffset - let additionalTopHeight = max(0.0, -contentOffset.y) - - if additionalTopHeight >= 30.0 { - self.cancelButtonPressed() - } - } - - func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - let isFirstTime = self.containerLayout == nil - self.containerLayout = (layout, navigationBarHeight) - - var insets = layout.insets(options: [.statusBar, .input]) - let cleanInsets = layout.insets(options: [.statusBar]) - insets.top = max(10.0, insets.top) - - let bottomInset: CGFloat = 10.0 + cleanInsets.bottom - - let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0) - - transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - - let iconSize = CGSize(width: 72.0, height: 72.0) - let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - iconSize.width) / 2.0), y: 36.0), size: iconSize) - - if let iconNode = self.iconNode { - transition.updateFrame(node: iconNode, frame: iconFrame) - } else if let animationNode = self.animationNode, let animationBackgroundNode = self.animationBackgroundNode { - transition.updateFrame(node: animationNode, frame: iconFrame) - transition.updateFrame(node: animationBackgroundNode, frame: iconFrame) - if #available(iOS 13.0, *) { - animationBackgroundNode.layer.cornerCurve = .continuous - } - if isFirstTime { - Queue.mainQueue().after(0.5) { - animationNode.playOnce() - } - } - } else if let avatarNode = self.avatarNode { - transition.updateFrame(node: avatarNode, frame: iconFrame) - } - - let inset: CGFloat = 16.0 - let titleSize = self.titleNode.updateLayout(CGSize(width: width - inset * 2.0, height: 100.0)) - let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - titleSize.width) / 2.0), y: 120.0), size: titleSize) - transition.updateFrame(node: self.titleNode, frame: titleFrame) - - let textSize = self.textNode.updateLayout(CGSize(width: width - inset * 2.0, height: 60.0)) - let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - textSize.width) / 2.0), y: titleFrame.maxY), size: textSize) - transition.updateFrame(node: self.textNode, frame: textFrame) - - let cancelSize = CGSize(width: 44.0, height: 44.0) - let cancelFrame = CGRect(origin: CGPoint(x: width - cancelSize.width - 3.0, y: 6.0), size: cancelSize) - transition.updateFrame(node: self.cancelButton, frame: cancelFrame) - - let fieldItemHeight: CGFloat = 44.0 - var fieldFrame = CGRect(x: inset, y: textFrame.maxY + 24.0, width: width - inset * 2.0, height: fieldItemHeight * 2.0) - if !(self.ipValueNode.attributedText?.string ?? "").isEmpty { - fieldFrame.size.height += fieldItemHeight + let titleFont = Font.bold(24.0) + let title = title.update( + component: BalancedTextComponent( + text: .markdown(text: titleString, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.primaryTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.controlAccentColor), link: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.primaryTextColor), linkAttribute: { _ in return nil })), + horizontalAlignment: .center, + maximumNumberOfLines: 2 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + title.size.height / 2.0)) + ) + contentHeight += title.size.height + contentHeight += 2.0 - self.ipTitleNode.isHidden = false - self.ipValueNode.isHidden = false - self.secondSeparatorNode.isHidden = false - } else { - self.ipTitleNode.isHidden = true - self.ipValueNode.isHidden = true - self.secondSeparatorNode.isHidden = true - } - transition.updateFrame(node: self.fieldBackgroundNode, frame: fieldFrame) - - let maxFieldTitleWidth = (width - inset * 4.0) * 0.4 - - let deviceTitleTextSize = self.deviceTitleNode.updateLayout(CGSize(width: maxFieldTitleWidth, height: fieldItemHeight)) - let deviceTitleTextFrame = CGRect(origin: CGPoint(x: fieldFrame.minX + inset, y: fieldFrame.minY + floorToScreenPixels((fieldItemHeight - deviceTitleTextSize.height) / 2.0)), size: deviceTitleTextSize) - transition.updateFrame(node: self.deviceTitleNode, frame: deviceTitleTextFrame) - - let deviceValueTextSize = self.deviceValueNode.updateLayout(CGSize(width: fieldFrame.width - inset * 2.0 - deviceTitleTextSize.width - 10.0, height: fieldItemHeight)) - let deviceValueTextFrame = CGRect(origin: CGPoint(x: fieldFrame.maxX - deviceValueTextSize.width - inset, y: fieldFrame.minY + floorToScreenPixels((fieldItemHeight - deviceValueTextSize.height) / 2.0)), size: deviceValueTextSize) - transition.updateFrame(node: self.deviceValueNode, frame: deviceValueTextFrame) - - transition.updateFrame(node: self.firstSeparatorNode, frame: CGRect(x: fieldFrame.minX + inset, y: fieldFrame.minY + fieldItemHeight, width: fieldFrame.width - inset, height: UIScreenPixel)) - - let ipTitleTextSize = self.ipTitleNode.updateLayout(CGSize(width: maxFieldTitleWidth, height: fieldItemHeight)) - let ipTitleTextFrame = CGRect(origin: CGPoint(x: fieldFrame.minX + inset, y: fieldFrame.minY + fieldItemHeight + floorToScreenPixels((fieldItemHeight - ipTitleTextSize.height) / 2.0)), size: ipTitleTextSize) - transition.updateFrame(node: self.ipTitleNode, frame: ipTitleTextFrame) - - let ipValueTextSize = self.ipValueNode.updateLayout(CGSize(width: fieldFrame.width - inset * 2.0 - ipTitleTextSize.width - 10.0, height: fieldItemHeight)) - let ipValueTextFrame = CGRect(origin: CGPoint(x: fieldFrame.maxX - ipValueTextSize.width - inset, y: fieldFrame.minY + fieldItemHeight + floorToScreenPixels((fieldItemHeight - ipValueTextSize.height) / 2.0)), size: ipValueTextSize) - transition.updateFrame(node: self.ipValueNode, frame: ipValueTextFrame) - - transition.updateFrame(node: self.secondSeparatorNode, frame: CGRect(x: fieldFrame.minX + inset, y: fieldFrame.minY + fieldItemHeight + fieldItemHeight, width: fieldFrame.width - inset, height: UIScreenPixel)) - - let locationTitleTextSize = self.locationTitleNode.updateLayout(CGSize(width: maxFieldTitleWidth, height: fieldItemHeight)) - let locationTitleTextFrame = CGRect(origin: CGPoint(x: fieldFrame.minX + inset, y: fieldFrame.maxY - fieldItemHeight + floorToScreenPixels((fieldItemHeight - locationTitleTextSize.height) / 2.0)), size: locationTitleTextSize) - transition.updateFrame(node: self.locationTitleNode, frame: locationTitleTextFrame) - - let locationValueTextSize = self.locationValueNode.updateLayout(CGSize(width: fieldFrame.width - inset * 2.0 - locationTitleTextSize.width - 10.0, height: fieldItemHeight)) - let locationValueTextFrame = CGRect(origin: CGPoint(x: fieldFrame.maxX - locationValueTextSize.width - inset, y: fieldFrame.maxY - fieldItemHeight + floorToScreenPixels((fieldItemHeight - locationValueTextSize.height) / 2.0)), size: locationValueTextSize) - transition.updateFrame(node: self.locationValueNode, frame: locationValueTextFrame) - - let locationInfoTextSize = self.locationInfoNode.updateLayout(CGSize(width: fieldFrame.width - inset * 2.0, height: fieldItemHeight * 2.0)) - let locationInfoTextFrame = CGRect(origin: CGPoint(x: fieldFrame.minX + inset, y: fieldFrame.maxY + 6.0), size: locationInfoTextSize) - transition.updateFrame(node: self.locationInfoNode, frame: locationInfoTextFrame) - - var contentHeight = locationInfoTextFrame.maxY + bottomInset + 64.0 - - var secretFrame = CGRect(x: inset, y: locationInfoTextFrame.maxY + 59.0, width: width - inset * 2.0, height: fieldItemHeight) - if let _ = self.secretChatsTitleNode.supernode { - secretFrame.size.height += fieldItemHeight - } - transition.updateFrame(node: self.acceptBackgroundNode, frame: secretFrame) - - let secretChatsHeaderTextSize = self.acceptHeaderNode.updateLayout(CGSize(width: secretFrame.width - inset * 2.0, height: fieldItemHeight)) - let secretChatsHeaderTextFrame = CGRect(origin: CGPoint(x: secretFrame.minX + inset, y: secretFrame.minY - secretChatsHeaderTextSize.height - 6.0), size: secretChatsHeaderTextSize) - transition.updateFrame(node: self.acceptHeaderNode, frame: secretChatsHeaderTextFrame) - - if let _ = self.secretChatsTitleNode.supernode { - let secretChatsTitleTextSize = self.secretChatsTitleNode.updateLayout(CGSize(width: width - inset * 4.0 - 80.0, height: fieldItemHeight)) - let secretChatsTitleTextFrame = CGRect(origin: CGPoint(x: secretFrame.minX + inset, y: secretFrame.minY + floorToScreenPixels((fieldItemHeight - secretChatsTitleTextSize.height) / 2.0)), size: secretChatsTitleTextSize) - transition.updateFrame(node: self.secretChatsTitleNode, frame: secretChatsTitleTextFrame) - - if let switchView = self.secretChatsSwitchNode.view as? UISwitch { - if self.secretChatsSwitchNode.bounds.size.width.isZero { - switchView.sizeToFit() - } - let switchSize = switchView.bounds.size - - self.secretChatsSwitchNode.frame = CGRect(origin: CGPoint(x: fieldFrame.maxX - switchSize.width - inset, y: secretFrame.minY + floorToScreenPixels((fieldItemHeight - switchSize.height) / 2.0)), size: switchSize) - self.secretChatsActivateAreaNode.frame = CGRect(origin: CGPoint(x: secretFrame.minX, y: secretFrame.minY), size: CGSize(width: fieldFrame.width, height: fieldItemHeight)) - } - } - - let incomingCallsTitleTextSize = self.incomingCallsTitleNode.updateLayout(CGSize(width: width - inset * 4.0 - 80.0, height: fieldItemHeight)) - let incomingCallsTitleTextFrame = CGRect(origin: CGPoint(x: secretFrame.minX + inset, y: secretFrame.maxY - fieldItemHeight + floorToScreenPixels((fieldItemHeight - incomingCallsTitleTextSize.height) / 2.0)), size: incomingCallsTitleTextSize) - transition.updateFrame(node: self.incomingCallsTitleNode, frame: incomingCallsTitleTextFrame) - - transition.updateFrame(node: self.acceptSeparatorNode, frame: CGRect(x: secretFrame.minX + inset, y: secretFrame.minY + fieldItemHeight, width: fieldFrame.width - inset, height: UIScreenPixel)) - - if let switchView = self.incomingCallsSwitchNode.view as? UISwitch { - if self.incomingCallsSwitchNode.bounds.size.width.isZero { - switchView.sizeToFit() - } - let switchSize = switchView.bounds.size + let textFont = Font.regular(15.0) + let description = description.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString(string: subtitleString, font: textFont, textColor: subtitleActive ? theme.actionSheet.controlAccentColor : theme.actionSheet.secondaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 3, + lineSpacing: 0.2 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(description + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + description.size.height / 2.0)) + ) + contentHeight += description.size.height + contentHeight += 22.0 - self.incomingCallsSwitchNode.frame = CGRect(origin: CGPoint(x: fieldFrame.maxX - switchSize.width - inset, y: secretFrame.maxY - fieldItemHeight + floorToScreenPixels((fieldItemHeight - switchSize.height) / 2.0)), size: switchSize) - self.incomingCallsActivateAreaNode.frame = CGRect(origin: CGPoint(x: secretFrame.minX, y: secretFrame.maxY - fieldItemHeight), size: CGSize(width: fieldFrame.width, height: fieldItemHeight)) + var clientSectionItems: [AnyComponentWithIdentity] = [] + clientSectionItems.append( + AnyComponentWithIdentity(id: "application", component: AnyComponent( + ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: applicationTitle, + font: Font.regular(17.0), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )), + accessory: .custom(ListActionItemComponent.CustomAccessory( + component: AnyComponentWithIdentity( + id: "info", + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: applicationString, + font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), + textColor: theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 1 + )) + ), + insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 14.0), + isInteractive: true + )), + action: nil + ) + )) + ) + + if let ipString { + clientSectionItems.append( + AnyComponentWithIdentity(id: "ip", component: AnyComponent( + ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.AuthSessions_View_IP, + font: Font.regular(17.0), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )), + accessory: .custom(ListActionItemComponent.CustomAccessory( + component: AnyComponentWithIdentity( + id: "info", + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: ipString, + font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), + textColor: theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 1 + )) + ), + insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 14.0), + isInteractive: true + )), + action: nil + ) + )) + ) + } + + clientSectionItems.append( + AnyComponentWithIdentity(id: "region", component: AnyComponent( + ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.AuthSessions_View_Location, + font: Font.regular(17.0), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )), + accessory: .custom(ListActionItemComponent.CustomAccessory( + component: AnyComponentWithIdentity( + id: "info", + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: locationString, + font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), + textColor: theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 1 + )) + ), + insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 14.0), + isInteractive: true + )), + action: nil + ) + )) + ) + + let clientSection = clientSection.update( + component: ListSectionComponent( + theme: theme, + style: .glass, + header: nil, + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.AuthSessions_View_LocationInfo, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: clientSectionItems + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), + transition: context.transition + ) + context.add(clientSection + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + clientSection.size.height / 2.0)) + ) + contentHeight += clientSection.size.height + + if state.allowSecretChats != nil || state.allowIncomingCalls != nil { + contentHeight += 38.0 + + var optionsSectionItems: [AnyComponentWithIdentity] = [] + + if let allowSecretChats = state.allowSecretChats { + optionsSectionItems.append(AnyComponentWithIdentity(id: "allowSecretChats", component: AnyComponent(ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.AuthSessions_View_AcceptSecretChats, + font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: allowSecretChats, action: { [weak state] _ in + guard let state else { + return + } + state.toggleAllowSecretChats() + })), + action: nil + )))) + } + if let allowIncomingCalls = state.allowIncomingCalls { + optionsSectionItems.append(AnyComponentWithIdentity(id: "allowIncomingCalls", component: AnyComponent(ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.AuthSessions_View_AcceptIncomingCalls, + font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: allowIncomingCalls, action: { [weak state] _ in + guard let state else { + return + } + state.toggleAllowIncomingCalls() + })), + action: nil + )))) + } + let optionsSection = optionsSection.update( + component: ListSectionComponent( + theme: theme, + style: .glass, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.AuthSessions_View_AcceptTitle.uppercased(), + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: optionsSectionItems + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), + transition: context.transition + ) + context.add(optionsSection + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + optionsSection.size.height / 2.0)) + ) + contentHeight += optionsSection.size.height + } + contentHeight += 32.0 + + if let buttonString { + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) + let button = button.update( + component: ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: theme.list.itemDestructiveColor, + foreground: .white, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) + ), + action: { [weak state] in + state?.terminate() + } + ), + availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0), + transition: .immediate + ) + context.add(button + .position(CGPoint(x: context.availableSize.width / 2.0 , y: contentHeight + button.size.height / 2.0)) + ) + contentHeight += button.size.height + contentHeight += buttonInsets.bottom + } + + return CGSize(width: context.availableSize.width, height: contentHeight) + } + } +} + +private final class RecentSessionSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let subject: RecentSessionScreen.Subject + + init( + context: AccountContext, + subject: RecentSessionScreen.Subject + ) { + self.context = context + self.subject = subject + } + + static func ==(lhs: RecentSessionSheetComponent, rhs: RecentSessionSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + static var body: Body { + let sheet = Child(SheetComponent.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(RecentSessionSheetContent( + context: context.component.context, + subject: context.component.subject, + cancel: { animate in + if animate { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else if let controller = controller() { + controller.dismiss(animated: false, completion: nil) + } + } + )), + style: .glass, + backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor), + followContentSizeChanges: true, + clipsContent: true, + animateOut: animateOut + ), + environment: { + environment + SheetComponentEnvironment( + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + if animated { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() { + controller.dismiss(completion: nil) + } + } + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + return context.availableSize + } + } +} + +public class RecentSessionScreen: ViewControllerComponentContainer { + public enum Subject { + case session(RecentAccountSession) + case website(WebAuthorization, EnginePeer?) + } + + private let context: AccountContext + fileprivate let updateAcceptSecretChats: (Bool) -> Void + fileprivate let updateAcceptIncomingCalls: (Bool) -> Void + fileprivate let remove: (@escaping () -> Void) -> Void + + public init( + context: AccountContext, + subject: RecentSessionScreen.Subject, + updateAcceptSecretChats: @escaping (Bool) -> Void, + updateAcceptIncomingCalls: @escaping (Bool) -> Void, + remove: @escaping (@escaping () -> Void) -> Void + ) { + self.context = context + self.updateAcceptSecretChats = updateAcceptSecretChats + self.updateAcceptIncomingCalls = updateAcceptIncomingCalls + self.remove = remove + + super.init( + context: context, + component: RecentSessionSheetComponent( + context: context, + subject: subject + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: .default + ) + + self.navigationPresentation = .flatModal + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func viewDidLoad() { + super.viewDidLoad() + + self.view.disablesInteractiveModalDismiss = true + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() } - - if let _ = self.acceptBackgroundNode.supernode { - contentHeight += secretFrame.maxY - locationInfoTextFrame.maxY - } - contentHeight += 40.0 - - let isCurrent: Bool - if case let .session(session) = self.subject, session.isCurrent { - isCurrent = true - } else { - isCurrent = false - } - - if isCurrent { - contentHeight -= 68.0 - self.terminateButton.isHidden = true - self.terminateButton.isAccessibilityElement = false - } else { - self.terminateButton.isHidden = false - self.terminateButton.isAccessibilityElement = true - } - - let sideInset = floor((layout.size.width - width) / 2.0) - let scrollContentHeight = max(layout.size.height, contentHeight) - let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: max(layout.statusBarHeight ?? 20.0, layout.size.height - contentHeight)), size: CGSize(width: width, height: contentHeight)) - let contentFrame = contentContainerFrame - - self.wrappingScrollNode.view.contentSize = CGSize(width: layout.size.width, height: scrollContentHeight) - - var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: width, height: contentFrame.height + 2000.0)) - if backgroundFrame.minY < contentFrame.minY { - backgroundFrame.origin.y = contentFrame.minY - } - transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) - - let doneButtonHeight = self.terminateButton.updateLayout(width: width - inset * 2.0, transition: transition) - transition.updateFrame(node: self.terminateButton, frame: CGRect(x: inset, y: contentHeight - doneButtonHeight - 40.0 - insets.bottom - 6.0, width: width, height: doneButtonHeight)) - - transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) - transition.updateFrame(node: self.topContentContainerNode, frame: contentContainerFrame) } } diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift index 6635834096..67ef6d1348 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift @@ -30,6 +30,7 @@ import ContextUI import QuickReactionSetupController import AvatarEditorScreen import PeerSelectionScreen +import DeviceModel enum SettingsSearchableItemIcon { case profile @@ -3895,7 +3896,7 @@ private func appearanceSearchableItems(context: AccountContext) -> [SettingsSear present(.push, themeSettingsController(context: context, focusOnItemTag: itemTag)) } - return [ + var items: [SettingsSearchableItem] = [ SettingsSearchableItem( id: "appearance", title: strings.Settings_Appearance, @@ -4094,14 +4095,31 @@ private func appearanceSearchableItems(context: AccountContext) -> [SettingsSear ), SettingsSearchableItem( id: "appearance/tap-for-next-media", + title: strings.Appearance_ShowNextMediaOnTap, icon: icon, breadcrumbs: [strings.Settings_Appearance], isVisible: false, present: { context, _, present in presentAppearanceSettings(context, present, .tapForNextMedia) } - ), + ) ] + + if DeviceModel.current.isIpad { + items.append( + SettingsSearchableItem( + id: "appearance/send-with-cmd-enter", + title: strings.Appearance_SendWithCmdEnter, + icon: icon, + breadcrumbs: [strings.Settings_Appearance], + isVisible: false, + present: { context, _, present in + presentAppearanceSettings(context, present, .sendWithCmdEnter) + } + ) + ) + } + return items } private func languageSearchableItems(context: AccountContext, localizations: [LocalizationInfo]) -> [SettingsSearchableItem] { diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift index 4ba2046088..8140064aff 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift @@ -12,12 +12,7 @@ import AppBundle private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selected: Bool) -> UIImage? { return generateImage(CGSize(width: 30.0, height: 30.0), rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) - context.setFillColor(theme.list.itemBlocksBackgroundColor.cgColor) - context.fill(bounds) - - context.setBlendMode(.clear) - context.fillEllipse(in: bounds) - context.setBlendMode(.normal) + context.clear(bounds) let lineWidth: CGFloat if selected { @@ -113,8 +108,10 @@ private final class ThemeSettingsAppIconNode : ASDisplayNode { override init() { self.iconNode = ASImageNode() + self.iconNode.clipsToBounds = true self.iconNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 63.0, height: 63.0)) self.iconNode.isLayerBacked = true + self.iconNode.cornerRadius = 15.0 self.overlayNode = ASImageNode() self.overlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 63.0, height: 63.0)) @@ -199,6 +196,7 @@ private let selectedTextFont = Font.medium(12.0) class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { private let backgroundNode: ASDisplayNode + private let highlightNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode private let maskNode: ASImageNode @@ -219,6 +217,9 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true + self.highlightNode = ASDisplayNode() + self.highlightNode.isLayerBacked = true + self.topStripeNode = ASDisplayNode() self.topStripeNode.isLayerBacked = true @@ -234,6 +235,20 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { self.addSubnode(self.containerNode) } + public func displayHighlight() { + if self.backgroundNode.supernode != nil { + self.insertSubnode(self.highlightNode, aboveSubnode: self.backgroundNode) + } else { + self.insertSubnode(self.highlightNode, at: 0) + } + + Queue.mainQueue().after(1.2, { + self.highlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + self.highlightNode.removeFromSupernode() + }) + }) + } + func asyncLayout() -> (_ item: ThemeSettingsAppIconItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { return { item, params, neighbors in let contentSize: CGSize @@ -280,6 +295,7 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.highlightNode.backgroundColor = item.theme.list.itemSearchHighlightColor if strongSelf.backgroundNode.supernode == nil { strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) @@ -321,6 +337,7 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.highlightNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift index 37d174b993..9b85255901 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift @@ -23,6 +23,7 @@ import ThemeCarouselItem import ThemeAccentColorScreen import WallpaperGridScreen import PeerNameColorItem +import DeviceModel private final class ThemeSettingsControllerArguments { let context: AccountContext @@ -38,13 +39,34 @@ private final class ThemeSettingsControllerArguments { let openBubbleSettings: () -> Void let openPowerSavingSettings: () -> Void let openStickersAndEmoji: () -> Void + let toggleSendWithCmdEnter: (Bool) -> Void let toggleShowNextMediaOnTap: (Bool) -> Void let selectAppIcon: (PresentationAppIcon) -> Void let editTheme: (PresentationCloudTheme) -> Void let themeContextAction: (Bool, PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void let colorContextAction: (Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void - init(context: AccountContext, selectTheme: @escaping (PresentationThemeReference) -> Void, openThemeSettings: @escaping () -> Void, openWallpaperSettings: @escaping () -> Void, openNameColorSettings: @escaping () -> Void, selectAccentColor: @escaping (PresentationThemeAccentColor?) -> Void, openAccentColorPicker: @escaping (PresentationThemeReference, Bool) -> Void, toggleNightTheme: @escaping (Bool) -> Void, openAutoNightTheme: @escaping () -> Void, openTextSize: @escaping () -> Void, openBubbleSettings: @escaping () -> Void, openPowerSavingSettings: @escaping () -> Void, openStickersAndEmoji: @escaping () -> Void, toggleShowNextMediaOnTap: @escaping (Bool) -> Void, selectAppIcon: @escaping (PresentationAppIcon) -> Void, editTheme: @escaping (PresentationCloudTheme) -> Void, themeContextAction: @escaping (Bool, PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void, colorContextAction: @escaping (Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void) { + init( + context: AccountContext, + selectTheme: @escaping (PresentationThemeReference) -> Void, + openThemeSettings: @escaping () -> Void, + openWallpaperSettings: @escaping () -> Void, + openNameColorSettings: @escaping () -> Void, + selectAccentColor: @escaping (PresentationThemeAccentColor?) -> Void, + openAccentColorPicker: @escaping (PresentationThemeReference, Bool) -> Void, + toggleNightTheme: @escaping (Bool) -> Void, + openAutoNightTheme: @escaping () -> Void, + openTextSize: @escaping () -> Void, + openBubbleSettings: @escaping () -> Void, + openPowerSavingSettings: @escaping () -> Void, + openStickersAndEmoji: @escaping () -> Void, + toggleSendWithCmdEnter: @escaping (Bool) -> Void, + toggleShowNextMediaOnTap: @escaping (Bool) -> Void, + selectAppIcon: @escaping (PresentationAppIcon) -> Void, + editTheme: @escaping (PresentationCloudTheme) -> Void, + themeContextAction: @escaping (Bool, PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void, + colorContextAction: @escaping (Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void + ) { self.context = context self.selectTheme = selectTheme self.openThemeSettings = openThemeSettings @@ -58,6 +80,7 @@ private final class ThemeSettingsControllerArguments { self.openBubbleSettings = openBubbleSettings self.openPowerSavingSettings = openPowerSavingSettings self.openStickersAndEmoji = openStickersAndEmoji + self.toggleSendWithCmdEnter = toggleSendWithCmdEnter self.toggleShowNextMediaOnTap = toggleShowNextMediaOnTap self.selectAppIcon = selectAppIcon self.editTheme = editTheme @@ -84,6 +107,7 @@ public enum ThemeSettingsEntryTag: ItemListItemTag { case powerSaving case stickersAndEmoji case animations + case sendWithCmdEnter case tapForNextMedia case nightMode @@ -112,6 +136,7 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { case powerSaving case stickersAndEmoji case otherHeader(PresentationTheme, String) + case sendWithCmdEnter(PresentationTheme, String, Bool) case showNextMediaOnTap(PresentationTheme, String, Bool) case showNextMediaOnTapInfo(PresentationTheme, String) @@ -127,7 +152,7 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { return ThemeSettingsControllerSection.icon.rawValue case .powerSaving, .stickersAndEmoji: return ThemeSettingsControllerSection.message.rawValue - case .otherHeader, .showNextMediaOnTap, .showNextMediaOnTapInfo: + case .otherHeader, .sendWithCmdEnter, .showNextMediaOnTap, .showNextMediaOnTapInfo: return ThemeSettingsControllerSection.other.rawValue } } @@ -164,10 +189,12 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { return 13 case .otherHeader: return 14 - case .showNextMediaOnTap: + case .sendWithCmdEnter: return 15 - case .showNextMediaOnTapInfo: + case .showNextMediaOnTap: return 16 + case .showNextMediaOnTapInfo: + return 17 } } @@ -263,6 +290,12 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { } else { return false } + case let .sendWithCmdEnter(lhsTheme, lhsTitle, lhsValue): + if case let .sendWithCmdEnter(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue { + return true + } else { + return false + } case let .showNextMediaOnTap(lhsTheme, lhsTitle, lhsValue): if case let .showNextMediaOnTap(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue { return true @@ -351,6 +384,10 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { }) case let .otherHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .sendWithCmdEnter(_, title, value): + return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in + arguments.toggleSendWithCmdEnter(value) + }, tag: ThemeSettingsEntryTag.sendWithCmdEnter) case let .showNextMediaOnTap(_, title, value): return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleShowNextMediaOnTap(value) @@ -361,7 +398,21 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { } } -private func themeSettingsControllerEntries(presentationData: PresentationData, presentationThemeSettings: PresentationThemeSettings, mediaSettings: MediaDisplaySettings, themeReference: PresentationThemeReference, availableThemes: [PresentationThemeReference], availableAppIcons: [PresentationAppIcon], currentAppIconName: String?, isPremium: Bool, chatThemes: [PresentationThemeReference], animatedEmojiStickers: [String: [StickerPackItem]], accountPeer: EnginePeer?, nameColors: PeerNameColors) -> [ThemeSettingsControllerEntry] { +private func themeSettingsControllerEntries( + presentationData: PresentationData, + presentationThemeSettings: PresentationThemeSettings, + chatSettings: ChatSettings, + mediaSettings: MediaDisplaySettings, + themeReference: PresentationThemeReference, + availableThemes: [PresentationThemeReference], + availableAppIcons: [PresentationAppIcon], + currentAppIconName: String?, + isPremium: Bool, + chatThemes: [PresentationThemeReference], + animatedEmojiStickers: [String: [StickerPackItem]], + accountPeer: EnginePeer?, + nameColors: PeerNameColors +) -> [ThemeSettingsControllerEntry] { var entries: [ThemeSettingsControllerEntry] = [] let strings = presentationData.strings @@ -437,6 +488,9 @@ private func themeSettingsControllerEntries(presentationData: PresentationData, } entries.append(.otherHeader(presentationData.theme, strings.Appearance_Other.uppercased())) + if DeviceModel.current.isIpad { + entries.append(.sendWithCmdEnter(presentationData.theme, strings.Appearance_SendWithCmdEnter, chatSettings.sendWithCmdEnter)) + } entries.append(.showNextMediaOnTap(presentationData.theme, strings.Appearance_ShowNextMediaOnTap, mediaSettings.showNextMediaOnTap)) entries.append(.showNextMediaOnTapInfo(presentationData.theme, strings.Appearance_ShowNextMediaOnTapInfo)) @@ -557,6 +611,10 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The pushControllerImpl?(installedStickerPacksController(context: context, mode: .general, archivedPacks: archivedStickerPacks, updatedPacks: { _ in })) }) + }, toggleSendWithCmdEnter: { value in + let _ = updateChatSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in + return current.withUpdatedSendWithCmdEnter(value) + }).start() }, toggleShowNextMediaOnTap: { value in let _ = updateMediaDisplaySettingsInteractively(accountManager: context.sharedContext.accountManager, { current in return current.withUpdatedShowNextMediaOnTap(value) @@ -1035,9 +1093,26 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The }) }) - let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings, SharedDataKeys.chatThemes, ApplicationSpecificSharedDataKeys.mediaDisplaySettings]), cloudThemes.get(), availableAppIcons, currentAppIconName.get(), removedThemeIndexesPromise.get(), animatedEmojiStickers, context.account.postbox.peerView(id: context.account.peerId), context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))) + let signal = combineLatest( + queue: .mainQueue(), + context.sharedContext.presentationData, + context.sharedContext.accountManager.sharedData(keys: [ + ApplicationSpecificSharedDataKeys.presentationThemeSettings, + ApplicationSpecificSharedDataKeys.chatSettings, + ApplicationSpecificSharedDataKeys.mediaDisplaySettings, + SharedDataKeys.chatThemes + ]), + cloudThemes.get(), + availableAppIcons, + currentAppIconName.get(), + removedThemeIndexesPromise.get(), + animatedEmojiStickers, + context.account.postbox.peerView(id: context.account.peerId), + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + ) |> map { presentationData, sharedData, cloudThemes, availableAppIcons, currentAppIconName, removedThemeIndexes, animatedEmojiStickers, peerView, accountPeer -> (ItemListControllerState, (ItemListNodeState, Any)) in let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings]?.get(PresentationThemeSettings.self) ?? PresentationThemeSettings.defaultSettings + let chatSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.chatSettings]?.get(ChatSettings.self) ?? ChatSettings.defaultSettings let mediaSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.mediaDisplaySettings]?.get(MediaDisplaySettings.self) ?? MediaDisplaySettings.defaultSettings let isPremium = peerView.peers[peerView.peerId]?.isPremium ?? false @@ -1077,7 +1152,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The chatThemes.insert(.builtin(.dayClassic), at: 0) let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Appearance_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: themeSettingsControllerEntries(presentationData: presentationData, presentationThemeSettings: settings, mediaSettings: mediaSettings, themeReference: themeReference, availableThemes: availableThemes, availableAppIcons: availableAppIcons, currentAppIconName: currentAppIconName, isPremium: isPremium, chatThemes: chatThemes, animatedEmojiStickers: animatedEmojiStickers, accountPeer: accountPeer, nameColors: context.peerNameColors), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: themeSettingsControllerEntries(presentationData: presentationData, presentationThemeSettings: settings, chatSettings: chatSettings, mediaSettings: mediaSettings, themeReference: themeReference, availableThemes: availableThemes, availableAppIcons: availableAppIcons, currentAppIconName: currentAppIconName, isPremium: isPremium, chatThemes: chatThemes, animatedEmojiStickers: animatedEmojiStickers, accountPeer: accountPeer, nameColors: context.peerNameColors), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false) return (controllerState, (listState, arguments)) } diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index bfdd214154..41b8ce6d22 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -289,9 +289,9 @@ private final class StickerPackContainer: ASDisplayNode { self.addSubnode(self.gridNode) self.titleContainer.addSubnode(self.titleNode) + self.addSubnode(self.topContainerNode) self.addSubnode(self.titleContainer) - self.addSubnode(self.topContainerNode) self.addSubnode(self.bottomContainerNode) self.gridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in @@ -654,6 +654,13 @@ private final class StickerPackContainer: ASDisplayNode { self.reorderingGestureRecognizer = reorderingGestureRecognizer self.gridNode.view.addGestureRecognizer(reorderingGestureRecognizer) + self.gridNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.gridNode.clipsToBounds = true + self.gridNode.layer.cornerRadius = 40.0 + + self.topEdgeEffectView.clipsToBounds = true + self.topEdgeEffectView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.topEdgeEffectView.layer.cornerRadius = 40.0 self.topContainerNode.view.addSubview(self.topEdgeEffectView) self.bottomContainerNode.view.addSubview(self.bottomEdgeEffectView) } @@ -1891,7 +1898,7 @@ private final class StickerPackContainer: ASDisplayNode { transition.updateFrame(node: self.bottomContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - actionAreaHeight), size: CGSize(width: layout.size.width, height: 90.0))) - let gridFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top + titleAreaInset), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - titleAreaInset)) + let gridFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top)) let itemsPerRow = 5 let fillingWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0) @@ -1923,12 +1930,13 @@ private final class StickerPackContainer: ASDisplayNode { let initialRevealedRowCount: CGFloat = 4.5 - let topInset: CGFloat + var topInset: CGFloat if case .regular = layout.metrics.widthClass { - topInset = 0.0 + topInset = titleAreaInset } else { topInset = insets.top + max(0.0, layout.size.height - floor(initialRevealedRowCount * itemWidth) - insets.top - actionAreaHeight - titleAreaInset) } + let additionalGridBottomInset = max(0.0, gridFrame.size.height - actionAreaHeight - contentHeight) let gridInsets = UIEdgeInsets(top: topInset, left: gridLeftInset, bottom: actionAreaHeight + additionalGridBottomInset, right: layout.size.width - fillingWidth - gridLeftInset) @@ -2112,6 +2120,10 @@ private final class StickerPackContainer: ASDisplayNode { buttonView.frame = buttonFrame } + let topEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: 90.0)) + transition.updateFrame(view: self.topEdgeEffectView, frame: topEdgeEffectFrame) + self.topEdgeEffectView.update(content: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 0.65, rect: topEdgeEffectFrame, edge: .top, edgeSize: topEdgeEffectFrame.height, transition: ComponentTransition(transition)) + let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: actionAreaHeight - 90.0), size: CGSize(width: layout.size.width, height: 90.0)) transition.updateFrame(view: self.bottomEdgeEffectView, frame: bottomEdgeEffectFrame) self.bottomEdgeEffectView.update(content: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 0.65, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectFrame.height, transition: ComponentTransition(transition)) @@ -2133,7 +2145,7 @@ private final class StickerPackContainer: ASDisplayNode { return } - let minBackgroundY = gridFrame.minY - titleAreaInset + let minBackgroundY = gridFrame.minY let unclippedBackgroundY = gridFrame.minY - presentationLayout.contentOffset.y - titleAreaInset let offsetFromInitialPosition = presentationLayout.contentOffset.y + gridInsets.top @@ -2173,7 +2185,7 @@ private final class StickerPackContainer: ASDisplayNode { var titleContainerFrame: CGRect if case .regular = layout.metrics.widthClass { backgroundFrame.origin.y = min(0.0, backgroundFrame.origin.y) - titleContainerFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width) / 2.0), y: floor((56.0) / 2.0)), size: CGSize()) + titleContainerFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width) / 2.0), y: floor((56.0) / 2.0) + 10.0), size: CGSize()) } else { titleContainerFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width) / 2.0), y: backgroundFrame.minY + floor((56.0) / 2.0) + 10.0), size: CGSize()) } @@ -3103,12 +3115,12 @@ private final class StickerPackContextReferenceContentSource: ContextReferenceCo private func generateShadowImage() -> UIImage? { - return generateImage(CGSize(width: 140.0, height: 140.0), rotatedContext: { size, context in + return generateImage(CGSize(width: 220.0, height: 220.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.saveGState() context.setShadow(offset: CGSize(), blur: 60.0, color: UIColor(white: 0.0, alpha: 0.4).cgColor) - let path = UIBezierPath(roundedRect: CGRect(x: 60.0, y: 60.0, width: 20.0, height: 20.0), cornerRadius: 10.0).cgPath + let path = UIBezierPath(roundedRect: CGRect(x: 60.0, y: 60.0, width: 100.0, height: 100.0), cornerRadius: 40.0).cgPath context.addPath(path) context.fillPath() @@ -3117,7 +3129,7 @@ private func generateShadowImage() -> UIImage? { context.setBlendMode(.clear) context.addPath(path) context.fillPath() - })?.stretchableImage(withLeftCapWidth: 70, topCapHeight: 70) + })?.stretchableImage(withLeftCapWidth: 110, topCapHeight: 110) } private func generateArrowImage(color: UIColor) -> UIImage? { @@ -3290,38 +3302,6 @@ private class ReorderingGestureRecognizer: UIGestureRecognizer { } } -private func generateShadowImage(corners: CACornerMask, radius: CGFloat) -> UIImage? { - return generateImage(CGSize(width: 120.0, height: 120), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - -// context.saveGState() - context.setShadow(offset: CGSize(), blur: 28.0, color: UIColor(white: 0.0, alpha: 0.4).cgColor) - - var rectCorners: UIRectCorner = [] - if corners.contains(.layerMinXMinYCorner) { - rectCorners.insert(.topLeft) - } - if corners.contains(.layerMaxXMinYCorner) { - rectCorners.insert(.topRight) - } - if corners.contains(.layerMinXMaxYCorner) { - rectCorners.insert(.bottomLeft) - } - if corners.contains(.layerMaxXMaxYCorner) { - rectCorners.insert(.bottomRight) - } - - let path = UIBezierPath(roundedRect: CGRect(x: 30.0, y: 30.0, width: 60.0, height: 60.0), byRoundingCorners: rectCorners, cornerRadii: CGSize(width: radius, height: radius)).cgPath - context.addPath(path) - context.fillPath() -// context.restoreGState() - -// context.setBlendMode(.clear) -// context.addPath(path) -// context.fillPath() - })?.stretchableImage(withLeftCapWidth: 60, topCapHeight: 60) -} - private final class CopyView: UIView { let shadow: UIImageView var snapshotView: UIView? diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 0c0d688c2d..2c64ac93c4 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -997,6 +997,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[2029777832] = { return Api.StarGiftAttributeRarity.parse_starGiftAttributeRarityEpic($0) } dict[-822614104] = { return Api.StarGiftAttributeRarity.parse_starGiftAttributeRarityLegendary($0) } dict[-259174037] = { return Api.StarGiftAttributeRarity.parse_starGiftAttributeRarityRare($0) } + dict[-607231095] = { return Api.StarGiftAttributeRarity.parse_starGiftAttributeRarityUncommon($0) } dict[1118831432] = { return Api.StarGiftAuctionAcquiredGift.parse_starGiftAuctionAcquiredGift($0) } dict[984483112] = { return Api.StarGiftAuctionRound.parse_starGiftAuctionRound($0) } dict[178266597] = { return Api.StarGiftAuctionRound.parse_starGiftAuctionRoundExtendable($0) } diff --git a/submodules/TelegramApi/Sources/Api25.swift b/submodules/TelegramApi/Sources/Api25.swift index db083b22fe..6193057ad5 100644 --- a/submodules/TelegramApi/Sources/Api25.swift +++ b/submodules/TelegramApi/Sources/Api25.swift @@ -1318,6 +1318,7 @@ public extension Api { case starGiftAttributeRarityEpic case starGiftAttributeRarityLegendary case starGiftAttributeRarityRare + case starGiftAttributeRarityUncommon public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -1342,6 +1343,11 @@ public extension Api { buffer.appendInt32(-259174037) } break + case .starGiftAttributeRarityUncommon: + if boxed { + buffer.appendInt32(-607231095) + } + break } } @@ -1355,6 +1361,8 @@ public extension Api { return ("starGiftAttributeRarityLegendary", []) case .starGiftAttributeRarityRare: return ("starGiftAttributeRarityRare", []) + case .starGiftAttributeRarityUncommon: + return ("starGiftAttributeRarityUncommon", []) } } @@ -1378,6 +1386,9 @@ public extension Api { public static func parse_starGiftAttributeRarityRare(_ reader: BufferReader) -> StarGiftAttributeRarity? { return Api.StarGiftAttributeRarity.starGiftAttributeRarityRare } + public static func parse_starGiftAttributeRarityUncommon(_ reader: BufferReader) -> StarGiftAttributeRarity? { + return Api.StarGiftAttributeRarity.starGiftAttributeRarityUncommon + } } } public extension Api { @@ -2127,76 +2138,3 @@ public extension Api { } } } -public extension Api { - enum StarsAmount: TypeConstructorDescription { - public class Cons_starsAmount { - public var amount: Int64 - public var nanos: Int32 - public init(amount: Int64, nanos: Int32) { - self.amount = amount - self.nanos = nanos - } - } - public class Cons_starsTonAmount { - public var amount: Int64 - public init(amount: Int64) { - self.amount = amount - } - } - case starsAmount(Cons_starsAmount) - case starsTonAmount(Cons_starsTonAmount) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .starsAmount(let _data): - if boxed { - buffer.appendInt32(-1145654109) - } - serializeInt64(_data.amount, buffer: buffer, boxed: false) - serializeInt32(_data.nanos, buffer: buffer, boxed: false) - break - case .starsTonAmount(let _data): - if boxed { - buffer.appendInt32(1957618656) - } - serializeInt64(_data.amount, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .starsAmount(let _data): - return ("starsAmount", [("amount", _data.amount as Any), ("nanos", _data.nanos as Any)]) - case .starsTonAmount(let _data): - return ("starsTonAmount", [("amount", _data.amount as Any)]) - } - } - - public static func parse_starsAmount(_ reader: BufferReader) -> StarsAmount? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.StarsAmount.starsAmount(Cons_starsAmount(amount: _1!, nanos: _2!)) - } - else { - return nil - } - } - public static func parse_starsTonAmount(_ reader: BufferReader) -> StarsAmount? { - var _1: Int64? - _1 = reader.readInt64() - let _c1 = _1 != nil - if _c1 { - return Api.StarsAmount.starsTonAmount(Cons_starsTonAmount(amount: _1!)) - } - else { - return nil - } - } - } -} diff --git a/submodules/TelegramApi/Sources/Api26.swift b/submodules/TelegramApi/Sources/Api26.swift index 3c8ff86c4b..a5dbafa1b7 100644 --- a/submodules/TelegramApi/Sources/Api26.swift +++ b/submodules/TelegramApi/Sources/Api26.swift @@ -1,3 +1,76 @@ +public extension Api { + enum StarsAmount: TypeConstructorDescription { + public class Cons_starsAmount { + public var amount: Int64 + public var nanos: Int32 + public init(amount: Int64, nanos: Int32) { + self.amount = amount + self.nanos = nanos + } + } + public class Cons_starsTonAmount { + public var amount: Int64 + public init(amount: Int64) { + self.amount = amount + } + } + case starsAmount(Cons_starsAmount) + case starsTonAmount(Cons_starsTonAmount) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .starsAmount(let _data): + if boxed { + buffer.appendInt32(-1145654109) + } + serializeInt64(_data.amount, buffer: buffer, boxed: false) + serializeInt32(_data.nanos, buffer: buffer, boxed: false) + break + case .starsTonAmount(let _data): + if boxed { + buffer.appendInt32(1957618656) + } + serializeInt64(_data.amount, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .starsAmount(let _data): + return ("starsAmount", [("amount", _data.amount as Any), ("nanos", _data.nanos as Any)]) + case .starsTonAmount(let _data): + return ("starsTonAmount", [("amount", _data.amount as Any)]) + } + } + + public static func parse_starsAmount(_ reader: BufferReader) -> StarsAmount? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.StarsAmount.starsAmount(Cons_starsAmount(amount: _1!, nanos: _2!)) + } + else { + return nil + } + } + public static func parse_starsTonAmount(_ reader: BufferReader) -> StarsAmount? { + var _1: Int64? + _1 = reader.readInt64() + let _c1 = _1 != nil + if _c1 { + return Api.StarsAmount.starsTonAmount(Cons_starsTonAmount(amount: _1!)) + } + else { + return nil + } + } + } +} public extension Api { enum StarsGiftOption: TypeConstructorDescription { public class Cons_starsGiftOption { @@ -1662,186 +1735,3 @@ public extension Api { } } } -public extension Api { - enum StickerSetCovered: TypeConstructorDescription { - public class Cons_stickerSetCovered { - public var set: Api.StickerSet - public var cover: Api.Document - public init(set: Api.StickerSet, cover: Api.Document) { - self.set = set - self.cover = cover - } - } - public class Cons_stickerSetFullCovered { - public var set: Api.StickerSet - public var packs: [Api.StickerPack] - public var keywords: [Api.StickerKeyword] - public var documents: [Api.Document] - public init(set: Api.StickerSet, packs: [Api.StickerPack], keywords: [Api.StickerKeyword], documents: [Api.Document]) { - self.set = set - self.packs = packs - self.keywords = keywords - self.documents = documents - } - } - public class Cons_stickerSetMultiCovered { - public var set: Api.StickerSet - public var covers: [Api.Document] - public init(set: Api.StickerSet, covers: [Api.Document]) { - self.set = set - self.covers = covers - } - } - public class Cons_stickerSetNoCovered { - public var set: Api.StickerSet - public init(set: Api.StickerSet) { - self.set = set - } - } - case stickerSetCovered(Cons_stickerSetCovered) - case stickerSetFullCovered(Cons_stickerSetFullCovered) - case stickerSetMultiCovered(Cons_stickerSetMultiCovered) - case stickerSetNoCovered(Cons_stickerSetNoCovered) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .stickerSetCovered(let _data): - if boxed { - buffer.appendInt32(1678812626) - } - _data.set.serialize(buffer, true) - _data.cover.serialize(buffer, true) - break - case .stickerSetFullCovered(let _data): - if boxed { - buffer.appendInt32(1087454222) - } - _data.set.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.packs.count)) - for item in _data.packs { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.keywords.count)) - for item in _data.keywords { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.documents.count)) - for item in _data.documents { - item.serialize(buffer, true) - } - break - case .stickerSetMultiCovered(let _data): - if boxed { - buffer.appendInt32(872932635) - } - _data.set.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.covers.count)) - for item in _data.covers { - item.serialize(buffer, true) - } - break - case .stickerSetNoCovered(let _data): - if boxed { - buffer.appendInt32(2008112412) - } - _data.set.serialize(buffer, true) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .stickerSetCovered(let _data): - return ("stickerSetCovered", [("set", _data.set as Any), ("cover", _data.cover as Any)]) - case .stickerSetFullCovered(let _data): - return ("stickerSetFullCovered", [("set", _data.set as Any), ("packs", _data.packs as Any), ("keywords", _data.keywords as Any), ("documents", _data.documents as Any)]) - case .stickerSetMultiCovered(let _data): - return ("stickerSetMultiCovered", [("set", _data.set as Any), ("covers", _data.covers as Any)]) - case .stickerSetNoCovered(let _data): - return ("stickerSetNoCovered", [("set", _data.set as Any)]) - } - } - - public static func parse_stickerSetCovered(_ reader: BufferReader) -> StickerSetCovered? { - var _1: Api.StickerSet? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.StickerSet - } - var _2: Api.Document? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.Document - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.StickerSetCovered.stickerSetCovered(Cons_stickerSetCovered(set: _1!, cover: _2!)) - } - else { - return nil - } - } - public static func parse_stickerSetFullCovered(_ reader: BufferReader) -> StickerSetCovered? { - var _1: Api.StickerSet? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.StickerSet - } - var _2: [Api.StickerPack]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerPack.self) - } - var _3: [Api.StickerKeyword]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerKeyword.self) - } - var _4: [Api.Document]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.StickerSetCovered.stickerSetFullCovered(Cons_stickerSetFullCovered(set: _1!, packs: _2!, keywords: _3!, documents: _4!)) - } - else { - return nil - } - } - public static func parse_stickerSetMultiCovered(_ reader: BufferReader) -> StickerSetCovered? { - var _1: Api.StickerSet? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.StickerSet - } - var _2: [Api.Document]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.StickerSetCovered.stickerSetMultiCovered(Cons_stickerSetMultiCovered(set: _1!, covers: _2!)) - } - else { - return nil - } - } - public static func parse_stickerSetNoCovered(_ reader: BufferReader) -> StickerSetCovered? { - var _1: Api.StickerSet? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.StickerSet - } - let _c1 = _1 != nil - if _c1 { - return Api.StickerSetCovered.stickerSetNoCovered(Cons_stickerSetNoCovered(set: _1!)) - } - else { - return nil - } - } - } -} diff --git a/submodules/TelegramApi/Sources/Api27.swift b/submodules/TelegramApi/Sources/Api27.swift index 350681bf02..395ba65b19 100644 --- a/submodules/TelegramApi/Sources/Api27.swift +++ b/submodules/TelegramApi/Sources/Api27.swift @@ -1,3 +1,186 @@ +public extension Api { + enum StickerSetCovered: TypeConstructorDescription { + public class Cons_stickerSetCovered { + public var set: Api.StickerSet + public var cover: Api.Document + public init(set: Api.StickerSet, cover: Api.Document) { + self.set = set + self.cover = cover + } + } + public class Cons_stickerSetFullCovered { + public var set: Api.StickerSet + public var packs: [Api.StickerPack] + public var keywords: [Api.StickerKeyword] + public var documents: [Api.Document] + public init(set: Api.StickerSet, packs: [Api.StickerPack], keywords: [Api.StickerKeyword], documents: [Api.Document]) { + self.set = set + self.packs = packs + self.keywords = keywords + self.documents = documents + } + } + public class Cons_stickerSetMultiCovered { + public var set: Api.StickerSet + public var covers: [Api.Document] + public init(set: Api.StickerSet, covers: [Api.Document]) { + self.set = set + self.covers = covers + } + } + public class Cons_stickerSetNoCovered { + public var set: Api.StickerSet + public init(set: Api.StickerSet) { + self.set = set + } + } + case stickerSetCovered(Cons_stickerSetCovered) + case stickerSetFullCovered(Cons_stickerSetFullCovered) + case stickerSetMultiCovered(Cons_stickerSetMultiCovered) + case stickerSetNoCovered(Cons_stickerSetNoCovered) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .stickerSetCovered(let _data): + if boxed { + buffer.appendInt32(1678812626) + } + _data.set.serialize(buffer, true) + _data.cover.serialize(buffer, true) + break + case .stickerSetFullCovered(let _data): + if boxed { + buffer.appendInt32(1087454222) + } + _data.set.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.packs.count)) + for item in _data.packs { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.keywords.count)) + for item in _data.keywords { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.documents.count)) + for item in _data.documents { + item.serialize(buffer, true) + } + break + case .stickerSetMultiCovered(let _data): + if boxed { + buffer.appendInt32(872932635) + } + _data.set.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.covers.count)) + for item in _data.covers { + item.serialize(buffer, true) + } + break + case .stickerSetNoCovered(let _data): + if boxed { + buffer.appendInt32(2008112412) + } + _data.set.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .stickerSetCovered(let _data): + return ("stickerSetCovered", [("set", _data.set as Any), ("cover", _data.cover as Any)]) + case .stickerSetFullCovered(let _data): + return ("stickerSetFullCovered", [("set", _data.set as Any), ("packs", _data.packs as Any), ("keywords", _data.keywords as Any), ("documents", _data.documents as Any)]) + case .stickerSetMultiCovered(let _data): + return ("stickerSetMultiCovered", [("set", _data.set as Any), ("covers", _data.covers as Any)]) + case .stickerSetNoCovered(let _data): + return ("stickerSetNoCovered", [("set", _data.set as Any)]) + } + } + + public static func parse_stickerSetCovered(_ reader: BufferReader) -> StickerSetCovered? { + var _1: Api.StickerSet? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.StickerSet + } + var _2: Api.Document? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.Document + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.StickerSetCovered.stickerSetCovered(Cons_stickerSetCovered(set: _1!, cover: _2!)) + } + else { + return nil + } + } + public static func parse_stickerSetFullCovered(_ reader: BufferReader) -> StickerSetCovered? { + var _1: Api.StickerSet? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.StickerSet + } + var _2: [Api.StickerPack]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerPack.self) + } + var _3: [Api.StickerKeyword]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerKeyword.self) + } + var _4: [Api.Document]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.StickerSetCovered.stickerSetFullCovered(Cons_stickerSetFullCovered(set: _1!, packs: _2!, keywords: _3!, documents: _4!)) + } + else { + return nil + } + } + public static func parse_stickerSetMultiCovered(_ reader: BufferReader) -> StickerSetCovered? { + var _1: Api.StickerSet? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.StickerSet + } + var _2: [Api.Document]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.StickerSetCovered.stickerSetMultiCovered(Cons_stickerSetMultiCovered(set: _1!, covers: _2!)) + } + else { + return nil + } + } + public static func parse_stickerSetNoCovered(_ reader: BufferReader) -> StickerSetCovered? { + var _1: Api.StickerSet? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.StickerSet + } + let _c1 = _1 != nil + if _c1 { + return Api.StickerSetCovered.stickerSetNoCovered(Cons_stickerSetNoCovered(set: _1!)) + } + else { + return nil + } + } + } +} public extension Api { enum StoriesStealthMode: TypeConstructorDescription { public class Cons_storiesStealthMode { @@ -1532,64 +1715,3 @@ public extension Api { } } } -public extension Api { - enum TopPeerCategoryPeers: TypeConstructorDescription { - public class Cons_topPeerCategoryPeers { - public var category: Api.TopPeerCategory - public var count: Int32 - public var peers: [Api.TopPeer] - public init(category: Api.TopPeerCategory, count: Int32, peers: [Api.TopPeer]) { - self.category = category - self.count = count - self.peers = peers - } - } - case topPeerCategoryPeers(Cons_topPeerCategoryPeers) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .topPeerCategoryPeers(let _data): - if boxed { - buffer.appendInt32(-75283823) - } - _data.category.serialize(buffer, true) - serializeInt32(_data.count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.peers.count)) - for item in _data.peers { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .topPeerCategoryPeers(let _data): - return ("topPeerCategoryPeers", [("category", _data.category as Any), ("count", _data.count as Any), ("peers", _data.peers as Any)]) - } - } - - public static func parse_topPeerCategoryPeers(_ reader: BufferReader) -> TopPeerCategoryPeers? { - var _1: Api.TopPeerCategory? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.TopPeerCategory - } - var _2: Int32? - _2 = reader.readInt32() - var _3: [Api.TopPeer]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.TopPeer.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.TopPeerCategoryPeers.topPeerCategoryPeers(Cons_topPeerCategoryPeers(category: _1!, count: _2!, peers: _3!)) - } - else { - return nil - } - } - } -} diff --git a/submodules/TelegramApi/Sources/Api28.swift b/submodules/TelegramApi/Sources/Api28.swift index 2fcefd2998..9cd5aa8cdf 100644 --- a/submodules/TelegramApi/Sources/Api28.swift +++ b/submodules/TelegramApi/Sources/Api28.swift @@ -1,3 +1,64 @@ +public extension Api { + enum TopPeerCategoryPeers: TypeConstructorDescription { + public class Cons_topPeerCategoryPeers { + public var category: Api.TopPeerCategory + public var count: Int32 + public var peers: [Api.TopPeer] + public init(category: Api.TopPeerCategory, count: Int32, peers: [Api.TopPeer]) { + self.category = category + self.count = count + self.peers = peers + } + } + case topPeerCategoryPeers(Cons_topPeerCategoryPeers) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .topPeerCategoryPeers(let _data): + if boxed { + buffer.appendInt32(-75283823) + } + _data.category.serialize(buffer, true) + serializeInt32(_data.count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.peers.count)) + for item in _data.peers { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .topPeerCategoryPeers(let _data): + return ("topPeerCategoryPeers", [("category", _data.category as Any), ("count", _data.count as Any), ("peers", _data.peers as Any)]) + } + } + + public static func parse_topPeerCategoryPeers(_ reader: BufferReader) -> TopPeerCategoryPeers? { + var _1: Api.TopPeerCategory? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.TopPeerCategory + } + var _2: Int32? + _2 = reader.readInt32() + var _3: [Api.TopPeer]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.TopPeer.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.TopPeerCategoryPeers.topPeerCategoryPeers(Cons_topPeerCategoryPeers(category: _1!, count: _2!, peers: _3!)) + } + else { + return nil + } + } + } +} public extension Api { indirect enum Update: TypeConstructorDescription { public class Cons_updateBotBusinessConnect { diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index 4a2a4cf19b..b57072e17f 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -248,7 +248,7 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe guard let gift = StarGift(apiStarGift: apiGift) else { return nil } - return TelegramMediaAction(action: .starGiftUnique(gift: gift, isUpgrade: (flags & (1 << 0)) != 0, isTransferred: (flags & (1 << 1)) != 0, savedToProfile: (flags & (1 << 2)) != 0, canExportDate: canExportAt, transferStars: transferStars, isRefunded: (flags & (1 << 5)) != 0, isPrepaidUpgrade: (flags & (1 << 11)) != 0, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, resaleAmount: resaleAmount.flatMap { CurrencyAmount(apiAmount: $0) }, canTransferDate: canTransferDate, canResaleDate: canResaleDate, dropOriginalDetailsStars: dropOriginalDetailsStars, assigned: (flags & (1 << 13)) != 0, fromOffer: (flags & (1 << 14)) != 0, canCraftAt: canCraftAt)) + return TelegramMediaAction(action: .starGiftUnique(gift: gift, isUpgrade: (flags & (1 << 0)) != 0, isTransferred: (flags & (1 << 1)) != 0, savedToProfile: (flags & (1 << 2)) != 0, canExportDate: canExportAt, transferStars: transferStars, isRefunded: (flags & (1 << 5)) != 0, isPrepaidUpgrade: (flags & (1 << 11)) != 0, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, resaleAmount: resaleAmount.flatMap { CurrencyAmount(apiAmount: $0) }, canTransferDate: canTransferDate, canResaleDate: canResaleDate, dropOriginalDetailsStars: dropOriginalDetailsStars, assigned: (flags & (1 << 13)) != 0, fromOffer: (flags & (1 << 14)) != 0, canCraftAt: canCraftAt, isCrafted: (flags & (1 << 16)) != 0)) case let .messageActionPaidMessagesRefunded(messageActionPaidMessagesRefundedData): let (count, stars) = (messageActionPaidMessagesRefundedData.count, messageActionPaidMessagesRefundedData.stars) return TelegramMediaAction(action: .paidMessagesRefunded(count: count, stars: stars)) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index 854b8e3caa..ddfa473090 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -256,7 +256,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case giftStars(currency: String, amount: Int64, count: Int64, cryptoCurrency: String?, cryptoAmount: Int64?, transactionId: String?) case prizeStars(amount: Int64, isUnclaimed: Bool, boostPeerId: PeerId?, transactionId: String?, giveawayMessageId: MessageId?) case starGift(gift: StarGift, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, upgradeMessageId: Int32?, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, prepaidUpgradeHash: String?, giftMessageId: Int32?, upgradeSeparate: Bool, isAuctionAcquired: Bool, toPeerId: EnginePeer.Id?, number: Int32?) - case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, resaleAmount: CurrencyAmount?, canTransferDate: Int32?, canResaleDate: Int32?, dropOriginalDetailsStars: Int64?, assigned: Bool, fromOffer: Bool, canCraftAt: Int32?) + case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, resaleAmount: CurrencyAmount?, canTransferDate: Int32?, canResaleDate: Int32?, dropOriginalDetailsStars: Int64?, assigned: Bool, fromOffer: Bool, canCraftAt: Int32?, isCrafted: Bool) case paidMessagesRefunded(count: Int32, stars: Int64) case paidMessagesPriceEdited(stars: Int64, broadcastMessagesAllowed: Bool) case conferenceCall(ConferenceCall) @@ -405,7 +405,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else if let stars = decoder.decodeOptionalInt64ForKey("resaleStars") { resaleAmount = CurrencyAmount(amount: StarsAmount(value: stars, nanos: 0), currency: .stars) } - self = .starGiftUnique(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, isUpgrade: decoder.decodeBoolForKey("isUpgrade", orElse: false), isTransferred: decoder.decodeBoolForKey("isTransferred", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), canExportDate: decoder.decodeOptionalInt32ForKey("canExportDate"), transferStars: decoder.decodeOptionalInt64ForKey("transferStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), isPrepaidUpgrade: decoder.decodeBoolForKey("isPrepaidUpgrade", orElse: false), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"), resaleAmount: resaleAmount, canTransferDate: decoder.decodeOptionalInt32ForKey("canTransferDate"), canResaleDate: decoder.decodeOptionalInt32ForKey("canResaleDate"), dropOriginalDetailsStars: decoder.decodeOptionalInt64ForKey("dropOriginalDetailsStars"), assigned: decoder.decodeBoolForKey("assigned", orElse: false), fromOffer: decoder.decodeBoolForKey("fromOffer", orElse: false), canCraftAt: decoder.decodeOptionalInt32ForKey("canCraftAt")) + self = .starGiftUnique(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, isUpgrade: decoder.decodeBoolForKey("isUpgrade", orElse: false), isTransferred: decoder.decodeBoolForKey("isTransferred", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), canExportDate: decoder.decodeOptionalInt32ForKey("canExportDate"), transferStars: decoder.decodeOptionalInt64ForKey("transferStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), isPrepaidUpgrade: decoder.decodeBoolForKey("isPrepaidUpgrade", orElse: false), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"), resaleAmount: resaleAmount, canTransferDate: decoder.decodeOptionalInt32ForKey("canTransferDate"), canResaleDate: decoder.decodeOptionalInt32ForKey("canResaleDate"), dropOriginalDetailsStars: decoder.decodeOptionalInt64ForKey("dropOriginalDetailsStars"), assigned: decoder.decodeBoolForKey("assigned", orElse: false), fromOffer: decoder.decodeBoolForKey("fromOffer", orElse: false), canCraftAt: decoder.decodeOptionalInt32ForKey("canCraftAt"), isCrafted: decoder.decodeBoolForKey("isCrafted", orElse: false)) case 46: self = .paidMessagesRefunded(count: decoder.decodeInt32ForKey("count", orElse: 0), stars: decoder.decodeInt64ForKey("stars", orElse: 0)) case 47: @@ -806,7 +806,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "number") } - case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, dropOriginalDetailsStars, assigned, fromOffer, canCraftAt): + case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, dropOriginalDetailsStars, assigned, fromOffer, canCraftAt, isCrafted): encoder.encodeInt32(45, forKey: "_rawValue") encoder.encodeObject(gift, forKey: "gift") encoder.encodeBool(isUpgrade, forKey: "isUpgrade") @@ -867,6 +867,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "canCraftAt") } + + encoder.encodeBool(isCrafted, forKey: "isCrafted") case let .paidMessagesRefunded(count, stars): encoder.encodeInt32(46, forKey: "_rawValue") encoder.encodeInt32(count, forKey: "count") @@ -976,7 +978,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { peerIds.append(toPeerId) } return peerIds - case let .starGiftUnique(gift, _, _, _, _, _, _, _, peerId, senderId, _, _, _, _, _, _, _, _): + case let .starGiftUnique(gift, _, _, _, _, _, _, _, peerId, senderId, _, _, _, _, _, _, _, _, _): var peerIds: [PeerId] = [] if let peerId { peerIds.append(peerId) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index c4285ba8de..cc6a6ce40c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -460,12 +460,13 @@ public enum StarGift: Equatable, Codable, PostboxCoding { case rare case epic case legendary + case uncommon public var permilleValue: Int32 { switch self { case let .permille(value): return value - case .rare, .epic, .legendary: + case .rare, .epic, .legendary, .uncommon: return 0 } } @@ -529,6 +530,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding { return .epic case 3: return .legendary + case 4: + return .uncommon default: let permille = try container.decodeIfPresent(Int32.self, forKey: .rarity) ?? 0 return .permille(permille) @@ -583,6 +586,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding { return .epic case 3: return .legendary + case 4: + return .uncommon default: let permille = decoder.decodeInt32ForKey(CodingKeys.rarity.rawValue, orElse: 0) return .permille(permille) @@ -642,6 +647,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding { try container.encode(Int32(2), forKey: .rarityType) case .legendary: try container.encode(Int32(3), forKey: .rarityType) + case .uncommon: + try container.encode(Int32(4), forKey: .rarityType) } } @@ -688,6 +695,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding { encoder.encodeInt32(2, forKey: CodingKeys.rarityType.rawValue) case .legendary: encoder.encodeInt32(3, forKey: CodingKeys.rarityType.rawValue) + case .uncommon: + encoder.encodeInt32(4, forKey: CodingKeys.rarityType.rawValue) } } @@ -1552,7 +1561,7 @@ func _internal_dropStarGiftOriginalDetails(account: Account, reference: StarGift storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags) } var media = currentMessage.media - if let action = media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, _, assigned, fromOffer, canCraftAt) = action.action, case let .unique(uniqueGift) = gift { + if let action = media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, _, assigned, fromOffer, canCraftAt, isCrafted) = action.action, case let .unique(uniqueGift) = gift { let updatedAttributes = uniqueGift.attributes.filter { $0.attributeType != .originalInfo } media = [ TelegramMediaAction( @@ -1574,7 +1583,8 @@ func _internal_dropStarGiftOriginalDetails(account: Account, reference: StarGift dropOriginalDetailsStars: nil, assigned: assigned, fromOffer: fromOffer, - canCraftAt: canCraftAt + canCraftAt: canCraftAt, + isCrafted: isCrafted ) ) ] @@ -1681,7 +1691,7 @@ func _internal_upgradeStarGift(account: Account, formId: Int64?, reference: Star let message = updateNewMessageData.message if let message = StoreMessage(apiMessage: message, accountPeerId: account.peerId, peerIsForum: false) { for media in message.media { - if let action = media as? TelegramMediaAction, case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _, canCraftAt) = action.action, case let .Id(messageId) = message.id { + if let action = media as? TelegramMediaAction, case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _, canCraftAt, _) = action.action, case let .Id(messageId) = message.id { let reference: StarGiftReference if let peerId, let savedId { reference = .peer(peerId: peerId, id: savedId) @@ -3214,12 +3224,6 @@ private final class CraftGiftsContextImpl { return _internal_craftStarGift(account: self.account, references: references) } - func addGift(gift: ProfileGiftsContext.State.StarGift) { - self.gifts.insert(gift, at: 0) - self.count = self.count + 1 - self.pushState() - } - func removeGifts(references: [StarGiftReference]) { let referencesSet = Set(references) self.gifts.removeAll { gift in @@ -3353,12 +3357,6 @@ public final class CraftGiftsContext { } } - public func addGift(gift: ProfileGiftsContext.State.StarGift) { - self.impl.with { impl in - impl.addGift(gift: gift) - } - } - public func removeGifts(references: [StarGiftReference]) { self.impl.with { impl in impl.removeGifts(references: references) @@ -3423,7 +3421,7 @@ func _internal_craftStarGift(account: Account, references: [StarGiftReference]) let message = updateNewMessageData.message if let message = StoreMessage(apiMessage: message, accountPeerId: account.peerId, peerIsForum: false) { for media in message.media { - if let action = media as? TelegramMediaAction, case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _, canCraftAt) = action.action, case let .Id(messageId) = message.id { + if let action = media as? TelegramMediaAction, case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _, canCraftAt, _) = action.action, case let .Id(messageId) = message.id { let reference: StarGiftReference if let peerId, let savedId { reference = .peer(peerId: peerId, id: savedId) @@ -3542,6 +3540,8 @@ extension StarGift.UniqueGift.Attribute { return .epic case .starGiftAttributeRarityLegendary: return .legendary + case .starGiftAttributeRarityUncommon: + return .uncommon } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index caa4c59dcc..fa6c51a454 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -1658,7 +1658,7 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot case .giftCode, .stars, .starsGift, .starsChatSubscription, .starGift, .starGiftUpgrade, .starGiftTransfer, .premiumGift, .starGiftResale, .starGiftPrepaidUpgrade, .starGiftDropOriginalDetails, .starGiftAuctionBid: receiptMessageId = nil } - } else if case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, isRefunded, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _, canCraftAt) = action.action, case let .Id(messageId) = message.id { + } else if case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, isRefunded, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _, canCraftAt, _) = action.action, case let .Id(messageId) = message.id { let reference: StarGiftReference if let peerId, let savedId { reference = .peer(peerId: peerId, id: savedId) diff --git a/submodules/TelegramPresentationData/Sources/PresentationData.swift b/submodules/TelegramPresentationData/Sources/PresentationData.swift index 17dc241a3d..ca3e549ff3 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationData.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationData.swift @@ -217,9 +217,10 @@ public final class InitialPresentationDataAndSettings { public let mediaInputSettings: MediaInputSettings public let mediaDisplaySettings: MediaDisplaySettings public let stickerSettings: StickerSettings + public let chatSettings: ChatSettings public let experimentalUISettings: ExperimentalUISettings - public init(presentationData: PresentationData, automaticMediaDownloadSettings: MediaAutoDownloadSettings, autodownloadSettings: AutodownloadSettings, callListSettings: CallListSettings, inAppNotificationSettings: InAppNotificationSettings, mediaInputSettings: MediaInputSettings, mediaDisplaySettings: MediaDisplaySettings, stickerSettings: StickerSettings, experimentalUISettings: ExperimentalUISettings) { + public init(presentationData: PresentationData, automaticMediaDownloadSettings: MediaAutoDownloadSettings, autodownloadSettings: AutodownloadSettings, callListSettings: CallListSettings, inAppNotificationSettings: InAppNotificationSettings, mediaInputSettings: MediaInputSettings, mediaDisplaySettings: MediaDisplaySettings, stickerSettings: StickerSettings, chatSettings: ChatSettings, experimentalUISettings: ExperimentalUISettings) { self.presentationData = presentationData self.automaticMediaDownloadSettings = automaticMediaDownloadSettings self.autodownloadSettings = autodownloadSettings @@ -228,6 +229,7 @@ public final class InitialPresentationDataAndSettings { self.mediaInputSettings = mediaInputSettings self.mediaDisplaySettings = mediaDisplaySettings self.stickerSettings = stickerSettings + self.chatSettings = chatSettings self.experimentalUISettings = experimentalUISettings } } @@ -245,6 +247,7 @@ public func currentPresentationDataAndSettings(accountManager: AccountManager deliverOn(Queue(name: "PresentationData-Load", qos: .userInteractive)) @@ -365,6 +372,13 @@ public func currentPresentationDataAndSettings(accountManager: AccountManager deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + self.peer = peer + self.updated() + }) } func displayPhoneNumberConfirmation(commit: @escaping (Bool) -> Void) { @@ -78,7 +89,7 @@ private final class AuthConfirmationSheetContent: CombinedComponent { let alertController = textAlertController( context: self.context, title: "Phone Number", - text: "**\(domain)** wants to access your phone number **\(phoneNumber)**. Allow access?", + text: "**\(domain)** wants to access your phone number **\(phoneNumber)**.\n\nAllow access?", actions: [ TextAlertAction(type: .genericAction, title: "Deny", action: { commit(false) @@ -99,13 +110,12 @@ private final class AuthConfirmationSheetContent: CombinedComponent { static var body: Body { let closeButton = Child(GlassBarButtonComponent.self) + let peerButton = Child(AvatarComponent.self) let avatar = Child(AvatarComponent.self) let title = Child(MultilineTextComponent.self) let description = Child(MultilineTextComponent.self) - let clientSection = Child(ListSectionComponent.self) let optionsSection = Child(ListSectionComponent.self) - let cancelButton = Child(ButtonComponent.self) let doneButton = Child(ButtonComponent.self) @@ -147,6 +157,24 @@ private final class AuthConfirmationSheetContent: CombinedComponent { availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) + context.add(closeButton + .position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0)) + ) + + if let peer = state.peer { + let peerButton = peerButton.update( + component: AvatarComponent( + context: component.context, + theme: environment.theme, + peer: peer + ), + availableSize: CGSize(width: 44.0, height: 44.0), + transition: .immediate + ) + context.add(peerButton + .position(CGPoint(x: context.availableSize.width - 16.0 - peerButton.size.width / 2.0, y: 16.0 + peerButton.size.height / 2.0)) + ) + } var contentHeight: CGFloat = 32.0 let avatar = avatar.update( @@ -320,7 +348,7 @@ private final class AuthConfirmationSheetContent: CombinedComponent { contentHeight += clientSection.size.height if flags.contains(.requestWriteAccess) { - contentHeight += 22.0 + contentHeight += 38.0 var optionsSectionItems: [AnyComponentWithIdentity] = [] optionsSectionItems.append(AnyComponentWithIdentity(id: "allowWrite", component: AnyComponent(ListActionItemComponent( @@ -441,10 +469,6 @@ private final class AuthConfirmationSheetContent: CombinedComponent { contentHeight += doneButton.size.height contentHeight += buttonInsets.bottom - context.add(closeButton - .position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0)) - ) - return CGSize(width: context.availableSize.width, height: contentHeight) } } diff --git a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift index 0887af088a..ce1af17211 100644 --- a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift +++ b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift @@ -466,6 +466,9 @@ public final class ButtonComponent: Component { super.init(frame: frame) + self.button.isExclusiveTouch = true + self.layer.rasterizationScale = UIScreenScale + self.addSubview(self.containerView) self.addSubview(self.button) @@ -477,12 +480,24 @@ public final class ButtonComponent: Component { case .glass: let transition = ComponentTransition(animation: .curve(duration: highlighted ? 0.25 : 0.35, curve: .spring)) if highlighted { + self.layer.shouldRasterize = true + let highlightedColor = component.background.color.withMultiplied(hue: 1.0, saturation: 0.77, brightness: 1.01) transition.setBackgroundColor(view: self.containerView, color: highlightedColor) - transition.setScale(view: self.containerView, scale: 1.05) + transition.setScale(view: self.containerView, scale: 1.05, completion: { finished in + if finished { + self.layer.shouldRasterize = false + } + }) } else { + self.layer.shouldRasterize = true + transition.setBackgroundColor(view: self.containerView, color: component.background.color) - transition.setScale(view: self.containerView, scale: 1.0) + transition.setScale(view: self.containerView, scale: 1.0, completion: { finished in + if finished { + self.layer.shouldRasterize = false + } + }) } case .legacy: if highlighted { @@ -598,6 +613,7 @@ public final class ButtonComponent: Component { if contentView.superview == nil { contentTransition = .immediate animateIn = true + contentView.layer.rasterizationScale = UIScreenScale contentView.isUserInteractionEnabled = false contentContainerView.addSubview(contentView) @@ -609,7 +625,10 @@ public final class ButtonComponent: Component { contentTransition.setAlpha(view: contentView, alpha: contentAlpha) if animateIn && previousContentItem != nil && !transition.animation.isImmediate { - contentView.layer.animateScale(from: 0.4, to: 1.0, duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring) + contentView.layer.shouldRasterize = true + contentView.layer.animateScale(from: 0.4, to: 1.0, duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in + contentView.layer.shouldRasterize = false + }) contentView.layer.animateAlpha(from: 0.0, to: contentAlpha, duration: 0.1) contentView.layer.animatePosition(from: CGPoint(x: 0.0, y: -size.height * 0.15), to: CGPoint(), duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } @@ -617,6 +636,7 @@ public final class ButtonComponent: Component { if let previousContentItem, let previousContentView = previousContentItem.view.view { if !transition.animation.isImmediate { + previousContentView.layer.shouldRasterize = true previousContentView.layer.animateScale(from: 1.0, to: 0.0, duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) previousContentView.layer.animateAlpha(from: contentAlpha, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousContentView] _ in previousContentView?.removeFromSuperview() diff --git a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/PublicHeaders/ChatInputTextViewImpl/ChatInputTextViewImpl.h b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/PublicHeaders/ChatInputTextViewImpl/ChatInputTextViewImpl.h index f4a0508002..08c09cff99 100755 --- a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/PublicHeaders/ChatInputTextViewImpl/ChatInputTextViewImpl.h +++ b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/PublicHeaders/ChatInputTextViewImpl/ChatInputTextViewImpl.h @@ -18,7 +18,7 @@ @property (nonatomic, copy) bool (^ _Nullable shouldPaste)(); @property (nonatomic, copy) bool (^ _Nullable shouldRespondToAction)(SEL _Nullable); @property (nonatomic, copy) ChatInputTextViewImplTargetForAction * _Nullable (^ _Nullable targetForAction)(SEL _Nullable); -@property (nonatomic, copy) bool (^ _Nullable shouldReturn)(); +@property (nonatomic, copy) bool (^ _Nullable shouldReturn)(UIKeyModifierFlags); @property (nonatomic, copy) void (^ _Nullable backspaceWhileEmpty)(); @property (nonatomic, copy) void (^ _Nullable dropAutocorrectioniOS16)(); diff --git a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/Sources/ChatInputTextViewImpl.m b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/Sources/ChatInputTextViewImpl.m index a3356a56de..cb582b8985 100755 --- a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/Sources/ChatInputTextViewImpl.m +++ b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/ChatInputTextViewImpl/Sources/ChatInputTextViewImpl.m @@ -136,15 +136,22 @@ } - (NSArray *)keyCommands { - UIKeyCommand *plainReturn = [UIKeyCommand keyCommandWithInput:@"\r" modifierFlags:kNilOptions action:@selector(handlePlainReturn:)]; + UIKeyCommand *plainReturn = [UIKeyCommand keyCommandWithInput:@"\r" modifierFlags:kNilOptions action:@selector(handleReturn:)]; + UIKeyCommand *cmdReturn = [UIKeyCommand keyCommandWithInput:@"\r" modifierFlags:UIKeyModifierCommand action:@selector(handleReturn:)]; return @[ - plainReturn + plainReturn, + cmdReturn ]; } -- (void)handlePlainReturn:(id)__unused sender { +- (void)handleReturn:(UIKeyCommand *)__unused sender { + UIKeyModifierFlags modifierFlags = 0; + if ([sender isKindOfClass:[UIKeyCommand class]]) { + modifierFlags = sender.modifierFlags; + } + if (_shouldReturn) { - _shouldReturn(); + _shouldReturn(modifierFlags); } } diff --git a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift index ecb4ecd4dc..3512bd952b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift @@ -11,7 +11,7 @@ import TextNodeWithEntities public protocol ChatInputTextNodeDelegate: AnyObject { func chatInputTextNodeDidUpdateText() - func chatInputTextNodeShouldReturn() -> Bool + func chatInputTextNodeShouldReturn(modifierFlags: UIKeyModifierFlags) -> Bool func chatInputTextNodeDidChangeSelection(dueToEditing: Bool) func chatInputTextNodeDidBeginEditing() func chatInputTextNodeDidFinishEditing() @@ -1180,11 +1180,11 @@ public final class ChatInputTextView: ChatInputTextViewImpl, UITextViewDelegate, } return self.customDelegate?.chatInputTextNodeShouldPaste() ?? true } - self.shouldReturn = { [weak self] in + self.shouldReturn = { [weak self] modifierFlags in guard let self else { return true } - return self.customDelegate?.chatInputTextNodeShouldReturn() ?? true + return self.customDelegate?.chatInputTextNodeShouldReturn(modifierFlags: modifierFlags) ?? true } self.backspaceWhileEmpty = { [weak self] in guard let self else { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 96ff976f1d..12e1c48300 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -282,7 +282,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { switch action.action { case let .starGift(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): releasedBy = gift.releasedBy - case let .starGiftUnique(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .starGiftUnique(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): releasedBy = gift.releasedBy default: break @@ -690,7 +690,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } } } - case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded, _, _, _, _, _, _, _, _, _, fromOffer, _): + case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded, _, _, _, _, _, _, _, _, _, fromOffer, _, _): if case let .unique(uniqueGift) = gift { isStarGift = true diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift index ff38c7c2d6..38970d80f3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift @@ -2520,7 +2520,9 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg case let .audio(recorder, isLocked): let hadAudioRecorder = self.mediaActionButtons.micButton.audioRecorder != nil if !hadAudioRecorder, isLocked { - self.mediaActionButtons.micButton.lock() + DispatchQueue.main.async { [weak self] in + self?.mediaActionButtons.micButton.lock() + } } self.mediaActionButtons.micButton.audioRecorder = recorder audioRecordingTimeNode.audioRecorder = recorder @@ -4528,15 +4530,29 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg } } - public func chatInputTextNodeShouldReturn() -> Bool { + public func chatInputTextNodeShouldReturn(modifierFlags: UIKeyModifierFlags) -> Bool { + var shouldSendMessage = false if self.sendActionButtons.sendButton.supernode != nil && !self.sendActionButtons.sendButton.isHidden && !self.sendActionButtons.sendContainerNode.alpha.isZero { - self.sendButtonPressed() + if let context = self.context, context.sharedContext.currentChatSettings.with({ $0 }).sendWithCmdEnter { + if modifierFlags.contains(.command) { + shouldSendMessage = true + } + } else { + if modifierFlags.isEmpty { + shouldSendMessage = true + } + } } - return false + if shouldSendMessage { + self.sendButtonPressed() + return false + } + + return true } @objc public func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool { - return self.chatInputTextNodeShouldReturn() + return self.chatInputTextNodeShouldReturn(modifierFlags: []) } private func applyUpdateSendButtonIcon() { diff --git a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift index 78cde2f0e3..b4c5117282 100644 --- a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift @@ -674,6 +674,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo context: reactionItems.context, animationCache: reactionItems.animationCache, presentationData: presentationData, + //style: .glass(isTinted: false), items: reactionItems.reactionItems, selectedItems: reactionItems.selectedReactionItems, title: reactionItems.reactionsTitle, @@ -1058,12 +1059,17 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo transition.updateFrame(node: self.contentRectDebugNode, frame: contentRect, beginWithCurrentState: true) + var totalActionsHeight: CGFloat = actionsSize.height + if additionalActionsSize.height != 0.0 { + totalActionsHeight += 10.0 + additionalActionsSize.height + } + var actionsFrame: CGRect if let contextExtractableContainer { let _ = contextExtractableContainer actionsFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: contentRect.minY), size: actionsSize) } else if case let .reference(source) = self.source, let actionsPosition = source.transitionInfo()?.actionsPosition, case .top = actionsPosition { - actionsFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: contentRect.minY - contentActionsSpacing - actionsSize.height), size: actionsSize) + actionsFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: contentRect.minY - contentActionsSpacing - totalActionsHeight), size: actionsSize) } else { actionsFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: contentRect.maxY + contentActionsSpacing), size: actionsSize) } diff --git a/submodules/TelegramUI/Components/EdgeEffect/Sources/EdgeEffect.swift b/submodules/TelegramUI/Components/EdgeEffect/Sources/EdgeEffect.swift index 510b0b1754..f9f7f795bc 100644 --- a/submodules/TelegramUI/Components/EdgeEffect/Sources/EdgeEffect.swift +++ b/submodules/TelegramUI/Components/EdgeEffect/Sources/EdgeEffect.swift @@ -591,11 +591,11 @@ public final class VariableBlurEffect { } public let position: Position - public let extendsInwards: Bool + public let inwardsExtension: CGFloat? - public init(position: Position, extendsInwards: Bool) { + public init(position: Position, inwardsExtension: CGFloat?) { self.position = position - self.extendsInwards = extendsInwards + self.inwardsExtension = inwardsExtension } } @@ -664,7 +664,7 @@ public final class VariableBlurEffect { let mainEffectFrame: CGRect let additionalEffectFrame: CGRect - if params.placement.extendsInwards { + if params.placement.inwardsExtension != nil { mainEffectFrame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)) additionalEffectFrame = CGRect() } else if params.placement.position == .bottom { @@ -698,8 +698,8 @@ public final class VariableBlurEffect { let isHeightUpdated = gradient.height != self.params?.gradient.height || size.height != self.params?.size.height if isGradientUpdated { - if params.placement.extendsInwards { - let baseHeight = max(1.0, params.gradient.height) + if let inwardsExtension = params.placement.inwardsExtension { + let baseHeight = max(1.0, params.gradient.height + inwardsExtension) let resizingInverted = params.placement.position != .bottom self.gradientImage = generateImage(CGSize(width: 1.0, height: baseHeight), opaque: false, rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) @@ -712,13 +712,17 @@ public final class VariableBlurEffect { let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! if params.placement.position == .bottom { - context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: size.height), end: CGPoint(x: 0.0, y: 0.0), options: CGGradientDrawingOptions()) + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: max(0.0, size.height - inwardsExtension)), end: CGPoint(x: 0.0, y: 0.0), options: CGGradientDrawingOptions()) + if inwardsExtension > 0.0 { + context.setFillColor(UIColor.white.cgColor) + context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.height - inwardsExtension), size: CGSize(width: size.width, height: inwardsExtension))) + } } else { context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) } })?.resizableImage(withCapInsets: UIEdgeInsets(top: resizingInverted ? baseHeight : 0.0, left: 0.0, bottom: resizingInverted ? 0.0 : baseHeight, right: 0.0), resizingMode: .stretch) } else { - self.gradientImage = EdgeEffectView.generateEdgeGradient(baseHeight: max(1.0, params.gradient.height), isInverted: params.placement.position == .bottom, extendsInwards: params.placement.extendsInwards) + self.gradientImage = EdgeEffectView.generateEdgeGradient(baseHeight: max(1.0, params.gradient.height), isInverted: params.placement.position == .bottom, extendsInwards: params.placement.inwardsExtension != nil) } } @@ -775,6 +779,62 @@ public final class VariableBlurView: UIView { } public func update(size: CGSize, constantHeight: CGFloat, isInverted: Bool, gradient: VariableBlurEffect.Gradient, transition: ContainedViewLayoutTransition) { - self.effect?.update(size: size, constantHeight: constantHeight, placement: VariableBlurEffect.Placement(position: isInverted ? .bottom : .top, extendsInwards: false), gradient: gradient, transition: transition) + self.effect?.update(size: size, constantHeight: constantHeight, placement: VariableBlurEffect.Placement(position: isInverted ? .bottom : .top, inwardsExtension: nil), gradient: gradient, transition: transition) + } +} + +public final class EdgeMaskView: UIView { + private struct MaskParams: Equatable { + let gradientHeight: CGFloat + let extensionHeight: CGFloat + + init(gradientHeight: CGFloat, extensionHeight: CGFloat) { + self.gradientHeight = gradientHeight + self.extensionHeight = extensionHeight + } + } + + private let imageView: UIImageView + + private var maskParams: MaskParams? + + override public init(frame: CGRect) { + self.imageView = UIImageView() + + super.init(frame: frame) + + self.addSubview(self.imageView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func update(size: CGSize, color: UIColor, gradientHeight: CGFloat, extensionHeight: CGFloat, transition: ComponentTransition) { + let maskParams = MaskParams(gradientHeight: gradientHeight, extensionHeight: extensionHeight) + if maskParams != self.maskParams { + self.maskParams = maskParams + + let baseHeight = max(1.0, maskParams.gradientHeight + maskParams.extensionHeight) + let resizingInverted = !"".isEmpty + self.imageView.image = generateImage(CGSize(width: 1.0, height: baseHeight), opaque: false, rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let gradientColors = [UIColor.white.withAlphaComponent(1.0).cgColor, UIColor.white.withAlphaComponent(0.0).cgColor] as CFArray + + var locations: [CGFloat] = [0.0, 1.0] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + + if "".isEmpty { + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height - maskParams.extensionHeight), options: CGGradientDrawingOptions()) + } else { + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + } + })?.resizableImage(withCapInsets: UIEdgeInsets(top: resizingInverted ? baseHeight : 0.0, left: 0.0, bottom: resizingInverted ? 0.0 : baseHeight, right: 0.0), resizingMode: .stretch).withRenderingMode(.alwaysTemplate) + } + self.imageView.tintColor = color + transition.setFrame(view: self.imageView, frame: CGRect(origin: CGPoint(), size: size)) } } diff --git a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift index 4fe96e94df..35cd956873 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift @@ -18,6 +18,7 @@ import UIKitRuntimeUtils public final class GiftCompositionComponent: Component { public class ExternalState { public fileprivate(set) var previewPatternColor: UIColor? + public fileprivate(set) var backgroundColor: UIColor? public fileprivate(set) var previewModel: StarGift.UniqueGift.Attribute? public fileprivate(set) var previewBackdrop: StarGift.UniqueGift.Attribute? public fileprivate(set) var previewSymbol: StarGift.UniqueGift.Attribute? @@ -766,6 +767,7 @@ public final class GiftCompositionComponent: Component { } } + component.externalState?.backgroundColor = backgroundColor component.externalState?.previewPatternColor = secondBackgroundColor var animateBackdropSwipe = false diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/BUILD index e5a3568ad1..7262f33a94 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/BUILD @@ -49,6 +49,7 @@ swift_library( "//submodules/TelegramUI/Components/SpaceWarpView", "//submodules/ConfettiEffect", "//submodules/TelegramNotices", + "//submodules/TelegramUI/Components/Gifts/GiftLoadingShimmerView", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CraftTableComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CraftTableComponent.swift index 251e5097e7..a6a3bff6eb 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CraftTableComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CraftTableComponent.swift @@ -108,7 +108,7 @@ final class CraftTableComponent: Component { fatalError("init(coder:) has not been implemented") } - func setupFailAnimation() { + func setupFailureAnimation() { guard !self.didSetupFinishAnimation else { return } @@ -127,41 +127,29 @@ final class CraftTableComponent: Component { } for i in 0 ..< min(2, availableStickers.count) { if let sticker = availableStickers[i].view { - self.animationView.setSticker(sticker, face: 3 - i, mirror: isUpsideDown) + let face: Int + if isUpsideDown { + face = i + 1 + } else { + face = 3 - i + } + self.animationView.setSticker(sticker, face: face, mirror: isUpsideDown) } } - self.state?.updated() + self.flipFaces = isUpsideDown + + Queue.mainQueue().after(0.3, { + self.failWillFinish = true + self.component?.willFinish(false) + }) - if let failOverlayView = self.failOverlay.view as? LottieComponent.View { - failOverlayView.isHidden = false - failOverlayView.onFrameUpdate = { [weak self] frameIndex in - guard let self else { - return - } - if frameIndex >= 5 && !self.failDidStartCrossAnimation { - self.failDidStartCrossAnimation = true - self.craftFailPlayOnce.invoke(Void()) - } - if frameIndex >= 65 && !self.failDidBringToFront { - self.failDidBringToFront = true - failOverlayView.superview?.bringSubviewToFront(failOverlayView) - - self.animationView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) - } - if frameIndex >= 75 && !self.failWillFinish { - self.failWillFinish = true - self.component?.willFinish(false) - } - if frameIndex >= 82 && !self.failDidFinish { - self.failDidFinish = true - - failOverlayView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.component?.finished(nil) - } - } - } - self.craftFailOverlayPlayOnce.invoke(Void()) + Queue.mainQueue().after(0.5, { + self.failDidFinish = true + self.component?.finished(nil) + }) + + self.state?.updated(transition: .easeInOut(duration: 0.4)) } } @@ -171,6 +159,8 @@ final class CraftTableComponent: Component { } self.didSetupFinishAnimation = true + self.animationView.isSuccess = true + self.animationView.onFinishApproach = { [weak self] isUpsideDown in guard let self else { return @@ -270,10 +260,10 @@ final class CraftTableComponent: Component { if index == 0 { faceItems.append( AnyComponentWithIdentity(id: "background", component: AnyComponent( - RoundedRectangle(color: component.buttonColor, cornerRadius: 28.0) + FilledRoundedRectangleComponent(color: component.buttonColor, cornerRadius: .value(28.0), smoothCorners: true) )) ) - if !component.isCrafting { + if !component.isCrafting || self.isFailed { faceItems.append( AnyComponentWithIdentity(id: "glass", component: AnyComponent( GlassBackgroundComponent(size: CGSize(width: cubeSide, height: cubeSide), cornerRadius: 28.0, isDark: true, tintColor: .init(kind: .custom(style: .default, color: component.buttonColor))) @@ -282,11 +272,20 @@ final class CraftTableComponent: Component { } if self.isFailed { faceItems.append( - AnyComponentWithIdentity(id: "fail", component: AnyComponent( - LottieComponent( - content: LottieComponent.AppBundleContent(name: "CraftFail"), - size: CGSize(width: 96.0, height: 96.0), - playOnce: self.craftFailPlayOnce + AnyComponentWithIdentity(id: "faildial", component: AnyComponent( + DialIndicatorComponent( + content: AnyComponentWithIdentity(id: "gift", component: AnyComponent(BundleIconComponent(name: "Premium/GiftCrash", tintColor: .white))), + backgroundColor: .white.withAlphaComponent(0.1), + foregroundColor: .white, + diameter: 84.0, + contentSize: CGSize(width: 44.0, height: 44.0), + lineWidth: 5.0, + fontSize: 18.0, + progress: 0.0, + value: component.gifts.count, + suffix: "", + isVisible: true, + isFlipped: self.flipFaces ) )) ) @@ -300,7 +299,9 @@ final class CraftTableComponent: Component { diameter: 84.0, lineWidth: 5.0, fontSize: 18.0, - percentage: permilleValue / 10, + progress: CGFloat(permilleValue / 10 / 100), + value: permilleValue / 10, + suffix: "%", isVisible: !component.isCrafting ) )) @@ -318,7 +319,7 @@ final class CraftTableComponent: Component { } else { faceItems.append( AnyComponentWithIdentity(id: "background", component: AnyComponent( - RoundedRectangle(color: component.buttonColor, cornerRadius: 28.0) + FilledRoundedRectangleComponent(color: component.buttonColor, cornerRadius: .value(28.0), smoothCorners: true) )) ) faceItems.append( @@ -412,35 +413,36 @@ final class CraftTableComponent: Component { self.setupSuccessAnimation(uniqueGift) } case .fail: - self.setupFailAnimation() + self.setupFailureAnimation() default: break } }) } + - if self.isFailed { - let failOverlaySize = self.failOverlay.update( - transition: .immediate, - component: AnyComponent( - LottieComponent( - content: LottieComponent.AppBundleContent(name: "CraftFailOverlay"), - size: CGSize(width: availableSize.width, height: availableSize.width), - playOnce: self.craftFailOverlayPlayOnce - ) - ), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: availableSize.width) - ) - let failOverlayFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - failOverlaySize.width) / 2.0), y: floor((availableSize.height - failOverlaySize.height) / 2.0)), size: failOverlaySize) - if let failOverlayView = self.failOverlay.view { - if failOverlayView.superview == nil { - failOverlayView.isHidden = true - self.insertSubview(failOverlayView, belowSubview: self.animationView) - } - failOverlayView.frame = failOverlayFrame - } - } +// if self.isFailed { +// let failOverlaySize = self.failOverlay.update( +// transition: .immediate, +// component: AnyComponent( +// LottieComponent( +// content: LottieComponent.AppBundleContent(name: "CraftFailOverlay"), +// size: CGSize(width: availableSize.width, height: availableSize.width), +// playOnce: self.craftFailOverlayPlayOnce +// ) +// ), +// environment: {}, +// containerSize: CGSize(width: availableSize.width, height: availableSize.width) +// ) +// let failOverlayFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - failOverlaySize.width) / 2.0), y: floor((availableSize.height - failOverlaySize.height) / 2.0)), size: failOverlaySize) +// if let failOverlayView = self.failOverlay.view { +// if failOverlayView.superview == nil { +// failOverlayView.isHidden = true +// self.insertSubview(failOverlayView, belowSubview: self.animationView) +// } +// failOverlayView.frame = failOverlayFrame +// } +// } return availableSize } @@ -544,9 +546,9 @@ final class GiftSlotComponent: Component { self.state = state let backgroundFrame = CGRect(origin: .zero, size: availableSize).insetBy(dx: 1.0, dy: 1.0) - self.backgroundView.update(size: backgroundFrame.size, cornerRadius: 28.0, isDark: true, tintColor: .init(kind: .custom(style: .default, color: component.buttonColor)), transition: .immediate) + self.backgroundView.update(size: backgroundFrame.size, cornerRadius: 28.0, isDark: true, tintColor: .init(kind: .custom(style: .default, color: component.buttonColor)), isInteractive: true, transition: .immediate) transition.setFrame(view: self.backgroundView, frame: backgroundFrame) - if component.gift == nil && component.isCrafting { + if component.gift == nil && component.isCrafting && previousComponent?.isCrafting == false { transition.setBlur(layer: self.backgroundView.layer, radius: 10.0) self.backgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false) transition.setBlur(layer: self.addIcon.layer, radius: 10.0) diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CubeAnimationView.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CubeAnimationView.swift index d65ce5986c..d8ef7f77b2 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CubeAnimationView.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CubeAnimationView.swift @@ -79,6 +79,8 @@ final class CubeAnimationView: UIView { private var finishDelayTimerY: Timer? private var cubeScale: Float = 1.0 private var hasFiredFinishApproach = false + + var isSuccess = false var onFinishApproach: ((Bool) -> Void)? @@ -485,7 +487,7 @@ final class CubeAnimationView: UIView { let duration: TimeInterval = 0.2 let startQuad = Quad(rect: sticker.frame) let animationView: UIView - if let snapshot = sticker.snapshotView(afterScreenUpdates: true) { + if let snapshot = sticker.snapshotView(afterScreenUpdates: false) { self.warpSnapshot?.removeFromSuperview() self.warpSnapshot = snapshot @@ -606,7 +608,7 @@ final class CubeAnimationView: UIView { } let stickerIndex = indices[index] if self.stickers.indices.contains(stickerIndex) { - let isLast = stickerIndex == indices.count - 1 + let isLast = index == indices.count - 1 self.launchStickerView(self.stickers[stickerIndex], emphasized: isLast, willFinish: isLast) } self.scheduleStickerSequence(from: index + 1, indices: indices) @@ -697,10 +699,12 @@ final class CubeAnimationView: UIView { let upsideDown = abs(shortestAngleDelta(from: self.rotation.x, to: Float.pi)) < (Float.pi / 2) self.onFinishApproach?(upsideDown) } - if absRemaining <= self.finishSuccessScaleTriggerAngle { + if self.isSuccess, absRemaining <= self.finishSuccessScaleTriggerAngle { let raw = (self.finishSuccessScaleTriggerAngle - absRemaining) / self.finishSuccessScaleTriggerAngle let eased = raw * raw * (3 - 2 * raw) self.cubeScale = 1.0 + (self.finishSuccessScale - 1.0) * eased + } else if !self.isSuccess { + self.cubeScale = 1.0 } if abs(remaining) < 0.0008 && abs(self.angularVelocity.y) < 0.0015 { self.finishRotationY = self.finishTargetYUnwrapped diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/DialIndicatorComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/DialIndicatorComponent.swift index accfe3fb76..468c44773f 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/DialIndicatorComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/DialIndicatorComponent.swift @@ -13,29 +13,41 @@ final class DialIndicatorComponent: Component { let backgroundColor: UIColor let foregroundColor: UIColor let diameter: CGFloat + let contentSize: CGSize? let lineWidth: CGFloat let fontSize: CGFloat - let percentage: Int + let progress: CGFloat + let value: Int + let suffix: String let isVisible: Bool + let isFlipped: Bool public init( content: AnyComponentWithIdentity, backgroundColor: UIColor, foregroundColor: UIColor, diameter: CGFloat, + contentSize: CGSize? = nil, lineWidth: CGFloat, fontSize: CGFloat, - percentage: Int, - isVisible: Bool = true + progress: CGFloat, + value: Int, + suffix: String, + isVisible: Bool = true, + isFlipped: Bool = false, ) { self.content = content self.backgroundColor = backgroundColor self.foregroundColor = foregroundColor self.diameter = diameter + self.contentSize = contentSize self.lineWidth = lineWidth self.fontSize = fontSize - self.percentage = percentage + self.progress = progress + self.value = value + self.suffix = suffix self.isVisible = isVisible + self.isFlipped = isFlipped } public static func ==(lhs: DialIndicatorComponent, rhs: DialIndicatorComponent) -> Bool { @@ -51,18 +63,30 @@ final class DialIndicatorComponent: Component { if lhs.diameter != rhs.diameter { return false } + if lhs.contentSize != rhs.contentSize { + return false + } if lhs.lineWidth != rhs.lineWidth { return false } if lhs.fontSize != rhs.fontSize { return false } - if lhs.percentage != rhs.percentage { + if lhs.progress != rhs.progress { + return false + } + if lhs.value != rhs.value { + return false + } + if lhs.suffix != rhs.suffix { return false } if lhs.isVisible != rhs.isVisible { return false } + if lhs.isFlipped != rhs.isFlipped { + return false + } return true } @@ -82,7 +106,7 @@ final class DialIndicatorComponent: Component { self.backgroundLayer.lineCap = .round self.foregroundLayer.lineCap = .round - + self.addSubview(self.containerView) self.containerView.layer.addSublayer(self.backgroundLayer) @@ -119,7 +143,7 @@ final class DialIndicatorComponent: Component { self.foregroundLayer.path = CGPath(ellipseIn: pathFrame, transform: nil) self.foregroundLayer.transform = CATransform3DMakeRotation(.pi / 2.0, 0.0, 0.0, 1.0) self.foregroundLayer.strokeStart = strokeStart - transition.setShapeLayerStrokeEnd(layer: self.foregroundLayer, strokeEnd: strokeStart + (strokeEnd - strokeStart) * (CGFloat(component.percentage) / 100.0)) + transition.setShapeLayerStrokeEnd(layer: self.foregroundLayer, strokeEnd: strokeStart + (strokeEnd - strokeStart) * component.progress) self.foregroundLayer.frame = CGRect(origin: .zero, size: pathSize) if previousComponent?.content.id != component.content.id { @@ -136,7 +160,8 @@ final class DialIndicatorComponent: Component { self.content = ComponentView() } - let contentFrame = CGRect(origin: CGPoint(x: 8.0, y: 8.0), size: CGSize(width: component.diameter - 16.0, height: component.diameter - 16.0)) + let contentSize = component.contentSize ?? CGSize(width: component.diameter - 16.0, height: component.diameter - 16.0) + let contentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((pathSize.width - contentSize.width) / 2.0), y: floorToScreenPixels((pathSize.height - contentSize.height) / 2.0)), size: contentSize) let _ = self.content.update( transition: .immediate, component: component.content.component, @@ -154,10 +179,12 @@ final class DialIndicatorComponent: Component { contentView.frame = contentFrame } - let labelItems: [AnimatedTextComponent.Item] = [ - AnimatedTextComponent.Item(id: "percent", content: .number(component.percentage, minDigits: 1)), - AnimatedTextComponent.Item(id: "suffix", content: .text("%")) + var labelItems: [AnimatedTextComponent.Item] = [ + AnimatedTextComponent.Item(id: "percent", content: .number(component.value, minDigits: 1)) ] + if !component.suffix.isEmpty { + labelItems.append(AnimatedTextComponent.Item(id: "suffix", content: .text(component.suffix))) + } let labelSize = self.label.update( transition: transition, @@ -181,6 +208,8 @@ final class DialIndicatorComponent: Component { transition.setAlpha(view: self.containerView, alpha: component.isVisible ? 1.0 : 0.0) transition.setBlur(layer: self.containerView.layer, radius: component.isVisible ? 0.0 : 10.0) + self.containerView.transform = CGAffineTransform(rotationAngle: component.isFlipped ? .pi : 0.0) + self.containerView.frame = CGRect(origin: .zero, size: pathSize) return pathSize diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift index 1a34ed0c24..8867406dd3 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift @@ -42,27 +42,35 @@ private final class CraftGiftPageContent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment class ExternalState { - fileprivate(set) var giftMap: [Int64: GiftItem] + fileprivate(set) var giftsMap: [Int64: GiftItem] + fileprivate(set) var starGiftsMap: [Int64: StarGift.Gift] = [:] + fileprivate(set) var displayFailure = false fileprivate(set) var testFailOrSuccess: Bool? public init() { - self.giftMap = [:] + self.giftsMap = [:] } } + enum DisplayState { + case `default` + case crafting + case failure + } + let context: AccountContext let craftContext: CraftGiftsContext let resaleContext: () -> ResaleGiftsContext? let colors: (UIColor, UIColor, UIColor, UIColor, UIColor) let gift: StarGift.UniqueGift let selectedGiftIds: [Int32: Int64] - let displayCraftInfo: Bool - let isCrafting: Bool - let inProgress: Bool + let displayState: DisplayState + let displayInfo: Bool let result: CraftTableComponent.Result? let screenSize: CGSize let externalState: ExternalState + let starsTopUpOptionsPromise: Promise<[StarsTopUpOption]?> let selectGift: (Int32, GiftItem) -> Void let removeGift: (Int32) -> Void let dismiss: () -> Void @@ -74,12 +82,12 @@ private final class CraftGiftPageContent: Component { colors: (UIColor, UIColor, UIColor, UIColor, UIColor), gift: StarGift.UniqueGift, selectedGiftIds: [Int32: Int64], - displayCraftInfo: Bool, - isCrafting: Bool, - inProgress: Bool, + displayState: DisplayState, + displayInfo: Bool, result: CraftTableComponent.Result?, screenSize: CGSize, externalState: ExternalState, + starsTopUpOptionsPromise: Promise<[StarsTopUpOption]?>, selectGift: @escaping (Int32, GiftItem) -> Void, removeGift: @escaping (Int32) -> Void, dismiss: @escaping () -> Void @@ -90,12 +98,12 @@ private final class CraftGiftPageContent: Component { self.colors = colors self.gift = gift self.selectedGiftIds = selectedGiftIds - self.displayCraftInfo = displayCraftInfo - self.isCrafting = isCrafting - self.inProgress = inProgress + self.displayState = displayState + self.displayInfo = displayInfo self.result = result self.screenSize = screenSize self.externalState = externalState + self.starsTopUpOptionsPromise = starsTopUpOptionsPromise self.selectGift = selectGift self.removeGift = removeGift self.dismiss = dismiss @@ -114,13 +122,10 @@ private final class CraftGiftPageContent: Component { if lhs.selectedGiftIds != rhs.selectedGiftIds { return false } - if lhs.displayCraftInfo != rhs.displayCraftInfo { + if lhs.displayState != rhs.displayState { return false } - if lhs.isCrafting != rhs.isCrafting { - return false - } - if lhs.inProgress != rhs.inProgress { + if lhs.displayInfo != rhs.displayInfo { return false } if lhs.screenSize != rhs.screenSize { @@ -138,19 +143,22 @@ private final class CraftGiftPageContent: Component { private let title = ComponentView() private let descriptionText = ComponentView() - private let craftingTitle = ComponentView() - private let craftingSubtitle = ComponentView() - private let craftingDescription = ComponentView() - private let craftingProbability = ComponentView() - private var craftingProbabilityMeasure = ComponentView() + private var craftTable = ComponentView() private var backdropDial = ComponentView() private var symbolDial = ComponentView() private var variantsButton = ComponentView() private var variantsButtonMeasure = ComponentView() - private var craftTable = ComponentView() - private var selectedGifts: [AnyHashable: ComponentView] = [:] + private let craftingTitle = ComponentView() + private let craftingSubtitle = ComponentView() + private let craftingDescription = ComponentView() + private let craftingProbability = ComponentView() + private var craftingProbabilityMeasure = ComponentView() + + private let failureTitle = ComponentView() + private let failureDescription = ComponentView() + private var failedGifts: [AnyHashable: ComponentView] = [:] private let infoContainer = UIView() private var infoBackground = SimpleLayer() @@ -158,22 +166,17 @@ private final class CraftGiftPageContent: Component { private let infoTitle = ComponentView() private let infoDescription = ComponentView() private var infoList = ComponentView() - - private var actionButton = ComponentView() - + private var craftState: CraftGiftsContext.State? private var craftStateDisposable: Disposable? private let upgradePreviewDisposable = DisposableSet() private var upgradePreview: [StarGift.UniqueGift.Attribute]? private var starGiftsMap: [Int64: StarGift.Gift] = [:] - - private let starsTopUpOptionsPromise = Promise<[StarsTopUpOption]?>(nil) - + private var availableGifts: [GiftItem] = [] private var giftMap: [Int64: GiftItem] = [:] private var isCrafting = false - private var isFailing = false private var component: CraftGiftPageContent? private weak var state: EmptyComponentState? @@ -236,7 +239,7 @@ private final class CraftGiftPageContent: Component { let variantsController = component.context.sharedContext.makeGiftUpgradeVariantsScreen( context: component.context, gift: .generic(gift), - onlyCrafted: true, + crafted: true, attributes: attributes, selectedAttributes: nil, focusedAttribute: nil @@ -260,7 +263,7 @@ private final class CraftGiftPageContent: Component { initialGiftItem ] self.giftMap = [initialGiftItem.gift.id: initialGiftItem] - component.externalState.giftMap = self.giftMap + component.externalState.giftsMap = self.giftMap self.craftStateDisposable = (component.craftContext.state |> deliverOnMainQueue).start(next: { [weak self] state in @@ -296,7 +299,7 @@ private final class CraftGiftPageContent: Component { } self.availableGifts = items self.giftMap = map - self.component?.externalState.giftMap = self.giftMap + self.component?.externalState.giftsMap = self.giftMap self.state?.updated(transition: .spring(duration: 0.4)) }) @@ -322,7 +325,7 @@ private final class CraftGiftPageContent: Component { self.upgradePreviewDisposable.add((.single(nil) |> then(component.context.engine.payments.cachedStarGifts()) |> deliverOnMainQueue).start(next: { [weak self] starGifts in - guard let self, let starGifts else { + guard let self, let component = self.component, let starGifts else { return } var starGiftsMap: [Int64: StarGift.Gift] = [:] @@ -332,9 +335,9 @@ private final class CraftGiftPageContent: Component { } } self.starGiftsMap = starGiftsMap + component.externalState.starGiftsMap = starGiftsMap + self.state?.updated() })) - - self.starsTopUpOptionsPromise.set(component.context.engine.payments.starsTopUpOptions() |> map(Optional.init)) } transition.setGradientColors(layer: self.background, colors: [component.colors.0, component.colors.1]) @@ -348,6 +351,8 @@ private final class CraftGiftPageContent: Component { self.state = state self.environment = environment + let isCrafting = [.crafting, .failure].contains(component.displayState) + var selectedGifts: [Int32: GiftItem] = [:] for (index, giftId) in component.selectedGiftIds { if let gift = self.giftMap[giftId] { @@ -388,14 +393,14 @@ private final class CraftGiftPageContent: Component { avatarScale: 1.0, defaultHeight: 300.0, gradientOnTop: true, - avatarTransitionFraction: self.isFailing ? 1.0 : 0.0, + avatarTransitionFraction: 0.0, patternTransitionFraction: 0.0, patternIconScale: 1.5 )), environment: {}, containerSize: CGSize(width: availableSize.width, height: 169.0 * 2.0) ) - let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: component.isCrafting ? floor((component.screenSize.height - backgroundSize.height) / 2.0) : 0.0), size: backgroundSize) + let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: isCrafting && !"".isEmpty ? floor((component.screenSize.height - backgroundSize.height) / 2.0) : 0.0), size: backgroundSize) if let backgroundView = self.pattern.view { if backgroundView.layer.superlayer == nil { backgroundTransition = .immediate @@ -409,7 +414,7 @@ private final class CraftGiftPageContent: Component { let titleSize = self.title.update( transition: transition, component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "Craft Gift", font: Font.semibold(17.0), textColor: .white))) + MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_Title, font: Font.semibold(17.0), textColor: .white))) ), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) @@ -420,18 +425,15 @@ private final class CraftGiftPageContent: Component { self.addSubview(titleView) } transition.setFrame(view: titleView, frame: titleFrame) - transition.setAlpha(view: titleView, alpha: component.isCrafting ? 0.0 : 1.0) - transition.setBlur(layer: titleView.layer, radius: component.isCrafting ? 10.0 : 0.0) + transition.setAlpha(view: titleView, alpha: 1.0) } let giftTitle = "\(component.gift.title) #\(formatCollectibleNumber(component.gift.number, dateTimeFormat: environment.dateTimeFormat))" - //TODO:localize - let descriptionFont = Font.regular(13.0) let descriptionBoldFont = Font.semibold(13.0) let descriptionColor = UIColor.white - let rawDescriptionString = "Add up to **4 gifts** to craft new\n**$ \(giftTitle)**.\n\nIf crafting fails, all selected gifts\nwill be consumed." + let rawDescriptionString = environment.strings.Gift_Craft_Description(giftTitle).string let descriptionString = parseMarkdownIntoAttributedString(rawDescriptionString, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), bold: MarkdownAttributeSet(font: descriptionBoldFont, textColor: descriptionColor), link: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), linkAttribute: { _ in return nil })).mutableCopy() as! NSMutableAttributedString if let gift = self.starGiftsMap[component.gift.giftId] { @@ -565,7 +567,9 @@ private final class CraftGiftPageContent: Component { diameter: 48.0, lineWidth: 4.0, fontSize: 10.0, - percentage: backdropPermille / 10 + progress: CGFloat(backdropPermille / 10 / 100), + value: backdropPermille / 10, + suffix: "%" ) ), action: { [weak self] in @@ -575,7 +579,7 @@ private final class CraftGiftPageContent: Component { #if DEBUG self.component?.externalState.testFailOrSuccess = true #endif - self.showAttributeInfo(tag: backdropButtonTag, text: "There's **\(backdropPermille / 10)%** chance the crafted gift will have **\(backdropName)** backdrop.") + self.showAttributeInfo(tag: backdropButtonTag, text: environment.strings.Gift_Craft_BackdropTooltip("\(backdropPermille / 10)", backdropName).string) }, tag: backdropButtonTag ) @@ -609,9 +613,12 @@ private final class CraftGiftPageContent: Component { backgroundColor: .white.withAlphaComponent(0.1), foregroundColor: .white, diameter: 48.0, + contentSize: CGSize(width: 28.0, height: 28.0), lineWidth: 4.0, fontSize: 10.0, - percentage: symbolPermille / 10 + progress: CGFloat(symbolPermille / 10 / 100), + value: symbolPermille / 10, + suffix: "%" ) ), action: { [weak self] in @@ -621,7 +628,7 @@ private final class CraftGiftPageContent: Component { #if DEBUG self.component?.externalState.testFailOrSuccess = false #endif - self.showAttributeInfo(tag: symbolButtonTag, text: "There's **\(symbolPermille / 10)%** chance the crafted gift will have **\(symbolName)** backdrop.") + self.showAttributeInfo(tag: symbolButtonTag, text: environment.strings.Gift_Craft_SymbolTooltip("\(symbolPermille / 10)", symbolName).string) }, tag: symbolButtonTag ) @@ -632,7 +639,7 @@ private final class CraftGiftPageContent: Component { craftContentHeight += backdropDialSize.height craftContentHeight += 15.0 - let variantsString = "View all new variants" + let variantsString = environment.strings.Gift_Craft_ViewVariants let variantsButtonMeasure = self.variantsButtonMeasure.update( transition: .immediate, component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: variantsString, font: Font.semibold(13.0), textColor: .clear)))), @@ -726,7 +733,7 @@ private final class CraftGiftPageContent: Component { craftContentHeight += 160.0 let originalCraftContentHeight = craftContentHeight - if component.isCrafting { + if !"".isEmpty, isCrafting { craftContentHeight = component.screenSize.height } @@ -736,8 +743,8 @@ private final class CraftGiftPageContent: Component { self.addSubview(descriptionTextView) } transition.setFrame(view: descriptionTextView, frame: descriptionTextFrame) - transition.setAlpha(view: descriptionTextView, alpha: component.isCrafting ? 0.0 : 1.0) - transition.setBlur(layer: descriptionTextView.layer, radius: component.isCrafting ? 10.0 : 0.0) + transition.setAlpha(view: descriptionTextView, alpha: isCrafting ? 0.0 : 1.0) + transition.setBlur(layer: descriptionTextView.layer, radius: isCrafting ? 10.0 : 0.0) } let backdropDialFrame = CGRect(origin: CGPoint(x: availableSize.width * 0.5 - 9.0 - backdropDialSize.width, y: craftContentHeight - 145.0 - 78.0), size: backdropDialSize) @@ -746,8 +753,8 @@ private final class CraftGiftPageContent: Component { self.addSubview(backdropDialView) } transition.setFrame(view: backdropDialView, frame: backdropDialFrame) - transition.setAlpha(view: backdropDialView, alpha: component.isCrafting ? 0.0 : 1.0) - transition.setBlur(layer: backdropDialView.layer, radius: component.isCrafting ? 10.0 : 0.0) + transition.setAlpha(view: backdropDialView, alpha: isCrafting ? 0.0 : 1.0) + transition.setBlur(layer: backdropDialView.layer, radius: isCrafting ? 10.0 : 0.0) } let symbolDialFrame = CGRect(origin: CGPoint(x: availableSize.width * 0.5 + 9.0, y: craftContentHeight - 145.0 - 78.0), size: symbolDialSize) @@ -756,14 +763,14 @@ private final class CraftGiftPageContent: Component { self.addSubview(symbolDialView) } transition.setFrame(view: symbolDialView, frame: symbolDialFrame) - transition.setAlpha(view: symbolDialView, alpha: component.isCrafting ? 0.0 : 1.0) - transition.setBlur(layer: symbolDialView.layer, radius: component.isCrafting ? 10.0 : 0.0) + transition.setAlpha(view: symbolDialView, alpha: isCrafting ? 0.0 : 1.0) + transition.setBlur(layer: symbolDialView.layer, radius: isCrafting ? 10.0 : 0.0) } let variantsButtonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - variantsButtonSize.width) / 2.0), y: craftContentHeight - 145.0), size: variantsButtonSize) var varitantsButtonTransition = transition if let variantsButtonView = self.variantsButton.view { - if variantsButtonView.superview == nil && !component.isCrafting { + if variantsButtonView.superview == nil && component.displayState == .default { varitantsButtonTransition = .immediate if let symbolDialView = self.symbolDial.view { self.insertSubview(variantsButtonView, aboveSubview: symbolDialView) @@ -772,43 +779,43 @@ private final class CraftGiftPageContent: Component { } } varitantsButtonTransition.setFrame(view: variantsButtonView, frame: variantsButtonFrame) - varitantsButtonTransition.setBlur(layer: variantsButtonView.layer, radius: component.isCrafting ? 10.0 : 0.0) - if component.isCrafting { - variantsButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.41, removeOnCompletion: false, completion: { _ in + varitantsButtonTransition.setBlur(layer: variantsButtonView.layer, radius: isCrafting ? 10.0 : 0.0) + if component.displayState == .crafting { + variantsButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in variantsButtonView.removeFromSuperview() }) } } - let permilleValue = selectedGifts.reduce(0, { $0 + Int($1.value.gift.craftChancePermille ?? 0) }) - if component.isCrafting { - var craftingOriginY = craftContentHeight * 0.5 + 160.0 + if component.displayState == .crafting { + //var craftingOriginY = craftContentHeight * 0.5 + 160.0 + var craftingOriginY = craftContentHeight * 0.5 - 16.0 let offset = -(craftContentHeight - originalCraftContentHeight) - let titleSize = self.craftingTitle.update( + let craftingTitleSize = self.craftingTitle.update( transition: transition, component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "Crafting", font: Font.bold(20.0), textColor: .white))) + MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_Crafting_Title, font: Font.bold(20.0), textColor: .white))) ), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) ) - let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) * 0.5), y: craftingOriginY), size: titleSize) - if let titleView = self.craftingTitle.view { - if titleView.superview == nil { - transition.animateAlpha(view: titleView, from: 0.0, to: 1.0) - transition.animateBlur(layer: titleView.layer, fromRadius: 10.0, toRadius: 0.0) - transition.animatePosition(view: titleView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true) + let craftingTitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - craftingTitleSize.width) * 0.5), y: craftingOriginY), size: craftingTitleSize) + if let craftingTitleView = self.craftingTitle.view { + if craftingTitleView.superview == nil { + transition.animateAlpha(view: craftingTitleView, from: 0.0, to: 1.0) + transition.animateBlur(layer: craftingTitleView.layer, fromRadius: 10.0, toRadius: 0.0) + transition.animatePosition(view: craftingTitleView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true) - self.addSubview(titleView) + self.addSubview(craftingTitleView) } - titleView.frame = titleFrame + craftingTitleView.frame = craftingTitleFrame } - craftingOriginY += titleSize.height - craftingOriginY += 7.0 + craftingOriginY += craftingTitleSize.height + craftingOriginY += 6.0 - let subtitleSize = self.craftingSubtitle.update( + let craftingSubtitleSize = self.craftingSubtitle.update( transition: transition, component: AnyComponent( MultilineTextComponent(text: .plain(NSAttributedString(string: giftTitle, font: Font.semibold(13.0), textColor: .white.withAlphaComponent(0.5)))) @@ -816,34 +823,30 @@ private final class CraftGiftPageContent: Component { environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) ) - let subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - subtitleSize.width) * 0.5), y: craftingOriginY), size: subtitleSize) - if let subtitleView = self.craftingSubtitle.view { - if subtitleView.superview == nil { - transition.animateAlpha(view: subtitleView, from: 0.0, to: 1.0) - transition.animateBlur(layer: subtitleView.layer, fromRadius: 10.0, toRadius: 0.0) - transition.animatePosition(view: subtitleView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true) + let craftingSubtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - craftingSubtitleSize.width) * 0.5), y: craftingOriginY), size: craftingSubtitleSize) + if let craftingSubtitleView = self.craftingSubtitle.view { + if craftingSubtitleView.superview == nil { + transition.animateAlpha(view: craftingSubtitleView, from: 0.0, to: 1.0) + transition.animateBlur(layer: craftingSubtitleView.layer, fromRadius: 10.0, toRadius: 0.0) + transition.animatePosition(view: craftingSubtitleView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true) - self.addSubview(subtitleView) + self.addSubview(craftingSubtitleView) } - subtitleView.frame = subtitleFrame + craftingSubtitleView.frame = craftingSubtitleFrame } - craftingOriginY += subtitleSize.height + craftingOriginY += craftingSubtitleSize.height craftingOriginY += 21.0 let descriptionFont = Font.regular(13.0) let descriptionBoldFont = Font.semibold(13.0) let descriptionColor = UIColor.white.withAlphaComponent(0.5) - let rawDescriptionString = "If crafting fails, all selected gifts\nwill be consumed." + let rawDescriptionString = environment.strings.Gift_Craft_Crafting_Description let descriptionString = parseMarkdownIntoAttributedString(rawDescriptionString, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), bold: MarkdownAttributeSet(font: descriptionBoldFont, textColor: descriptionColor), link: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), linkAttribute: { _ in return nil })).mutableCopy() as! NSMutableAttributedString let craftingDescriptionSize = self.craftingDescription.update( transition: transition, component: AnyComponent( - MultilineTextWithEntitiesComponent( - context: component.context, - animationCache: component.context.animationCache, - animationRenderer: component.context.animationRenderer, - placeholderColor: .white.withAlphaComponent(0.3), + MultilineTextComponent( text: .plain(descriptionString), horizontalAlignment: .center, maximumNumberOfLines: 0, @@ -867,7 +870,7 @@ private final class CraftGiftPageContent: Component { craftingOriginY += craftingDescriptionSize.height craftingOriginY += 24.0 - let craftingProbabilityString = "\(permilleValue / 10)% Success Chance" + let craftingProbabilityString = environment.strings.Gift_Craft_Crafting_SuccessChance("\(permilleValue / 10)").string let craftingProbabilityMeasure = self.craftingProbabilityMeasure.update( transition: .immediate, component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: craftingProbabilityString, font: Font.semibold(13.0), textColor: .clear)))), @@ -904,6 +907,165 @@ private final class CraftGiftPageContent: Component { } craftingProbabilityView.frame = craftingProbabilityFrame } + } else { + if let craftingTitleView = self.craftingTitle.view { + transition.setAlpha(view: craftingTitleView, alpha: 0.0, completion: { _ in + craftingTitleView.removeFromSuperview() + }) + transition.animateBlur(layer: craftingTitleView.layer, fromRadius: 0.0, toRadius: 10.0) + } + if let craftingSubtitleView = self.craftingSubtitle.view { + transition.setAlpha(view: craftingSubtitleView, alpha: 0.0, completion: { _ in + craftingSubtitleView.removeFromSuperview() + }) + transition.animateBlur(layer: craftingSubtitleView.layer, fromRadius: 0.0, toRadius: 10.0) + } + if let craftingDescriptionView = self.craftingDescription.view { + transition.setAlpha(view: craftingDescriptionView, alpha: 0.0, completion: { _ in + craftingDescriptionView.removeFromSuperview() + }) + transition.animateBlur(layer: craftingDescriptionView.layer, fromRadius: 0.0, toRadius: 10.0) + } + if let craftingProbabilityView = self.craftingProbability.view { + craftingProbabilityView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + craftingProbabilityView.removeFromSuperview() + }) + transition.animateBlur(layer: craftingProbabilityView.layer, fromRadius: 0.0, toRadius: 10.0) + } + } + + if component.displayState == .failure { + var failureOriginY = craftContentHeight * 0.5 - 16.0 + let offset = -(craftContentHeight - originalCraftContentHeight) + + let failureTitleSize = self.failureTitle.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_CraftingFailed_Title, font: Font.bold(20.0), textColor: UIColor(rgb: 0xff746d)))) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + let failureTitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - failureTitleSize.width) * 0.5), y: failureOriginY), size: failureTitleSize) + if let failureTitleView = self.failureTitle.view { + if failureTitleView.superview == nil { + transition.animateAlpha(view: failureTitleView, from: 0.0, to: 1.0) + transition.animateBlur(layer: failureTitleView.layer, fromRadius: 10.0, toRadius: 0.0) + transition.animatePosition(view: failureTitleView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true) + + self.addSubview(failureTitleView) + } + failureTitleView.frame = failureTitleFrame + } + failureOriginY += failureTitleSize.height + failureOriginY += 17.0 + + let descriptionFont = Font.regular(13.0) + let descriptionBoldFont = Font.semibold(13.0) + let descriptionColor = UIColor(rgb: 0xf7af8c) + let rawDescriptionString = environment.strings.Gift_Craft_CraftingFailed_Text(Int32(component.selectedGiftIds.count)) + let descriptionString = parseMarkdownIntoAttributedString(rawDescriptionString, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), bold: MarkdownAttributeSet(font: descriptionBoldFont, textColor: descriptionColor), link: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), linkAttribute: { _ in return nil })).mutableCopy() as! NSMutableAttributedString + + let failureDescriptionSize = self.failureDescription.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent( + text: .plain(descriptionString), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + let failureDescriptionFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - failureDescriptionSize.width) * 0.5), y: failureOriginY), size: failureDescriptionSize) + if let failureDescriptionView = self.failureDescription.view { + if failureDescriptionView.superview == nil { + transition.animateAlpha(view: failureDescriptionView, from: 0.0, to: 1.0) + transition.animateBlur(layer: failureDescriptionView.layer, fromRadius: 10.0, toRadius: 0.0) + transition.animatePosition(view: failureDescriptionView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true) + + self.addSubview(failureDescriptionView) + } + failureDescriptionView.frame = failureDescriptionFrame + } + failureOriginY += failureDescriptionSize.height + failureOriginY += 34.0 + + var indices: [Int] = [] + for index in component.selectedGiftIds.keys.sorted() { + indices.append(Int(index)) + } + var lostGifts: [GiftItem] = [] + for index in indices { + if let giftId = component.selectedGiftIds[Int32(index)], let gift = self.giftMap[giftId] { + lostGifts.append(gift) + } + } + + let itemSize = CGSize(width: 80.0, height: 80.0) + let itemSpacing: CGFloat = 16.0 + var itemDelay: Double = 0.2 + + let totalItemsWidth: CGFloat = itemSize.width * CGFloat(lostGifts.count) + itemSpacing * CGFloat(lostGifts.count - 1) + var itemOriginX: CGFloat = floor((availableSize.width - totalItemsWidth) / 2.0) + + for gift in lostGifts { + let itemId = AnyHashable(gift.gift.id) + var itemTransition = transition + let visibleItem: ComponentView + if let current = self.failedGifts[itemId] { + visibleItem = current + } else { + visibleItem = ComponentView() + self.failedGifts[itemId] = visibleItem + itemTransition = .immediate + } + + let ribbonText = "#\(gift.gift.number)" + let ribbonColor: GiftItemComponent.Ribbon.Color = .custom(0xff645b, 0xff645b) + + let _ = visibleItem.update( + transition: itemTransition, + component: AnyComponent( + GiftItemComponent( + context: component.context, + style: .glass, + theme: environment.theme, + strings: environment.strings, + peer: nil, + subject: .uniqueGift(gift: gift.gift, price: nil), + ribbon: GiftItemComponent.Ribbon(text: ribbonText, font: .monospaced, color: ribbonColor, outline: nil), + badge: nil, + resellPrice: nil, + isHidden: false, + isSelected: false, + isPinned: false, + isEditing: false, + mode: .grid, + action: nil, + contextAction: nil + ) + ), + environment: {}, + containerSize: itemSize + ) + let itemFrame = CGRect(origin: CGPoint(x: itemOriginX, y: failureOriginY), size: itemSize) + if let itemView = visibleItem.view { + if itemView.superview == nil { + self.addSubview(itemView) + + if !transition.animation.isImmediate { + itemView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25, delay: itemDelay) + itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: itemDelay) + } + } + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + itemOriginX += itemSize.width + itemSpacing + itemDelay += 0.07 + } } let tableSize = CGSize(width: availableSize.width, height: 320.0) @@ -914,7 +1076,7 @@ private final class CraftGiftPageContent: Component { context: component.context, gifts: selectedGifts, buttonColor: component.colors.3, - isCrafting: component.isCrafting, + isCrafting: isCrafting, result: component.result, select: { [weak self] index in guard let self, let component = self.component, let environment = self.environment, let genericGift = self.starGiftsMap[component.gift.giftId], let resaleContext = component.resaleContext() else { @@ -927,7 +1089,7 @@ private final class CraftGiftPageContent: Component { gift: component.gift, genericGift: genericGift, selectedGiftIds: Set(component.selectedGiftIds.values), - starsTopUpOptions: self.starsTopUpOptionsPromise.get(), + starsTopUpOptions: component.starsTopUpOptionsPromise.get(), selectGift: { [weak self] item in guard let self, let component = self.component else { return @@ -947,11 +1109,11 @@ private final class CraftGiftPageContent: Component { self.component?.removeGift(index) }, willFinish: { [weak self] success in - guard let self else { + guard let self, let component = self.component else { return } if !success { - self.isFailing = true + component.externalState.displayFailure = true } self.state?.updated(transition: .easeInOut(duration: 0.5)) }, @@ -972,13 +1134,7 @@ private final class CraftGiftPageContent: Component { controller.dismiss() }) } else { - if let navigationController = controller.navigationController { - controller.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { _ in - controller.dismiss() - }) - let previousController = navigationController.viewControllers[max(0, navigationController.viewControllers.count - 2)] - animateRipple(parentView: previousController.view, screenCornerRadius: environment.deviceMetrics.screenCornerRadius, location: CGPoint(x: previousController.view.bounds.midX, y: previousController.view.bounds.midY)) - } + } } ) @@ -986,7 +1142,7 @@ private final class CraftGiftPageContent: Component { environment: {}, containerSize: CGSize(width: availableSize.width, height: tableSize.height) ) - let craftTableFrame = CGRect(origin: CGPoint(x: 0.0, y: component.isCrafting ? floor((component.screenSize.height - craftTableSize.height) / 2.0) : 10.0), size: craftTableSize) + let craftTableFrame = CGRect(origin: CGPoint(x: 0.0, y: isCrafting && !"".isEmpty ? floor((component.screenSize.height - craftTableSize.height) / 2.0) : 10.0), size: craftTableSize) if let craftTableView = self.craftTable.view { if craftTableView.superview == nil { craftTableView.layer.cornerRadius = 40.0 @@ -997,7 +1153,7 @@ private final class CraftGiftPageContent: Component { transition.setFrame(view: craftTableView, frame: craftTableFrame) } - transition.setAlpha(view: self.infoContainer, alpha: component.displayCraftInfo ? 1.0 : 0.0) + transition.setAlpha(view: self.infoContainer, alpha: component.displayInfo ? 1.0 : 0.0) let infoHeaderSize = self.infoHeader.update( transition: transition, @@ -1016,7 +1172,7 @@ private final class CraftGiftPageContent: Component { ) ), environment: {}, - containerSize: CGSize(width: availableSize.width, height: 245.0) + containerSize: CGSize(width: availableSize.width, height: 260.0) ) let infoHeaderFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - infoHeaderSize.width) * 0.5), y: 0.0), size: infoHeaderSize) if let infoHeaderView = self.infoHeader.view { @@ -1033,6 +1189,10 @@ private final class CraftGiftPageContent: Component { self.infoContainer.addSubview(infoHeaderView) } transition.setFrame(view: infoHeaderView, frame: infoHeaderFrame) + + if self.subviews.last !== self.infoContainer { + self.bringSubviewToFront(self.infoContainer) + } } infoContentHeight += infoHeaderSize.height infoContentHeight += 16.0 @@ -1040,12 +1200,12 @@ private final class CraftGiftPageContent: Component { let infoTitleSize = self.infoTitle.update( transition: transition, component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "Craft Gift", font: Font.bold(20.0), textColor: .white))) + MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_Info_Title, font: Font.bold(20.0), textColor: .white))) ), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) ) - let infoTitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - infoTitleSize.width) * 0.5), y: infoHeaderSize.height - 73.0), size: infoTitleSize) + let infoTitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - infoTitleSize.width) * 0.5), y: infoHeaderSize.height - 87.0), size: infoTitleSize) if let infoTitleView = self.infoTitle.view { if infoTitleView.superview == nil { self.infoContainer.addSubview(infoTitleView) @@ -1057,7 +1217,7 @@ private final class CraftGiftPageContent: Component { transition: transition, component: AnyComponent(MultilineTextComponent( text: .markdown( - text: "Use your existing gifts to craft new ones.", + text: environment.strings.Gift_Craft_Info_Description, attributes: MarkdownAttributes( body: MarkdownAttributeSet(font: Font.regular(14.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.6)), bold: MarkdownAttributeSet(font: Font.semibold(14.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.6)), @@ -1072,7 +1232,7 @@ private final class CraftGiftPageContent: Component { environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) ) - let infoDescriptionTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - infoDescriptionTextSize.width) * 0.5), y: infoHeaderSize.height - 40.0), size: infoDescriptionTextSize) + let infoDescriptionTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - infoDescriptionTextSize.width) * 0.5), y: infoHeaderSize.height - 56.0), size: infoDescriptionTextSize) if let infoDescriptionTextView = self.infoDescription.view { if infoDescriptionTextView.superview == nil { self.infoContainer.addSubview(infoDescriptionTextView) @@ -1092,42 +1252,42 @@ private final class CraftGiftPageContent: Component { var items: [AnyComponentWithIdentity] = [] items.append( AnyComponentWithIdentity( - id: "combine", + id: "paragraph1", component: AnyComponent(InfoParagraphComponent( - title: "Combine Gifts", + title: environment.strings.Gift_Craft_Info_Paragraph1_Title, titleColor: titleColor, - text: "Add up to 3 Gifts to attempt crafting a new upgraded model.", + text: environment.strings.Gift_Craft_Info_Paragraph1_Text, textColor: textColor, accentColor: accentColor, - iconName: "Premium/Collectible/Badge", + iconName: "Premium/Craft/Rare", iconColor: environment.theme.list.itemAccentColor )) ) ) items.append( AnyComponentWithIdentity( - id: "input", + id: "paragraph2", component: AnyComponent(InfoParagraphComponent( - title: "Input Matters", + title: environment.strings.Gift_Craft_Info_Paragraph2_Title, titleColor: titleColor, - text: "Each craft has a success chance. Better combinations improve the outcome.", + text: environment.strings.Gift_Craft_Info_Paragraph2_Text, textColor: textColor, accentColor: accentColor, - iconName: "Premium/Collectible/Transferable", + iconName: "Premium/Craft/Chance", iconColor: accentColor )) ) ) items.append( AnyComponentWithIdentity( - id: "exclusive", + id: "paragraph3", component: AnyComponent(InfoParagraphComponent( - title: "Exclusive Look", + title: environment.strings.Gift_Craft_Info_Paragraph3_Title, titleColor: titleColor, - text: "Reforge gifts into a rarer collectibles with a new look", + text: environment.strings.Gift_Craft_Info_Paragraph3_Text, textColor: textColor, accentColor: accentColor, - iconName: "Premium/Collectible/Unique", + iconName: "Premium/Craft/Result", iconColor: accentColor )) ) @@ -1149,17 +1309,17 @@ private final class CraftGiftPageContent: Component { transition.setFrame(view: infoListView, frame: infoListFrame) } - if component.displayCraftInfo { + if component.displayInfo { infoContentHeight += infoListSize.height infoContentHeight += 95.0 } transition.setFrame(view: self.infoContainer, frame: CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: infoContentHeight))) transition.setFrame(layer: self.background, frame: CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: craftContentHeight))) - transition.setFrame(layer: self.overlay, frame: CGRect(origin: CGPoint(x: 0.0, y: component.isCrafting ? floor((component.screenSize.height - availableSize.width) / 2.0) : 169.0 - availableSize.width * 0.5), size: CGSize(width: availableSize.width, height: availableSize.width))) + transition.setFrame(layer: self.overlay, frame: CGRect(origin: CGPoint(x: 0.0, y: isCrafting && !"".isEmpty ? floor((component.screenSize.height - availableSize.width) / 2.0) : 169.0 - availableSize.width * 0.5), size: CGSize(width: availableSize.width, height: availableSize.width))) let effectiveContentHeight: CGFloat - if component.displayCraftInfo { + if component.displayInfo { effectiveContentHeight = infoContentHeight } else { effectiveContentHeight = craftContentHeight @@ -1209,11 +1369,14 @@ private final class SheetContainerComponent: CombinedComponent { private let context: AccountContext private let giftId: Int64 - var displayCraftInfo = false + var displayInfo = false var isCrafting = false var inProgress = false + var displayFailure = false var result: CraftTableComponent.Result? var selectedGiftIds: [Int32: Int64] = [:] + + let starsTopUpOptionsPromise = Promise<[StarsTopUpOption]?>(nil) private var _resaleContext: ResaleGiftsContext? var resaleContext: ResaleGiftsContext { @@ -1241,12 +1404,14 @@ private final class SheetContainerComponent: CombinedComponent { return } if count < 1 { - self.displayCraftInfo = true + self.displayInfo = true self.updated() let _ = ApplicationSpecificNotice.incrementGiftCraftingTips(accountManager: context.sharedContext.accountManager).start() } }) + + self.starsTopUpOptionsPromise.set(context.engine.payments.starsTopUpOptions() |> map(Optional.init)) } deinit { @@ -1263,12 +1428,20 @@ private final class SheetContainerComponent: CombinedComponent { let animateOut = StoredActionSlot(Action.self) let externalState = CraftGiftPageContent.ExternalState() + let playButtonAnimation = ActionSlot() return { context in let component = context.component let environment = context.environment[EnvironmentType.self] let state = context.state + let strings = environment.strings + + if externalState.displayFailure { + state.displayFailure = true + state.inProgress = false + } + let controller = environment.controller let craftContext = context.component.craftContext @@ -1290,11 +1463,15 @@ private final class SheetContainerComponent: CombinedComponent { let theme = environment.theme var colors: (UIColor, UIColor, UIColor, UIColor, UIColor) = ( - UIColor(rgb: 0x263245), UIColor(rgb: 0x232e3f), UIColor(rgb: 0x304059), UIColor(rgb: 0x425168), theme.list.itemCheckColors.fillColor + UIColor(rgb: 0x263245), + UIColor(rgb: 0x232e3f), + UIColor(rgb: 0x304059), + UIColor(rgb: 0x425168), + theme.list.itemCheckColors.fillColor ) var permilleValue: Int32 = 0 for id in state.selectedGiftIds.values { - if let gift = externalState.giftMap[id] { + if let gift = externalState.giftsMap[id] { permilleValue += gift.gift.craftChancePermille ?? 0 } } @@ -1303,38 +1480,66 @@ private final class SheetContainerComponent: CombinedComponent { colors.1 = UIColor(rgb: 0x1a2f38) colors.2 = UIColor(rgb: 0x22464a) colors.3 = UIColor(rgb: 0x2d4e50) - if !state.displayCraftInfo { + if !state.displayInfo { colors.4 = UIColor(rgb: 0x33bf54) } } + if state.displayFailure { + colors.0 = UIColor(rgb: 0x46231a) + colors.1 = UIColor(rgb: 0x381b1a) + colors.2 = UIColor(rgb: 0x51291f) + colors.3 = UIColor(rgb: 0x683e34) + if !state.displayInfo { + colors.4 = UIColor(rgb: 0x683e34) + } + } var buttonColor = colors.3 - if state.displayCraftInfo, let backdropAttribute = component.gift.attributes.first(where: { attribute in + if state.displayInfo, let backdropAttribute = component.gift.attributes.first(where: { attribute in if case .backdrop = attribute { return true } else { return false } - }), case let .backdrop(_, _, innerColor, _, _, _, _) = backdropAttribute { - buttonColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultipliedBrightnessBy(1.05) + }), case let .backdrop(_, _, _, outerColor, _, _, _) = backdropAttribute { + buttonColor = UIColor(rgb: UInt32(bitPattern: outerColor)).mixedWith(.white, alpha: 0.2) } var backgroundColor = colors.1 - if state.displayCraftInfo { + if state.displayInfo { backgroundColor = environment.theme.list.plainBackgroundColor } let giftTitle = "\(component.gift.title) #\(formatCollectibleNumber(component.gift.number, dateTimeFormat: environment.dateTimeFormat))" let buttonContent: AnyComponentWithIdentity - if state.displayCraftInfo { + if state.displayInfo { + var buttonTitle: [AnyComponentWithIdentity] = [] + buttonTitle.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "anim_ok"), + color: environment.theme.list.itemCheckColors.foregroundColor, + startingPosition: .begin, + size: CGSize(width: 28.0, height: 28.0), + playOnce: playButtonAnimation + )))) + buttonTitle.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(ButtonTextContentComponent( + text: strings.Gift_Craft_Info_Understood, + badge: 0, + textColor: environment.theme.list.itemCheckColors.foregroundColor, + badgeBackground: environment.theme.list.itemCheckColors.foregroundColor, + badgeForeground: environment.theme.list.itemCheckColors.fillColor + )))) buttonContent = AnyComponentWithIdentity(id: "info", component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "Select Gifts", font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor))) + HStack(buttonTitle, spacing: 2.0) + )) + } else if state.displayFailure { + buttonContent = AnyComponentWithIdentity(id: "fail", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_CraftingFailed_CraftAnotherGift, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor))) )) } else { var buttonAnimatedItems: [AnimatedTextComponent.Item] = [] buttonAnimatedItems.append(AnimatedTextComponent.Item(id: "percent", content: .number(Int(permilleValue / 10), minDigits: 1))) - buttonAnimatedItems.append(AnimatedTextComponent.Item(id: "suffix", content: .text("% Success Chance"))) + buttonAnimatedItems.append(AnimatedTextComponent.Item(id: "suffix", content: .text(environment.strings.Gift_Craft_SuccessChanceSuffix))) buttonContent = AnyComponentWithIdentity(id: "craft", component: AnyComponent( VStack([ @@ -1344,7 +1549,7 @@ private final class SheetContainerComponent: CombinedComponent { HStack([ AnyComponentWithIdentity( id: AnyHashable("label"), - component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Craft \(giftTitle)", font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor)))) + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_Craft(giftTitle).string, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor)))) ) ], spacing: 2.0) ) @@ -1364,6 +1569,15 @@ private final class SheetContainerComponent: CombinedComponent { )) } + var displayState: CraftGiftPageContent.DisplayState = .default + if state.displayFailure { + displayState = .failure + } else if state.isCrafting { + displayState = .crafting + } + + let hideButtons = displayState == .crafting + let sheet = sheet.update( component: ResizableSheetComponent( content: AnyComponent( @@ -1376,12 +1590,12 @@ private final class SheetContainerComponent: CombinedComponent { colors: colors, gift: component.gift, selectedGiftIds: state.selectedGiftIds, - displayCraftInfo: state.displayCraftInfo, - isCrafting: state.isCrafting, - inProgress: state.inProgress, + displayState: displayState, + displayInfo: state.displayInfo, result: state.result, screenSize: context.availableSize, externalState: externalState, + starsTopUpOptionsPromise: state.starsTopUpOptionsPromise, selectGift: { [weak state] index, gift in guard let state else { return @@ -1401,7 +1615,7 @@ private final class SheetContainerComponent: CombinedComponent { } ) ), - leftItem: state.isCrafting ? nil : AnyComponent( + leftItem: hideButtons ? nil : AnyComponent( GlassBarButtonComponent( size: CGSize(width: 44.0, height: 44.0), backgroundColor: buttonColor, @@ -1413,12 +1627,20 @@ private final class SheetContainerComponent: CombinedComponent { tintColor: .white ) )), - action: { _ in - dismiss(true) + action: { [weak state] _ in + guard let state else { + return + } + if state.displayInfo { + state.displayInfo = false + state.updated(transition: .spring(duration: 0.3)) + } else { + dismiss(true) + } } ) ), - rightItem: state.isCrafting || state.displayCraftInfo ? nil : AnyComponent( + rightItem: hideButtons || state.displayInfo ? nil : AnyComponent( GlassBarButtonComponent( size: CGSize(width: 44.0, height: 44.0), backgroundColor: buttonColor, @@ -1431,16 +1653,17 @@ private final class SheetContainerComponent: CombinedComponent { ) )), action: { [weak state] _ in - guard let state, !state.isCrafting else { + guard let state, !state.inProgress else { return } - state.displayCraftInfo = !state.displayCraftInfo + state.displayInfo = true state.updated(transition: .spring(duration: 0.3)) + playButtonAnimation.invoke(Void()) } ) ), hasTopEdgeEffect: false, - bottomItem: state.isCrafting ? nil : AnyComponent( + bottomItem: hideButtons ? nil : AnyComponent( ButtonComponent( background: ButtonComponent.Background( style: .glass, @@ -1449,15 +1672,33 @@ private final class SheetContainerComponent: CombinedComponent { pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) ), content: buttonContent, - isEnabled: state.displayCraftInfo ? true : state.selectedGiftIds.count > 0, + isEnabled: state.displayInfo ? true : state.selectedGiftIds.count > 0, displaysProgress: state.inProgress, action: { [weak state] in guard let state else { return } - if state.displayCraftInfo { - state.displayCraftInfo = false + if state.displayInfo { + state.displayInfo = false state.updated(transition: .spring(duration: 0.3)) + } else if state.displayFailure, let genericGift = externalState.starGiftsMap[component.gift.giftId] { + let selectController = SelectCraftGiftScreen( + context: component.context, + craftContext: component.craftContext, + resaleContext: state.resaleContext, + gift: component.gift, + genericGift: genericGift, + selectedGiftIds: Set(), + starsTopUpOptions: state.starsTopUpOptionsPromise.get(), + selectGift: { item in + let craftController = GiftCraftScreen(context: component.context, gift: item.gift) + if let controller = controller() as? GiftCraftScreen, let navigationController = controller.navigationController as? NavigationController { + controller.dismissAnimated() + navigationController.pushViewController(craftController) + } + } + ) + environment.controller()?.push(selectController) } else { state.inProgress = true state.updated(transition: .spring(duration: 0.3)) @@ -1481,7 +1722,7 @@ private final class SheetContainerComponent: CombinedComponent { } var references: [StarGiftReference] = [] for index in indices { - if let giftId = state.selectedGiftIds[Int32(index)], let gift = externalState.giftMap[giftId] { + if let giftId = state.selectedGiftIds[Int32(index)], let gift = externalState.giftsMap[giftId] { references.append(gift.reference) } } @@ -1512,10 +1753,14 @@ private final class SheetContainerComponent: CombinedComponent { state.isCrafting = true state.result = .fail state.updated(transition: .spring(duration: 0.8)) + + Queue.mainQueue().after(1.0) { + craftContext.reload() + } default: if let navigationController = controller()?.navigationController { dismiss(true) - let alertController = textAlertController(context: component.context, title: nil, text: "Unknown Error", actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]) + let alertController = textAlertController(context: component.context, title: nil, text: strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})]) (navigationController.topViewController as? ViewController)?.present(alertController, in: .window(.root)) } } @@ -1525,7 +1770,7 @@ private final class SheetContainerComponent: CombinedComponent { ) ), backgroundColor: .color(backgroundColor), - isFullscreen: state.isCrafting, + isFullscreen: false, //state.isCrafting, animateOut: animateOut ), environment: { diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/SelectCraftGiftScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/SelectCraftGiftScreen.swift index 435b1905f3..ed0b4dc0af 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/SelectCraftGiftScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/SelectCraftGiftScreen.swift @@ -21,6 +21,7 @@ import ResizableSheetComponent import TooltipUI import GlassBarButtonComponent import ConfettiEffect +import GiftLoadingShimmerView final class SelectGiftPageContent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -74,6 +75,7 @@ final class SelectGiftPageContent: Component { private let myGiftsTitle = ComponentView() private var gifts: [AnyHashable: ComponentView] = [:] private let myGiftsPlaceholder = ComponentView() + private let loadingView = GiftLoadingShimmerView() private let storeGiftsTitle = ComponentView() private let storeGifts = ComponentView() @@ -94,6 +96,8 @@ final class SelectGiftPageContent: Component { self.layer.cornerRadius = 40.0 self.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + + self.addSubview(self.loadingView) } required init?(coder: NSCoder) { @@ -204,10 +208,25 @@ final class SelectGiftPageContent: Component { } let itemWidth = (availableSize.width - itemSideInset * 2.0 - itemSpacing * CGFloat(itemsInRow - 1)) / CGFloat(itemsInRow) let itemSize = CGSize(width: itemWidth, height: itemWidth) - var itemFrame = CGRect(origin: CGPoint(x: itemSideInset, y: contentHeight), size: itemSize) + + var isLoading = false + if self.availableGifts.isEmpty, case .loading = (self.craftState?.dataState ?? .loading) { + isLoading = true + } + let loadingTransition: ComponentTransition = .easeInOut(duration: 0.25) + let loadingSize = CGSize(width: availableSize.width, height: 180.0) + if isLoading { + contentHeight += 120.0 + self.loadingView.update(size: loadingSize, theme: environment.theme, itemSize: itemSize, showFilters: false, isPlain: true, transition: .immediate) + loadingTransition.setAlpha(view: self.loadingView, alpha: 1.0) + } else { + loadingTransition.setAlpha(view: self.loadingView, alpha: 0.0) + } + transition.setFrame(view: self.loadingView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight - 170.0), size: loadingSize)) + + var itemFrame = CGRect(origin: CGPoint(x: itemSideInset, y: contentHeight), size: itemSize) var itemsHeight: CGFloat = 0.0 - var validIds: [AnyHashable] = [] for gift in self.availableGifts { let isVisible = "".isEmpty @@ -272,17 +291,15 @@ final class SelectGiftPageContent: Component { ) if let itemView = visibleItem.view { if itemView.superview == nil { - self.addSubview(itemView) - + if let _ = self.loadingView.superview { + self.insertSubview(itemView, belowSubview: self.loadingView) + } else { + self.addSubview(itemView) + } if !transition.animation.isImmediate { - let delay = ((itemFrame.minY - contentHeight) / itemSize.height) * 0.07 - itemView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25, delay: delay) - itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: delay) + itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } } - itemView.isUserInteractionEnabled = !component.selectedGiftIds.contains(gift.gift.id) - itemView.alpha = component.selectedGiftIds.contains(gift.gift.id) ? 0.4 : 1.0 - itemView.layer.allowsGroupOpacity = itemView.alpha < 1.0 itemTransition.setFrame(view: itemView, frame: itemFrame) } } @@ -315,7 +332,7 @@ final class SelectGiftPageContent: Component { for id in removeIds { self.gifts.removeValue(forKey: id) } - + if let state = self.craftState, case .ready = state.dataState, self.availableGifts.isEmpty { contentHeight += 10.0 let myGiftsPlaceholderSize = self.myGiftsPlaceholder.update( @@ -379,6 +396,7 @@ final class SelectGiftPageContent: Component { starsContext: component.context.starsContext!, peerId: component.context.account.peerId, gift: component.genericGift, + isPlain: true, confirmPurchaseImmediately: true, starsTopUpOptions: component.starsTopUpOptions, scrollToTop: {}, @@ -530,9 +548,9 @@ private final class SheetContainerComponent: CombinedComponent { backgroundColor: nil, isDark: theme.overallDarkAppearance, state: .glass, - component: AnyComponentWithIdentity(id: "back", component: AnyComponent( + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( - name: "Navigation/Back", + name: "Navigation/Close", tintColor: theme.chat.inputPanel.panelControlColor ) )), diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index 2a7d190d3f..07ede21c03 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -792,9 +792,7 @@ public final class GiftItemComponent: Component { func formatPercentage(_ value: Float) -> String { return String(format: "%0.1f", value).replacingOccurrences(of: ".0", with: "").replacingOccurrences(of: ",0", with: "") + "%" } - - //TODO:localize - + let badgeString: String var badgeColor: UIColor? switch rarity { @@ -805,21 +803,28 @@ public final class GiftItemComponent: Component { badgeString = formatPercentage(Float(value) * 0.1) } case .epic: - badgeString = "epic" + badgeString = component.strings.Gift_Attribute_Epic badgeColor = UIColor(rgb: 0xaf52de) case .legendary: - badgeString = "legendary" + badgeString = component.strings.Gift_Attribute_Legendary badgeColor = UIColor(rgb: 0xd57e32) case .rare: - badgeString = "rare" - badgeColor = UIColor(rgb: 0x79993d) + badgeString = component.strings.Gift_Attribute_Rare + badgeColor = UIColor(rgb: 0x25a3b9) + case .uncommon: + badgeString = component.strings.Gift_Attribute_Uncommon + badgeColor = UIColor(rgb: 0x22b447) } var badgeTextColor = isColored ? .white : component.theme.list.itemSecondaryTextColor var badgeBackgroundColor = isColored ? UIColor(white: 0.0, alpha: 0.2) : component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.06) if let badgeColor { badgeTextColor = badgeColor - badgeBackgroundColor = badgeColor.withMultipliedAlpha(0.1) + if let backgroundColor { + badgeBackgroundColor = badgeColor.mixedWith(backgroundColor, alpha: 0.9) + } else { + badgeBackgroundColor = badgeColor.withMultipliedAlpha(0.1) + } } let badgeTextSize = self.badgeText.update( transition: .spring(duration: 0.2), diff --git a/submodules/TelegramUI/Components/Gifts/GiftLoadingShimmerView/Sources/GiftLoadingShimmerView.swift b/submodules/TelegramUI/Components/Gifts/GiftLoadingShimmerView/Sources/GiftLoadingShimmerView.swift index a51f903f6e..2ba1af60ce 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftLoadingShimmerView/Sources/GiftLoadingShimmerView.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftLoadingShimmerView/Sources/GiftLoadingShimmerView.swift @@ -139,7 +139,7 @@ public final class GiftLoadingShimmerView: UIView { required init?(coder: NSCoder) { fatalError() } - public func update(size: CGSize, theme: PresentationTheme, showFilters: Bool = false, isPlain: Bool = false, transition: ContainedViewLayoutTransition) { + public func update(size: CGSize, theme: PresentationTheme, itemSize: CGSize? = nil, showFilters: Bool = false, isPlain: Bool = false, transition: ContainedViewLayoutTransition) { let backgroundColor = isPlain ? theme.list.itemBlocksBackgroundColor : theme.list.blocksBackgroundColor let color = theme.list.itemSecondaryTextColor.mixedWith(theme.list.blocksBackgroundColor, alpha: 0.85) @@ -169,7 +169,7 @@ public final class GiftLoadingShimmerView: UIView { let optionSpacing: CGFloat = 10.0 let optionWidth = (size.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0 - let itemSize = CGSize(width: optionWidth, height: 154.0) + let itemSize = itemSize ?? CGSize(width: optionWidth, height: 154.0) context.setBlendMode(.copy) context.setFillColor(UIColor.clear.cgColor) diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 3cc6bfae11..a095f5449b 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -1605,7 +1605,7 @@ private final class GiftSetupScreenComponent: Component { guard let self, let component = self.component, let controller = self.environment?.controller(), let upgradePreview else { return } - let previewController = component.context.sharedContext.makeGiftUpgradePreviewScreen(context: component.context, attributes: upgradePreview.attributes, peerName: peerName) + let previewController = component.context.sharedContext.makeGiftUpgradePreviewScreen(context: component.context, gift: gift, attributes: upgradePreview.attributes, peerName: peerName) controller.push(previewController) }) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/BUILD index 4b2ada38af..700ed3aaa2 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/BUILD @@ -24,6 +24,7 @@ swift_library( "//submodules/AccountContext", "//submodules/AppBundle", "//submodules/ItemListUI", + "//submodules/ContextUI", "//submodules/TelegramStringFormatting", "//submodules/PresentationDataUtils", "//submodules/Components/SheetComponent", diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/BalanceComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/BalanceComponent.swift new file mode 100644 index 0000000000..c8ac352788 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/BalanceComponent.swift @@ -0,0 +1,174 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import AccountContext +import MultilineTextComponent +import MultilineTextWithEntitiesComponent +import TextFormat +import TelegramStringFormatting +import Markdown + +final class BalanceComponent: Component { + private let context: AccountContext + private let theme: PresentationTheme + private let action: () -> Void + + init( + context: AccountContext, + theme: PresentationTheme, + action: @escaping () -> Void + ) { + self.context = context + self.theme = theme + self.action = action + } + + static func ==(lhs: BalanceComponent, rhs: BalanceComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + return true + } + + public final class View: HighlightTrackingButton { + private var text = ComponentView() + + private var component: BalanceComponent? + private var componentState: EmptyComponentState? + + private var starsBalance: Int64 = 0 + private var tonBalance: Int64 = 0 + private var balanceDisposable: Disposable? + + override public init(frame: CGRect) { + super.init(frame: frame) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.balanceDisposable?.dispose() + } + + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action() + } + + private var isUpdating = false + func update(component: BalanceComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + self.component = component + self.componentState = state + + if self.balanceDisposable == nil { + if let starsContext = component.context.starsContext, let tonContext = component.context.tonContext { + self.balanceDisposable = combineLatest(queue: Queue.mainQueue(), + starsContext.state, + tonContext.state + ).start(next: { [weak self] starsState, tonState in + guard let self else { + return + } + self.starsBalance = starsState?.balance.value ?? 0 + self.tonBalance = tonState?.balance.value ?? 0 + if !self.isUpdating { + self.componentState?.updated() + } + }) + } + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + var rawString: String = "" + let starsBalanceString = "**⭐️\(presentationStringsFormattedNumber(Int32(clamping: self.starsBalance), presentationData.dateTimeFormat.groupingSeparator))**" + if self.tonBalance > 0 { + let tonBalanceString = "**💎\(formatTonAmountText(self.tonBalance, dateTimeFormat: presentationData.dateTimeFormat))**" + rawString = starsBalanceString + "\n" + tonBalanceString + } else { + rawString = presentationData.strings.Stars_Purchase_Balance + "\n" + starsBalanceString + } + + let attributedText = parseMarkdownIntoAttributedString( + rawString, + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(12.0), textColor: component.theme.rootController.navigationBar.primaryTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(12.0), textColor: component.theme.rootController.navigationBar.primaryTextColor), + link: MarkdownAttributeSet(font: Font.regular(12.0), textColor: component.theme.rootController.navigationBar.primaryTextColor), + linkAttribute: { _ in + return nil + } + ), + textAlignment: .right + ).mutableCopy() as! NSMutableAttributedString + let starRange = (attributedText.string as NSString).range(of: "⭐️") + if starRange.location != NSNotFound { + attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: starRange) + attributedText.addAttribute(.baselineOffset, value: 1.0, range: starRange) + } + let tonRange = (attributedText.string as NSString).range(of: "💎") + if tonRange.location != NSNotFound { + attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .ton(tinted: false)), range: tonRange) + attributedText.addAttribute(.baselineOffset, value: 1.0, range: tonRange) + } + + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent( + MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: .white, + text: .plain(attributedText), + horizontalAlignment: .right, + maximumNumberOfLines: 2, + lineSpacing: 0.1, + displaysAsynchronously: false + ) + ), + environment: {}, + containerSize: availableSize + ) + + let inset: CGFloat = 12.0 + let size = CGSize(width: textSize.width + inset * 2.0, height: 44.0) + + if let textView = self.text.view { + if textView.superview == nil { + textView.isUserInteractionEnabled = false + self.addSubview(textView) + + if !transition.animation.isImmediate { + transition.animateAlpha(view: textView, from: 0.0, to: 1.0) + } + } + textView.frame = CGRect(origin: CGPoint(x: inset, y: 8.0 - UIScreenPixel), size: textSize) + } + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift index 3639d65c48..73be35b2dd 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift @@ -29,6 +29,7 @@ import LottieComponent import GiftLoadingShimmerView import EdgeEffect import GlassBackgroundComponent +import ContextUI private let minimumCountToDisplayFilters = 18 @@ -45,6 +46,7 @@ public final class GiftStoreContentComponent: Component { let starsContext: StarsContext let peerId: EnginePeer.Id let gift: StarGift.Gift + let isPlain: Bool let confirmPurchaseImmediately: Bool let starsTopUpOptions: Signal<[StarsTopUpOption]?, NoError>? let scrollToTop: () -> Void @@ -64,6 +66,7 @@ public final class GiftStoreContentComponent: Component { starsContext: StarsContext, peerId: EnginePeer.Id, gift: StarGift.Gift, + isPlain: Bool, confirmPurchaseImmediately: Bool, starsTopUpOptions: Signal<[StarsTopUpOption]?, NoError>?, scrollToTop: @escaping () -> Void, @@ -82,6 +85,7 @@ public final class GiftStoreContentComponent: Component { self.starsContext = starsContext self.peerId = peerId self.gift = gift + self.isPlain = isPlain self.confirmPurchaseImmediately = confirmPurchaseImmediately self.starsTopUpOptions = starsTopUpOptions self.scrollToTop = scrollToTop @@ -1075,7 +1079,7 @@ public final class GiftStoreContentComponent: Component { let loadingSize = CGSize(width: availableSize.width, height: min(1000.0, availableSize.height)) if isLoading && self.showLoading { - self.loadingView.update(size: loadingSize, theme: component.theme, showFilters: !showingFilters, isPlain: true, transition: .immediate) + self.loadingView.update(size: loadingSize, theme: component.theme, showFilters: !showingFilters, isPlain: component.isPlain, transition: .immediate) loadingTransition.setAlpha(view: self.loadingView, alpha: 1.0) } else { loadingTransition.setAlpha(view: self.loadingView, alpha: 0.0) @@ -1145,10 +1149,8 @@ final class GiftStoreScreenComponent: Component { private let edgeEffectView: EdgeEffectView - private let balanceBackgroundView: GlassBackgroundView - private let balanceTitle = ComponentView() - private let balanceValue = ComponentView() - private let balanceIcon = ComponentView() + private let balance = ComponentView() + private let balanceBackgroundView: GlassContextExtractableContainer private let title = ComponentView() private let subtitle = ComponentView() @@ -1165,7 +1167,7 @@ final class GiftStoreScreenComponent: Component { private var isUpdating: Bool = false override init(frame: CGRect) { - self.balanceBackgroundView = GlassBackgroundView() + self.balanceBackgroundView = GlassContextExtractableContainer() self.scrollView = ScrollView() self.scrollView.showsVerticalScrollIndicator = true @@ -1211,6 +1213,68 @@ final class GiftStoreScreenComponent: Component { contentView.updateScrolling(bounds: bounds, interactive: interactive, transition: transition) } } + + func presentBalanceMenu() { + guard let component = self.component, let starsContext = component.context.starsContext, let tonContext = component.context.tonContext, let controller = self.environment?.controller() else { + return + } + let tonBalance = tonContext.currentState?.balance.value ?? 0 + if tonBalance == 0 { + let controller = component.context.sharedContext.makeStarsTransactionsScreen(context: component.context, starsContext: tonContext) + controller.push(controller) + return + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + let sourceView = self.balanceBackgroundView + + let items: Signal<[ContextMenuItem], NoError> = combineLatest( + queue: Queue.mainQueue(), + starsContext.state, + tonContext.state + ) + |> take(1) + |> map { starsState, tonState -> [ContextMenuItem] in + let starsBalance = starsState?.balance ?? .zero + let tonBalance = tonState?.balance.value ?? 0 + + var items: [ContextMenuItem] = [] + + items.append(.action(ContextMenuActionItem( + text: "My Stars", + textLayout: .secondLineWithValue(formatStarsAmountText(starsBalance, dateTimeFormat: presentationData.dateTimeFormat)), + icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Stars"), color: theme.contextMenu.primaryColor) }, + action: { [weak self] _, f in + f(.dismissWithoutContent) + guard let self, let component = self.component, let environment = self.environment else { + return + } + let controller = component.context.sharedContext.makeStarsTransactionsScreen(context: component.context, starsContext: starsContext) + environment.controller()?.push(controller) + } + ))) + + items.append(.action(ContextMenuActionItem( + text: "My TON", + textLayout: .secondLineWithValue(formatTonAmountText(tonBalance, dateTimeFormat: presentationData.dateTimeFormat)), + icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Ton"), color: theme.contextMenu.primaryColor) }, + action: { [weak self] _, f in + f(.dismissWithoutContent) + guard let self, let component = self.component, let environment = self.environment else { + return + } + let controller = component.context.sharedContext.makeStarsTransactionsScreen(context: component.context, starsContext: tonContext) + environment.controller()?.push(controller) + } + ))) + + return items + } + + let contextController = makeContextController(presentationData: presentationData, source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: nil) + controller.presentInGlobalOverlay(contextController) + } func update(component: GiftStoreScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true @@ -1255,42 +1319,37 @@ final class GiftStoreScreenComponent: Component { let edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: edgeEffectHeight)) transition.setFrame(view: self.edgeEffectView, frame: edgeEffectFrame) self.edgeEffectView.update(content: environment.theme.list.blocksBackgroundColor, blur: true, rect: edgeEffectFrame, edge: .top, edgeSize: min(30, edgeEffectFrame.height), transition: transition) - - let balanceTitleSize = self.balanceTitle.update( + + + let balanceSize = self.balance.update( transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: strings.Stars_Purchase_Balance, - font: Font.regular(14.0), - textColor: environment.theme.actionSheet.primaryTextColor - )), - maximumNumberOfLines: 1 - )), + component: AnyComponent( + BalanceComponent( + context: component.context, + theme: environment.theme, + action: { [weak self] in + guard let self else { + return + } + self.presentBalanceMenu() + } + ) + ), environment: {}, containerSize: availableSize ) - - let formattedBalance = formatStarsAmountText(self.starsState?.balance ?? StarsAmount.zero, dateTimeFormat: environment.dateTimeFormat) - let smallLabelFont = Font.regular(11.0) - let labelFont = Font.semibold(14.0) - let balanceText = tonAmountAttributedString(formattedBalance, integralFont: labelFont, fractionalFont: smallLabelFont, color: environment.theme.actionSheet.primaryTextColor, decimalSeparator: environment.dateTimeFormat.decimalSeparator) - - let balanceValueSize = self.balanceValue.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(balanceText), - maximumNumberOfLines: 1 - )), - environment: {}, - containerSize: availableSize - ) - let balanceIconSize = self.balanceIcon.update( - transition: .immediate, - component: AnyComponent(BundleIconComponent(name: "Premium/Stars/StarSmall", tintColor: nil)), - environment: {}, - containerSize: availableSize - ) - + let balanceFrame = CGRect(origin: .zero, size: balanceSize) + if let balanceView = self.balance.view { + if balanceView.superview == nil { + self.balanceBackgroundView.contentView.addSubview(balanceView) + } + balanceView.frame = balanceFrame + + let balanceBackgroundFrame = CGRect(origin: CGPoint(x: availableSize.width - environment.safeInsets.right - 16.0 - balanceSize.width, y: environment.navigationHeight - 60.0 + 2.0 + floor((60.0 - 44.0) * 0.5)), size: balanceSize) + + transition.setFrame(view: self.balanceBackgroundView, frame: balanceBackgroundFrame) + self.balanceBackgroundView.update(size: balanceBackgroundFrame.size, cornerRadius: balanceBackgroundFrame.height * 0.5, isDark: environment.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, transition: transition) + } if self.balanceBackgroundView.superview == nil { component.overNavigationContainer.addSubview(self.balanceBackgroundView) } @@ -1300,32 +1359,6 @@ final class GiftStoreScreenComponent: Component { topInset = environment.statusBarHeight - 6.0 } - if let balanceTitleView = self.balanceTitle.view, let balanceValueView = self.balanceValue.view, let balanceIconView = self.balanceIcon.view { - if balanceTitleView.superview == nil { - self.balanceBackgroundView.contentView.addSubview(balanceTitleView) - self.balanceBackgroundView.contentView.addSubview(balanceValueView) - self.balanceBackgroundView.contentView.addSubview(balanceIconView) - } - - let topBalanceOriginY = (44.0 - balanceTitleSize.height - balanceValueSize.height) / 2.0 - - let balanceSideInset: CGFloat = 12.0 - var balanceBackgroundSize = CGSize(width: balanceTitleSize.width + balanceSideInset * 2.0, height: 44.0) - balanceBackgroundSize.width = max(balanceBackgroundSize.width, balanceValueSize.width + balanceIconSize.width + 2.0 + balanceSideInset * 2.0) - - let balanceBackgroundFrame = CGRect(origin: CGPoint(x: availableSize.width - environment.safeInsets.right - 16.0 - balanceBackgroundSize.width, y: environment.navigationHeight - 60.0 + 2.0 + floor((60.0 - 44.0) * 0.5)), size: balanceBackgroundSize) - - transition.setFrame(view: self.balanceBackgroundView, frame: balanceBackgroundFrame) - self.balanceBackgroundView.update(size: balanceBackgroundFrame.size, cornerRadius: balanceBackgroundFrame.height * 0.5, isDark: environment.theme.overallDarkAppearance, tintColor: .init(kind: .panel), transition: transition) - - balanceTitleView.center = CGPoint(x: balanceBackgroundFrame.width - balanceSideInset - balanceTitleSize.width / 2.0, y: topBalanceOriginY + balanceTitleSize.height / 2.0) - balanceTitleView.bounds = CGRect(origin: .zero, size: balanceTitleSize) - balanceValueView.center = CGPoint(x: balanceBackgroundFrame.width - balanceSideInset - balanceValueSize.width / 2.0, y: topBalanceOriginY + balanceTitleSize.height + balanceValueSize.height / 2.0) - balanceValueView.bounds = CGRect(origin: .zero, size: balanceValueSize) - balanceIconView.center = CGPoint(x: balanceBackgroundFrame.width - balanceSideInset - balanceValueSize.width - balanceIconSize.width / 2.0 - 2.0, y: topBalanceOriginY + balanceTitleSize.height + balanceValueSize.height / 2.0 - UIScreenPixel) - balanceIconView.bounds = CGRect(origin: .zero, size: balanceIconSize) - } - let titleSize = self.title.update( transition: transition, component: AnyComponent(MultilineTextComponent( @@ -1360,6 +1393,7 @@ final class GiftStoreScreenComponent: Component { starsContext: component.starsContext, peerId: component.peerId, gift: component.gift, + isPlain: false, confirmPurchaseImmediately: false, starsTopUpOptions: nil, scrollToTop: { [weak self] in diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/ButtonsComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/ButtonsComponent.swift deleted file mode 100644 index 5d05bf9d1c..0000000000 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/ButtonsComponent.swift +++ /dev/null @@ -1,267 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import ComponentFlow -import TelegramCore -import BundleIconComponent -import MultilineTextComponent -import MoreButtonNode -import AccountContext -import TelegramPresentationData -import TelegramStringFormatting - -final class PriceButtonComponent: Component { - let price: CurrencyAmount - let dateTimeFormat: PresentationDateTimeFormat - - init( - price: CurrencyAmount, - dateTimeFormat: PresentationDateTimeFormat - ) { - self.price = price - self.dateTimeFormat = dateTimeFormat - } - - static func ==(lhs: PriceButtonComponent, rhs: PriceButtonComponent) -> Bool { - return lhs.price == rhs.price && lhs.dateTimeFormat == rhs.dateTimeFormat - } - - final class View: UIView { - private let backgroundView = UIView() - - private let icon = UIImageView() - private let text = ComponentView() - - private var component: PriceButtonComponent? - private weak var state: EmptyComponentState? - - override init(frame: CGRect) { - super.init(frame: frame) - - self.backgroundView.clipsToBounds = true - self.addSubview(self.backgroundView) - - self.backgroundView.addSubview(self.icon) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func update(component: PriceButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - let previousComponent = self.component - self.component = component - self.state = state - - if previousComponent?.price.currency != component.price.currency { - switch component.price.currency { - case .stars: - self.icon.image = UIImage(bundleImageName: "Premium/Stars/ButtonStar")?.withRenderingMode(.alwaysTemplate) - case .ton: - self.icon.image = UIImage(bundleImageName: "Ads/TonAbout")?.withRenderingMode(.alwaysTemplate) - } - } - - var backgroundSize = CGSize(width: 42.0, height: 30.0) - let textSize = self.text.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: formatCurrencyAmountText(component.price, dateTimeFormat: component.dateTimeFormat), - font: Font.semibold(11.0), - textColor: UIColor(rgb: 0xffffff) - )) - )), - environment: {}, - containerSize: availableSize - ) - let textFrame = CGRect(origin: CGPoint(x: 32.0, y: floorToScreenPixels((backgroundSize.height - textSize.height) / 2.0)), size: textSize) - if let textView = self.text.view { - if textView.superview == nil { - self.backgroundView.addSubview(textView) - } - transition.setFrame(view: textView, frame: textFrame) - } - backgroundSize.width += textSize.width - - self.backgroundView.layer.cornerRadius = backgroundSize.height / 2.0 - - let backgroundColor: UIColor = UIColor(rgb: 0xffffff, alpha: 0.1) - transition.setBackgroundColor(view: self.backgroundView, color: backgroundColor) - - let backgroundFrame = CGRect(origin: .zero, size: backgroundSize) - transition.setFrame(view: self.backgroundView, frame: backgroundFrame) - - if let iconSize = self.icon.image?.size { - let iconFrame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((backgroundSize.height - iconSize.height) / 2.0)), size: iconSize) - transition.setFrame(view: self.icon, frame: iconFrame) - } - self.icon.tintColor = UIColor(rgb: 0xffffff) - - return backgroundSize - } - } - - func makeView() -> View { - return View(frame: CGRect()) - } - - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } -} - -final class ButtonsComponent: Component { - let theme: PresentationTheme - let isOverlay: Bool - let showMoreButton: Bool - let closePressed: () -> Void - let morePressed: (ASDisplayNode, ContextGesture?) -> Void - - init( - theme: PresentationTheme, - isOverlay: Bool, - showMoreButton: Bool, - closePressed: @escaping () -> Void, - morePressed: @escaping (ASDisplayNode, ContextGesture?) -> Void - ) { - self.theme = theme - self.isOverlay = isOverlay - self.showMoreButton = showMoreButton - self.closePressed = closePressed - self.morePressed = morePressed - } - - static func ==(lhs: ButtonsComponent, rhs: ButtonsComponent) -> Bool { - return lhs.theme === rhs.theme && lhs.isOverlay == rhs.isOverlay && lhs.showMoreButton == rhs.showMoreButton - } - - final class View: UIView { - private let backgroundView = UIView() - - private let closeButton = HighlightTrackingButton() - private let closeIcon = UIImageView() - private let moreNode = MoreButtonNode(theme: defaultPresentationTheme, size: CGSize(width: 36.0, height: 36.0), encircled: false) - - private var component: ButtonsComponent? - private weak var state: EmptyComponentState? - - override init(frame: CGRect) { - super.init(frame: frame) - - self.backgroundView.clipsToBounds = true - self.addSubview(self.backgroundView) - - self.closeIcon.image = generateCloseButtonImage() - self.moreNode.updateColor(.white, transition: .immediate) - - self.backgroundView.addSubview(self.moreNode.view) - self.backgroundView.addSubview(self.closeButton) - self.backgroundView.addSubview(self.closeIcon) - - self.closeButton.highligthedChanged = { [weak self] highlighted in - guard let self else { - return - } - if highlighted { - self.closeIcon.layer.removeAnimation(forKey: "opacity") - self.closeIcon.alpha = 0.6 - } else { - self.closeIcon.alpha = 1.0 - self.closeIcon.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) - } - } - self.closeButton.addTarget(self, action: #selector(self.closePressed), for: .touchUpInside) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc private func closePressed() { - guard let component = self.component else { - return - } - component.closePressed() - } - - func update(component: ButtonsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - self.component = component - self.state = state - - let backgroundSize = CGSize(width: component.showMoreButton ? 70.0 : 30.0, height: 30.0) - self.backgroundView.layer.cornerRadius = backgroundSize.height / 2.0 - - let backgroundColor: UIColor = component.isOverlay ? UIColor(rgb: 0xffffff, alpha: 0.1) : UIColor(rgb: 0x808084, alpha: 0.1) - let foregroundColor: UIColor = component.isOverlay ? .white : component.theme.actionSheet.inputClearButtonColor - transition.setBackgroundColor(view: self.backgroundView, color: backgroundColor) - transition.setTintColor(view: self.closeIcon, color: foregroundColor) - - let backgroundFrame = CGRect(origin: .zero, size: backgroundSize) - transition.setFrame(view: self.backgroundView, frame: backgroundFrame) - - transition.setFrame(view: self.moreNode.view, frame: CGRect(origin: CGPoint(x: -7.0, y: -4.0), size: CGSize(width: 36.0, height: 36.0))) - transition.setAlpha(view: self.moreNode.view, alpha: component.showMoreButton ? 1.0 : 0.0) - self.moreNode.action = { [weak self] node, gesture in - guard let self, let component = self.component else { - return - } - component.morePressed(node, gesture) - } - - let closeFrame = CGRect(origin: CGPoint(x: backgroundSize.width - 30.0 - (component.showMoreButton ? 3.0 : 0.0), y: 0.0), size: CGSize(width: 30.0, height: 30.0)) - transition.setFrame(view: self.closeIcon, frame: closeFrame) - transition.setFrame(view: self.closeButton, frame: closeFrame) - - return backgroundSize - } - } - - func makeView() -> View { - return View(frame: CGRect()) - } - - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } -} - -private func generateCloseButtonImage() -> UIImage? { - return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - - context.setLineWidth(2.0) - context.setLineCap(.round) - context.setStrokeColor(UIColor.white.cgColor) - - context.move(to: CGPoint(x: 10.0, y: 10.0)) - context.addLine(to: CGPoint(x: 20.0, y: 20.0)) - context.strokePath() - - context.move(to: CGPoint(x: 20.0, y: 10.0)) - context.addLine(to: CGPoint(x: 10.0, y: 20.0)) - context.strokePath() - })?.withRenderingMode(.alwaysTemplate) -} - -func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { - return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - - context.setFillColor(backgroundColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - - context.setLineWidth(2.0) - context.setLineCap(.round) - context.setStrokeColor(foregroundColor.cgColor) - - context.move(to: CGPoint(x: 10.0, y: 10.0)) - context.addLine(to: CGPoint(x: 20.0, y: 20.0)) - context.strokePath() - - context.move(to: CGPoint(x: 20.0, y: 10.0)) - context.addLine(to: CGPoint(x: 10.0, y: 20.0)) - context.strokePath() - }) -} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift index 777b3f5aa4..e4b70c9faf 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift @@ -746,8 +746,8 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { } else { return false } - }), case let .backdrop(_, _, innerColor, _, _, _, _) = backdropAttribute { - buttonColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultipliedBrightnessBy(1.05) + }), case let .backdrop(_, _, innerColor, outerColor, _, _, _) = backdropAttribute { + buttonColor = UIColor(rgb: UInt32(bitPattern: outerColor)).mixedWith(.white, alpha: 0.2) secondaryTextColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.3) } @@ -1083,7 +1083,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { guard let state, let attributes = state.giftUpgradeAttributes else { return } - let variantsController = component.context.sharedContext.makeGiftUpgradeVariantsScreen(context: component.context, gift: .generic(gift), onlyCrafted: false, attributes: attributes, selectedAttributes: nil, focusedAttribute: nil) + let variantsController = component.context.sharedContext.makeGiftUpgradeVariantsScreen(context: component.context, gift: .generic(gift), crafted: false, attributes: attributes, selectedAttributes: nil, focusedAttribute: nil) environment.controller()?.push(variantsController) }, animateScale: false), availableSize: CGSize(width: context.availableSize.width - 64.0, height: context.availableSize.height), diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift index ce2128889d..6383c04808 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift @@ -294,8 +294,8 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent { } else { return false } - }), case let .backdrop(_, _, innerColor, _, _, _, _) = backdropAttribute { - buttonColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultipliedBrightnessBy(1.05) + }), case let .backdrop(_, _, innerColor, outerColor, _, _, _) = backdropAttribute { + buttonColor = UIColor(rgb: UInt32(bitPattern: outerColor)).mixedWith(.white, alpha: 0.2) secondaryTextColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.3) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift index 8c1535be94..dfb46929db 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift @@ -29,7 +29,7 @@ private final class GiftUpgradeVariantsScreenComponent: Component { let context: AccountContext let gift: StarGift - let onlyCrafted: Bool + let crafted: Bool let attributes: [StarGift.UniqueGift.Attribute] let selectedAttributes: [StarGift.UniqueGift.Attribute]? let focusedAttribute: StarGift.UniqueGift.Attribute? @@ -37,14 +37,14 @@ private final class GiftUpgradeVariantsScreenComponent: Component { init( context: AccountContext, gift: StarGift, - onlyCrafted: Bool, + crafted: Bool, attributes: [StarGift.UniqueGift.Attribute], selectedAttributes: [StarGift.UniqueGift.Attribute]?, focusedAttribute: StarGift.UniqueGift.Attribute? ) { self.context = context self.gift = gift - self.onlyCrafted = onlyCrafted + self.crafted = crafted self.attributes = attributes self.selectedAttributes = selectedAttributes self.focusedAttribute = focusedAttribute @@ -112,6 +112,7 @@ private final class GiftUpgradeVariantsScreenComponent: Component { private var giftItems: [AnyHashable: ComponentView] = [:] private var selectedSection: SelectedSection = .models + private var displayCraftableModels = false private let giftCompositionExternalState = GiftCompositionComponent.ExternalState() @@ -122,7 +123,8 @@ private final class GiftUpgradeVariantsScreenComponent: Component { private var previewBackdropIndex: Int = 0 private var previewSymbolIndex: Int = 0 - private var previewModels: [StarGift.UniqueGift.Attribute] = [] + private var previewPrimaryModels: [StarGift.UniqueGift.Attribute] = [] + private var previewCraftableModels: [StarGift.UniqueGift.Attribute] = [] private var previewBackdrops: [StarGift.UniqueGift.Attribute] = [] private var previewSymbols: [StarGift.UniqueGift.Attribute] = [] @@ -130,10 +132,15 @@ private final class GiftUpgradeVariantsScreenComponent: Component { private var selectedBackdrop: StarGift.UniqueGift.Attribute? private var selectedSymbol: StarGift.UniqueGift.Attribute? - private var modelCount: Int32 = 0 + private var craftableModelCount: Int32 = 0 + private var primaryModelCount: Int32 = 0 private var backdropCount: Int32 = 0 private var symbolCount: Int32 = 0 + private var currentDescriptionHeight: CGFloat = 0.0 + + private var cachedSmallChevronImage: (UIImage, PresentationTheme)? + private var ignoreScrolling: Bool = false private var component: GiftUpgradeVariantsScreenComponent? @@ -202,6 +209,8 @@ private final class GiftUpgradeVariantsScreenComponent: Component { self.containerView.addSubview(self.navigationBarContainer) self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + + self.alpha = 0.0 } required init?(coder: NSCoder) { @@ -271,6 +280,7 @@ private final class GiftUpgradeVariantsScreenComponent: Component { } func animateIn() { + self.alpha = 1.0 self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) @@ -290,8 +300,9 @@ private final class GiftUpgradeVariantsScreenComponent: Component { } private func previewTimerTick() { - guard !self.previewModels.isEmpty else { return } - self.previewModelIndex = (self.previewModelIndex + 1) % self.previewModels.count + let previewModels = self.displayCraftableModels ? self.previewCraftableModels : self.previewPrimaryModels + guard !previewModels.isEmpty else { return } + self.previewModelIndex = (self.previewModelIndex + 1) % previewModels.count let previousSymbolIndex = self.previewSymbolIndex var randomSymbolIndex = previousSymbolIndex @@ -326,13 +337,15 @@ private final class GiftUpgradeVariantsScreenComponent: Component { } private var effectiveGifts: [[StarGift.UniqueGift.Attribute]] = [] - private func updateEffectiveGifts(attributes: [StarGift.UniqueGift.Attribute], onlyCrafted: Bool) { + private func updateEffectiveGifts(attributes: [StarGift.UniqueGift.Attribute]) { var effectiveGifts: [[StarGift.UniqueGift.Attribute]] = [] switch self.selectedSection { case .models: let models = Array(attributes.filter({ attribute in if case let .model(_, _, _, crafted) = attribute { - if onlyCrafted && !crafted { + if self.displayCraftableModels && !crafted { + return false + } else if !self.displayCraftableModels && crafted { return false } return true @@ -344,7 +357,8 @@ private final class GiftUpgradeVariantsScreenComponent: Component { effectiveGifts.append([model]) } case .backdrops: - let selectedModel = self.selectedModel ?? self.previewModels[self.previewModelIndex] + let previewModels = self.displayCraftableModels ? self.previewCraftableModels : previewPrimaryModels + let selectedModel = self.selectedModel ?? previewModels[self.previewModelIndex] let selectedSymbol = self.selectedSymbol ?? self.previewSymbols[self.previewSymbolIndex] let backdrops = Array(attributes.filter({ attribute in if case .backdrop = attribute { @@ -400,7 +414,7 @@ private final class GiftUpgradeVariantsScreenComponent: Component { let optionWidth = (fillingSize - 16.0 * 2.0 - optionSpacing * 2.0) / 3.0 let optionSize = CGSize(width: optionWidth, height: 126.0) - let topInset: CGFloat = 393.0 + let topInset: CGFloat = 375.0 + self.currentDescriptionHeight var validIds: [AnyHashable] = [] var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset + 9.0), size: optionSize) @@ -604,15 +618,19 @@ private final class GiftUpgradeVariantsScreenComponent: Component { let sideInset: CGFloat = rawSideInset + 16.0 if self.component == nil { - var modelCount: Int32 = 0 + self.displayCraftableModels = component.crafted + + var primaryModelCount: Int32 = 0 + var craftableModelCount: Int32 = 0 var backdropCount: Int32 = 0 var symbolCount: Int32 = 0 for attribute in component.attributes { switch attribute { case let .model(_, _, _, crafted): - if component.onlyCrafted && !crafted { + if crafted { + craftableModelCount += 1 } else { - modelCount += 1 + primaryModelCount += 1 } case .backdrop: backdropCount += 1 @@ -622,13 +640,14 @@ private final class GiftUpgradeVariantsScreenComponent: Component { break } } - self.modelCount = modelCount + self.primaryModelCount = primaryModelCount + self.craftableModelCount = craftableModelCount self.backdropCount = backdropCount self.symbolCount = symbolCount - let randomModels = Array(component.attributes.filter({ attribute in + let randomPrimaryModels = Array(component.attributes.filter({ attribute in if case let .model(_, _, _, crafted) = attribute { - if component.onlyCrafted && !crafted { + if crafted { return false } return true @@ -636,7 +655,19 @@ private final class GiftUpgradeVariantsScreenComponent: Component { return false } }).shuffled().prefix(15)) - self.previewModels = randomModels + self.previewPrimaryModels = randomPrimaryModels + + let randomCraftableModels = Array(component.attributes.filter({ attribute in + if case let .model(_, _, _, crafted) = attribute { + if !crafted { + return false + } + return true + } else { + return false + } + }).shuffled().prefix(15)) + self.previewCraftableModels = randomCraftableModels let randomBackdrops = Array(component.attributes.filter({ attribute in if case .backdrop = attribute { @@ -684,7 +715,7 @@ private final class GiftUpgradeVariantsScreenComponent: Component { } } - self.updateEffectiveGifts(attributes: component.attributes, onlyCrafted: component.onlyCrafted) + self.updateEffectiveGifts(attributes: component.attributes) } self.component = component @@ -705,14 +736,15 @@ private final class GiftUpgradeVariantsScreenComponent: Component { var badgeColor: UIColor = .white.withAlphaComponent(0.4) var attributes: [StarGift.UniqueGift.Attribute] = [] - if !self.previewModels.isEmpty { + let previewModels = self.displayCraftableModels ? self.previewCraftableModels : self.previewPrimaryModels + if !previewModels.isEmpty { if self.isPlaying { - attributes.append(self.previewModels[self.previewModelIndex]) + attributes.append(previewModels[self.previewModelIndex]) attributes.append(self.previewBackdrops[self.previewBackdropIndex]) attributes.append(self.previewSymbols[self.previewSymbolIndex]) } else { if self.selectedModel == nil { - self.selectedModel = self.previewModels[self.previewModelIndex] + self.selectedModel = previewModels[self.previewModelIndex] } if self.selectedBackdrop == nil { self.selectedBackdrop = self.previewBackdrops[self.previewBackdropIndex] @@ -739,7 +771,7 @@ private final class GiftUpgradeVariantsScreenComponent: Component { return false } }), case let .backdrop(_, _, innerColor, outerColor, _, _, _) = backdropAttribute { - buttonColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultipliedBrightnessBy(1.05) + buttonColor = UIColor(rgb: UInt32(bitPattern: outerColor)).mixedWith(.white, alpha: 0.2) badgeColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultipliedBrightnessBy(1.05) let outer = UIColor(rgb: UInt32(bitPattern: outerColor)) @@ -786,8 +818,11 @@ private final class GiftUpgradeVariantsScreenComponent: Component { contentHeight += headerSize.height var titleText: String = "" - if case let .generic(gift) = component.gift { + switch component.gift { + case let .generic(gift): titleText = gift.title ?? "" + case let .unique(gift): + titleText = gift.title } let titleSize = self.title.update( @@ -838,7 +873,7 @@ private final class GiftUpgradeVariantsScreenComponent: Component { } let attributeSpacing: CGFloat = 10.0 - let attributeWidth: CGFloat = floor((fillingSize - attributeSpacing * CGFloat(attributes.count - 1)) / CGFloat(attributes.count)) + let attributeWidth: CGFloat = floor((fillingSize - 32.0 - attributeSpacing * CGFloat(attributes.count - 1)) / CGFloat(attributes.count)) let attributeHeight: CGFloat = 45.0 for i in 0 ..< attributes.count { @@ -911,7 +946,7 @@ private final class GiftUpgradeVariantsScreenComponent: Component { self.selectedSection = id self.isPlaying = false - self.updateEffectiveGifts(attributes: component.attributes, onlyCrafted: component.onlyCrafted) + self.updateEffectiveGifts(attributes: component.attributes) self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) })), environment: {}, @@ -932,17 +967,22 @@ private final class GiftUpgradeVariantsScreenComponent: Component { let itemHeight: CGFloat = 126.0 let itemSpacing: CGFloat = 10.0 - let descriptionText: String + var descriptionText: String let itemCount: Int32 switch self.selectedSection { case .models: - //TODO:localize - if component.onlyCrafted { - descriptionText = environment.strings.Gift_Variants_CollectionInfo("**\(self.modelCount)** craftable models").string + if self.displayCraftableModels { + descriptionText = environment.strings.Gift_Variants_CollectionInfo(environment.strings.Gift_Variants_CollectionInfo_CraftableModel(self.craftableModelCount)).string + itemCount = self.craftableModelCount + descriptionText += "\n[\(environment.strings.Gift_Variants_ViewPrimaryModels) >]()" } else { - descriptionText = environment.strings.Gift_Variants_CollectionInfo(environment.strings.Gift_Variants_CollectionInfo_Model(self.modelCount)).string + descriptionText = environment.strings.Gift_Variants_CollectionInfo(environment.strings.Gift_Variants_CollectionInfo_Model(self.primaryModelCount)).string + itemCount = self.primaryModelCount + + if self.craftableModelCount > 0 { + descriptionText += "\n[\(environment.strings.Gift_Variants_ViewCraftableModels) >]()" + } } - itemCount = self.modelCount case .backdrops: descriptionText = environment.strings.Gift_Variants_CollectionInfo(environment.strings.Gift_Variants_CollectionInfo_Backdrop(self.backdropCount)).string itemCount = self.backdropCount @@ -954,14 +994,45 @@ private final class GiftUpgradeVariantsScreenComponent: Component { let descriptionFont = Font.regular(13.0) let descriptionBoldFont = Font.semibold(13.0) let descriptionTextColor = theme.list.itemSecondaryTextColor - let descriptionMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionTextColor), bold: MarkdownAttributeSet(font: descriptionBoldFont, textColor: descriptionTextColor), link: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionTextColor), linkAttribute: { contents in + let descriptionLinkColor = theme.list.itemAccentColor + let descriptionMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionTextColor), bold: MarkdownAttributeSet(font: descriptionBoldFont, textColor: descriptionTextColor), link: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionLinkColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) }) + if self.cachedSmallChevronImage == nil || self.cachedSmallChevronImage?.1 !== environment.theme { + self.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: descriptionLinkColor)!, theme) + } + + let descriptionAttributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: descriptionMarkdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString + if let range = descriptionAttributedString.string.range(of: ">"), let chevronImage = self.cachedSmallChevronImage?.0 { + descriptionAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: descriptionAttributedString.string)) + } + let descriptionSize = self.descriptionText.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .markdown(text: descriptionText, attributes: descriptionMarkdownAttributes) + text: .plain(descriptionAttributedString), + horizontalAlignment: .center, + maximumNumberOfLines: 3, + lineSpacing: 0.2, + highlightColor: descriptionLinkColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { [weak self] attributes, _ in + if let self, let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + self.displayCraftableModels = !self.displayCraftableModels + self.isPlaying = false + + self.updateEffectiveGifts(attributes: component.attributes) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) + } + } )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) @@ -973,6 +1044,7 @@ private final class GiftUpgradeVariantsScreenComponent: Component { } descriptionView.frame = descriptionFrame } + self.currentDescriptionHeight = descriptionSize.height contentHeight += descriptionSize.height contentHeight += 26.0 @@ -1061,6 +1133,13 @@ private final class GiftUpgradeVariantsScreenComponent: Component { self.previewTimerTick() } self.state?.updated(transition: .easeInOut(duration: 0.25)) + + if let buttonView = self.playbackButton.view { + buttonView.isUserInteractionEnabled = false + Queue.mainQueue().after(0.3, { + buttonView.isUserInteractionEnabled = true + }) + } } )), environment: {}, @@ -1156,7 +1235,7 @@ public class GiftUpgradeVariantsScreen: ViewControllerComponentContainer { public init( context: AccountContext, gift: StarGift, - onlyCrafted: Bool = false, + crafted: Bool = false, attributes: [StarGift.UniqueGift.Attribute], selectedAttributes: [StarGift.UniqueGift.Attribute]?, focusedAttribute: StarGift.UniqueGift.Attribute? @@ -1166,7 +1245,7 @@ public class GiftUpgradeVariantsScreen: ViewControllerComponentContainer { super.init(context: context, component: GiftUpgradeVariantsScreenComponent( context: context, gift: gift, - onlyCrafted: onlyCrafted, + crafted: crafted, attributes: attributes, selectedAttributes: selectedAttributes, focusedAttribute: focusedAttribute @@ -1194,7 +1273,9 @@ public class GiftUpgradeVariantsScreen: ViewControllerComponentContainer { self.didPlayAppearAnimation = true if let componentView = self.node.hostView.componentView as? GiftUpgradeVariantsScreenComponent.View { - componentView.animateIn() + Queue.mainQueue().justDispatch { + componentView.animateIn() + } } } } @@ -1352,8 +1433,8 @@ private final class AttributeInfoComponent: Component { } var badgeString = "" + var badgeColor = component.badgeColor if let rarity { - //TODO:localize switch rarity { case let .permille(value): if value == 0 { @@ -1362,11 +1443,17 @@ private final class AttributeInfoComponent: Component { badgeString = formatPercentage(Float(value) * 0.1) } case .epic: - badgeString = "epic" + badgeString = component.strings.Gift_Attribute_Epic + badgeColor = UIColor(rgb: 0xaf52de) case .legendary: - badgeString = "legendary" + badgeString = component.strings.Gift_Attribute_Legendary + badgeColor = UIColor(rgb: 0xd57e32) case .rare: - badgeString = "rare" + badgeString = component.strings.Gift_Attribute_Rare + badgeColor = UIColor(rgb: 0x25a3b9) + case .uncommon: + badgeString = component.strings.Gift_Attribute_Uncommon + badgeColor = UIColor(rgb: 0x22b447) } } @@ -1406,8 +1493,7 @@ private final class AttributeInfoComponent: Component { let badgeBackgroundFrame = badgeFrame.insetBy(dx: -5.5, dy: -2.0) transition.setFrame(layer: self.badgeBackground, frame: badgeBackgroundFrame) - transition.setBackgroundColor(layer: self.badgeBackground, color: component.badgeColor) - + transition.setBackgroundColor(layer: self.badgeBackground, color: badgeColor) return availableSize } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewBuyGift.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewBuyGift.swift index 48cbb81e84..ff996afa9c 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewBuyGift.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewBuyGift.swift @@ -37,6 +37,8 @@ public func buyStarGiftImpl( parseMarkdown: true ) controller.present(alertController, in: .window(.root)) + + beforeCompletion() return } @@ -207,6 +209,8 @@ public func buyStarGiftImpl( context.sharedContext.applicationBindings.openUrl(fragmentUrl) } )) + + beforeCompletion() } else { proceed() } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index ecbc6c58ef..1f921373e4 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -46,6 +46,7 @@ import PeerTableCellComponent import AvatarComponent import GlassControls import GlassBarButtonComponent +import GlassBackgroundComponent private final class GiftViewSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -748,7 +749,7 @@ private final class GiftViewSheetContent: CombinedComponent { let updatedAttributes = uniqueGift.attributes.filter { $0.attributeType != .originalInfo } self.subject = .profileGift(peerId, gift.withGift(.unique(uniqueGift.withAttributes(updatedAttributes)))) case let .message(message): - if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, _, assigned, fromOffer, canCraftAt) = action.action, case let .unique(uniqueGift) = gift { + if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, _, assigned, fromOffer, canCraftAt, isCrafted) = action.action, case let .unique(uniqueGift) = gift { let updatedAttributes = uniqueGift.attributes.filter { $0.attributeType != .originalInfo } let updatedMedia: [Media] = [ TelegramMediaAction( @@ -770,7 +771,8 @@ private final class GiftViewSheetContent: CombinedComponent { dropOriginalDetailsStars: nil, assigned: assigned, fromOffer: fromOffer, - canCraftAt: canCraftAt + canCraftAt: canCraftAt, + isCrafted: isCrafted ) ) ] @@ -1114,11 +1116,10 @@ private final class GiftViewSheetContent: CombinedComponent { let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) if let canCraftDate = arguments.canCraftDate, currentTime < canCraftDate { let dateString = stringForFullDate(timestamp: canCraftDate, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) - //TODO:localize let alertController = textAlertController( context: self.context, - title: "Try Later", - text: "You will be able to craft this gift on \(dateString).", + title: presentationData.strings.Gift_Craft_Unavailable_Title, + text: presentationData.strings.Gift_Craft_Unavailable_Text(dateString).string, actions: [ TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}) ], @@ -1405,15 +1406,26 @@ private final class GiftViewSheetContent: CombinedComponent { func openUpgradeVariants(attribute: StarGift.UniqueGift.Attribute? = nil) { - guard let controller = self.getController() as? GiftViewScreen, let arguments = self.subject.arguments else { + guard let controller = self.getController() as? GiftViewScreen else { return } + + var gift: StarGift? var selectedAttributes: [StarGift.UniqueGift.Attribute]? - if case let .unique(uniqueGift) = arguments.gift { - selectedAttributes = uniqueGift.attributes + if let arguments = self.subject.arguments { + gift = arguments.gift + if case let .unique(uniqueGift) = arguments.gift { + selectedAttributes = uniqueGift.attributes + } + } else if case let .upgradePreview(genericGift, _, _) = self.subject { + gift = .generic(genericGift) } - self.giftVariantsDisposable.set((self.context.engine.payments.getStarGiftUpgradeAttributes(giftId: arguments.gift.giftId) + guard let gift else { + return + } + + self.giftVariantsDisposable.set((self.context.engine.payments.getStarGiftUpgradeAttributes(giftId: gift.giftId) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] attributes in guard let self, let attributes else { @@ -1421,8 +1433,8 @@ private final class GiftViewSheetContent: CombinedComponent { } let variantsController = self.context.sharedContext.makeGiftUpgradeVariantsScreen( context: self.context, - gift: arguments.gift, - onlyCrafted: false, + gift: gift, + crafted: false, attributes: attributes, selectedAttributes: selectedAttributes, focusedAttribute: attribute @@ -2387,8 +2399,6 @@ private final class GiftViewSheetContent: CombinedComponent { } static var body: Body { - let priceButton = Child(PlainButtonComponent.self) - let buttons = Child(GlassControlPanelComponent.self) let animation = Child(GiftCompositionComponent.self) let title = Child(MultilineTextComponent.self) @@ -2398,9 +2408,9 @@ private final class GiftViewSheetContent: CombinedComponent { let description = Child(MultilineTextComponent.self) let animatedDescription = Child(HStack.self) - let transferButton = Child(PlainButtonComponent.self) - let wearButton = Child(PlainButtonComponent.self) - let resellButton = Child(PlainButtonComponent.self) + let transferButton = Child(HeaderButtonComponent.self) + let wearButton = Child(HeaderButtonComponent.self) + let resellButton = Child(HeaderButtonComponent.self) let wearAvatar = Child(AvatarComponent.self) let wearPeerName = Child(MultilineTextComponent.self) @@ -2420,7 +2430,10 @@ private final class GiftViewSheetContent: CombinedComponent { let upgradePerks = Child(List.self) let upgradeKeepName = Child(PlainButtonComponent.self) let upgradePriceButton = Child(PlainButtonComponent.self) - let variantsMeasureDescription = Child(MultilineTextComponent.self) + let upgradeDescriptionMeasure = Child(MultilineTextComponent.self) + + let priceButtonMeasure = Child(MultilineTextWithEntitiesComponent.self) + let priceButton = Child(GlassBarButtonComponent.self) let spaceRegex = try? NSRegularExpression(pattern: "\\[(.*?)\\]", options: []) @@ -2592,7 +2605,7 @@ private final class GiftViewSheetContent: CombinedComponent { } else if state.inUpgradePreview, let attributes = state.upgradePreview?.attributes { headerHeight = 246.0 headerSubject = .preview(attributes) - } else if case let .upgradePreview(attributes, _) = component.subject { + } else if case let .upgradePreview(_, attributes, _) = component.subject { headerHeight = 246.0 headerSubject = .preview(attributes) } else if case let .wearPreview(_, attributes) = component.subject, let attributes { @@ -2606,6 +2619,19 @@ private final class GiftViewSheetContent: CombinedComponent { headerSubject = nil } + var buttonsBackground: GlassControlGroupComponent.Background = .panel + if let uniqueGift, let backdropAttribute = uniqueGift.attributes.first(where: { attribute in + if case .backdrop = attribute { + return true + } else { + return false + } + }), case let .backdrop(_, _, _, outerColor, _, _, _) = backdropAttribute { + buttonsBackground = .color(UIColor(rgb: UInt32(bitPattern: outerColor)).mixedWith(.white, alpha: 0.2)) + } else if showUpgradePreview, let backgroundColor = giftCompositionExternalState.backgroundColor { + buttonsBackground = .color(backgroundColor.mixedWith(.white, alpha: 0.2)) + } + var ownerPeerId: EnginePeer.Id? if let uniqueGift { if case let .peerId(peerId) = uniqueGift.owner { @@ -2719,6 +2745,16 @@ private final class GiftViewSheetContent: CombinedComponent { let tableTextColor = theme.list.itemPrimaryTextColor let tableLinkColor = theme.list.itemAccentColor + var resellAmount: CurrencyAmount? + var selling = false + if let uniqueGift { + if uniqueGift.resellForTonOnly { + resellAmount = uniqueGift.resellAmounts?.first(where: { $0.currency == .ton }) + } else { + resellAmount = uniqueGift.resellAmounts?.first(where: { $0.currency == .stars }) + } + } + if let headerSubject { let animation = animation.update( component: GiftCompositionComponent( @@ -2857,7 +2893,7 @@ private final class GiftViewSheetContent: CombinedComponent { title = environment.strings.Gift_Upgrade_GiftTitle uniqueText = strings.Gift_Upgrade_Unique_GiftDescription(peerName).string tradableText = strings.Gift_Upgrade_Tradable_GiftDescription(peerName).string - } else if case let .upgradePreview(_, peerName) = component.subject { + } else if case let .upgradePreview(_, _, peerName) = component.subject { var peerName = peerName if peerName.count > 22 { peerName = "\(peerName.prefix(22))…" @@ -2871,28 +2907,27 @@ private final class GiftViewSheetContent: CombinedComponent { tradableText = strings.Gift_Upgrade_Tradable_Description } - let upgradeTitle = upgradeTitle.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString( - string: title, - font: Font.bold(20.0), - textColor: .white, - paragraphAlignment: .center - )), - horizontalAlignment: .center, - maximumNumberOfLines: 1 - ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), - transition: .immediate - ) + var variant1: GiftItemComponent.Subject? + var variant2: GiftItemComponent.Subject? + var variant3: GiftItemComponent.Subject? - if case let .generic(gift) = component.subject.arguments?.gift, let upgradePreview = state.upgradePreview { - var variant1: GiftItemComponent.Subject = .starGift(gift: gift, price: "") - var variant2: GiftItemComponent.Subject = .starGift(gift: gift, price: "") - var variant3: GiftItemComponent.Subject = .starGift(gift: gift, price: "") + var upgradeAttributes: [StarGift.UniqueGift.Attribute]? + + if case let .generic(gift) = component.subject.arguments?.gift { + variant1 = .starGift(gift: gift, price: "") + variant2 = .starGift(gift: gift, price: "") + variant3 = .starGift(gift: gift, price: "") + } + + if let upgradePreview = state.upgradePreview { + upgradeAttributes = upgradePreview.attributes + } else if case let .upgradePreview(_, attributes, _) = component.subject { + upgradeAttributes = attributes + } + if let upgradeAttributes { var i = 0 - for attribute in upgradePreview.attributes { + for attribute in upgradeAttributes { if case .model = attribute { switch i { case 0: @@ -2907,23 +2942,39 @@ private final class GiftViewSheetContent: CombinedComponent { i += 1 } } - + } + + if let variant1, let variant2, let variant3 { var buttonColor: UIColor = UIColor.white.withAlphaComponent(0.16) - if let previewPatternColor = giftCompositionExternalState.previewPatternColor { - buttonColor = previewPatternColor + if let backgroundColor = giftCompositionExternalState.backgroundColor { + buttonColor = backgroundColor.mixedWith(.white, alpha: 0.2) } - let variantsMeasureDescription = variantsMeasureDescription.update( + let upgradeDescriptionMeasure = upgradeDescriptionMeasure.update( component: MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Upgrade_ViewAllVariants, font: Font.semibold(13.0), textColor: .clear))), availableSize: context.availableSize, transition: .immediate ) - context.add(variantsMeasureDescription + context.add(upgradeDescriptionMeasure .position(CGPoint(x: -10000.0, y: -10000.0)) ) - let variantsButtonSize = CGSize(width: variantsMeasureDescription.size.width + 87.0, height: 24.0) - + let variantsButtonSize = CGSize(width: upgradeDescriptionMeasure.size.width + 87.0, height: 24.0) + + let upgradeTitle = upgradeTitle.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: title, + font: Font.bold(20.0), + textColor: .white, + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) let upgradeDescription = upgradeDescription.update( component: GlassBarButtonComponent( size: variantsButtonSize, @@ -3063,7 +3114,6 @@ private final class GiftViewSheetContent: CombinedComponent { originY += 16.0 if case .upgradePreview = component.subject { - } else if !incoming { } else { let checkTheme = CheckComponent.Theme( @@ -3120,19 +3170,15 @@ private final class GiftViewSheetContent: CombinedComponent { var descriptionText: String var hasDescriptionButton = false if let uniqueGift { - titleString = uniqueGift.title - var isCrafted = false + titleString = uniqueGift.title + " **#\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: environment.dateTimeFormat))**" + descriptionText = "\(strings.Gift_Unique_Collectible) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: environment.dateTimeFormat))" for attribute in uniqueGift.attributes { - if case let .model(_, _, _, crafted) = attribute { - isCrafted = crafted + if case let .model(name, _, _, _) = attribute { + descriptionText = name } } - //TODO:localize - let prefix: String = isCrafted ? "Crafted Collectible" : strings.Gift_Unique_Collectible - descriptionText = "\(prefix) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: environment.dateTimeFormat))" - if let releasedBy = uniqueGift.releasedBy, let peer = state.peerMap[releasedBy], let addressName = peer.addressName { - descriptionText = strings.Gift_Unique_CollectibleBy("#\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: environment.dateTimeFormat))", "[@\(addressName)]()").string + descriptionText = strings.Gift_View_ReleasedBy("[@\(addressName)]()").string hasDescriptionButton = true releasedByPeer = peer } @@ -3204,14 +3250,24 @@ private final class GiftViewSheetContent: CombinedComponent { descriptionText = modifiedString } + let titleFont = Font.bold(20.0) + let smallTitleFont = Font.bold(15.0) + let titleAttributedString: NSAttributedString + if let uniqueGift { + let numberFont = uniqueGift.number < 1000 ? titleFont : smallTitleFont + titleAttributedString = parseMarkdownIntoAttributedString(titleString, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: .white), bold: MarkdownAttributeSet(font: numberFont, textColor: vibrantColor), link: MarkdownAttributeSet(font: titleFont, textColor: .white), linkAttribute: { _ in return nil }), textAlignment: .center) + } else { + titleAttributedString = NSAttributedString( + string: titleString, + font: titleFont, + textColor: theme.actionSheet.primaryTextColor, + paragraphAlignment: .center + ) + } + let title = title.update( component: MultilineTextComponent( - text: .plain(NSAttributedString( - string: titleString, - font: Font.bold(20.0), - textColor: uniqueGift != nil ? .white : theme.actionSheet.primaryTextColor, - paragraphAlignment: .center - )), + text: .plain(titleAttributedString), horizontalAlignment: .center, maximumNumberOfLines: 1 ), @@ -3269,6 +3325,88 @@ private final class GiftViewSheetContent: CombinedComponent { }) descriptionOffset += subtitle.size.height } + + if let resellAmount { + if incoming || ownerPeerId == component.context.account.peerId { + var valueString = formatCurrencyAmountText(resellAmount, dateTimeFormat: environment.dateTimeFormat) + switch resellAmount.currency { + case .stars: + valueString = "⭐️\(valueString)" + case .ton: + valueString = "💎\(valueString)" + } + let priceButtonAttributedString = NSMutableAttributedString(string: strings.Gift_View_OnSale(valueString).string, font: Font.regular(13.0), textColor: .white) + let starRange = (priceButtonAttributedString.string as NSString).range(of: "⭐️") + if starRange.location != NSNotFound { + priceButtonAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: true)), range: starRange) + priceButtonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: starRange) + } + let tonRange = (priceButtonAttributedString.string as NSString).range(of: "💎") + if tonRange.location != NSNotFound { + priceButtonAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 1, file: nil, custom: .ton(tinted: true)), range: tonRange) + priceButtonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: tonRange) + } + + let priceButtonMeasure = priceButtonMeasure.update( + component: MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: .white, + text: .plain(priceButtonAttributedString) + ), + availableSize: context.availableSize, + transition: context.transition + ) + context.add(priceButtonMeasure + .position(CGPoint(x: -10000.0, y: -10000.0)) + ) + + var buttonColor: UIColor = UIColor(rgb: 0xffffff, alpha: 0.1) + if case let .color(color) = buttonsBackground { + buttonColor = color + } + + let priceButtonSize = CGSize(width: priceButtonMeasure.size.width + 18.0, height: 19.0) + let priceButton = priceButton.update( + component: GlassBarButtonComponent( + size: priceButtonSize, + backgroundColor: buttonColor, + isDark: true, + state: .tintedGlass, + component: AnyComponentWithIdentity(id: "label", component: AnyComponent( + MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: .white, + text: .plain(priceButtonAttributedString) + ) + )), + action: { [weak state] _ in + state?.resellGift(update: true) + } + ), + availableSize: priceButtonSize, + transition: context.transition + ) + headerComponents.append({ + context.add(priceButton + .position(CGPoint(x: context.availableSize.width / 2.0, y: 207.0 + descriptionOffset + priceButton.size.height / 2.0)) + .appear(.default(scale: true, alpha: true)) + .disappear(.default(scale: true, alpha: true)) + ) + }) + + descriptionText = "" + originY += 7.0 + } + if case let .uniqueGift(_, recipientPeerId) = component.subject, recipientPeerId != nil { + } else if ownerPeerId != component.context.account.peerId { + selling = true + } + } + var useDescriptionTint = false if !descriptionText.isEmpty { var linkColor = theme.actionSheet.controlAccentColor @@ -3756,18 +3894,19 @@ private final class GiftViewSheetContent: CombinedComponent { let buttonWidth = floor(context.availableSize.width - sideInset * 2.0 - buttonSpacing * CGFloat(buttonsCount - 1)) / CGFloat(buttonsCount) let buttonHeight: CGFloat = 58.0 + var buttonColor: UIColor = UIColor(rgb: 0xffffff, alpha: 0.1) + if case let .color(color) = buttonsBackground { + buttonColor = color + } + var buttonOriginX = sideInset if canTransfer { let transferButton = transferButton.update( - component: PlainButtonComponent( - content: AnyComponent( - HeaderButtonComponent( - title: strings.Gift_View_Header_Transfer, - iconName: "Premium/Collectible/Transfer", - isLocked: isMyHostedUniqueGift - ) - ), - effectAlignment: .center, + component: HeaderButtonComponent( + title: strings.Gift_View_Header_Transfer, + buttonColor: buttonColor, + iconName: "Premium/Collectible/Transfer", + isLocked: isMyHostedUniqueGift, action: { [weak state] in state?.transferGift() } @@ -3788,19 +3927,15 @@ private final class GiftViewSheetContent: CombinedComponent { } let wearButton = wearButton.update( - component: PlainButtonComponent( - content: AnyComponent( - HeaderButtonComponent( - title: isWearing ? strings.Gift_View_Header_TakeOff : strings.Gift_View_Header_Wear, - iconName: isWearing ? "Premium/Collectible/Unwear" : "Premium/Collectible/Wear" - ) - ), - effectAlignment: .center, + component: HeaderButtonComponent( + title: isWearing ? strings.Gift_View_Header_TakeOff : strings.Gift_View_Header_Wear, + buttonColor: buttonColor, + iconName: isWearing ? "Premium/Collectible/Unwear" : "Premium/Collectible/Wear", action: { [weak state] in if let state { if isWearing { state.commitTakeOff() - + state.showAttributeInfo(tag: state.statusTag, text: strings.Gift_View_TookOff("\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: environment.dateTimeFormat))").string) } else { if let controller = controller() as? GiftViewScreen { @@ -3820,7 +3955,7 @@ private final class GiftViewSheetContent: CombinedComponent { canWear = component.context.isPremium } let _ = (ApplicationSpecificNotice.getStarGiftWearTips(accountManager: component.context.sharedContext.accountManager) - |> deliverOnMainQueue).start(next: { [weak state] count in + |> deliverOnMainQueue).start(next: { [weak state] count in guard let state else { return } @@ -3851,15 +3986,11 @@ private final class GiftViewSheetContent: CombinedComponent { if canResell { let resellButton = resellButton.update( - component: PlainButtonComponent( - content: AnyComponent( - HeaderButtonComponent( - title: (uniqueGift.resellAmounts ?? []).isEmpty ? strings.Gift_View_Sell : strings.Gift_View_Unlist, - iconName: (uniqueGift.resellAmounts ?? []).isEmpty ? "Premium/Collectible/Sell" : "Premium/Collectible/Unlist", - isLocked: isMyHostedUniqueGift - ) - ), - effectAlignment: .center, + component: HeaderButtonComponent( + title: (uniqueGift.resellAmounts ?? []).isEmpty ? strings.Gift_View_Sell : strings.Gift_View_Unlist, + buttonColor: buttonColor, + iconName: (uniqueGift.resellAmounts ?? []).isEmpty ? "Premium/Collectible/Sell" : "Premium/Collectible/Unlist", + isLocked: isMyHostedUniqueGift, action: { [weak state] in state?.resellGift() } @@ -4103,14 +4234,17 @@ private final class GiftViewSheetContent: CombinedComponent { badgeString = formatPercentage(Float(value) * 0.1) } case .epic: - badgeString = "epic" + badgeString = strings.Gift_Attribute_Epic badgeColor = UIColor(rgb: 0xaf52de) case .legendary: - badgeString = "legendary" + badgeString = strings.Gift_Attribute_Legendary badgeColor = UIColor(rgb: 0xd57e32) case .rare: - badgeString = "rare" - badgeColor = UIColor(rgb: 0x79993d) + badgeString = strings.Gift_Attribute_Rare + badgeColor = UIColor(rgb: 0x25a3b9) + case .uncommon: + badgeString = strings.Gift_Attribute_Uncommon + badgeColor = UIColor(rgb: 0x22b447) } items.append(AnyComponentWithIdentity( id: AnyHashable(1), @@ -4406,44 +4540,6 @@ private final class GiftViewSheetContent: CombinedComponent { component() } - var resellAmount: CurrencyAmount? - var selling = false - if let uniqueGift { - if uniqueGift.resellForTonOnly { - resellAmount = uniqueGift.resellAmounts?.first(where: { $0.currency == .ton }) - } else { - resellAmount = uniqueGift.resellAmounts?.first(where: { $0.currency == .stars }) - } - if let resellAmount, wearPeerNameChild == nil { - if incoming || ownerPeerId == component.context.account.peerId { - let priceButton = priceButton.update( - component: PlainButtonComponent( - content: AnyComponent( - PriceButtonComponent(price: resellAmount, dateTimeFormat: environment.dateTimeFormat) - ), - effectAlignment: .center, - action: { [weak state] in - state?.resellGift(update: true) - }, - animateScale: false - ), - availableSize: CGSize(width: 150.0, height: 30.0), - transition: context.transition - ) - context.add(priceButton - .position(CGPoint(x: environment.safeInsets.left + 16.0 + priceButton.size.width / 2.0, y: 76.0 + priceButton.size.height / 2.0)) - .appear(.default(scale: true, alpha: true)) - .disappear(.default(scale: true, alpha: true)) - ) - } - if case let .uniqueGift(_, recipientPeerId) = component.subject, recipientPeerId != nil { - } else if ownerPeerId != component.context.account.peerId { - selling = true - } - } - } - - var isChatTheme = false if let controller = controller() as? GiftViewScreen, controller.openChatTheme != nil { isChatTheme = true @@ -5121,20 +5217,7 @@ private final class GiftViewSheetContent: CombinedComponent { ) originY += upgradePriceButton.size.height } - - var buttonsBackground: GlassControlGroupComponent.Background = .panel - if let uniqueGift, let backdropAttribute = uniqueGift.attributes.first(where: { attribute in - if case .backdrop = attribute { - return true - } else { - return false - } - }), case let .backdrop(_, _, innerColor, _, _, _, _) = backdropAttribute { - buttonsBackground = .color(UIColor(rgb: UInt32(bitPattern: innerColor)).withMultipliedBrightnessBy(1.05)) - } else if showUpgradePreview, let previewPatternColor = giftCompositionExternalState.previewPatternColor { - buttonsBackground = .color(previewPatternColor.withMultipliedBrightnessBy(1.05)) - } - + var isBackButton = false if state.inWearPreview || state.inUpgradePreview { isBackButton = true @@ -5164,16 +5247,27 @@ private final class GiftViewSheetContent: CombinedComponent { var rightControlItems: [GlassControlGroupComponent.Item] = [] if uniqueGift != nil && !showWearPreview { if let _ = component.subject.arguments?.canCraftDate { - rightControlItems.append(GlassControlGroupComponent.Item( - id: AnyHashable("craft"), - content: .icon("Premium/Craft"), - action: { [weak state] in - guard let state else { - return - } - state.craftGift() + var canCraft = false + if let data = component.context.currentAppConfiguration.with({ $0 }).data { + if let _ = data["ios_enable_crafting"] { + canCraft = true + } else if let isDev = data["dev"] as? Double, isDev == 1.0 { + canCraft = true } - )) + } + + if canCraft { + rightControlItems.append(GlassControlGroupComponent.Item( + id: AnyHashable("craft"), + content: .icon("Premium/Craft"), + action: { [weak state] in + guard let state else { + return + } + state.craftGift() + } + )) + } } rightControlItems.append(GlassControlGroupComponent.Item( @@ -5188,6 +5282,11 @@ private final class GiftViewSheetContent: CombinedComponent { )) } + var buttonsIsDark = theme.overallDarkAppearance + if case .color = buttonsBackground { + buttonsIsDark = true + } + let buttons = buttons.update( component: GlassControlPanelComponent( theme: theme, @@ -5201,6 +5300,7 @@ private final class GiftViewSheetContent: CombinedComponent { background: buttonsBackground ), centerAlignmentIfPossible: true, + isDark: buttonsIsDark, tag: state.controlButtonsTag ), availableSize: CGSize(width: context.availableSize.width - 16.0 * 2.0, height: 44.0), @@ -5396,7 +5496,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { case uniqueGift(StarGift.UniqueGift, EnginePeer.Id?) case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift) case soldOutGift(StarGift.Gift) - case upgradePreview([StarGift.UniqueGift.Attribute], String) + case upgradePreview(StarGift.Gift, [StarGift.UniqueGift.Attribute], String) case wearPreview(StarGift, [StarGift.UniqueGift.Attribute]?) var arguments: ( @@ -5449,7 +5549,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { let fromPeerId = senderId ?? message.author?.id return (message.id.peerId, fromPeerId, message.author?.debugDisplayTitle, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId, nil, nil, prepaidUpgradeHash, upgradeSeparate, nil, toPeerId, number, nil) - case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, _, peerId, senderId, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _, canCraftDate): + case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, _, peerId, senderId, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _, canCraftDate, _): var reference: StarGiftReference if let peerId, let savedId { reference = .peer(peerId: peerId, id: savedId) @@ -6050,25 +6150,34 @@ private final class GiftViewContextReferenceContentSource: ContextReferenceConte } } -private final class HeaderButtonComponent: CombinedComponent { +private final class HeaderButtonComponent: Component { let title: String + let buttonColor: UIColor let iconName: String let isLocked: Bool + let action: () -> Void public init( title: String, + buttonColor: UIColor, iconName: String, - isLocked: Bool = false + isLocked: Bool = false, + action: @escaping () -> Void ) { self.title = title + self.buttonColor = buttonColor self.iconName = iconName self.isLocked = isLocked + self.action = action } static func ==(lhs: HeaderButtonComponent, rhs: HeaderButtonComponent) -> Bool { if lhs.title != rhs.title { return false } + if lhs.buttonColor != rhs.buttonColor { + return false + } if lhs.iconName != rhs.iconName { return false } @@ -6078,79 +6187,205 @@ private final class HeaderButtonComponent: CombinedComponent { return true } - static var body: Body { - let background = Child(RoundedRectangle.self) - let title = Child(MultilineTextComponent.self) - let icon = Child(BundleIconComponent.self) - let lockIcon = Child(BundleIconComponent.self) + final class View: UIView { + private var component: HeaderButtonComponent? + private weak var componentState: EmptyComponentState? - return { context in - let component = context.component + private let backgroundView = GlassBackgroundView() + private let title = ComponentView() + private let icon = ComponentView() + private let lockIcon = ComponentView() + private let button = HighlightTrackingButton() + + override init(frame: CGRect) { + super.init(frame: frame) - let background = background.update( - component: RoundedRectangle( - color: UIColor.white.withAlphaComponent(0.16), - cornerRadius: 16.0 + self.addSubview(self.backgroundView) + self.backgroundView.contentView.addSubview(self.button) + + self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func buttonPressed() { + if let component = self.component { + component.action() + } + } + + func update(component: HeaderButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.componentState = state + + let bounds = CGRect(origin: .zero, size: availableSize) + + self.backgroundView.update(size: bounds.size, cornerRadius: 16.0, isDark: true, tintColor: .init(kind: .custom(style: .default, color: component.buttonColor)), isInteractive: true, transition: transition) + transition.setFrame(view: self.backgroundView, frame: bounds) + + let iconSize = self.icon.update( + transition: transition, + component: AnyComponent( + BundleIconComponent( + name: component.iconName, + tintColor: UIColor.white + ) ), - availableSize: context.availableSize, - transition: .immediate + environment: {}, + containerSize: availableSize ) - context.add(background - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + if let iconView = self.icon.view { + if iconView.superview == nil { + iconView.isUserInteractionEnabled = false + self.backgroundView.contentView.addSubview(iconView) + } + iconView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - iconSize.width) / 2.0), y: floorToScreenPixels(22.0 - iconSize.height * 0.5)), size: iconSize) + } + + let titleSize = self.title.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.title, + font: Font.regular(11.0), + textColor: UIColor.white, + paragraphAlignment: .natural + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - 16.0, height: availableSize.height) ) - let icon = icon.update( - component: BundleIconComponent( - name: component.iconName, - tintColor: UIColor.white - ), - availableSize: context.availableSize, - transition: .immediate - ) - context.add(icon - .position(CGPoint(x: context.availableSize.width / 2.0, y: 22.0)) - ) - - let title = title.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString( - string: component.title, - font: Font.regular(11.0), - textColor: UIColor.white, - paragraphAlignment: .natural - )), - horizontalAlignment: .center, - maximumNumberOfLines: 1 - ), - availableSize: CGSize(width: context.availableSize.width - 16.0, height: context.availableSize.height), - transition: .immediate - ) - var totalTitleWidth = title.size.width - var titleOriginX = context.availableSize.width / 2.0 - totalTitleWidth / 2.0 + var totalTitleWidth = titleSize.width + var titleOriginX = availableSize.width / 2.0 - totalTitleWidth / 2.0 if component.isLocked { let titleSpacing: CGFloat = 3.0 - let lockIcon = lockIcon.update( - component: BundleIconComponent( - name: "Chat List/StatusLockIcon", - tintColor: UIColor.white + + let lockIconSize = self.lockIcon.update( + transition: transition, + component: AnyComponent( + BundleIconComponent( + name: "Chat List/StatusLockIcon", + tintColor: .white + ) ), - availableSize: context.availableSize, - transition: .immediate + environment: {}, + containerSize: availableSize ) - totalTitleWidth += lockIcon.size.width + titleSpacing - titleOriginX = context.availableSize.width / 2.0 - totalTitleWidth / 2.0 - context.add(lockIcon - .position(CGPoint(x: titleOriginX + lockIcon.size.width / 2.0, y: 42.0)) - ) - titleOriginX += lockIcon.size.width + titleSpacing + totalTitleWidth += lockIconSize.width + titleSpacing + titleOriginX = availableSize.width / 2.0 - totalTitleWidth / 2.0 + + if let lockIconView = self.lockIcon.view { + if lockIconView.superview == nil { + lockIconView.isUserInteractionEnabled = false + self.backgroundView.contentView.addSubview(lockIconView) + } + lockIconView.frame = CGRect(origin: CGPoint(x: titleOriginX, y: floorToScreenPixels(42.0 - lockIconSize.height * 0.5)), size: lockIconSize) + } + titleOriginX += lockIconSize.width + titleSpacing } - context.add(title - .position(CGPoint(x: titleOriginX + title.size.width / 2.0, y: 42.0)) - ) - return context.availableSize + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.backgroundView.contentView.addSubview(titleView) + } + titleView.frame = CGRect(origin: CGPoint(x: titleOriginX, y: floorToScreenPixels(42.0 - titleSize.height * 0.5)), size: titleSize) + } + + self.button.frame = bounds + + return availableSize } } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } + +// static var body: Body { +// let background = Child(RoundedRectangle.self) +// let title = Child(MultilineTextComponent.self) +// let icon = Child(BundleIconComponent.self) +// let lockIcon = Child(BundleIconComponent.self) +// +// return { context in +// let component = context.component +// +// let background = background.update( +// component: RoundedRectangle( +// color: UIColor.white.withAlphaComponent(0.16), +// cornerRadius: 16.0 +// ), +// availableSize: context.availableSize, +// transition: .immediate +// ) +// context.add(background +// .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) +// ) +// +// let icon = icon.update( +// component: BundleIconComponent( +// name: component.iconName, +// tintColor: UIColor.white +// ), +// availableSize: context.availableSize, +// transition: .immediate +// ) +// context.add(icon +// .position(CGPoint(x: context.availableSize.width / 2.0, y: 22.0)) +// ) +// +// let title = title.update( +// component: MultilineTextComponent( +// text: .plain(NSAttributedString( +// string: component.title, +// font: Font.regular(11.0), +// textColor: UIColor.white, +// paragraphAlignment: .natural +// )), +// horizontalAlignment: .center, +// maximumNumberOfLines: 1 +// ), +// availableSize: CGSize(width: context.availableSize.width - 16.0, height: context.availableSize.height), +// transition: .immediate +// ) +// var totalTitleWidth = title.size.width +// var titleOriginX = context.availableSize.width / 2.0 - totalTitleWidth / 2.0 +// if component.isLocked { +// let titleSpacing: CGFloat = 3.0 +// let lockIcon = lockIcon.update( +// component: BundleIconComponent( +// name: "Chat List/StatusLockIcon", +// tintColor: UIColor.white +// ), +// availableSize: context.availableSize, +// transition: .immediate +// ) +// totalTitleWidth += lockIcon.size.width + titleSpacing +// titleOriginX = context.availableSize.width / 2.0 - totalTitleWidth / 2.0 +// context.add(lockIcon +// .position(CGPoint(x: titleOriginX + lockIcon.size.width / 2.0, y: 42.0)) +// ) +// titleOriginX += lockIcon.size.width + titleSpacing +// } +// context.add(title +// .position(CGPoint(x: titleOriginX + title.size.width / 2.0, y: 42.0)) +// ) +// +// return context.availableSize +// } +// } } private struct GiftViewConfiguration { diff --git a/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlGroup.swift b/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlGroup.swift index 1c99e3468e..b39a135a44 100644 --- a/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlGroup.swift +++ b/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlGroup.swift @@ -11,10 +11,31 @@ import LottieComponent public final class GlassControlGroupComponent: Component { public final class Item: Equatable { - public enum Content: Hashable { + public enum Content: Equatable { case icon(String) case text(String) case animation(String) + case customIcon(id: AnyHashable, component: AnyComponent) + + enum Id: Hashable { + case icon(String) + case text(String) + case animation(String) + case customIcon(AnyHashable) + } + + var id: Id { + switch self { + case let .icon(icon): + return .icon(icon) + case let .text(text): + return .text(text) + case let .animation(animation): + return .animation(animation) + case let .customIcon(id, _): + return .customIcon(id) + } + } } public let id: AnyHashable @@ -143,7 +164,7 @@ public final class GlassControlGroupComponent: Component { var validIds: [AnyHashable] = [] var isInteractive = false for item in component.items { - let itemId = ItemId(id: item.id, contentId: item.content) + let itemId = ItemId(id: item.id, contentId: item.content.id) validIds.append(itemId) @@ -160,7 +181,7 @@ public final class GlassControlGroupComponent: Component { if item.action != nil { isInteractive = true } - + let content: AnyComponent var itemInsets = UIEdgeInsets() switch item.content { @@ -189,6 +210,8 @@ public final class GlassControlGroupComponent: Component { size: CGSize(width: 32.0, height: 32.0), playOnce: playOnce )) + case let .customIcon(_, customIcon): + content = customIcon } var minItemWidth: CGFloat = availableSize.height diff --git a/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlPanel.swift b/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlPanel.swift index 44e6dcb841..351a2144d4 100644 --- a/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlPanel.swift +++ b/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlPanel.swift @@ -36,6 +36,7 @@ public final class GlassControlPanelComponent: Component { public let rightItem: Item? public let centralItem: Item? public let centerAlignmentIfPossible: Bool + public let isDark: Bool? public let tag: AnyObject? public init( @@ -44,6 +45,7 @@ public final class GlassControlPanelComponent: Component { centralItem: Item?, rightItem: Item?, centerAlignmentIfPossible: Bool = false, + isDark: Bool? = nil, tag: AnyObject? = nil ) { self.theme = theme @@ -51,6 +53,7 @@ public final class GlassControlPanelComponent: Component { self.centralItem = centralItem self.rightItem = rightItem self.centerAlignmentIfPossible = centerAlignmentIfPossible + self.isDark = isDark self.tag = tag } @@ -70,6 +73,9 @@ public final class GlassControlPanelComponent: Component { if lhs.centerAlignmentIfPossible != rhs.centerAlignmentIfPossible { return false } + if lhs.isDark != rhs.isDark { + return false + } if lhs.tag !== rhs.tag { return false } @@ -298,7 +304,7 @@ public final class GlassControlPanelComponent: Component { } transition.setFrame(view: self.glassContainerView, frame: CGRect(origin: CGPoint(), size: availableSize)) - self.glassContainerView.update(size: availableSize, isDark: component.theme.overallDarkAppearance, transition: transition) + self.glassContainerView.update(size: availableSize, isDark: component.isDark ?? component.theme.overallDarkAppearance, transition: transition) return availableSize } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 83f5f107da..91567bcc58 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -3734,7 +3734,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } } else if case let .gift(gift) = subject { isGift = true - let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, isPrepaidUpgrade: false, peerId: nil, senderId: nil, savedId: nil, resaleAmount: nil, canTransferDate: nil, canResaleDate: nil, dropOriginalDetailsStars: nil, assigned: false, fromOffer: false, canCraftAt: nil))] + let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, isPrepaidUpgrade: false, peerId: nil, senderId: nil, savedId: nil, resaleAmount: nil, canTransferDate: nil, canResaleDate: nil, dropOriginalDetailsStars: nil, assigned: false, fromOffer: false, canCraftAt: nil, isCrafted: false))] let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: self.context.account.peerId, namespace: Namespaces.Message.Cloud, id: -1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: media, peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) messages = .single([message]) } else { diff --git a/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/Sources/MessagePriceItem.swift b/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/Sources/MessagePriceItem.swift index 131a83ec67..7d92402b8d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/Sources/MessagePriceItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/Sources/MessagePriceItem.swift @@ -197,6 +197,8 @@ private class MessagePriceItemNode: ListViewItemNode, ItemListItemNode { } private let backgroundNode: ASDisplayNode + private let highlightNode: ASDisplayNode + private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode private let maskNode: ASImageNode @@ -225,6 +227,9 @@ private class MessagePriceItemNode: ListViewItemNode, ItemListItemNode { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true + self.highlightNode = ASDisplayNode() + self.highlightNode.isLayerBacked = true + self.topStripeNode = ASDisplayNode() self.topStripeNode.isLayerBacked = true @@ -297,6 +302,20 @@ private class MessagePriceItemNode: ListViewItemNode, ItemListItemNode { self.item?.openSetCustom?() } + public func displayHighlight() { + if self.backgroundNode.supernode != nil { + self.insertSubnode(self.highlightNode, aboveSubnode: self.backgroundNode) + } else { + self.insertSubnode(self.highlightNode, at: 0) + } + + Queue.mainQueue().after(1.2, { + self.highlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + self.highlightNode.removeFromSupernode() + }) + }) + } + func asyncLayout() -> (_ item: MessagePriceItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let currentItem = self.item @@ -329,6 +348,7 @@ private class MessagePriceItemNode: ListViewItemNode, ItemListItemNode { strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.highlightNode.backgroundColor = item.theme.list.itemSearchHighlightColor if strongSelf.backgroundNode.supernode == nil { strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) @@ -370,6 +390,7 @@ private class MessagePriceItemNode: ListViewItemNode, ItemListItemNode { strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.highlightNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight)) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift index 1aacc73113..6f22305c27 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift @@ -587,8 +587,7 @@ public final class PeerInfoCoverComponent: Component { self.avatarPatternContentLayers.append(itemLayer) } - //itemLayer.frame = itemFrame - transition.setFrame(layer: itemLayer, frame: itemFrame) + itemLayer.frame = itemFrame itemLayer.layerTintColor = UIColor(white: 0.0, alpha: 0.8).cgColor transition.setAlpha(layer: itemLayer, alpha: 1.0 - itemScaleFraction) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift index 27ddd942a1..ba7530fe28 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift @@ -89,7 +89,7 @@ private func generateNumberOffsets() -> [CGPoint] { CGPoint(x: 0.9900000000000001, y: 0.0), CGPoint(x: 0.66, y: 0.0), CGPoint(x: 0.5775, y: 0.0), - CGPoint(x: 1.2375, y: 0.0), + CGPoint(x: 0.7425, y: 0.0), CGPoint(x: 0.9900000000000001, y: 0.0), CGPoint(x: 1.32, y: 0.0), CGPoint(x: 1.155, y: 0.0), diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 638ab2ec2a..89dc5d3362 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -313,7 +313,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let tipsPeerDisposable = MetaDisposable() let cachedFaq = Promise(nil) - private var didSetCachedFaq = false + var didSetCachedFaq = false weak var copyProtectionTooltipController: TooltipController? weak var emojiStatusSelectionController: ViewController? @@ -4804,10 +4804,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.headerNode.navigationButtonContainer.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) if self.isSettings { - if !self.didSetCachedFaq { - self.cachedFaq.set(.single(nil) |> then(cachedFaqInstantPage(context: self.context) |> map(Optional.init))) - self.didSetCachedFaq = true - } + self.setupFaqIfNeeded() if let settings = self.data?.globalSettings { self.searchDisplayController = SearchDisplayController( diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenSettingsActions.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenSettingsActions.swift index 6a324daddc..0e64cec1d7 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenSettingsActions.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenSettingsActions.swift @@ -15,6 +15,7 @@ import PremiumUI import TelegramPresentationData import PresentationDataUtils import PasswordSetupUI +import InstantPageCache extension PeerInfoScreenNode { func openSettings(section: PeerInfoSettingsSection) { @@ -298,7 +299,16 @@ extension PeerInfoScreenNode { } } + func setupFaqIfNeeded() { + if !self.didSetCachedFaq { + self.cachedFaq.set(.single(nil) |> then(cachedFaqInstantPage(context: self.context) |> map(Optional.init))) + self.didSetCachedFaq = true + } + } + func openFaq(anchor: String? = nil) { + self.setupFaqIfNeeded() + let presentationData = self.presentationData let progressSignal = Signal { [weak self] subscriber in let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) @@ -314,6 +324,7 @@ extension PeerInfoScreenNode { let progressDisposable = progressSignal.start() let _ = (self.cachedFaq.get() + |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] resolvedUrl in progressDisposable.dispose() diff --git a/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift b/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift index d36b09e40f..fadcc5c0d6 100644 --- a/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift +++ b/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift @@ -108,6 +108,7 @@ public final class PlainButtonComponent: Component { super.init(frame: frame) self.isExclusiveTouch = true + self.layer.rasterizationScale = UIScreenScale self.contentContainer.isUserInteractionEnabled = false self.addSubview(self.contentContainer) @@ -130,8 +131,13 @@ public final class PlainButtonComponent: Component { self.contentContainer.alpha = 0.7 } if animateScale { + self.layer.shouldRasterize = true let transition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) - transition.setScale(layer: self.contentContainer.layer, scale: topScale) + transition.setScale(layer: self.contentContainer.layer, scale: topScale, completion: { finished in + if finished { + self.layer.shouldRasterize = false + } + }) } } else { if animateAlpha { @@ -140,15 +146,21 @@ public final class PlainButtonComponent: Component { } if animateScale { + self.layer.shouldRasterize = true + let transition = ComponentTransition(animation: .none) transition.setScale(layer: self.contentContainer.layer, scale: 1.0) - + self.contentContainer.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in guard let self else { return } - self.contentContainer.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue) + self.contentContainer.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue, completion: { finished in + if finished { + self.layer.shouldRasterize = false + } + }) }) } } diff --git a/submodules/TelegramUI/Components/ProxyServerPreviewScreen/Sources/ProxyServerPreviewScreen.swift b/submodules/TelegramUI/Components/ProxyServerPreviewScreen/Sources/ProxyServerPreviewScreen.swift index d7953bc77b..15b38f83d7 100644 --- a/submodules/TelegramUI/Components/ProxyServerPreviewScreen/Sources/ProxyServerPreviewScreen.swift +++ b/submodules/TelegramUI/Components/ProxyServerPreviewScreen/Sources/ProxyServerPreviewScreen.swift @@ -195,15 +195,14 @@ private final class ProxyServerPreviewSheetContent: CombinedComponent { commit() return } - //TODO:localize let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let alertController = textAlertController( context: context, - title: "Warning", - text: "This action will expose your IP address to the admin of the proxy server.", + title: presentationData.strings.SocksProxySetup_Warning_Title, + text: presentationData.strings.SocksProxySetup_Warning_Text, actions: [ TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), - TextAlertAction(type: .defaultAction, title: "Proceed", action: { + TextAlertAction(type: .defaultAction, title: presentationData.strings.SocksProxySetup_Warning_Proceed, action: { commit() }) ] @@ -253,14 +252,11 @@ private final class ProxyServerPreviewSheetContent: CombinedComponent { availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) - - let titleText: String = "Proxy" - let buttonText: String = "Connect Proxy" - + let title = title.update( component: MultilineTextComponent( text: .plain(NSAttributedString( - string: titleText, + string: strings.SocksProxySetup_Title, font: Font.semibold(17.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center @@ -323,7 +319,7 @@ private final class ProxyServerPreviewSheetContent: CombinedComponent { )) } - var statusText = "Check Status" + var statusText = strings.SocksProxySetup_CheckStatus var statusColor = tableLinkColor var statusIsActive = true if let status = state.status { @@ -378,7 +374,7 @@ private final class ProxyServerPreviewSheetContent: CombinedComponent { ), content: AnyComponentWithIdentity( id: AnyHashable(0), - component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: buttonText, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) + component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: strings.SocksProxySetup_ConnectAndSave, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) ), displaysProgress: state.inProgress, action: { diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index ac25591b55..3cc56e06f8 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -589,7 +589,7 @@ public final class TextFieldComponent: Component { } } - public func chatInputTextNodeShouldReturn() -> Bool { + public func chatInputTextNodeShouldReturn(modifierFlags: UIKeyModifierFlags) -> Bool { guard let component = self.component else { return true } diff --git a/submodules/TelegramUI/Components/VideoPlaybackControlsComponent/Sources/VideoPlaybackControlsComponent.swift b/submodules/TelegramUI/Components/VideoPlaybackControlsComponent/Sources/VideoPlaybackControlsComponent.swift index dea2dcc6c4..3804ed3694 100644 --- a/submodules/TelegramUI/Components/VideoPlaybackControlsComponent/Sources/VideoPlaybackControlsComponent.swift +++ b/submodules/TelegramUI/Components/VideoPlaybackControlsComponent/Sources/VideoPlaybackControlsComponent.swift @@ -142,19 +142,21 @@ public final class VideoPlaybackControlsComponent: Component { self.isUserInteractionEnabled = component.isVisible + let containerInset: CGFloat = 32.0 + let size = CGSize(width: component.layoutParams.sideButtonSize * 2.0 + component.layoutParams.centerButtonSize + component.layoutParams.spacing * 2.0, height: component.layoutParams.centerButtonSize) - let leftButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - component.layoutParams.sideButtonSize) * 0.5)), size: CGSize(width: component.layoutParams.sideButtonSize, height: component.layoutParams.sideButtonSize)) - let centerButtonFrame = CGRect(origin: CGPoint(x: component.layoutParams.sideButtonSize + component.layoutParams.spacing, y: floorToScreenPixels((size.height - component.layoutParams.centerButtonSize) * 0.5)), size: CGSize(width: component.layoutParams.centerButtonSize, height: component.layoutParams.centerButtonSize)) - let rightButtonFrame = CGRect(origin: CGPoint(x: size.width - component.layoutParams.sideButtonSize, y: floorToScreenPixels((size.height - component.layoutParams.sideButtonSize) * 0.5)), size: CGSize(width: component.layoutParams.sideButtonSize, height: component.layoutParams.sideButtonSize)) + let leftButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - component.layoutParams.sideButtonSize) * 0.5)), size: CGSize(width: component.layoutParams.sideButtonSize, height: component.layoutParams.sideButtonSize)).offsetBy(dx: containerInset, dy: containerInset) + let centerButtonFrame = CGRect(origin: CGPoint(x: component.layoutParams.sideButtonSize + component.layoutParams.spacing, y: floorToScreenPixels((size.height - component.layoutParams.centerButtonSize) * 0.5)), size: CGSize(width: component.layoutParams.centerButtonSize, height: component.layoutParams.centerButtonSize)).offsetBy(dx: containerInset, dy: containerInset) + let rightButtonFrame = CGRect(origin: CGPoint(x: size.width - component.layoutParams.sideButtonSize, y: floorToScreenPixels((size.height - component.layoutParams.sideButtonSize) * 0.5)), size: CGSize(width: component.layoutParams.sideButtonSize, height: component.layoutParams.sideButtonSize)).offsetBy(dx: containerInset, dy: containerInset) if isVisibleChanged && !transition.animation.isImmediate { self.backgroundContainer.isHidden = true self.backgroundContainer.isHidden = false } - transition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: CGPoint(), size: size)) - self.backgroundContainer.update(size: size, isDark: true, transition: transition) + transition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: -containerInset, dy: -containerInset)) + self.backgroundContainer.update(size: CGSize(width: size.width + containerInset * 2.0, height: size.height + containerInset * 2.0), isDark: true, transition: transition) let areSideButtonsVisible = component.isVisible && component.displaySeekControls let buttonsTintColor: GlassBackgroundView.TintColor = .init(kind: .custom(style: .clear, color: UIColor(white: 0.0, alpha: 0.2))) diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Stars.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Stars.imageset/Contents.json new file mode 100644 index 0000000000..bdcc010937 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Stars.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "star (3).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Stars.imageset/star (3).pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Stars.imageset/star (3).pdf new file mode 100644 index 0000000000..67f351d03a Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Stars.imageset/star (3).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Ton.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Ton.imageset/Contents.json new file mode 100644 index 0000000000..24d7aec578 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Ton.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ton (3).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Ton.imageset/ton (3).pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Ton.imageset/ton (3).pdf new file mode 100644 index 0000000000..494a8fa170 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Ton.imageset/ton (3).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Craft/Chance.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Craft/Chance.imageset/Contents.json new file mode 100644 index 0000000000..e731ca1da7 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Craft/Chance.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "puzzle.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Craft/Chance.imageset/puzzle.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Craft/Chance.imageset/puzzle.pdf new file mode 100644 index 0000000000..f72afb8193 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Craft/Chance.imageset/puzzle.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Craft/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Craft/Contents.json new file mode 100644 index 0000000000..6e965652df --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Craft/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Craft/Rare.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Craft/Rare.imageset/Contents.json new file mode 100644 index 0000000000..a9f02c0826 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Craft/Rare.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "diamond (2).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Craft/Rare.imageset/diamond (2).pdf b/submodules/TelegramUI/Images.xcassets/Premium/Craft/Rare.imageset/diamond (2).pdf new file mode 100644 index 0000000000..840ee0e553 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Craft/Rare.imageset/diamond (2).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Craft/Result.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Craft/Result.imageset/Contents.json new file mode 100644 index 0000000000..62994ab790 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Craft/Result.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "chance.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Craft/Result.imageset/chance.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Craft/Result.imageset/chance.pdf new file mode 100644 index 0000000000..a6ca2bbfb2 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Craft/Result.imageset/chance.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/GiftCrash.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/GiftCrash.imageset/Contents.json new file mode 100644 index 0000000000..27e29557d3 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/GiftCrash.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "giftcrash.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/GiftCrash.imageset/giftcrash.pdf b/submodules/TelegramUI/Images.xcassets/Premium/GiftCrash.imageset/giftcrash.pdf new file mode 100644 index 0000000000..2bdf30a259 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/GiftCrash.imageset/giftcrash.pdf differ diff --git a/submodules/TelegramUI/Sources/ChatControllerKeyShortcuts.swift b/submodules/TelegramUI/Sources/ChatControllerKeyShortcuts.swift index 65ad49f397..9a93f12694 100644 --- a/submodules/TelegramUI/Sources/ChatControllerKeyShortcuts.swift +++ b/submodules/TelegramUI/Sources/ChatControllerKeyShortcuts.swift @@ -15,16 +15,15 @@ import ChatControllerInteraction extension ChatControllerImpl { var keyShortcutsInternal: [KeyShortcut] { - if !self.traceVisibility() || !isTopmostChatController(self) { + if !isTopmostChatController(self) { return [] } let strings = self.presentationData.strings - var inputShortcuts: [KeyShortcut] + var inputShortcuts: [KeyShortcut] = [] if self.chatDisplayNode.isInputViewFocused { inputShortcuts = [ - KeyShortcut(title: strings.KeyCommand_SendMessage, input: "\r", action: {}), KeyShortcut(input: "B", modifiers: [.command], action: { [weak self] in if let strongSelf = self { strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in @@ -80,6 +79,16 @@ extension ChatControllerImpl { } }) ] + + if self.context.sharedContext.currentChatSettings.with({ $0 }).sendWithCmdEnter { + inputShortcuts.append( + KeyShortcut(title: strings.KeyCommand_SendMessage, input: "\r", modifiers: [.command], action: {}) + ) + } else { + inputShortcuts.append( + KeyShortcut(title: strings.KeyCommand_SendMessage, input: "\r", action: {}) + ) + } } else if UIResponder.currentFirst() == nil { inputShortcuts = [ KeyShortcut(title: strings.KeyCommand_FocusOnInputField, input: "\r", action: { [weak self] in diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 7b19171d2c..a916a270ac 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -2256,6 +2256,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { if let value = self.backgroundNode.makeEdgeEffectNode() { bottomBackgroundEdgeEffectNode = value self.bottomBackgroundEdgeEffectNode = value + value.isUserInteractionEnabled = false self.historyNodeContainer.view.superview?.insertSubview(value.view, aboveSubview: self.historyNodeContainer.view) } } @@ -2466,6 +2467,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { if let value = self.backgroundNode.makeEdgeEffectNode() { topBackgroundEdgeEffectNode = value self.topBackgroundEdgeEffectNode = value + value.isUserInteractionEnabled = false self.historyNodeContainer.view.superview?.insertSubview(value.view, aboveSubview: self.historyNodeContainer.view) } } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift index 9358698e53..1278c6bb61 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift @@ -182,7 +182,7 @@ func textInputAccessoryPanel( let alertController = AlertScreen( context: context, - configuration: AlertScreen.Configuration(actionAlignment: .vertical), + configuration: AlertScreen.Configuration(actionAlignment: .vertical, allowInputInset: true), content: content, actions: [ .init(title: strings.Conversation_ForwardOptions_ShowOptions, action: { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 1bcadb3599..49e039959f 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -1136,7 +1136,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState let sendGiftTitle: String var isIncoming = message.effectivelyIncoming(context.account.peerId) for media in message.media { - if let action = media as? TelegramMediaAction, case let .starGiftUnique(_, isUpgrade, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = action.action { + if let action = media as? TelegramMediaAction, case let .starGiftUnique(_, isUpgrade, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = action.action { if isUpgrade && message.author?.id == context.account.peerId { isIncoming = true } @@ -2725,14 +2725,14 @@ private final class ChatDeleteMessageContextItemNode: ASDisplayNode, ContextMenu return (CGSize(width: max(textSize.width, statusSize.width) + sideInset + rightTextInset, height: verticalInset * 2.0 + combinedTextHeight), { size, transition in self.validLayout = size let verticalOrigin = floor((size.height - combinedTextHeight) / 2.0) - let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin), size: textSize) + let textFrame = CGRect(origin: CGPoint(x: sideInset + 42.0, y: verticalOrigin), size: textSize) transition.updateFrameAdditive(node: self.textNode, frame: textFrame) - transition.updateFrame(node: self.textIconNode, frame: CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin + verticalSpacing + textSize.height + floorToScreenPixels((statusSize.height - textIconSize.height) / 2.0) + 1.0), size: textIconSize)) - transition.updateFrameAdditive(node: self.statusNode, frame: CGRect(origin: CGPoint(x: sideInset + textIconSize.width + 2.0, y: verticalOrigin + verticalSpacing + textSize.height), size: statusSize)) + transition.updateFrame(node: self.textIconNode, frame: CGRect(origin: CGPoint(x: sideInset + 42.0, y: verticalOrigin + verticalSpacing + textSize.height + floorToScreenPixels((statusSize.height - textIconSize.height) / 2.0) + 1.0), size: textIconSize)) + transition.updateFrameAdditive(node: self.statusNode, frame: CGRect(origin: CGPoint(x: sideInset + 42.0 + textIconSize.width + 2.0, y: verticalOrigin + verticalSpacing + textSize.height), size: statusSize)) if !iconSize.width.isZero { - transition.updateFrameAdditive(node: self.iconNode, frame: CGRect(origin: CGPoint(x: size.width - standardIconWidth - iconSideInset + floor((standardIconWidth - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)) + transition.updateFrameAdditive(node: self.iconNode, frame: CGRect(origin: CGPoint(x: iconSideInset + 12.0, y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)) } transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 55403ba2bb..7af47de9ab 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -255,6 +255,9 @@ public final class SharedAccountContextImpl: SharedAccountContext { public let currentMediaDisplaySettings: Atomic private var mediaDisplaySettingsDisposable: Disposable? + public let currentChatSettings: Atomic + private var chatSettingsDisposable: Disposable? + public let currentStickerSettings: Atomic private var stickerSettingsDisposable: Disposable? @@ -339,6 +342,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.currentMediaDisplaySettings = Atomic(value: initialPresentationDataAndSettings.mediaDisplaySettings) self.currentStickerSettings = Atomic(value: initialPresentationDataAndSettings.stickerSettings) self.currentInAppNotificationSettings = Atomic(value: initialPresentationDataAndSettings.inAppNotificationSettings) + self.currentChatSettings = Atomic(value: initialPresentationDataAndSettings.chatSettings) if automaticEnergyUsageShouldBeOnNow(settings: self.currentAutomaticMediaDownloadSettings) { self.energyUsageSettings = EnergyUsageSettings.powerSavingDefault @@ -485,6 +489,15 @@ public final class SharedAccountContextImpl: SharedAccountContext { } }) + self.chatSettingsDisposable = (self.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.chatSettings]) + |> deliverOnMainQueue).start(next: { [weak self] sharedData in + if let strongSelf = self { + if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.chatSettings]?.get(ChatSettings.self) { + let _ = strongSelf.currentChatSettings.swap(settings) + } + } + }) + let immediateExperimentalUISettingsValue = self.immediateExperimentalUISettingsValue let _ = immediateExperimentalUISettingsValue.swap(initialPresentationDataAndSettings.experimentalUISettings) @@ -1078,6 +1091,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { self.inAppNotificationSettingsDisposable?.dispose() self.mediaInputSettingsDisposable?.dispose() self.mediaDisplaySettingsDisposable?.dispose() + self.chatSettingsDisposable?.dispose() + self.stickerSettingsDisposable?.dispose() self.callDisposable?.dispose() self.groupCallDisposable?.dispose() self.callStateDisposable?.dispose() @@ -3852,8 +3867,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return GiftViewScreen(context: context, subject: .wearPreview(gift, attributes)) } - public func makeGiftUpgradePreviewScreen(context: AccountContext, attributes: [StarGift.UniqueGift.Attribute], peerName: String) -> ViewController { - return GiftViewScreen(context: context, subject: .upgradePreview(attributes, peerName)) + public func makeGiftUpgradePreviewScreen(context: AccountContext, gift: StarGift.Gift, attributes: [StarGift.UniqueGift.Attribute], peerName: String) -> ViewController { + return GiftViewScreen(context: context, subject: .upgradePreview(gift, attributes, peerName)) } public func makeGiftAuctionInfoScreen(context: AccountContext, auctionContext: GiftAuctionContext, completion: (() -> Void)?) -> ViewController { @@ -3876,8 +3891,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return giftOfferAlertController(context: context, updatedPresentationData: updatedPresentationData, gift: gift, peer: peer, amount: amount, commit: commit) } - public func makeGiftUpgradeVariantsScreen(context: AccountContext, gift: StarGift, onlyCrafted: Bool, attributes: [StarGift.UniqueGift.Attribute], selectedAttributes: [StarGift.UniqueGift.Attribute]?, focusedAttribute: StarGift.UniqueGift.Attribute?) -> ViewController { - return GiftUpgradeVariantsScreen(context: context, gift: gift, onlyCrafted: onlyCrafted, attributes: attributes, selectedAttributes: selectedAttributes, focusedAttribute: focusedAttribute) + public func makeGiftUpgradeVariantsScreen(context: AccountContext, gift: StarGift, crafted: Bool, attributes: [StarGift.UniqueGift.Attribute], selectedAttributes: [StarGift.UniqueGift.Attribute]?, focusedAttribute: StarGift.UniqueGift.Attribute?) -> ViewController { + return GiftUpgradeVariantsScreen(context: context, gift: gift, crafted: crafted, attributes: attributes, selectedAttributes: selectedAttributes, focusedAttribute: focusedAttribute) } public func makeGiftAuctionWearPreviewScreen(context: AccountContext, auctionContext: GiftAuctionContext, acquiredGifts: Signal<[GiftAuctionAcquiredGift], NoError>?, attributes: [StarGift.UniqueGift.Attribute], completion: @escaping () -> Void) -> ViewController { diff --git a/submodules/TelegramUIPreferences/Sources/ChatSettings.swift b/submodules/TelegramUIPreferences/Sources/ChatSettings.swift new file mode 100644 index 0000000000..a6cfee2e81 --- /dev/null +++ b/submodules/TelegramUIPreferences/Sources/ChatSettings.swift @@ -0,0 +1,49 @@ +import Foundation +import TelegramCore +import SwiftSignalKit + +public struct ChatSettings: Codable, Equatable { + public let sendWithCmdEnter: Bool + + public static var defaultSettings: ChatSettings { + return ChatSettings(sendWithCmdEnter: false) + } + + public init(sendWithCmdEnter: Bool) { + self.sendWithCmdEnter = sendWithCmdEnter + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + self.sendWithCmdEnter = (try container.decode(Int32.self, forKey: "sendWithCmdEnter")) != 0 + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encode((self.sendWithCmdEnter ? 1 : 0) as Int32, forKey: "sendWithCmdEnter") + } + + public static func ==(lhs: ChatSettings, rhs: ChatSettings) -> Bool { + return lhs.sendWithCmdEnter == rhs.sendWithCmdEnter + } + + public func withUpdatedSendWithCmdEnter(_ sendWithCmdEnter: Bool) -> ChatSettings { + return ChatSettings(sendWithCmdEnter: sendWithCmdEnter) + } +} + +public func updateChatSettingsInteractively(accountManager: AccountManager, _ f: @escaping (ChatSettings) -> ChatSettings) -> Signal { + return accountManager.transaction { transaction -> Void in + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.chatSettings, { entry in + let currentSettings: ChatSettings + if let entry = entry?.get(ChatSettings.self) { + currentSettings = entry + } else { + currentSettings = ChatSettings.defaultSettings + } + return SharedPreferencesEntry(f(currentSettings)) + }) + } +} diff --git a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift index 755c04caef..879c0c608c 100644 --- a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift +++ b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift @@ -43,6 +43,7 @@ private enum ApplicationSpecificSharedDataKeyValues: Int32 { case drawingSettings = 19 case mediaDisplaySettings = 20 case updateSettings = 21 + case chatSettings = 22 } public struct ApplicationSpecificSharedDataKeys { @@ -68,6 +69,7 @@ public struct ApplicationSpecificSharedDataKeys { public static let drawingSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.drawingSettings.rawValue) public static let mediaDisplaySettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.mediaDisplaySettings.rawValue) public static let updateSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.updateSettings.rawValue) + public static let chatSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.chatSettings.rawValue) } private enum ApplicationSpecificItemCacheCollectionIdValues: Int8 { diff --git a/submodules/WallpaperBackgroundNode/Sources/WallpaperEdgeEffectNodeImpl.swift b/submodules/WallpaperBackgroundNode/Sources/WallpaperEdgeEffectNodeImpl.swift index ff46717703..3cc29ae11f 100644 --- a/submodules/WallpaperBackgroundNode/Sources/WallpaperEdgeEffectNodeImpl.swift +++ b/submodules/WallpaperBackgroundNode/Sources/WallpaperEdgeEffectNodeImpl.swift @@ -121,6 +121,7 @@ final class WallpaperEdgeEffectNodeImpl: ASDisplayNode, WallpaperEdgeEffectNode return } self.contentNode.contents = parentNode.contentNode.contents + self.contentNode.contentMode = parentNode.contentNode.contentMode self.contentNode.backgroundColor = parentNode.contentNode.backgroundColor self.contentNode.alpha = parentNode.contentNode.alpha self.contentNode.isHidden = parentNode.contentNode.isHidden